Ensure first argument to var(…) still unescapes \_ (#16206)

Resolves #16170

This PR fixes an issue where the previously opted-out escaping of the
first argument for the `var(…)` function was not unescaped at all. This
was introduced in https://github.com/tailwindlabs/tailwindcss/pull/14776
where the intention was to not require escaping of underscores in the
var function (e.g. `ml-[var(--spacing-1_5)]`). However, I do think it
still makes sense to unescape an eventually escaped underline for
consistency.

## Test plan

The example from #1670 now parses as expected:

<img width="904" alt="Screenshot 2025-02-03 at 13 51 35"
src="https://github.com/user-attachments/assets/cac0f06e-37da-4dcb-a554-9606d144a8d5"
/>

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Philipp Spiess 2025-02-04 15:13:18 +01:00 • committed by GitHub
parent e1a85ac260
commit 9fd6766308
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 157 additions and 10 deletions

View file

@ -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))

View file

@ -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'],

View file

@ -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

View file

@ -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', () => [])

View file

@ -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]])

View file

@ -2,7 +2,6 @@ export function App() {
return (
<div className="m-3 p-3 border">
<h1 className="text-blue-500">Hello World</h1>
<div className="-inset-x-full -inset-y-full -space-x-full -space-y-full -inset-full"></div>
</div>
)
}