Skip to content

Commit

Permalink
Skip over classes inside :not(…) when nested in an at-rule (#12105)
Browse files Browse the repository at this point in the history
* Skip over classes inside `:not(…)` when nested in an at-rule

When defining a utility we skip over classes inside `:not(…)` but we missed doing this when classes were contained within an at-rule. This fixes that.

* Update changelog
  • Loading branch information
thecrypticace committed Oct 23, 2023
1 parent 666c7e4 commit 3fa8ab1
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
60 changes: 31 additions & 29 deletions src/lib/setupContextUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
}
// 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))
}

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 selector of selectors) {
let classCandidates = getClasses(selector, ignoreNot)

for (let classCandidate of classCandidates) {
classes.push(classCandidate)
}
// At least one of the selectors contains non-"on-demandable" candidates.
if (classCandidates.length === 0) {
state.containsNonOnDemandable = true
}
}

// 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 classCandidate of classCandidates) {
classes.push(classCandidate)
}
}

if (depth === 0) {
Expand Down
96 changes: 95 additions & 1 deletion tests/basic-usage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -1017,4 +1017,98 @@ crosscheck(({ stable, oxide }) => {
}
`)
})

test('detects quoted arbitrary values containing a slash', async () => {
let config = {
content: [
{
raw: html`<div class="group-[[href^='/']]:hidden"></div>`,
},
],
}

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`<div class="group-[[href^='_bar']]:hidden"></div>`,
},
],
}

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` <div class="disabled !disabled"></div> `,
},
],
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``)
})
})

0 comments on commit 3fa8ab1

Please sign in to comment.