mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Fix @apply selector rewriting when multiple classes are involved (#9107)
* Rewrite `replaceSelector` using `postcss-selector-parser` * Sort classes between tags and pseudos when rewriting selectors * Update changelog
This commit is contained in:
parent
b0018e20bf
commit
ef74fd3db6
@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Use absolute paths when resolving changed files for resilience against working directory changes ([#9032](https://github.com/tailwindlabs/tailwindcss/pull/9032))
|
||||
- Fix ring color utility generation when using `respectDefaultRingColorOpacity` ([#9070](https://github.com/tailwindlabs/tailwindcss/pull/9070))
|
||||
- Replace all occurrences of a class in a selector when using `@apply` ([#9107](https://github.com/tailwindlabs/tailwindcss/pull/9107))
|
||||
- Sort tags before classes when `@applying` a selector with joined classes ([#9107](https://github.com/tailwindlabs/tailwindcss/pull/9107))
|
||||
|
||||
## [3.1.8] - 2022-08-05
|
||||
|
||||
|
||||
@ -34,13 +34,13 @@ function extractClasses(node) {
|
||||
return Object.assign(classes, { groups: normalizedGroups })
|
||||
}
|
||||
|
||||
let selectorExtractor = parser((root) => root.nodes.map((node) => node.toString()))
|
||||
let selectorExtractor = parser()
|
||||
|
||||
/**
|
||||
* @param {string} ruleSelectors
|
||||
*/
|
||||
function extractSelectors(ruleSelectors) {
|
||||
return selectorExtractor.transformSync(ruleSelectors)
|
||||
return selectorExtractor.astSync(ruleSelectors)
|
||||
}
|
||||
|
||||
function extractBaseCandidates(candidates, separator) {
|
||||
@ -299,30 +299,61 @@ function processApply(root, context, localCache) {
|
||||
* What happens in this function is that we prepend a `.` and escape the candidate.
|
||||
* This will result in `.hover\:font-bold`
|
||||
* Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {string} utilitySelectors
|
||||
* @param {string} candidate
|
||||
*/
|
||||
// TODO: Should we use postcss-selector-parser for this instead?
|
||||
function replaceSelector(selector, utilitySelectors, candidate) {
|
||||
let needle = `.${escapeClassName(candidate)}`
|
||||
let needles = [...new Set([needle, needle.replace(/\\2c /g, '\\,')])]
|
||||
let selectorList = extractSelectors(selector)
|
||||
let utilitySelectorsList = extractSelectors(utilitySelectors)
|
||||
let candidateList = extractSelectors(`.${escapeClassName(candidate)}`)
|
||||
let candidateClass = candidateList.nodes[0].nodes[0]
|
||||
|
||||
return extractSelectors(selector)
|
||||
.map((s) => {
|
||||
let replaced = []
|
||||
selectorList.each((sel) => {
|
||||
/** @type {Set<import('postcss-selector-parser').Selector>} */
|
||||
let replaced = new Set()
|
||||
|
||||
for (let utilitySelector of utilitySelectorsList) {
|
||||
let replacedSelector = utilitySelector
|
||||
for (const needle of needles) {
|
||||
replacedSelector = replacedSelector.replace(needle, s)
|
||||
utilitySelectorsList.each((utilitySelector) => {
|
||||
utilitySelector = utilitySelector.clone()
|
||||
|
||||
utilitySelector.walkClasses((node) => {
|
||||
if (node.value !== candidateClass.value) {
|
||||
return
|
||||
}
|
||||
if (replacedSelector === utilitySelector) {
|
||||
continue
|
||||
}
|
||||
replaced.push(replacedSelector)
|
||||
}
|
||||
return replaced.join(', ')
|
||||
|
||||
// Since you can only `@apply` class names this is sufficient
|
||||
// We want to replace the matched class name with the selector the user is using
|
||||
// Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)`
|
||||
node.replaceWith(...sel.nodes.map((node) => node.clone()))
|
||||
|
||||
// Record that we did something and we want to use this new selector
|
||||
replaced.add(utilitySelector)
|
||||
})
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
// Sort tag names before class names
|
||||
// This happens when replacing `.bar` in `.foo.bar` with a tag like `section`
|
||||
for (const sel of replaced) {
|
||||
sel.sort((a, b) => {
|
||||
if (a.type === 'tag' && b.type === 'class') {
|
||||
return -1
|
||||
} else if (a.type === 'class' && b.type === 'tag') {
|
||||
return 1
|
||||
} else if (a.type === 'class' && b.type === 'pseudo') {
|
||||
return -1
|
||||
} else if (a.type === 'pseudo' && b.type === 'class') {
|
||||
return 1
|
||||
}
|
||||
|
||||
return sel.index(a) - sel.index(b)
|
||||
})
|
||||
}
|
||||
|
||||
sel.replaceWith(...replaced)
|
||||
})
|
||||
|
||||
return selectorList.toString()
|
||||
}
|
||||
|
||||
let perParentApplies = new Map()
|
||||
|
||||
@ -1584,3 +1584,105 @@ it('can apply user utilities that start with a dash', async () => {
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('can apply joined classes when using elements', async () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="foo-1 -foo-1 new-class"></div>` }],
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
.foo.bar {
|
||||
color: red;
|
||||
}
|
||||
.bar.foo {
|
||||
color: green;
|
||||
}
|
||||
header:nth-of-type(odd) {
|
||||
@apply foo;
|
||||
}
|
||||
main {
|
||||
@apply foo bar;
|
||||
}
|
||||
footer {
|
||||
@apply bar;
|
||||
}
|
||||
`
|
||||
|
||||
let result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.foo.bar {
|
||||
color: red;
|
||||
}
|
||||
.bar.foo {
|
||||
color: green;
|
||||
}
|
||||
header.bar:nth-of-type(odd) {
|
||||
color: red;
|
||||
color: green;
|
||||
}
|
||||
main.bar {
|
||||
color: red;
|
||||
}
|
||||
main.foo {
|
||||
color: red;
|
||||
}
|
||||
main.bar {
|
||||
color: green;
|
||||
}
|
||||
main.foo {
|
||||
color: green;
|
||||
}
|
||||
footer.foo {
|
||||
color: red;
|
||||
color: green;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('can produce selectors that replace multiple instances of the same class', async () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="foo-1 -foo-1 new-class"></div>` }],
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
.foo + .foo {
|
||||
color: blue;
|
||||
}
|
||||
.bar + .bar {
|
||||
color: fuchsia;
|
||||
}
|
||||
header {
|
||||
@apply foo;
|
||||
}
|
||||
main {
|
||||
@apply foo bar;
|
||||
}
|
||||
footer {
|
||||
@apply bar;
|
||||
}
|
||||
`
|
||||
|
||||
let result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.foo + .foo {
|
||||
color: blue;
|
||||
}
|
||||
.bar + .bar {
|
||||
color: fuchsia;
|
||||
}
|
||||
header + header {
|
||||
color: blue;
|
||||
}
|
||||
main + main {
|
||||
color: blue;
|
||||
color: fuchsia;
|
||||
}
|
||||
footer + footer {
|
||||
color: fuchsia;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user