Skip to content

Commit 6ad3945

Browse files
Escape group names in selectors (#10276)
* Handle escaped selector characters in parseVariantFormatString * Escape group names in selectors Otherwise special characters would break O_O * Update changelog
1 parent 7b3de61 commit 6ad3945

File tree

4 files changed

+70
-23
lines changed

4 files changed

+70
-23
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- Don't prefix classes in arbitrary variants ([#10214](https://github.com/tailwindlabs/tailwindcss/pull/10214))
3535
- Fix perf regression when checking for changed content ([#10234](https://github.com/tailwindlabs/tailwindcss/pull/10234))
3636
- Fix missing `blocklist` member in the `Config` type ([#10239](https://github.com/tailwindlabs/tailwindcss/pull/10239))
37+
- Escape group names in selectors ([#10276](https://github.com/tailwindlabs/tailwindcss/pull/10276))
3738

3839
### Changed
3940

src/corePlugins.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,13 @@ export let variantPlugins = {
150150

151151
let variants = {
152152
group: (_, { modifier }) =>
153-
modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'],
153+
modifier
154+
? [`:merge(.group\\/${escapeClassName(modifier)})`, ' &']
155+
: [`:merge(.group)`, ' &'],
154156
peer: (_, { modifier }) =>
155-
modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
157+
modifier
158+
? [`:merge(.peer\\/${escapeClassName(modifier)})`, ' ~ &']
159+
: [`:merge(.peer)`, ' ~ &'],
156160
}
157161

158162
for (let [name, fn] of Object.entries(variants)) {

src/lib/setupContextUtils.js

+39-21
Original file line numberDiff line numberDiff line change
@@ -54,32 +54,50 @@ function normalizeOptionTypes({ type = 'any', ...options }) {
5454
}
5555

5656
function parseVariantFormatString(input) {
57-
if (input.includes('{')) {
58-
if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
59-
60-
return input
61-
.split(/{(.*)}/gim)
62-
.flatMap((line) => parseVariantFormatString(line))
63-
.filter(Boolean)
64-
}
65-
66-
return [input.trim()]
67-
}
68-
69-
function isBalanced(input) {
70-
let count = 0
71-
72-
for (let char of input) {
73-
if (char === '{') {
74-
count++
57+
/** @type {string[]} */
58+
let parts = []
59+
60+
// When parsing whitespace around special characters are insignificant
61+
// However, _inside_ of a variant they could be
62+
// Because the selector could look like this
63+
// @media { &[data-name="foo bar"] }
64+
// This is why we do not skip whitespace
65+
66+
let current = ''
67+
let depth = 0
68+
69+
for (let idx = 0; idx < input.length; idx++) {
70+
let char = input[idx]
71+
72+
if (char === '\\') {
73+
// Escaped characters are not special
74+
current += '\\' + input[++idx]
75+
} else if (char === '{') {
76+
// Nested rule: start
77+
++depth
78+
parts.push(current.trim())
79+
current = ''
7580
} else if (char === '}') {
76-
if (--count < 0) {
77-
return false // unbalanced
81+
// Nested rule: end
82+
if (--depth < 0) {
83+
throw new Error(`Your { and } are unbalanced.`)
7884
}
85+
86+
parts.push(current.trim())
87+
current = ''
88+
} else {
89+
// Normal character
90+
current += char
7991
}
8092
}
8193

82-
return count === 0
94+
if (current.length > 0) {
95+
parts.push(current.trim())
96+
}
97+
98+
parts = parts.filter((part) => part !== '')
99+
100+
return parts
83101
}
84102

85103
function insertInto(list, value, { before = [] } = {}) {

tests/basic-usage.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,27 @@ it('Ring color utilities are generated when using respectDefaultRingColorOpacity
689689
`)
690690
})
691691
})
692+
693+
it('should not crash when group names contain special characters', () => {
694+
let config = {
695+
future: { respectDefaultRingColorOpacity: true },
696+
content: [
697+
{
698+
raw: '<div class="group/${id}"><div class="group-hover/${id}:visible"></div></div>',
699+
},
700+
],
701+
corePlugins: { preflight: false },
702+
}
703+
704+
let input = css`
705+
@tailwind utilities;
706+
`
707+
708+
return run(input, config).then((result) => {
709+
expect(result.css).toMatchFormattedCss(css`
710+
.group\/\$\{id\}:hover .group-hover\/\$\{id\}\:visible {
711+
visibility: visible;
712+
}
713+
`)
714+
})
715+
})

0 commit comments

Comments
 (0)