diff --git a/CHANGELOG.md b/CHANGELOG.md index f046da58c0f9..49e087548bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Ensure CSS variables in arbitrary values are properly decoded ([#16206](https://github.com/tailwindlabs/tailwindcss/pull/16206)) - Ensure that the `containers` JS theme key is added to the `--container-*` namespace ([#16169](https://github.com/tailwindlabs/tailwindcss/pull/16169)) - Fix missing `@keyframes` definition ([#16237](https://github.com/tailwindlabs/tailwindcss/pull/16237)) - Vite: Skip parsing stylesheets with the `?commonjs-proxy` flag ([#16238](https://github.com/tailwindlabs/tailwindcss/pull/16238)) diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts index 74f5974fcb8d..5ec45fa81c06 100644 --- a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts @@ -110,6 +110,10 @@ const candidates = [ ['!bg-[#0088cc]', 'bg-[#0088cc]!'], ['bg-[var(--spacing)-1px]', 'bg-[var(--spacing)-1px]'], ['bg-[var(--spacing)_-_1px]', 'bg-[var(--spacing)-1px]'], + ['bg-[var(--_spacing)]', 'bg-(--_spacing)'], + ['bg-(--_spacing)', 'bg-(--_spacing)'], + ['bg-[var(--\_spacing)]', 'bg-(--_spacing)'], + ['bg-(--\_spacing)', 'bg-(--_spacing)'], ['bg-[-1px_-1px]', 'bg-[-1px_-1px]'], ['p-[round(to-zero,1px)]', 'p-[round(to-zero,1px)]'], ['w-1/2', 'w-1/2'], diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.ts b/packages/@tailwindcss-upgrade/src/template/candidates.ts index 6a0ab65bd78f..e292168a5d5a 100644 --- a/packages/@tailwindcss-upgrade/src/template/candidates.ts +++ b/packages/@tailwindcss-upgrade/src/template/candidates.ts @@ -241,13 +241,8 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) { node.value === 'theme' || node.value.endsWith('_theme') ) { - // Don't decode underscores in the first argument of var() and theme() - // but do decode the function name node.value = escapeUnderscore(node.value) for (let i = 0; i < node.nodes.length; i++) { - if (i == 0 && node.nodes[i].kind === 'word') { - continue - } recursivelyEscapeUnderscores([node.nodes[i]]) } break diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 20f89a940d3c..2892f0053c9e 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -935,6 +935,154 @@ it('should parse a utility with an implicit variable as the modifier', () => { `) }) +it('should properly decode escaped underscores but not convert underscores to spaces for CSS variables in arbitrary positions', () => { + let utilities = new Utilities() + utilities.functional('flex', () => []) + let variants = new Variants() + variants.functional('supports', () => {}) + + expect(run('flex-(--\\_foo)', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-(--\\_foo)", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--_foo)", + }, + "variants": [], + }, + ] + `) + expect(run('flex-(--_foo)', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-(--_foo)", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--_foo)", + }, + "variants": [], + }, + ] + `) + expect(run('flex-[var(--\\_foo)]', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-[var(--\\_foo)]", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--_foo)", + }, + "variants": [], + }, + ] + `) + expect(run('flex-[var(--_foo)]', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-[var(--_foo)]", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--_foo)", + }, + "variants": [], + }, + ] + `) + + expect(run('flex-[calc(var(--\\_foo)*0.2)]', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-[calc(var(--\\_foo)*0.2)]", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "calc(var(--_foo) * 0.2)", + }, + "variants": [], + }, + ] + `) + expect(run('flex-[calc(var(--_foo)*0.2)]', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-[calc(var(--_foo)*0.2)]", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "calc(var(--_foo) * 0.2)", + }, + "variants": [], + }, + ] + `) + + // Due to limitations in the CSS value parser, the `var(…)` inside the `calc(…)` is not correctly + // scanned here. + expect(run('flex-[calc(0.2*var(--\\_foo)]', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-[calc(0.2*var(--\\_foo)]", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "calc(0.2 * var(--_foo))", + }, + "variants": [], + }, + ] + `) + expect(run('flex-[calc(0.2*var(--_foo)]', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "flex-[calc(0.2*var(--_foo)]", + "root": "flex", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "calc(0.2 * var(-- foo))", + }, + "variants": [], + }, + ] + `) +}) + it('should parse a utility with an implicit variable as the modifier using the shorthand', () => { let utilities = new Utilities() utilities.functional('bg', () => []) diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts index 11ec871c5b90..145103188131 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts @@ -20,7 +20,7 @@ export function decodeArbitraryValue(input: string): string { * Convert `_` to ` `, except for escaped underscores `\_` they should be * converted to `_` instead. */ -function convertUnderscoresToWhitespace(input: string) { +function convertUnderscoresToWhitespace(input: string, skipUnderscoreToSpace = false) { let output = '' for (let i = 0; i < input.length; i++) { let char = input[i] @@ -32,7 +32,7 @@ function convertUnderscoresToWhitespace(input: string) { } // Unescaped underscore - else if (char === '_') { + else if (char === '_' && !skipUnderscoreToSpace) { output += ' ' } @@ -61,11 +61,11 @@ function recursivelyDecodeArbitraryValues(ast: ValueParser.ValueAstNode[]) { node.value === 'theme' || node.value.endsWith('_theme') ) { - // Don't decode underscores in the first argument of var() but do - // decode the function name node.value = convertUnderscoresToWhitespace(node.value) for (let i = 0; i < node.nodes.length; i++) { + // Don't decode underscores to spaces in the first argument of var() if (i == 0 && node.nodes[i].kind === 'word') { + node.nodes[i].value = convertUnderscoresToWhitespace(node.nodes[i].value, true) continue } recursivelyDecodeArbitraryValues([node.nodes[i]]) diff --git a/playgrounds/vite/src/app.tsx b/playgrounds/vite/src/app.tsx index 4abc17cb52e1..8ec50298951f 100644 --- a/playgrounds/vite/src/app.tsx +++ b/playgrounds/vite/src/app.tsx @@ -2,7 +2,6 @@ export function App() { return (

Hello World

-
) }