From 6ad3945fe7d572227289c18b0c53add63b5ed415 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 9 Jan 2023 11:14:16 -0500 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + src/corePlugins.js | 8 +++-- src/lib/setupContextUtils.js | 60 +++++++++++++++++++++++------------- tests/basic-usage.test.js | 24 +++++++++++++++ 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fce71eae9f8..fd1a71587b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/corePlugins.js b/src/corePlugins.js index f92767e24cbe..47521e5eed10 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -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)) { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 2bb9350d77ce..445fceb23a57 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -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 = [] } = {}) { diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js index d310fc0e3a28..a644cbd3571c 100644 --- a/tests/basic-usage.test.js +++ b/tests/basic-usage.test.js @@ -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: '
', + }, + ], + 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; + } + `) + }) +})