Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Escape group names in selectors #10276

Merged
merged 3 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
}
`)
})
})