From b7c4d25ae4cbf99ba3bf60633fd3a9004f688fd1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Oct 2024 19:51:43 +0200 Subject: [PATCH] Ensure existing spaces in attribute selectors are valid (#14703) This PR fixes an issue where spaces in a selector generated invalid CSS. Lightning CSS will throw those incorrect lines of CSS out, but if you are in an environment where Lightning CSS doesn't run then invalid CSS is generated. Given this input: ```html data-[foo_=_"true"]:flex ``` This will be generated: ```css .data-\[foo_\=_\"true\"\]\:flex[data-foo=""true] { display: flex; } ``` With this PR in place, the generated CSS will now be: ```css .data-\[foo_\=_\"true\"\]\:flex[data-foo="true"] { display: flex; } ``` --------- Co-authored-by: Adam Wathan --- CHANGELOG.md | 4 ++ packages/tailwindcss/src/variants.test.ts | 28 ++++++++++++- packages/tailwindcss/src/variants.ts | 48 ++++++++++++----------- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de378c91a..63b4dc2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700)) +### Fixed + +- Allow spaces spaces around operators in attribute selector variants ([#14703](https://github.com/tailwindlabs/tailwindcss/pull/14703)) + ### Changed - _Upgrade (experimental)_: Don't create `@source` rules for `content` paths that are already covered by automatic source detection ([#14714](https://github.com/tailwindlabs/tailwindcss/pull/14714)) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index f43945f89..97e358ef0 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1985,6 +1985,7 @@ test('aria', async () => { 'aria-checked:flex', 'aria-[invalid=spelling]:flex', 'aria-[valuenow=1]:flex', + 'aria-[valuenow_=_"1"]:flex', 'group-aria-[modal]:flex', 'group-aria-checked:flex', @@ -2059,6 +2060,10 @@ test('aria', async () => { .aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] { display: flex; + } + + .aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"] { + display: flex; }" `) expect(await run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('') @@ -2069,6 +2074,9 @@ test('data', async () => { await run([ 'data-disabled:flex', 'data-[potato=salad]:flex', + 'data-[potato_=_"salad"]:flex', + 'data-[potato_^=_"salad"]:flex', + 'data-[potato="^_="]:flex', 'data-[foo=1]:flex', 'data-[foo=bar_baz]:flex', "data-[foo$='bar'_i]:flex", @@ -2155,6 +2163,18 @@ test('data', async () => { display: flex; } + .data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"] { + display: flex; + } + + .data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"] { + display: flex; + } + + .data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="] { + display: flex; + } + .data-\\[foo\\=1\\]\\:flex[data-foo="1"] { display: flex; } @@ -2171,7 +2191,13 @@ test('data', async () => { display: flex; }" `) - expect(await run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('') + expect( + await run([ + 'data-[foo_^_=_"bar"]:flex', // Can't have spaces between `^` and `=` + 'data-disabled/foo:flex', + 'data-[potato=salad]/foo:flex', + ]), + ).toEqual('') }) test('portrait', async () => { diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 693222860..4999439e6 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -898,32 +898,34 @@ export function createVariants(theme: Theme): Variants { return variants } -function quoteAttributeValue(value: string) { - if (value.includes('=')) { - value = value.replace(/(=.*)/g, (_fullMatch, match) => { - // If the value is already quoted, skip. - if (match[1] === "'" || match[1] === '"') { - return match - } +function quoteAttributeValue(input: string) { + if (input.includes('=')) { + let [attribute, ...after] = segment(input, '=') + let value = after.join('=').trim() - // Handle regex flags on unescaped values - if (match.length > 2) { - let trailingCharacter = match[match.length - 1] - if ( - match[match.length - 2] === ' ' && - (trailingCharacter === 'i' || - trailingCharacter === 'I' || - trailingCharacter === 's' || - trailingCharacter === 'S') - ) { - return `="${match.slice(1, -2)}" ${match[match.length - 1]}` - } - } + // If the value is already quoted, skip. + if (value[0] === "'" || value[0] === '"') { + return input + } - return `="${match.slice(1)}"` - }) + // Handle case sensitivity flags on unescaped values + if (value.length > 1) { + let trailingCharacter = value[value.length - 1] + if ( + value[value.length - 2] === ' ' && + (trailingCharacter === 'i' || + trailingCharacter === 'I' || + trailingCharacter === 's' || + trailingCharacter === 'S') + ) { + return `${attribute}="${value.slice(0, -2)}" ${trailingCharacter}` + } + } + + return `${attribute}="${value}"` } - return value + + return input } export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {