diff --git a/CHANGELOG.md b/CHANGELOG.md index e76da292e3f4..c4d96da84e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `caption-side` utilities ([#10470](https://github.com/tailwindlabs/tailwindcss/pull/10470)) - Add `justify-normal` and `justify-stretch` utilities ([#10560](https://github.com/tailwindlabs/tailwindcss/pull/10560)) +### Fixed + +- Disallow multiple selectors in arbitrary variants ([#10655](https://github.com/tailwindlabs/tailwindcss/pull/10655)) + ### Changed - [Oxide] Disable color opacity plugins by default in the `oxide` engine ([#10618](https://github.com/tailwindlabs/tailwindcss/pull/10618)) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index f7096e556602..0306893ad3ef 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -205,17 +205,26 @@ function applyVariant(variant, matches, context) { // Register arbitrary variants if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { + let sort = context.offsets.recordVariant(variant) + let selector = normalize(variant.slice(1, -1)) + let selectors = splitAtTopLevelOnly(selector, ',') - if (!isValidVariantFormatString(selector)) { + // We do not support multiple selectors for arbitrary variants + if (selectors.length > 1) { return [] } - let fn = parseVariant(selector) + if (!selectors.every(isValidVariantFormatString)) { + return [] + } - let sort = context.offsets.recordVariant(variant) + let records = selectors.map((sel, idx) => [ + context.offsets.applyParallelOffset(sort, idx), + parseVariant(sel.trim()), + ]) - context.variantMap.set(variant, [[sort, fn]]) + context.variantMap.set(variant, records) } if (context.variantMap.has(variant)) { diff --git a/src/util/splitAtTopLevelOnly.js b/src/util/splitAtTopLevelOnly.js index 63fd767403ce..a749c7932ed9 100644 --- a/src/util/splitAtTopLevelOnly.js +++ b/src/util/splitAtTopLevelOnly.js @@ -17,17 +17,24 @@ export function splitAtTopLevelOnly(input, separator) { let stack = [] let parts = [] let lastPos = 0 + let isEscaped = false for (let idx = 0; idx < input.length; idx++) { let char = input[idx] - if (stack.length === 0 && char === separator[0]) { + if (stack.length === 0 && char === separator[0] && !isEscaped) { if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) { parts.push(input.slice(lastPos, idx)) lastPos = idx + separator.length } } + if (isEscaped) { + isEscaped = false + } else if (char === '\\') { + isEscaped = true + } + if (char === '(' || char === '[' || char === '{') { stack.push(char) } else if ( diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 46da5cdf2027..b210bdc77ecb 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -1116,6 +1116,53 @@ crosscheck(({ stable, oxide }) => { }) }) + it('it should discard arbitrary variants with multiple selectors', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `, + }, + { + // escaped commas are a-ok + // This is separate because prettier complains about `\,` in the template string + raw: '
', + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .p-1, + .span\,div .hover\:\[\.span\\\,div_\&\]\:p-1:hover, + :is(span, div) .hover\:\[\:is\(span\,div\)_\&\]\:p-1:hover, + div .\[div_\&\]\:p-1, + div .hover\:\[div_\&\]\:p-1:hover { + padding: 0.25rem; + } + `) + }) + }) + it('should sort multiple variant fns with normal variants between them', () => { /** @type {string[]} */ let lines = []