Skip to content

Commit 4c9df22

Browse files
Don't escape underscores for the first parameter of var() (#14776)
This PR updates our arbitrary value decoder to: - No longer require an escaping for underscores in the first parameter of `var()`. Example: ``` ml-[var(--spacing-1_5,_1rem)] ``` - Ensures that properties before an eventual `url()` are properly unescaped. Example: ``` bg-[no-repeat_url(./image.jpg)] ``` I will ensure that this properly works for the migrate use case in a follow-up PR in the stack. ## Test Plan Added unit tests as well as tests for the variant decoder. Additionally this PR also adds a higher-level test using the public Tailwind APIs to ensure this is properly propagated. --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 35cd2ff commit 4c9df22

File tree

6 files changed

+139
-29
lines changed

6 files changed

+139
-29
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Don't convert underscores in the first argument to `var()` to spaces ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776))
13+
1014
### Fixed
1115

1216
- Ensure individual logical property utilities are sorted later than left/right pair utilities ([#14777](https://github.com/tailwindlabs/tailwindcss/pull/14777))
1317
- Don't migrate important modifiers inside conditional statements in Vue and Alpine (e.g. `<div v-if="!border" />`) ([#14774](https://github.com/tailwindlabs/tailwindcss/pull/14774))
1418
- Ensure third-party plugins with `exports` in their `package.json` are resolved correctly ([#14775](https://github.com/tailwindlabs/tailwindcss/pull/14775))
19+
- Ensure underscores in the `url()` function are never unescaped ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776))
1520
- _Upgrade (experimental)_: Ensure `@import` statements for relative CSS files are actually migrated to use relative path syntax ([#14769](https://github.com/tailwindlabs/tailwindcss/pull/14769))
1621

1722
## [4.0.0-alpha.29] - 2024-10-23

packages/tailwindcss/src/candidate.test.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,24 +1059,50 @@ it('should not replace `_` inside of `url()`', () => {
10591059
let utilities = new Utilities()
10601060
utilities.functional('bg', () => [])
10611061

1062-
expect(run('bg-[url(https://example.com/some_page)]', { utilities })).toMatchInlineSnapshot(`
1063-
[
1064-
{
1065-
"important": false,
1066-
"kind": "functional",
1067-
"modifier": null,
1068-
"negative": false,
1069-
"raw": "bg-[url(https://example.com/some_page)]",
1070-
"root": "bg",
1071-
"value": {
1072-
"dataType": null,
1073-
"kind": "arbitrary",
1074-
"value": "url(https://example.com/some_page)",
1062+
expect(run('bg-[no-repeat_url(https://example.com/some_page)]', { utilities }))
1063+
.toMatchInlineSnapshot(`
1064+
[
1065+
{
1066+
"important": false,
1067+
"kind": "functional",
1068+
"modifier": null,
1069+
"negative": false,
1070+
"raw": "bg-[no-repeat_url(https://example.com/some_page)]",
1071+
"root": "bg",
1072+
"value": {
1073+
"dataType": null,
1074+
"kind": "arbitrary",
1075+
"value": "no-repeat url(https://example.com/some_page)",
1076+
},
1077+
"variants": [],
10751078
},
1076-
"variants": [],
1077-
},
1078-
]
1079-
`)
1079+
]
1080+
`)
1081+
})
1082+
1083+
it('should not replace `_` in the first argument to `var()`', () => {
1084+
let utilities = new Utilities()
1085+
utilities.functional('ml', () => [])
1086+
1087+
expect(run('ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]', { utilities }))
1088+
.toMatchInlineSnapshot(`
1089+
[
1090+
{
1091+
"important": false,
1092+
"kind": "functional",
1093+
"modifier": null,
1094+
"negative": false,
1095+
"raw": "ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]",
1096+
"root": "ml",
1097+
"value": {
1098+
"dataType": null,
1099+
"kind": "arbitrary",
1100+
"value": "var(--spacing-1_5, var(--spacing-2_5, 1rem))",
1101+
},
1102+
"variants": [],
1103+
},
1104+
]
1105+
`)
10801106
})
10811107

10821108
it('should parse arbitrary properties', () => {

packages/tailwindcss/src/index.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,34 @@ describe('compiling CSS', () => {
115115
),
116116
).toMatchSnapshot()
117117
})
118+
119+
test('unescapes underscores to spaces inside arbitrary values except for `url()` and first argument of `var()`', async () => {
120+
expect(
121+
await compileCss(
122+
css`
123+
@theme {
124+
--spacing-1_5: 1.5rem;
125+
--spacing-2_5: 2.5rem;
126+
}
127+
@tailwind utilities;
128+
`,
129+
['bg-[no-repeat_url(./my_file.jpg)', 'ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]'],
130+
),
131+
).toMatchInlineSnapshot(`
132+
":root {
133+
--spacing-1_5: 1.5rem;
134+
--spacing-2_5: 2.5rem;
135+
}
136+
137+
.ml-\\[var\\(--spacing-1_5\\,_var\\(--spacing-2_5\\,_1rem\\)\\)\\] {
138+
margin-left: var(--spacing-1_5, var(--spacing-2_5, 1rem));
139+
}
140+
141+
.bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\) {
142+
background-color: no-repeat url("./")my file. jpg;
143+
}"
144+
`)
145+
})
118146
})
119147

120148
describe('arbitrary properties', () => {

packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ describe('decoding arbitrary values', () => {
1616

1717
it('should not replace underscores in url()', () => {
1818
expect(decodeArbitraryValue('url(./my_file.jpg)')).toBe('url(./my_file.jpg)')
19+
expect(decodeArbitraryValue('no-repeat_url(./my_file.jpg)')).toBe(
20+
'no-repeat url(./my_file.jpg)',
21+
)
22+
})
23+
24+
it('should not replace underscores in the first argument of var()', () => {
25+
expect(decodeArbitraryValue('var(--spacing-1_5)')).toBe('var(--spacing-1_5)')
26+
expect(decodeArbitraryValue('var(--spacing-1_5,_1rem)')).toBe('var(--spacing-1_5, 1rem)')
27+
expect(decodeArbitraryValue('var(--spacing-1_5,_var(--spacing-2_5,_1rem))')).toBe(
28+
'var(--spacing-1_5, var(--spacing-2_5, 1rem))',
29+
)
1930
})
2031

2132
it('should leave var(…) as is', () => {
@@ -55,8 +66,8 @@ describe('adds spaces around math operators', () => {
5566
['calc(24px+(-1rem))', 'calc(24px + (-1rem))'],
5667
['calc(24px_+_(-1rem))', 'calc(24px + (-1rem))'],
5768
[
58-
'calc(var(--10-10px,calc(-20px-(-30px--40px)-50px)',
59-
'calc(var(--10-10px,calc(-20px - (-30px - -40px) - 50px)',
69+
'calc(var(--10-10px,calc(-20px-(-30px--40px)-50px)))',
70+
'calc(var(--10-10px,calc(-20px - (-30px - -40px) - 50px)))',
6071
],
6172
['calc(theme(spacing.1-bar))', 'calc(theme(spacing.1-bar))'],
6273
['theme(spacing.1-bar)', 'theme(spacing.1-bar)'],

packages/tailwindcss/src/utils/decode-arbitrary-value.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import * as ValueParser from '../value-parser'
12
import { addWhitespaceAroundMathOperators } from './math-operators'
23

34
export function decodeArbitraryValue(input: string): string {
4-
// We do not want to normalize anything inside of a url() because if we
5-
// replace `_` with ` `, then it will very likely break the url.
6-
if (input.startsWith('url(')) {
7-
return input
5+
// There are definitely no functions in the input, so bail early
6+
if (input.indexOf('(') === -1) {
7+
return convertUnderscoresToWhitespace(input)
88
}
99

10-
input = convertUnderscoresToWhitespace(input)
10+
let ast = ValueParser.parse(input)
11+
recursivelyDecodeArbitraryValues(ast)
12+
input = ValueParser.toCss(ast)
13+
1114
input = addWhitespaceAroundMathOperators(input)
1215

1316
return input
@@ -41,3 +44,45 @@ function convertUnderscoresToWhitespace(input: string) {
4144

4245
return output
4346
}
47+
48+
function recursivelyDecodeArbitraryValues(ast: ValueParser.ValueAstNode[]) {
49+
for (let node of ast) {
50+
switch (node.kind) {
51+
case 'function': {
52+
if (node.value === 'url' || node.value.endsWith('_url')) {
53+
// Don't decode underscores in url() but do decode the function name
54+
node.value = convertUnderscoresToWhitespace(node.value)
55+
break
56+
}
57+
58+
if (node.value === 'var' || node.value.endsWith('_var')) {
59+
// Don't decode underscores in the first argument of var() but do
60+
// decode the function name
61+
node.value = convertUnderscoresToWhitespace(node.value)
62+
for (let i = 0; i < node.nodes.length; i++) {
63+
if (i == 0 && node.nodes[i].kind === 'word') {
64+
continue
65+
}
66+
recursivelyDecodeArbitraryValues([node.nodes[i]])
67+
}
68+
break
69+
}
70+
71+
node.value = convertUnderscoresToWhitespace(node.value)
72+
recursivelyDecodeArbitraryValues(node.nodes)
73+
break
74+
}
75+
case 'separator':
76+
case 'word': {
77+
node.value = convertUnderscoresToWhitespace(node.value)
78+
break
79+
}
80+
default:
81+
never()
82+
}
83+
}
84+
}
85+
86+
function never(): never {
87+
throw new Error('This should never happen')
88+
}

packages/tailwindcss/src/utils/math-operators.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ export function hasMathFn(input: string) {
2828
}
2929

3030
export function addWhitespaceAroundMathOperators(input: string) {
31-
// There's definitely no functions in the input, so bail early
32-
if (input.indexOf('(') === -1) {
33-
return input
34-
}
35-
3631
// Bail early if there are no math functions in the input
3732
if (!MATH_FUNCTIONS.some((fn) => input.includes(fn))) {
3833
return input

0 commit comments

Comments
 (0)