Skip to content

Commit 12b61ca

Browse files
Upgrade: Ensure underscores in url() and var() are not escaped
1 parent 75aa966 commit 12b61ca

File tree

4 files changed

+80
-5
lines changed

4 files changed

+80
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Ensure third-party plugins with `exports` in their `package.json` are resolved correctly ([#14775](https://github.com/tailwindlabs/tailwindcss/pull/14775))
1919
- Ensure underscores in the `url()` function are never unescaped ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776))
2020
- _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))
21+
- _Upgrade (experimental)_: Ensure that CSS variable access when migrating arbitrary candidates that reference theme values with dots in the key path do not require escaping (e.g. `m-[var(--spacing-1_5)]`) ([#14778](https://github.com/tailwindlabs/tailwindcss/pull/14778))
2122

2223
## [4.0.0-alpha.29] - 2024-10-23
2324

integrations/upgrade/index.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from 'vitest'
2-
import { css, html, js, json, test } from '../utils'
2+
import { candidate, css, html, js, json, test } from '../utils'
33

44
test(
55
`upgrades a v3 project to v4`,
@@ -9,6 +9,9 @@ test(
99
{
1010
"dependencies": {
1111
"@tailwindcss/upgrade": "workspace:^"
12+
},
13+
"devDependencies": {
14+
"@tailwindcss/cli": "workspace:^"
1215
}
1316
}
1417
`,
@@ -20,7 +23,9 @@ test(
2023
`,
2124
'src/index.html': html`
2225
<h1>🤠👋</h1>
23-
<div class="!flex sm:!block bg-gradient-to-t bg-[--my-red] max-w-screen-md"></div>
26+
<div
27+
class="!flex sm:!block bg-gradient-to-t bg-[--my-red] max-w-screen-md ml-[theme(spacing[1.5])]"
28+
></div>
2429
`,
2530
'src/input.css': css`
2631
@tailwind base;
@@ -42,7 +47,9 @@ test(
4247
"
4348
--- ./src/index.html ---
4449
<h1>🤠👋</h1>
45-
<div class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)] max-w-[var(--breakpoint-md)]"></div>
50+
<div
51+
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)] max-w-[var(--breakpoint-md)] ml-[var(--spacing-1_5)]"
52+
></div>
4653
4754
--- ./src/input.css ---
4855
@import 'tailwindcss';
@@ -92,6 +99,18 @@ test(
9299
expect(packageJson.dependencies).toMatchObject({
93100
tailwindcss: expect.stringContaining('4.0.0'),
94101
})
102+
103+
// Ensure the v4 project compiles correctly
104+
await exec('npx tailwindcss --input src/input.css --output dist/out.css')
105+
106+
await fs.expectFileToContain('dist/out.css', [
107+
candidate`flex!`,
108+
candidate`sm:block!`,
109+
candidate`bg-linear-to-t`,
110+
candidate`bg-[var(--my-red)]`,
111+
candidate`max-w-[var(--breakpoint-md)]`,
112+
candidate`ml-[var(--spacing-1\_5)`,
113+
])
95114
},
96115
)
97116

packages/@tailwindcss-upgrade/src/template/candidates.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ const candidates = [
118118
// Keep spaces in strings
119119
['content-["hello_world"]', 'content-["hello_world"]'],
120120
['content-[____"hello_world"___]', 'content-["hello_world"]'],
121+
122+
// Do not escape underscores for url() and CSS variable in var()
123+
['bg-[no-repeat_url(/image_13.png)]', 'bg-[no-repeat_url(/image_13.png)]'],
124+
[
125+
'bg-[var(--spacing-0_5,_var(--spacing-1_5,_3rem))]',
126+
'bg-[var(--spacing-0_5,_var(--spacing-1_5,_3rem))]',
127+
],
121128
]
122129

123130
const variants = [

packages/@tailwindcss-upgrade/src/template/candidates.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,9 @@ function printArbitraryValue(input: string) {
187187
})
188188
}
189189

190+
recursivelyEscapeUnderscores(ast)
191+
190192
return ValueParser.toCss(ast)
191-
.replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is
192-
.replaceAll(' ', '_') // Replace spaces with underscores
193193
}
194194

195195
function simplifyArbitraryVariant(input: string) {
@@ -213,3 +213,51 @@ function simplifyArbitraryVariant(input: string) {
213213

214214
return input
215215
}
216+
217+
function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) {
218+
for (let node of ast) {
219+
switch (node.kind) {
220+
case 'function': {
221+
if (node.value === 'url' || node.value.endsWith('_url')) {
222+
// Don't decode underscores in url() but do decode the function name
223+
node.value = escapeUnderscore(node.value)
224+
break
225+
}
226+
227+
if (node.value === 'var' || node.value.endsWith('_var')) {
228+
// Don't decode underscores in the first argument of var() but do
229+
// decode the function name
230+
node.value = escapeUnderscore(node.value)
231+
for (let i = 0; i < node.nodes.length; i++) {
232+
if (i == 0 && node.nodes[i].kind === 'word') {
233+
continue
234+
}
235+
recursivelyEscapeUnderscores([node.nodes[i]])
236+
}
237+
break
238+
}
239+
240+
node.value = escapeUnderscore(node.value)
241+
recursivelyEscapeUnderscores(node.nodes)
242+
break
243+
}
244+
case 'separator':
245+
case 'word': {
246+
node.value = escapeUnderscore(node.value)
247+
break
248+
}
249+
default:
250+
never()
251+
}
252+
}
253+
}
254+
255+
function never(): never {
256+
throw new Error('This should never happen')
257+
}
258+
259+
function escapeUnderscore(value: string): string {
260+
return value
261+
.replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is
262+
.replaceAll(' ', '_') // Replace spaces with underscores
263+
}

0 commit comments

Comments
 (0)