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">
This commit is contained in:
Philipp Spiess 2024-11-08 16:14:11 +01:00 committed by GitHub
parent c1c94d8d7a
commit aaa32e23e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 12 deletions

View File

@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
- 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))
- Ensure the CSS `theme()` function handles newlines and tabs in its arguments list ([#14917](https://github.com/tailwindlabs/tailwindcss/pull/14917))
- Don't unset keys like `--inset-shadow-*` when unsetting keys like `--inset-*` ([#14906](https://github.com/tailwindlabs/tailwindcss/pull/14906))
- Ensure spacing utilities with no value (e.g. `px` or `translate-y`) don't generate CSS ([#14911](https://github.com/tailwindlabs/tailwindcss/pull/14911))
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))

View File

@ -134,6 +134,11 @@ function substituteFunctionsInValue(
if (node.kind === 'function' && node.value === 'theme') {
if (node.nodes.length < 1) return
// Ignore whitespace before the first argument
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
node.nodes.shift()
}
let pathNode = node.nodes[0]
if (pathNode.kind !== 'word') return

View File

@ -236,6 +236,11 @@ function substituteFunctionsInValue(
if (node.kind === 'function' && node.value === 'theme') {
if (node.nodes.length < 1) return
// Ignore whitespace before the first argument
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
node.nodes.shift()
}
let pathNode = node.nodes[0]
if (pathNode.kind !== 'word') return

View File

@ -473,6 +473,26 @@ describe('theme function', () => {
}"
`)
})
test('theme(\n\tfontFamily.unknown,\n\tHelvetica Neue,\n\tHelvetica,\n\tsans-serif\n)', async () => {
expect(
// prettier-ignore
await compileCss(css`
.fam {
font-family: theme(
fontFamily.unknown,
Helvetica Neue,
Helvetica,
sans-serif
);
}
`),
).toMatchInlineSnapshot(`
".fam {
font-family: Helvetica Neue, Helvetica, sans-serif;
}"
`)
})
})
describe('recursive theme()', () => {

View File

@ -42,6 +42,11 @@ export function substituteFunctionsInValue(
)
}
// Ignore whitespace before the first argument
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
node.nodes.shift()
}
let pathNode = node.nodes[0]
if (pathNode.kind !== 'word') {
throw new Error(

View File

@ -52,6 +52,22 @@ describe('parse', () => {
])
})
it('should parse a function with multiple arguments across lines', () => {
expect(parse('theme(\n\tfoo,\n\tbar\n)')).toEqual([
{
kind: 'function',
value: 'theme',
nodes: [
{ kind: 'separator', value: '\n\t' },
{ kind: 'word', value: 'foo' },
{ kind: 'separator', value: ',\n\t' },
{ kind: 'word', value: 'bar' },
{ kind: 'separator', value: '\n' },
],
},
])
})
it('should parse a function with nested arguments', () => {
expect(parse('theme(foo, theme(bar))')).toEqual([
{

View File

@ -109,13 +109,15 @@ const CLOSE_PAREN = 0x29
const COLON = 0x3a
const COMMA = 0x2c
const DOUBLE_QUOTE = 0x22
const EQUALS = 0x3d
const GREATER_THAN = 0x3e
const LESS_THAN = 0x3c
const NEWLINE = 0x0a
const OPEN_PAREN = 0x28
const SINGLE_QUOTE = 0x27
const SPACE = 0x20
const LESS_THAN = 0x3c
const GREATER_THAN = 0x3e
const EQUALS = 0x3d
const SLASH = 0x2f
const SPACE = 0x20
const TAB = 0x09
export function parse(input: string) {
input = input.replaceAll('\r\n', '\n')
@ -144,11 +146,13 @@ export function parse(input: string) {
// ```
case COLON:
case COMMA:
case SPACE:
case SLASH:
case LESS_THAN:
case EQUALS:
case GREATER_THAN:
case EQUALS: {
case LESS_THAN:
case NEWLINE:
case SLASH:
case SPACE:
case TAB: {
// 1. Handle everything before the separator as a word
// Handle everything before the closing paren as a word
if (buffer.length > 0) {
@ -169,11 +173,13 @@ export function parse(input: string) {
if (
peekChar !== COLON &&
peekChar !== COMMA &&
peekChar !== SPACE &&
peekChar !== SLASH &&
peekChar !== LESS_THAN &&
peekChar !== EQUALS &&
peekChar !== GREATER_THAN &&
peekChar !== EQUALS
peekChar !== LESS_THAN &&
peekChar !== NEWLINE &&
peekChar !== SLASH &&
peekChar !== SPACE &&
peekChar !== TAB
) {
break
}