Skip to content

Commit 9bbdd9b

Browse files
Disallow multi-selector arbitrary variants (#10655)
* Allow escaping in `splitAtTopLevelOnly` * Correctly parse arbitrary variants that have multiple selectors * Explicitly disallow multiple selector arbitrary variants Now that we parse them correctly we can restrict them to explicitly supporting only a single selector * Add test to verify that multiple selector arbitrary variants are dropped * Add test * Make prettier happy * Fix CS * Update changelog
1 parent d6121f0 commit 9bbdd9b

File tree

4 files changed

+72
-5
lines changed

4 files changed

+72
-5
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Add `caption-side` utilities ([#10470](https://github.com/tailwindlabs/tailwindcss/pull/10470))
2020
- Add `justify-normal` and `justify-stretch` utilities ([#10560](https://github.com/tailwindlabs/tailwindcss/pull/10560))
2121

22+
### Fixed
23+
24+
- Disallow multiple selectors in arbitrary variants ([#10655](https://github.com/tailwindlabs/tailwindcss/pull/10655))
25+
2226
### Changed
2327

2428
- [Oxide] Disable color opacity plugins by default in the `oxide` engine ([#10618](https://github.com/tailwindlabs/tailwindcss/pull/10618))

src/lib/generateRules.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -205,17 +205,26 @@ function applyVariant(variant, matches, context) {
205205

206206
// Register arbitrary variants
207207
if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
208+
let sort = context.offsets.recordVariant(variant)
209+
208210
let selector = normalize(variant.slice(1, -1))
211+
let selectors = splitAtTopLevelOnly(selector, ',')
209212

210-
if (!isValidVariantFormatString(selector)) {
213+
// We do not support multiple selectors for arbitrary variants
214+
if (selectors.length > 1) {
211215
return []
212216
}
213217

214-
let fn = parseVariant(selector)
218+
if (!selectors.every(isValidVariantFormatString)) {
219+
return []
220+
}
215221

216-
let sort = context.offsets.recordVariant(variant)
222+
let records = selectors.map((sel, idx) => [
223+
context.offsets.applyParallelOffset(sort, idx),
224+
parseVariant(sel.trim()),
225+
])
217226

218-
context.variantMap.set(variant, [[sort, fn]])
227+
context.variantMap.set(variant, records)
219228
}
220229

221230
if (context.variantMap.has(variant)) {

src/util/splitAtTopLevelOnly.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,24 @@ export function splitAtTopLevelOnly(input, separator) {
1717
let stack = []
1818
let parts = []
1919
let lastPos = 0
20+
let isEscaped = false
2021

2122
for (let idx = 0; idx < input.length; idx++) {
2223
let char = input[idx]
2324

24-
if (stack.length === 0 && char === separator[0]) {
25+
if (stack.length === 0 && char === separator[0] && !isEscaped) {
2526
if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) {
2627
parts.push(input.slice(lastPos, idx))
2728
lastPos = idx + separator.length
2829
}
2930
}
3031

32+
if (isEscaped) {
33+
isEscaped = false
34+
} else if (char === '\\') {
35+
isEscaped = true
36+
}
37+
3138
if (char === '(' || char === '[' || char === '{') {
3239
stack.push(char)
3340
} else if (

tests/arbitrary-variants.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,53 @@ crosscheck(({ stable, oxide }) => {
11161116
})
11171117
})
11181118

1119+
it('it should discard arbitrary variants with multiple selectors', () => {
1120+
let config = {
1121+
content: [
1122+
{
1123+
raw: html`
1124+
<div class="p-1"></div>
1125+
<div class="[div]:p-1"></div>
1126+
<div class="[div_&]:p-1"></div>
1127+
<div class="[div,span]:p-1"></div>
1128+
<div class="[div_&,span]:p-1"></div>
1129+
<div class="[div,span_&]:p-1"></div>
1130+
<div class="[div_&,span_&]:p-1"></div>
1131+
<div class="hover:[div]:p-1"></div>
1132+
<div class="hover:[div_&]:p-1"></div>
1133+
<div class="hover:[div,span]:p-1"></div>
1134+
<div class="hover:[div_&,span]:p-1"></div>
1135+
<div class="hover:[div,span_&]:p-1"></div>
1136+
<div class="hover:[div_&,span_&]:p-1"></div>
1137+
<div class="hover:[:is(span,div)_&]:p-1"></div>
1138+
`,
1139+
},
1140+
{
1141+
// escaped commas are a-ok
1142+
// This is separate because prettier complains about `\,` in the template string
1143+
raw: '<div class="hover:[.span\\,div_&]:p-1"></div>',
1144+
},
1145+
],
1146+
corePlugins: { preflight: false },
1147+
}
1148+
1149+
let input = css`
1150+
@tailwind utilities;
1151+
`
1152+
1153+
return run(input, config).then((result) => {
1154+
expect(result.css).toMatchFormattedCss(css`
1155+
.p-1,
1156+
.span\,div .hover\:\[\.span\\\,div_\&\]\:p-1:hover,
1157+
:is(span, div) .hover\:\[\:is\(span\,div\)_\&\]\:p-1:hover,
1158+
div .\[div_\&\]\:p-1,
1159+
div .hover\:\[div_\&\]\:p-1:hover {
1160+
padding: 0.25rem;
1161+
}
1162+
`)
1163+
})
1164+
})
1165+
11191166
it('should sort multiple variant fns with normal variants between them', () => {
11201167
/** @type {string[]} */
11211168
let lines = []

0 commit comments

Comments
 (0)