diff --git a/CHANGELOG.md b/CHANGELOG.md index 80665591d0fa..ca9046c08d84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 7088337abbeb..6d23b88112e2 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -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} */ + let replaced = new Set() - for (let utilitySelector of utilitySelectorsList) { - let replacedSelector = utilitySelector - for (const needle of needles) { - replacedSelector = replacedSelector.replace(needle, s) - } - if (replacedSelector === utilitySelector) { - continue + utilitySelectorsList.each((utilitySelector) => { + utilitySelector = utilitySelector.clone() + + utilitySelector.walkClasses((node) => { + if (node.value !== candidateClass.value) { + return } - 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() diff --git a/tests/apply.test.js b/tests/apply.test.js index b1a2ac461d2f..300aa8a8c7a2 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -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`
` }], + 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`
` }], + 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; + } + `) +})