From 1abedd4bf1d74396d89b2002d86d2999d1c1741f Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 24 Oct 2024 16:10:53 +0200 Subject: [PATCH 1/2] Don't escape underscores for the first parameter of `var()` --- CHANGELOG.md | 9 +++ packages/tailwindcss/src/candidate.test.ts | 60 +++++++++++++------ packages/tailwindcss/src/index.test.ts | 28 +++++++++ .../src/utils/decode-arbitrary-value.test.ts | 15 ++++- .../src/utils/decode-arbitrary-value.ts | 55 +++++++++++++++-- .../tailwindcss/src/utils/math-operators.ts | 5 -- 6 files changed, 143 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c2a97bf85e..4fa55055f730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- No longer require escaping underscores for CSS variables used in `var()` ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) + ### Fixed - Ensure individual logical property utilities are sorted later than left/right pair utilities ([#14777](https://github.com/tailwindlabs/tailwindcss/pull/14777)) - Don't migrate important modifiers inside conditional statements in Vue and Alpine (e.g. `
`) ([#14774](https://github.com/tailwindlabs/tailwindcss/pull/14774)) +<<<<<<< HEAD - Ensure third-party plugins with `exports` in their `package.json` are resolved correctly ([#14775](https://github.com/tailwindlabs/tailwindcss/pull/14775)) +||||||| parent of 9bf8e915 (Don't escape underscores for the first parameter of `var()`) +======= +- Ensure underscores in the `url()` function are never unescaped ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) +>>>>>>> 9bf8e915 (Don't escape underscores for the first parameter of `var()`) - _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)) ## [4.0.0-alpha.29] - 2024-10-23 diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index a0a84ce5c647..6cf252354ec0 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1059,24 +1059,50 @@ it('should not replace `_` inside of `url()`', () => { let utilities = new Utilities() utilities.functional('bg', () => []) - expect(run('bg-[url(https://example.com/some_page)]', { utilities })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "raw": "bg-[url(https://example.com/some_page)]", - "root": "bg", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "url(https://example.com/some_page)", + expect(run('bg-[no-repeat_url(https://example.com/some_page)]', { utilities })) + .toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "negative": false, + "raw": "bg-[no-repeat_url(https://example.com/some_page)]", + "root": "bg", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "no-repeat url(https://example.com/some_page)", + }, + "variants": [], }, - "variants": [], - }, - ] - `) + ] + `) +}) + +it('should not replace `_` for the first value of `var()`', () => { + let utilities = new Utilities() + utilities.functional('ml', () => []) + + expect(run('ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]', { utilities })) + .toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "negative": false, + "raw": "ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]", + "root": "ml", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--spacing-1_5, var(--spacing-2_5, 1rem))", + }, + "variants": [], + }, + ] + `) }) it('should parse arbitrary properties', () => { diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index ccae67e19651..3d6dd69c5348 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -115,6 +115,34 @@ describe('compiling CSS', () => { ), ).toMatchSnapshot() }) + + test('unescapes underscores to spaces inside arbitrary values expect for url() and first parameter of var()', async () => { + expect( + await compileCss( + css` + @theme { + --spacing-1_5: 1.5rem; + --spacing-2_5: 2.5rem; + } + @tailwind utilities; + `, + ['bg-[no-repeat_url(./my_file.jpg)', 'ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]'], + ), + ).toMatchInlineSnapshot(` + ":root { + --spacing-1_5: 1.5rem; + --spacing-2_5: 2.5rem; + } + + .ml-\\[var\\(--spacing-1_5\\,_var\\(--spacing-2_5\\,_1rem\\)\\)\\] { + margin-left: var(--spacing-1_5, var(--spacing-2_5, 1rem)); + } + + .bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\) { + background-color: no-repeat url("./")my file. jpg; + }" + `) + }) }) describe('arbitrary properties', () => { diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts index 9b6540ead857..351a993308ab 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts @@ -16,6 +16,17 @@ describe('decoding arbitrary values', () => { it('should not replace underscores in url()', () => { expect(decodeArbitraryValue('url(./my_file.jpg)')).toBe('url(./my_file.jpg)') + expect(decodeArbitraryValue('no-repeat_url(./my_file.jpg)')).toBe( + 'no-repeat url(./my_file.jpg)', + ) + }) + + it('should not replace underscores in the first argument of var()', () => { + expect(decodeArbitraryValue('var(--spacing-1_5)')).toBe('var(--spacing-1_5)') + expect(decodeArbitraryValue('var(--spacing-1_5,_1rem)')).toBe('var(--spacing-1_5, 1rem)') + expect(decodeArbitraryValue('var(--spacing-1_5,_var(--spacing-2_5,_1rem))')).toBe( + 'var(--spacing-1_5, var(--spacing-2_5, 1rem))', + ) }) it('should leave var(…) as is', () => { @@ -55,8 +66,8 @@ describe('adds spaces around math operators', () => { ['calc(24px+(-1rem))', 'calc(24px + (-1rem))'], ['calc(24px_+_(-1rem))', 'calc(24px + (-1rem))'], [ - 'calc(var(--10-10px,calc(-20px-(-30px--40px)-50px)', - 'calc(var(--10-10px,calc(-20px - (-30px - -40px) - 50px)', + 'calc(var(--10-10px,calc(-20px-(-30px--40px)-50px)))', + 'calc(var(--10-10px,calc(-20px - (-30px - -40px) - 50px)))', ], ['calc(theme(spacing.1-bar))', 'calc(theme(spacing.1-bar))'], ['theme(spacing.1-bar)', 'theme(spacing.1-bar)'], diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts index f644e663995a..ea4ab53ddf63 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts @@ -1,13 +1,16 @@ +import * as ValueParser from '../value-parser' import { addWhitespaceAroundMathOperators } from './math-operators' export function decodeArbitraryValue(input: string): string { - // We do not want to normalize anything inside of a url() because if we - // replace `_` with ` `, then it will very likely break the url. - if (input.startsWith('url(')) { - return input + // There's definitely no functions in the input, so bail early + if (input.indexOf('(') === -1) { + return convertUnderscoresToWhitespace(input) } - input = convertUnderscoresToWhitespace(input) + let ast = ValueParser.parse(input) + recursivelyDecodeArbitraryValues(ast) + input = ValueParser.toCss(ast) + input = addWhitespaceAroundMathOperators(input) return input @@ -41,3 +44,45 @@ function convertUnderscoresToWhitespace(input: string) { return output } + +function recursivelyDecodeArbitraryValues(ast: ValueParser.ValueAstNode[]) { + for (let node of ast) { + switch (node.kind) { + case 'function': { + if (node.value === 'url' || node.value.endsWith('_url')) { + // Don't decode underscores in url() but do decode the function name + node.value = convertUnderscoresToWhitespace(node.value) + break + } + + if (node.value === 'var' || node.value.endsWith('_var')) { + // Don't decode underscores in the first argument of var() but do + // decode the function name + node.value = convertUnderscoresToWhitespace(node.value) + for (let i = 0; i < node.nodes.length; i++) { + if (i == 0 && node.nodes[i].kind === 'word') { + continue + } + recursivelyDecodeArbitraryValues([node.nodes[i]]) + } + break + } + + node.value = convertUnderscoresToWhitespace(node.value) + recursivelyDecodeArbitraryValues(node.nodes) + break + } + case 'separator': + case 'word': { + node.value = convertUnderscoresToWhitespace(node.value) + break + } + default: + never() + } + } +} + +function never(): never { + throw new Error('This should never happen') +} diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 2fd1e3d78a68..e6e83fe79b0c 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -28,11 +28,6 @@ export function hasMathFn(input: string) { } export function addWhitespaceAroundMathOperators(input: string) { - // There's definitely no functions in the input, so bail early - if (input.indexOf('(') === -1) { - return input - } - // Bail early if there are no math functions in the input if (!MATH_FUNCTIONS.some((fn) => input.includes(fn))) { return input From 75aa9662c53074787892d6d45fdc0b00ac12a188 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 24 Oct 2024 17:08:15 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Adam Wathan --- CHANGELOG.md | 8 ++------ packages/tailwindcss/src/candidate.test.ts | 2 +- packages/tailwindcss/src/index.test.ts | 2 +- packages/tailwindcss/src/utils/decode-arbitrary-value.ts | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa55055f730..24d35cb1e67f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,20 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added +### Changed -- No longer require escaping underscores for CSS variables used in `var()` ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) +- Don't convert underscores in the first argument to `var()` to spaces ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) ### Fixed - Ensure individual logical property utilities are sorted later than left/right pair utilities ([#14777](https://github.com/tailwindlabs/tailwindcss/pull/14777)) - Don't migrate important modifiers inside conditional statements in Vue and Alpine (e.g. `
`) ([#14774](https://github.com/tailwindlabs/tailwindcss/pull/14774)) -<<<<<<< HEAD - Ensure third-party plugins with `exports` in their `package.json` are resolved correctly ([#14775](https://github.com/tailwindlabs/tailwindcss/pull/14775)) -||||||| parent of 9bf8e915 (Don't escape underscores for the first parameter of `var()`) -======= - Ensure underscores in the `url()` function are never unescaped ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) ->>>>>>> 9bf8e915 (Don't escape underscores for the first parameter of `var()`) - _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)) ## [4.0.0-alpha.29] - 2024-10-23 diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 6cf252354ec0..c1280a2fd405 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1080,7 +1080,7 @@ it('should not replace `_` inside of `url()`', () => { `) }) -it('should not replace `_` for the first value of `var()`', () => { +it('should not replace `_` in the first argument to `var()`', () => { let utilities = new Utilities() utilities.functional('ml', () => []) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 3d6dd69c5348..d7d1f0f56a17 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -116,7 +116,7 @@ describe('compiling CSS', () => { ).toMatchSnapshot() }) - test('unescapes underscores to spaces inside arbitrary values expect for url() and first parameter of var()', async () => { + test('unescapes underscores to spaces inside arbitrary values except for `url()` and first argument of `var()`', async () => { expect( await compileCss( css` diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts index ea4ab53ddf63..349d2dddcb77 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts @@ -2,7 +2,7 @@ import * as ValueParser from '../value-parser' import { addWhitespaceAroundMathOperators } from './math-operators' export function decodeArbitraryValue(input: string): string { - // There's definitely no functions in the input, so bail early + // There are definitely no functions in the input, so bail early if (input.indexOf('(') === -1) { return convertUnderscoresToWhitespace(input) }