From 49405fd4b796ef9c98c88c207ceb4eb57c558ad6 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 3 Feb 2025 13:41:19 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Ensure=20first=20argument=20to=20`var(?= =?UTF-8?q?=E2=80=A6)`=20still=20unescapes=20`\=5F`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/template/candidates.test.ts | 4 + .../src/template/candidates.ts | 5 - packages/tailwindcss/src/candidate.test.ts | 148 ++++++++++++++++++ .../src/utils/decode-arbitrary-value.ts | 8 +- 4 files changed, 156 insertions(+), 9 deletions(-) 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..02e8258df07d 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 underlines but not underlines 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]]) From aead9861af3673607a744fc35f1e08c17edcc59d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 3 Feb 2025 13:53:01 +0100 Subject: [PATCH 2/4] Add change log --- CHANGELOG.md | 4 +++- playgrounds/vite/src/app.tsx | 2 +- playgrounds/vite/src/index.css | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7501b1399f2a..b756b6cf9e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Ensure CSS variables in arbitrary values are properly decoded ([#16206](https://github.com/tailwindlabs/tailwindcss/pull/16206)) ## [4.0.3] - 2025-02-01 diff --git a/playgrounds/vite/src/app.tsx b/playgrounds/vite/src/app.tsx index 4abc17cb52e1..5b3397d2b10c 100644 --- a/playgrounds/vite/src/app.tsx +++ b/playgrounds/vite/src/app.tsx @@ -2,7 +2,7 @@ export function App() { return (

Hello World

-
+
) } diff --git a/playgrounds/vite/src/index.css b/playgrounds/vite/src/index.css index d4b5078586e2..6e3435008ee6 100644 --- a/playgrounds/vite/src/index.css +++ b/playgrounds/vite/src/index.css @@ -1 +1,2 @@ -@import 'tailwindcss'; +@reference 'tailwindcss/theme.css'; +@import 'tailwindcss/utilities.css'; From 736439aa53e547933b8a6d4273a21236fd8fc652 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 3 Feb 2025 14:09:50 +0100 Subject: [PATCH 3/4] Revert playground changes --- playgrounds/vite/src/app.tsx | 1 - playgrounds/vite/src/index.css | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/playgrounds/vite/src/app.tsx b/playgrounds/vite/src/app.tsx index 5b3397d2b10c..8ec50298951f 100644 --- a/playgrounds/vite/src/app.tsx +++ b/playgrounds/vite/src/app.tsx @@ -2,7 +2,6 @@ export function App() { return (

Hello World

-
) } diff --git a/playgrounds/vite/src/index.css b/playgrounds/vite/src/index.css index 6e3435008ee6..d4b5078586e2 100644 --- a/playgrounds/vite/src/index.css +++ b/playgrounds/vite/src/index.css @@ -1,2 +1 @@ -@reference 'tailwindcss/theme.css'; -@import 'tailwindcss/utilities.css'; +@import 'tailwindcss'; From 38af0a706880afe6029dab0ca19fe5bfc20ccd9c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 4 Feb 2025 14:19:34 +0100 Subject: [PATCH 4/4] Update packages/tailwindcss/src/candidate.test.ts Co-authored-by: Robin Malfait --- packages/tailwindcss/src/candidate.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 02e8258df07d..2892f0053c9e 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -935,7 +935,7 @@ it('should parse a utility with an implicit variable as the modifier', () => { `) }) -it('should properly decode escaped underlines but not underlines to spaces for CSS variables in arbitrary positions', () => { +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()