From 0f6313bb0f71ca7c902bceed56b13d6e73d32399 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 8 Nov 2024 13:47:23 +0100 Subject: [PATCH] Allow newlines and tabs in the argument list of the `theme()` function --- CHANGELOG.md | 1 + .../codemods/migrate-border-compatibility.ts | 5 ++++ .../src/template/codemods/theme-to-var.ts | 5 ++++ .../tailwindcss/src/css-functions.test.ts | 20 +++++++++++++ packages/tailwindcss/src/css-functions.ts | 5 ++++ packages/tailwindcss/src/value-parser.test.ts | 16 ++++++++++ packages/tailwindcss/src/value-parser.ts | 30 +++++++++++-------- 7 files changed, 70 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a97d63d85e..efe588a1b51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) - _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830)) - _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838)) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts index a3165ae77915..8f762c4e3218 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts index 21ddf7dcd1c7..78b4ee2c62f9 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts @@ -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 diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 26443abcb81e..b00541d55101 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -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()', () => { diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index 7b51f25699b9..e55405ddc8a7 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -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( diff --git a/packages/tailwindcss/src/value-parser.test.ts b/packages/tailwindcss/src/value-parser.test.ts index 2814b2e9efa1..e1e64832c5db 100644 --- a/packages/tailwindcss/src/value-parser.test.ts +++ b/packages/tailwindcss/src/value-parser.test.ts @@ -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([ { diff --git a/packages/tailwindcss/src/value-parser.ts b/packages/tailwindcss/src/value-parser.ts index 1b6677cfb95a..18ee3e404747 100644 --- a/packages/tailwindcss/src/value-parser.ts +++ b/packages/tailwindcss/src/value-parser.ts @@ -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 }