Skip to content

Commit aaa32e2

Browse files
Allow newlines and tabs in the argument list of the theme() function (#14917)
We noticed an issue that the `theme()` function wourld not properly parse in CSS if you split the argument list over multiple lines. This is fixed by treating `\n` and `\t` the same as space: ```css .custom-font { font-family: theme( fontFamily.unknown, Helvetica Neue, Helvetica, sans-serif ); } ``` ## Test plan Added tests, but also tried it in the Vite example: <img width="1995" alt="Screenshot 2024-11-08 at 13 46 09" src="https://github.com/user-attachments/assets/f9bf94b0-3f9b-4334-8911-9190987e2df5">
1 parent c1c94d8 commit aaa32e2

File tree

7 files changed

+70
-12
lines changed

7 files changed

+70
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
3232
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
3333
- Ensure that CSS transforms from other Vite plugins correctly work in full builds (e.g. `:deep()` in Vue) ([#14871](https://github.com/tailwindlabs/tailwindcss/pull/14871))
34+
- Ensure the CSS `theme()` function handles newlines and tabs in its arguments list ([#14917](https://github.com/tailwindlabs/tailwindcss/pull/14917))
3435
- Don't unset keys like `--inset-shadow-*` when unsetting keys like `--inset-*` ([#14906](https://github.com/tailwindlabs/tailwindcss/pull/14906))
3536
- Ensure spacing utilities with no value (e.g. `px` or `translate-y`) don't generate CSS ([#14911](https://github.com/tailwindlabs/tailwindcss/pull/14911))
3637
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))

packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ function substituteFunctionsInValue(
134134
if (node.kind === 'function' && node.value === 'theme') {
135135
if (node.nodes.length < 1) return
136136

137+
// Ignore whitespace before the first argument
138+
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
139+
node.nodes.shift()
140+
}
141+
137142
let pathNode = node.nodes[0]
138143
if (pathNode.kind !== 'word') return
139144

packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ function substituteFunctionsInValue(
236236
if (node.kind === 'function' && node.value === 'theme') {
237237
if (node.nodes.length < 1) return
238238

239+
// Ignore whitespace before the first argument
240+
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
241+
node.nodes.shift()
242+
}
243+
239244
let pathNode = node.nodes[0]
240245
if (pathNode.kind !== 'word') return
241246

packages/tailwindcss/src/css-functions.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,26 @@ describe('theme function', () => {
473473
}"
474474
`)
475475
})
476+
477+
test('theme(\n\tfontFamily.unknown,\n\tHelvetica Neue,\n\tHelvetica,\n\tsans-serif\n)', async () => {
478+
expect(
479+
// prettier-ignore
480+
await compileCss(css`
481+
.fam {
482+
font-family: theme(
483+
fontFamily.unknown,
484+
Helvetica Neue,
485+
Helvetica,
486+
sans-serif
487+
);
488+
}
489+
`),
490+
).toMatchInlineSnapshot(`
491+
".fam {
492+
font-family: Helvetica Neue, Helvetica, sans-serif;
493+
}"
494+
`)
495+
})
476496
})
477497

478498
describe('recursive theme()', () => {

packages/tailwindcss/src/css-functions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export function substituteFunctionsInValue(
4242
)
4343
}
4444

45+
// Ignore whitespace before the first argument
46+
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
47+
node.nodes.shift()
48+
}
49+
4550
let pathNode = node.nodes[0]
4651
if (pathNode.kind !== 'word') {
4752
throw new Error(

packages/tailwindcss/src/value-parser.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ describe('parse', () => {
5252
])
5353
})
5454

55+
it('should parse a function with multiple arguments across lines', () => {
56+
expect(parse('theme(\n\tfoo,\n\tbar\n)')).toEqual([
57+
{
58+
kind: 'function',
59+
value: 'theme',
60+
nodes: [
61+
{ kind: 'separator', value: '\n\t' },
62+
{ kind: 'word', value: 'foo' },
63+
{ kind: 'separator', value: ',\n\t' },
64+
{ kind: 'word', value: 'bar' },
65+
{ kind: 'separator', value: '\n' },
66+
],
67+
},
68+
])
69+
})
70+
5571
it('should parse a function with nested arguments', () => {
5672
expect(parse('theme(foo, theme(bar))')).toEqual([
5773
{

packages/tailwindcss/src/value-parser.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,15 @@ const CLOSE_PAREN = 0x29
109109
const COLON = 0x3a
110110
const COMMA = 0x2c
111111
const DOUBLE_QUOTE = 0x22
112+
const EQUALS = 0x3d
113+
const GREATER_THAN = 0x3e
114+
const LESS_THAN = 0x3c
115+
const NEWLINE = 0x0a
112116
const OPEN_PAREN = 0x28
113117
const SINGLE_QUOTE = 0x27
114-
const SPACE = 0x20
115-
const LESS_THAN = 0x3c
116-
const GREATER_THAN = 0x3e
117-
const EQUALS = 0x3d
118118
const SLASH = 0x2f
119+
const SPACE = 0x20
120+
const TAB = 0x09
119121

120122
export function parse(input: string) {
121123
input = input.replaceAll('\r\n', '\n')
@@ -144,11 +146,13 @@ export function parse(input: string) {
144146
// ```
145147
case COLON:
146148
case COMMA:
147-
case SPACE:
148-
case SLASH:
149-
case LESS_THAN:
149+
case EQUALS:
150150
case GREATER_THAN:
151-
case EQUALS: {
151+
case LESS_THAN:
152+
case NEWLINE:
153+
case SLASH:
154+
case SPACE:
155+
case TAB: {
152156
// 1. Handle everything before the separator as a word
153157
// Handle everything before the closing paren as a word
154158
if (buffer.length > 0) {
@@ -169,11 +173,13 @@ export function parse(input: string) {
169173
if (
170174
peekChar !== COLON &&
171175
peekChar !== COMMA &&
172-
peekChar !== SPACE &&
173-
peekChar !== SLASH &&
174-
peekChar !== LESS_THAN &&
176+
peekChar !== EQUALS &&
175177
peekChar !== GREATER_THAN &&
176-
peekChar !== EQUALS
178+
peekChar !== LESS_THAN &&
179+
peekChar !== NEWLINE &&
180+
peekChar !== SLASH &&
181+
peekChar !== SPACE &&
182+
peekChar !== TAB
177183
) {
178184
break
179185
}

0 commit comments

Comments
 (0)