Skip to content

Commit

Permalink
Escape group names in selectors (#10276)
Browse files Browse the repository at this point in the history
* Handle escaped selector characters in parseVariantFormatString

* Escape group names in selectors

Otherwise special characters would break O_O

* Update changelog
  • Loading branch information
thecrypticace committed Jan 9, 2023
1 parent 7b3de61 commit 6ad3945
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Don't prefix classes in arbitrary variants ([#10214](https://github.com/tailwindlabs/tailwindcss/pull/10214))
- Fix perf regression when checking for changed content ([#10234](https://github.com/tailwindlabs/tailwindcss/pull/10234))
- Fix missing `blocklist` member in the `Config` type ([#10239](https://github.com/tailwindlabs/tailwindcss/pull/10239))
- Escape group names in selectors ([#10276](https://github.com/tailwindlabs/tailwindcss/pull/10276))

### Changed

Expand Down
8 changes: 6 additions & 2 deletions src/corePlugins.js
Expand Up @@ -150,9 +150,13 @@ export let variantPlugins = {

let variants = {
group: (_, { modifier }) =>
modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'],
modifier
? [`:merge(.group\\/${escapeClassName(modifier)})`, ' &']
: [`:merge(.group)`, ' &'],
peer: (_, { modifier }) =>
modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
modifier
? [`:merge(.peer\\/${escapeClassName(modifier)})`, ' ~ &']
: [`:merge(.peer)`, ' ~ &'],
}

for (let [name, fn] of Object.entries(variants)) {
Expand Down
60 changes: 39 additions & 21 deletions src/lib/setupContextUtils.js
Expand Up @@ -54,32 +54,50 @@ function normalizeOptionTypes({ type = 'any', ...options }) {
}

function parseVariantFormatString(input) {
if (input.includes('{')) {
if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)

return input
.split(/{(.*)}/gim)
.flatMap((line) => parseVariantFormatString(line))
.filter(Boolean)
}

return [input.trim()]
}

function isBalanced(input) {
let count = 0

for (let char of input) {
if (char === '{') {
count++
/** @type {string[]} */
let parts = []

// When parsing whitespace around special characters are insignificant
// However, _inside_ of a variant they could be
// Because the selector could look like this
// @media { &[data-name="foo bar"] }
// This is why we do not skip whitespace

let current = ''
let depth = 0

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

if (char === '\\') {
// Escaped characters are not special
current += '\\' + input[++idx]
} else if (char === '{') {
// Nested rule: start
++depth
parts.push(current.trim())
current = ''
} else if (char === '}') {
if (--count < 0) {
return false // unbalanced
// Nested rule: end
if (--depth < 0) {
throw new Error(`Your { and } are unbalanced.`)
}

parts.push(current.trim())
current = ''
} else {
// Normal character
current += char
}
}

return count === 0
if (current.length > 0) {
parts.push(current.trim())
}

parts = parts.filter((part) => part !== '')

return parts
}

function insertInto(list, value, { before = [] } = {}) {
Expand Down
24 changes: 24 additions & 0 deletions tests/basic-usage.test.js
Expand Up @@ -689,3 +689,27 @@ it('Ring color utilities are generated when using respectDefaultRingColorOpacity
`)
})
})

it('should not crash when group names contain special characters', () => {
let config = {
future: { respectDefaultRingColorOpacity: true },
content: [
{
raw: '<div class="group/${id}"><div class="group-hover/${id}:visible"></div></div>',
},
],
corePlugins: { preflight: false },
}

let input = css`
@tailwind utilities;
`

return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.group\/\$\{id\}:hover .group-hover\/\$\{id\}\:visible {
visibility: visible;
}
`)
})
})

0 comments on commit 6ad3945

Please sign in to comment.