diff --git a/CHANGELOG.md b/CHANGELOG.md index 0018fe9eb3f5..a2039b71145c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Consider earlier variants before sorting functions ([#10288](https://github.com/tailwindlabs/tailwindcss/pull/10288)) - Allow variants with slashes ([#10336](https://github.com/tailwindlabs/tailwindcss/pull/10336)) - Ensure generated CSS is always sorted in the same order for a given set of templates ([#10382](https://github.com/tailwindlabs/tailwindcss/pull/10382)) +- Handle variants when the same class appears multiple times in a selector ([#10397](https://github.com/tailwindlabs/tailwindcss/pull/10397)) +- Handle group/peer variants with quoted strings ([#10400](https://github.com/tailwindlabs/tailwindcss/pull/10400)) ### Changed diff --git a/src/corePlugins.js b/src/corePlugins.js index 0a6c49db27c1..436f008aa78a 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -168,7 +168,30 @@ export let variantPlugins = { if (!result.includes('&')) result = '&' + result let [a, b] = fn('', extra) - return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b) + + let start = null + let end = null + let quotes = 0 + + for (let i = 0; i < result.length; ++i) { + let c = result[i] + if (c === '&') { + start = i + } else if (c === "'" || c === '"') { + quotes += 1 + } else if (start !== null && c === ' ' && !quotes) { + end = i + } + } + + if (start !== null && end === null) { + end = result.length + } + + // Basically this but can handle quotes: + // result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b) + + return result.slice(0, start) + a + result.slice(start + 1, end) + b + result.slice(end) }, { values: Object.fromEntries(pseudoVariants) } ) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index c08cf1171236..0533a9e86c51 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -152,10 +152,18 @@ function applyVariant(variant, matches, context) { // Retrieve "modifier" { - let match = /(.*)\/(.*)$/g.exec(variant) - if (match && !context.variantMap.has(variant)) { - variant = match[1] - args.modifier = match[2] + let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/') + + // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500` + // In this case 1/10 is a value but /20 is a modifier + if (modifiers.length > 1) { + baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/') + modifiers = modifiers.slice(-1) + } + + if (modifiers.length && !context.variantMap.has(variant)) { + variant = baseVariant + args.modifier = modifiers[0] if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) { return [] diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js index b8849fe83a58..11f8c30b7018 100644 --- a/tests/basic-usage.test.js +++ b/tests/basic-usage.test.js @@ -949,4 +949,66 @@ crosscheck(({ stable, oxide }) => { `) }) }) + + test('detects quoted arbitrary values containing a slash', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + oxide.expect(result.css).toMatchFormattedCss(css` + .group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden { + display: none; + } + `) + + stable.expect(result.css).toMatchFormattedCss(css` + .hidden { + display: none; + } + .group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden { + display: none; + } + `) + }) + + test('handled quoted arbitrary values containing escaped spaces', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + oxide.expect(result.css).toMatchFormattedCss(css` + .group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden { + display: none; + } + `) + + stable.expect(result.css).toMatchFormattedCss(css` + .hidden { + display: none; + } + .group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden { + display: none; + } + `) + }) }) diff --git a/tests/util/run.js b/tests/util/run.js index 9a5bfc309902..167b7fc78ce2 100644 --- a/tests/util/run.js +++ b/tests/util/run.js @@ -60,6 +60,18 @@ let nullProxy = new Proxy( } ) +/** + * @typedef {object} CrossCheck + * @property {typeof import('@jest/globals')} oxide + * @property {typeof import('@jest/globals')} stable + * @property {object} engine + * @property {boolean} engine.oxide + * @property {boolean} engine.stable + */ + +/** + * @param {(data: CrossCheck) => void} fn + */ export function crosscheck(fn) { let engines = env.ENGINE === 'oxide' ? [{ engine: 'Stable' }, { engine: 'Oxide' }] : [{ engine: 'Stable' }]