diff --git a/CHANGELOG.md b/CHANGELOG.md index 092cc699f..26a888572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make `content` optional for presets in TypeScript types ([#11730](https://github.com/tailwindlabs/tailwindcss/pull/11730)) - Handle variable colors that have variable fallback values ([#12049](https://github.com/tailwindlabs/tailwindcss/pull/12049)) - Batch reading content files to prevent `too many open files` error ([#12079](https://github.com/tailwindlabs/tailwindcss/pull/12079)) +- Skip over classes inside `:not(…)` when nested in an at-rule ([#12105](https://github.com/tailwindlabs/tailwindcss/pull/12105)) ## [3.3.3] - 2023-07-13 diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 4d85e9d8c..b173d419f 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -148,43 +148,45 @@ function getClasses(selector, mutate) { return parser.transformSync(selector) } +/** + * Ignore everything inside a :not(...). This allows you to write code like + * `div:not(.foo)`. If `.foo` is never found in your code, then we used to + * not generated it. But now we will ignore everything inside a `:not`, so + * that it still gets generated. + * + * @param {selectorParser.Root} selectors + */ +function ignoreNot(selectors) { + selectors.walkPseudos((pseudo) => { + if (pseudo.value === ':not') { + pseudo.remove() + } + }) +} + function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) { let classes = [] + let selectors = [] - // Handle normal rules if (node.type === 'rule') { - // Ignore everything inside a :not(...). This allows you to write code like - // `div:not(.foo)`. If `.foo` is never found in your code, then we used to - // not generated it. But now we will ignore everything inside a `:not`, so - // that it still gets generated. - function ignoreNot(selectors) { - selectors.walkPseudos((pseudo) => { - if (pseudo.value === ':not') { - pseudo.remove() - } - }) - } - - for (let selector of node.selectors) { - let classCandidates = getClasses(selector, ignoreNot) - // At least one of the selectors contains non-"on-demandable" candidates. - if (classCandidates.length === 0) { - state.containsNonOnDemandable = true - } - - for (let classCandidate of classCandidates) { - classes.push(classCandidate) - } - } + // Handle normal rules + selectors.push(...node.selectors) + } else if (node.type === 'atrule') { + // Handle at-rules (which contains nested rules) + node.walkRules((rule) => selectors.push(...rule.selectors)) } - // Handle at-rules (which contains nested rules) - else if (node.type === 'atrule') { - node.walkRules((rule) => { - for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) { - classes.push(classCandidate) - } - }) + for (let selector of selectors) { + let classCandidates = getClasses(selector, ignoreNot) + + // At least one of the selectors contains non-"on-demandable" candidates. + if (classCandidates.length === 0) { + state.containsNonOnDemandable = true + } + + for (let classCandidate of classCandidates) { + classes.push(classCandidate) + } } if (depth === 0) { diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js index 53c2a4418..66a5c67d4 100644 --- a/tests/basic-usage.test.js +++ b/tests/basic-usage.test.js @@ -2,7 +2,7 @@ import fs from 'fs' import path from 'path' import { crosscheck, run, html, css, defaults } from './util/run' -crosscheck(({ stable, oxide }) => { +crosscheck(({ stable, oxide, engine }) => { test('basic usage', () => { let config = { content: [ @@ -1017,4 +1017,98 @@ crosscheck(({ stable, oxide }) => { } `) }) + + test('detects quoted arbitrary values containing a slash', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss( + engine.oxide + ? css` + .group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden { + display: none; + } + ` + : css` + .hidden, + .group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden { + display: none; + } + ` + ) + }) + + test('handled quoted arbitrary values containing escaped spaces', async () => { + let config = { + content: [ + { + raw: html``, + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss( + engine.oxide + ? css` + .group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden { + display: none; + } + ` + : css` + .hidden, + .group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden { + display: none; + } + ` + ) + }) + + test('Skips classes inside :not() when nested inside an at-rule', async () => { + let config = { + content: [ + { + raw: html` `, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.hand:not(.disabled)': { + '@supports (cursor: pointer)': { + cursor: 'pointer', + }, + }, + }) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + // We didn't find the hand class therefore + // nothing should be generated + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css``) + }) })