Skip to content

Commit 3fa8ab1

Browse files
committed
Skip over classes inside :not(…) when nested in an at-rule (#12105)
* 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
1 parent 666c7e4 commit 3fa8ab1

File tree

3 files changed

+127
-30
lines changed

3 files changed

+127
-30
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Make `content` optional for presets in TypeScript types ([#11730](https://github.com/tailwindlabs/tailwindcss/pull/11730))
2020
- Handle variable colors that have variable fallback values ([#12049](https://github.com/tailwindlabs/tailwindcss/pull/12049))
2121
- Batch reading content files to prevent `too many open files` error ([#12079](https://github.com/tailwindlabs/tailwindcss/pull/12079))
22+
- Skip over classes inside `:not(…)` when nested in an at-rule ([#12105](https://github.com/tailwindlabs/tailwindcss/pull/12105))
2223

2324
## [3.3.3] - 2023-07-13
2425

src/lib/setupContextUtils.js

+31-29
Original file line numberDiff line numberDiff line change
@@ -148,43 +148,45 @@ function getClasses(selector, mutate) {
148148
return parser.transformSync(selector)
149149
}
150150

151+
/**
152+
* Ignore everything inside a :not(...). This allows you to write code like
153+
* `div:not(.foo)`. If `.foo` is never found in your code, then we used to
154+
* not generated it. But now we will ignore everything inside a `:not`, so
155+
* that it still gets generated.
156+
*
157+
* @param {selectorParser.Root} selectors
158+
*/
159+
function ignoreNot(selectors) {
160+
selectors.walkPseudos((pseudo) => {
161+
if (pseudo.value === ':not') {
162+
pseudo.remove()
163+
}
164+
})
165+
}
166+
151167
function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
152168
let classes = []
169+
let selectors = []
153170

154-
// Handle normal rules
155171
if (node.type === 'rule') {
156-
// Ignore everything inside a :not(...). This allows you to write code like
157-
// `div:not(.foo)`. If `.foo` is never found in your code, then we used to
158-
// not generated it. But now we will ignore everything inside a `:not`, so
159-
// that it still gets generated.
160-
function ignoreNot(selectors) {
161-
selectors.walkPseudos((pseudo) => {
162-
if (pseudo.value === ':not') {
163-
pseudo.remove()
164-
}
165-
})
166-
}
172+
// Handle normal rules
173+
selectors.push(...node.selectors)
174+
} else if (node.type === 'atrule') {
175+
// Handle at-rules (which contains nested rules)
176+
node.walkRules((rule) => selectors.push(...rule.selectors))
177+
}
167178

168-
for (let selector of node.selectors) {
169-
let classCandidates = getClasses(selector, ignoreNot)
170-
// At least one of the selectors contains non-"on-demandable" candidates.
171-
if (classCandidates.length === 0) {
172-
state.containsNonOnDemandable = true
173-
}
179+
for (let selector of selectors) {
180+
let classCandidates = getClasses(selector, ignoreNot)
174181

175-
for (let classCandidate of classCandidates) {
176-
classes.push(classCandidate)
177-
}
182+
// At least one of the selectors contains non-"on-demandable" candidates.
183+
if (classCandidates.length === 0) {
184+
state.containsNonOnDemandable = true
178185
}
179-
}
180186

181-
// Handle at-rules (which contains nested rules)
182-
else if (node.type === 'atrule') {
183-
node.walkRules((rule) => {
184-
for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) {
185-
classes.push(classCandidate)
186-
}
187-
})
187+
for (let classCandidate of classCandidates) {
188+
classes.push(classCandidate)
189+
}
188190
}
189191

190192
if (depth === 0) {

tests/basic-usage.test.js

+95-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs'
22
import path from 'path'
33
import { crosscheck, run, html, css, defaults } from './util/run'
44

5-
crosscheck(({ stable, oxide }) => {
5+
crosscheck(({ stable, oxide, engine }) => {
66
test('basic usage', () => {
77
let config = {
88
content: [
@@ -1017,4 +1017,98 @@ crosscheck(({ stable, oxide }) => {
10171017
}
10181018
`)
10191019
})
1020+
1021+
test('detects quoted arbitrary values containing a slash', async () => {
1022+
let config = {
1023+
content: [
1024+
{
1025+
raw: html`<div class="group-[[href^='/']]:hidden"></div>`,
1026+
},
1027+
],
1028+
}
1029+
1030+
let input = css`
1031+
@tailwind utilities;
1032+
`
1033+
1034+
let result = await run(input, config)
1035+
1036+
expect(result.css).toMatchFormattedCss(
1037+
engine.oxide
1038+
? css`
1039+
.group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
1040+
display: none;
1041+
}
1042+
`
1043+
: css`
1044+
.hidden,
1045+
.group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
1046+
display: none;
1047+
}
1048+
`
1049+
)
1050+
})
1051+
1052+
test('handled quoted arbitrary values containing escaped spaces', async () => {
1053+
let config = {
1054+
content: [
1055+
{
1056+
raw: html`<div class="group-[[href^='_bar']]:hidden"></div>`,
1057+
},
1058+
],
1059+
}
1060+
1061+
let input = css`
1062+
@tailwind utilities;
1063+
`
1064+
1065+
let result = await run(input, config)
1066+
1067+
expect(result.css).toMatchFormattedCss(
1068+
engine.oxide
1069+
? css`
1070+
.group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
1071+
display: none;
1072+
}
1073+
`
1074+
: css`
1075+
.hidden,
1076+
.group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
1077+
display: none;
1078+
}
1079+
`
1080+
)
1081+
})
1082+
1083+
test('Skips classes inside :not() when nested inside an at-rule', async () => {
1084+
let config = {
1085+
content: [
1086+
{
1087+
raw: html` <div class="disabled !disabled"></div> `,
1088+
},
1089+
],
1090+
corePlugins: { preflight: false },
1091+
plugins: [
1092+
function ({ addUtilities }) {
1093+
addUtilities({
1094+
'.hand:not(.disabled)': {
1095+
'@supports (cursor: pointer)': {
1096+
cursor: 'pointer',
1097+
},
1098+
},
1099+
})
1100+
},
1101+
],
1102+
}
1103+
1104+
let input = css`
1105+
@tailwind utilities;
1106+
`
1107+
1108+
// We didn't find the hand class therefore
1109+
// nothing should be generated
1110+
let result = await run(input, config)
1111+
1112+
expect(result.css).toMatchFormattedCss(css``)
1113+
})
10201114
})

0 commit comments

Comments
 (0)