From c7184103f74d7ad1a1fa5d2c2dc57e9a91f0e591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:11:03 +0200 Subject: [PATCH 01/19] Update math-operators.ts --- packages/tailwindcss/src/utils/math-operators.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index e6e83fe79b0c..2b17360df627 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -124,6 +124,12 @@ export function addWhitespaceAroundMathOperators(input: string) { continue } + // If there was a letter both before and after, don't add spaces + else if (!/\d/.test(prev + next)) { + result += char + continue + } + // Add spaces only after the operator if we already have spaces before it else if (input[i - 1] === ' ') { result += `${char} ` From 980dedeb67d676d7c41396e4e75bfa6586cbdf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:17:23 +0200 Subject: [PATCH 02/19] Update math-operators.ts --- packages/tailwindcss/src/utils/math-operators.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 2b17360df627..a46a483901d7 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -111,6 +111,7 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] + let next = input[trimmed.length] // If we're preceded by an operator don't add spaces if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { From 555a60116d46387090217b9d94bad7bfe96ab82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:48:31 +0200 Subject: [PATCH 03/19] Update math-operators.ts --- packages/tailwindcss/src/utils/math-operators.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index a46a483901d7..af4d1cd65d97 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -111,7 +111,8 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] - let next = input[trimmed.length] + let rest = input.slice(i + 1) || '' + let next = input[i + 1] || '' // If we're preceded by an operator don't add spaces if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { @@ -125,8 +126,8 @@ export function addWhitespaceAroundMathOperators(input: string) { continue } - // If there was a letter both before and after, don't add spaces - else if (!/\d/.test(prev + next)) { + // If there's no number before or after it and it's not followed by a math function, don't add spaces. + else if (next.length > 0 && !/\d/.test(prev + next) && !MATH_FUNCTIONS.some(fn => rest.startsWith(`${fn}(`))) { result += char continue } From 1b6ef1ca2cd69541755dc05f5f18f76a8fc02698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:50:18 +0200 Subject: [PATCH 04/19] Update math-operators.ts --- packages/tailwindcss/src/utils/math-operators.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index af4d1cd65d97..5904336e03e7 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -111,7 +111,6 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] - let rest = input.slice(i + 1) || '' let next = input[i + 1] || '' // If we're preceded by an operator don't add spaces @@ -127,7 +126,7 @@ export function addWhitespaceAroundMathOperators(input: string) { } // If there's no number before or after it and it's not followed by a math function, don't add spaces. - else if (next.length > 0 && !/\d/.test(prev + next) && !MATH_FUNCTIONS.some(fn => rest.startsWith(`${fn}(`))) { + else if (next.length > 0 && !/\d/.test(prev + next) && !MATH_FUNCTIONS.some(fn => input.slice(i + 1).startsWith(`${fn}(`))) { result += char continue } From 6407489813c862b6375c8985e04f2e89b32c16de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:55:39 +0200 Subject: [PATCH 05/19] remove fallback value There can be no case that would cause concern about the operator being followed by no character. --- packages/tailwindcss/src/utils/math-operators.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 5904336e03e7..2baecd64fb69 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -111,7 +111,7 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] - let next = input[i + 1] || '' + let next = input[i + 1] // If we're preceded by an operator don't add spaces if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { @@ -126,7 +126,7 @@ export function addWhitespaceAroundMathOperators(input: string) { } // If there's no number before or after it and it's not followed by a math function, don't add spaces. - else if (next.length > 0 && !/\d/.test(prev + next) && !MATH_FUNCTIONS.some(fn => input.slice(i + 1).startsWith(`${fn}(`))) { + else if (!/\d/.test(prev + next) && !MATH_FUNCTIONS.some(fn => input.slice(i + 1).startsWith(`${fn}(`))) { result += char continue } From 8358ed2fdd66a4f389fe18e8f68db30cf006c509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:47:32 +0200 Subject: [PATCH 06/19] check (+- opeartors; allowing any standard function --- packages/tailwindcss/src/utils/math-operators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 2baecd64fb69..a88be5ce511f 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -126,7 +126,7 @@ export function addWhitespaceAroundMathOperators(input: string) { } // If there's no number before or after it and it's not followed by a math function, don't add spaces. - else if (!/\d/.test(prev + next) && !MATH_FUNCTIONS.some(fn => input.slice(i + 1).startsWith(`${fn}(`))) { + else if (!/\d/.test(prev + next) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*\(/.test(input.slice(i + 1)) && next !== '(' && next !== '+' && next !== '-') { result += char continue } From be1b49c2aa2134bf1337222a1a6e607114a2fd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:49:25 +0200 Subject: [PATCH 07/19] tests --- .../tailwindcss/src/utils/decode-arbitrary-value.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts index 2dccd897f5e2..73556191c54b 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts @@ -84,6 +84,11 @@ describe('adds spaces around math operators', () => { ['calc(theme(spacing.foo-2))', 'calc(theme(spacing.foo-2))'], ['calc(theme(spacing.foo-bar))', 'calc(theme(spacing.foo-bar))'], + // Preserving CSS keyword tokens like fit-content without splitting around hyphens in complex expressions + ['min(fit-content,calc(100dvh-4rem))', 'min(fit-content, calc(100dvh - 4rem))'], + ['min(theme(spacing.foo-bar),fit-content,calc(20*calc(40-30)))', 'min(theme(spacing.foo-bar), fit-content, calc(20 * calc(40 - 30)))'], + ['min(fit-content,calc(100dvh-4rem)-calc(50dvh-2px))', 'min(fit-content, calc(100dvh - 4rem) - calc(50dvh - 2px))'], + // A negative number immediately after a `,` should not have spaces inserted ['clamp(-3px+4px,-3px+4px,-3px+4px)', 'clamp(-3px + 4px, -3px + 4px, -3px + 4px)'], From d11f09575a08da6708dcecee46eb0a2eb1fdef5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:50:25 +0200 Subject: [PATCH 08/19] Update decode-arbitrary-value.test.ts --- packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts index 73556191c54b..8bcc68224acd 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts @@ -87,7 +87,7 @@ describe('adds spaces around math operators', () => { // Preserving CSS keyword tokens like fit-content without splitting around hyphens in complex expressions ['min(fit-content,calc(100dvh-4rem))', 'min(fit-content, calc(100dvh - 4rem))'], ['min(theme(spacing.foo-bar),fit-content,calc(20*calc(40-30)))', 'min(theme(spacing.foo-bar), fit-content, calc(20 * calc(40 - 30)))'], - ['min(fit-content,calc(100dvh-4rem)-calc(50dvh-2px))', 'min(fit-content, calc(100dvh - 4rem) - calc(50dvh - 2px))'], + ['min(fit-content,calc(100dvh-4rem)-calc(50dvh--2px))', 'min(fit-content, calc(100dvh - 4rem) - calc(50dvh - -2px))'], // A negative number immediately after a `,` should not have spaces inserted ['clamp(-3px+4px,-3px+4px,-3px+4px)', 'clamp(-3px + 4px, -3px + 4px, -3px + 4px)'], From 1cd177f534a3140e446112a6708b6c824897d573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:52:21 +0200 Subject: [PATCH 09/19] Update math-operators.ts --- packages/tailwindcss/src/utils/math-operators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index a88be5ce511f..8a303951fbad 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -125,7 +125,7 @@ export function addWhitespaceAroundMathOperators(input: string) { continue } - // If there's no number before or after it and it's not followed by a math function, don't add spaces. + // If there's no number/operator before or after it and it's not followed by a math function, don't add spaces else if (!/\d/.test(prev + next) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*\(/.test(input.slice(i + 1)) && next !== '(' && next !== '+' && next !== '-') { result += char continue From 8d637786f124ef224a44d458569bd033bde63973 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 09:23:08 +0200 Subject: [PATCH 10/19] revert regex-based solution --- packages/tailwindcss/src/utils/math-operators.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 8a303951fbad..e6e83fe79b0c 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -111,7 +111,6 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] - let next = input[i + 1] // If we're preceded by an operator don't add spaces if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { @@ -125,12 +124,6 @@ export function addWhitespaceAroundMathOperators(input: string) { continue } - // If there's no number/operator before or after it and it's not followed by a math function, don't add spaces - else if (!/\d/.test(prev + next) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*\(/.test(input.slice(i + 1)) && next !== '(' && next !== '+' && next !== '-') { - result += char - continue - } - // Add spaces only after the operator if we already have spaces before it else if (input[i - 1] === ' ') { result += `${char} ` From 34b8c1eac38ff91c4ec32a70d9f9673c711e51f0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 10:04:48 +0200 Subject: [PATCH 11/19] add a few more test cases --- .../src/utils/decode-arbitrary-value.test.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts index 8bcc68224acd..eab8030fdc9a 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts @@ -86,8 +86,19 @@ describe('adds spaces around math operators', () => { // Preserving CSS keyword tokens like fit-content without splitting around hyphens in complex expressions ['min(fit-content,calc(100dvh-4rem))', 'min(fit-content, calc(100dvh - 4rem))'], - ['min(theme(spacing.foo-bar),fit-content,calc(20*calc(40-30)))', 'min(theme(spacing.foo-bar), fit-content, calc(20 * calc(40 - 30)))'], - ['min(fit-content,calc(100dvh-4rem)-calc(50dvh--2px))', 'min(fit-content, calc(100dvh - 4rem) - calc(50dvh - -2px))'], + [ + 'min(theme(spacing.foo-bar),fit-content,calc(20*calc(40-30)))', + 'min(theme(spacing.foo-bar), fit-content, calc(20 * calc(40 - 30)))', + ], + [ + 'min(fit-content,calc(100dvh-4rem)-calc(50dvh--2px))', + 'min(fit-content, calc(100dvh - 4rem) - calc(50dvh - -2px))', + ], + ['min(-3.4e-2-var(--foo),calc-size(auto))', 'min(-3.4e-2 - var(--foo), calc-size(auto))'], + [ + 'clamp(-10e3-var(--foo),calc-size(max-content),var(--foo)+-10e3)', + 'clamp(-10e3 - var(--foo), calc-size(max-content), var(--foo) + -10e3)', + ], // A negative number immediately after a `,` should not have spaces inserted ['clamp(-3px+4px,-3px+4px,-3px+4px)', 'clamp(-3px + 4px, -3px + 4px, -3px + 4px)'], @@ -127,7 +138,7 @@ describe('adds spaces around math operators', () => { // round(…) function ['round(1+2,1+3)', 'round(1 + 2, 1 + 3)'], - ['round(to-zero,1+2,1+3)', 'round(to-zero,1 + 2, 1 + 3)'], + ['round(to-zero,1+2,1+3)', 'round(to-zero, 1 + 2, 1 + 3)'], // Nested parens in non-math functions don't format their contents ['env((safe-area-inset-bottom))', 'env((safe-area-inset-bottom))'], From 614f896b3a25373231f7b933dee6bd5de8b4ede7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 10:10:17 +0200 Subject: [PATCH 12/19] handle scientific notation --- packages/tailwindcss/src/utils/math-operators.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index e6e83fe79b0c..8b9ee4a274a8 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -111,9 +111,16 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] + let prevprevCode = trimmed.charCodeAt(trimmed.length - 2) + + // Do not add spaces for scientific notation, e.g.: `-3.4e-2` + if ((prev === 'e' || prev === 'E') && prevprevCode >= 48 && prevprevCode <= 57) { + result += char + continue + } // If we're preceded by an operator don't add spaces - if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { + else if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { result += char continue } From 6ac3bd711c76f0a60733a2b587a9905c48ac7114 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 10:37:56 +0200 Subject: [PATCH 13/19] add test with function that looks like a dashed keyword --- .../tailwindcss/src/utils/decode-arbitrary-value.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts index eab8030fdc9a..19025d5b4bfa 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts @@ -109,6 +109,12 @@ describe('adds spaces around math operators', () => { // Prevent formatting inside `env()` functions ['calc(env(safe-area-inset-bottom)*2)', 'calc(env(safe-area-inset-bottom) * 2)'], + // Handle dashed functions that look like known dashed idents + [ + 'fit-content(min(max-content,max(min-content,calc(20px+1em))))', + 'fit-content(min(max-content, max(min-content, calc(20px + 1em))))', + ], + // Should format inside `calc()` nested in `env()` [ 'env(safe-area-inset-bottom,calc(10px+20px))', From 1a13d13502de53598401a6c761edf369397e0d2f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 10:38:19 +0200 Subject: [PATCH 14/19] do not add spaces around `-` in dashed keywords --- .../tailwindcss/src/utils/math-operators.ts | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 8b9ee4a274a8..780177fc2ea5 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -20,7 +20,11 @@ const MATH_FUNCTIONS = [ 'round', ] -const KNOWN_DASHED_FUNCTIONS = ['anchor-size'] +// List of known keywords that can be used in math functions +const KNOWN_DASHED_KEYWORDS = ['fit-content', 'min-content', 'max-content', 'to-zero'] +const DASHED_KEYWORDS_REGEX = new RegExp(`(${KNOWN_DASHED_KEYWORDS.join('|')})`, 'g') + +const KNOWN_DASHED_FUNCTIONS = ['anchor-size', 'calc-size'] const DASHED_FUNCTIONS_REGEX = new RegExp(`(${KNOWN_DASHED_FUNCTIONS.join('|')})\\(`, 'g') export function hasMathFn(input: string) { @@ -45,6 +49,7 @@ export function addWhitespaceAroundMathOperators(input: string) { let result = '' let formattable: boolean[] = [] + let resumeAtIdx = 0 for (let i = 0; i < input.length; i++) { let char = input[i] @@ -142,7 +147,7 @@ export function addWhitespaceAroundMathOperators(input: string) { } } - // Skip over `to-zero` when in a math function. + // Skip over hyphenated keywords when in a math function. // // This is specifically to handle this value in the round(…) function: // @@ -151,12 +156,40 @@ export function addWhitespaceAroundMathOperators(input: string) { // ^^^^^^^ // ``` // - // This is because the first argument is optionally a keyword and `to-zero` - // contains a hyphen and we want to avoid adding spaces inside it. - else if (formattable[0] && input.startsWith('to-zero', i)) { + // Or when using `fit-content`, `min-content` or `max-content` in a math + // function: + // + // ``` + // min(fit-content, calc(100dvh - 4rem) - calc(50dvh - -2px)) + // ^^^^^^^^^^^ + // ``` + else if (formattable[0] && i >= resumeAtIdx) { + DASHED_KEYWORDS_REGEX.lastIndex = 0 + let match = DASHED_KEYWORDS_REGEX.exec(input.slice(i)) + if (match === null) { + // No match at all, we can skip this check entirely for the rest of the + // string because we known nothing else will match. + resumeAtIdx = input.length + result += char + continue + } + + // Match must be at the start of the string, otherwise track the index + // to resume at. + if (match.index !== 0) { + resumeAtIdx = i + match.index + result += char + continue + } + + let keyword = match[1] let start = i - i += 7 + i += keyword.length result += input.slice(start, i + 1) + + // If the keyword is followed by a `,`, it means we're in a math function. + // Adding a space for pretty-printing purposes. + if (input[i] === ',') result += ' ' } // Handle all other characters From 02ef291183ccd58f2d2d9aa96564aaadc0a7d8ac Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 13:09:45 +0200 Subject: [PATCH 15/19] cleanup: do not hardcode dashed ident information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead, we can rely on some heuristics to know whether we want to inject spaces around operators or note. Some are required, some just look better with spaces around it. The current rules are: - There is a digit before the operator - There is a digit after the operator - There is a `(` after the operator (start of a parenthesized expression) - There is a `)` before the operator (end of a parenthesized expression / function) - The operator is followed by another operator. E.g.: `1px + -2px` - The operator was not preceded by a value (+unit). This is important to handle scenarios like this: `calc(1rem-theme(…))`. In this case `rem-theme` could look like a valid function, but since `1rem` was used it definitely is _not_ part of the function. --- .../tailwindcss/src/utils/math-operators.ts | 112 +++++++----------- 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 780177fc2ea5..d1c5125f4363 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -20,13 +20,6 @@ const MATH_FUNCTIONS = [ 'round', ] -// List of known keywords that can be used in math functions -const KNOWN_DASHED_KEYWORDS = ['fit-content', 'min-content', 'max-content', 'to-zero'] -const DASHED_KEYWORDS_REGEX = new RegExp(`(${KNOWN_DASHED_KEYWORDS.join('|')})`, 'g') - -const KNOWN_DASHED_FUNCTIONS = ['anchor-size', 'calc-size'] -const DASHED_FUNCTIONS_REGEX = new RegExp(`(${KNOWN_DASHED_FUNCTIONS.join('|')})\\(`, 'g') - export function hasMathFn(input: string) { return input.indexOf('(') !== -1 && MATH_FUNCTIONS.some((fn) => input.includes(`${fn}(`)) } @@ -37,22 +30,33 @@ export function addWhitespaceAroundMathOperators(input: string) { return input } - // Replace known functions with a placeholder - let hasKnownFunctions = false - if (KNOWN_DASHED_FUNCTIONS.some((fn) => input.includes(fn))) { - DASHED_FUNCTIONS_REGEX.lastIndex = 0 - input = input.replace(DASHED_FUNCTIONS_REGEX, (_, fn) => { - hasKnownFunctions = true - return `$${KNOWN_DASHED_FUNCTIONS.indexOf(fn)}$(` - }) - } - let result = '' let formattable: boolean[] = [] - let resumeAtIdx = 0 + + let valuePos = null + let lastValuePos = null for (let i = 0; i < input.length; i++) { let char = input[i] + let charCode = char.charCodeAt(0) + + // Track if we see a number followed by a unit, then we know for sure that + // this is not a function call. + if (charCode >= 48 && charCode <= 57) { + valuePos = i + } + + // If we saw a number before, and we see normal a-z character, then we + // assume this is a value such as `123px` + else if (valuePos !== null && charCode >= 97 && charCode <= 122) { + valuePos = i + } + + // Once we see something else, we reset the value position + else { + lastValuePos = valuePos + valuePos = null + } // Determine if we're inside a math function if (char === '(') { @@ -116,8 +120,12 @@ export function addWhitespaceAroundMathOperators(input: string) { else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] + let prevCode = prev.charCodeAt(0) let prevprevCode = trimmed.charCodeAt(trimmed.length - 2) + let next = input[i + 1] + let nextCode = next?.charCodeAt(0) + // Do not add spaces for scientific notation, e.g.: `-3.4e-2` if ((prev === 'e' || prev === 'E') && prevprevCode >= 48 && prevprevCode <= 57) { result += char @@ -141,55 +149,31 @@ export function addWhitespaceAroundMathOperators(input: string) { result += `${char} ` } - // Add spaces around the operator - else { + // Add spaces around the operator, if... + else if ( + // Previous is a digit + (prevCode >= 48 && prevCode <= 57) || + // Next is a digit + (nextCode >= 48 && nextCode <= 57) || + // Previous is end of a function call (or parenthesized expression) + prev === ')' || + // Next is start of a parenthesized expression + next === '(' || + // Next is an operator + next === '+' || + next === '*' || + next === '/' || + next === '-' || + // Previous position was a value (+ unit) + (lastValuePos !== null && lastValuePos === i - 1) + ) { result += ` ${char} ` } - } - // Skip over hyphenated keywords when in a math function. - // - // This is specifically to handle this value in the round(…) function: - // - // ``` - // round(to-zero, 1px) - // ^^^^^^^ - // ``` - // - // Or when using `fit-content`, `min-content` or `max-content` in a math - // function: - // - // ``` - // min(fit-content, calc(100dvh - 4rem) - calc(50dvh - -2px)) - // ^^^^^^^^^^^ - // ``` - else if (formattable[0] && i >= resumeAtIdx) { - DASHED_KEYWORDS_REGEX.lastIndex = 0 - let match = DASHED_KEYWORDS_REGEX.exec(input.slice(i)) - if (match === null) { - // No match at all, we can skip this check entirely for the rest of the - // string because we known nothing else will match. - resumeAtIdx = input.length + // Everything else + else { result += char - continue } - - // Match must be at the start of the string, otherwise track the index - // to resume at. - if (match.index !== 0) { - resumeAtIdx = i + match.index - result += char - continue - } - - let keyword = match[1] - let start = i - i += keyword.length - result += input.slice(start, i + 1) - - // If the keyword is followed by a `,`, it means we're in a math function. - // Adding a space for pretty-printing purposes. - if (input[i] === ',') result += ' ' } // Handle all other characters @@ -198,9 +182,5 @@ export function addWhitespaceAroundMathOperators(input: string) { } } - if (hasKnownFunctions) { - return result.replace(/\$(\d+)\$/g, (fn, idx) => KNOWN_DASHED_FUNCTIONS[idx] ?? fn) - } - return result } From d5e1206066769b23f7da1f84aa85c2c190664f69 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 15:34:00 +0200 Subject: [PATCH 16/19] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1694dfe4da..a8e41a138241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade: migrate CSS variable shorthand if fallback value contains function call ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184)) - Upgrade: Migrate negative arbitrary values to negative bare values, e.g.: `mb-[-32rem]` → `-mb-128` ([#18212](https://github.com/tailwindlabs/tailwindcss/pull/18212)) - Upgrade: Do not migrate `blur` in `wire:model.blur` ([#18216](https://github.com/tailwindlabs/tailwindcss/pull/18216)) +- Don't add spaces around CSS dashed idents when formatting math expressions ([#18220](https://github.com/tailwindlabs/tailwindcss/pull/18220)) ## [4.1.8] - 2025-05-27 From 6f62ce18566024c29e42ab63e8eaa53de06fe730 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 15:52:08 +0200 Subject: [PATCH 17/19] =?UTF-8?q?rename=20`prevprevCode`=20=E2=86=92=20`pr?= =?UTF-8?q?evPrevCode`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tailwindcss/src/utils/math-operators.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index d1c5125f4363..09f7546db2ef 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -121,13 +121,13 @@ export function addWhitespaceAroundMathOperators(input: string) { let trimmed = result.trimEnd() let prev = trimmed[trimmed.length - 1] let prevCode = prev.charCodeAt(0) - let prevprevCode = trimmed.charCodeAt(trimmed.length - 2) + let prevPrevCode = trimmed.charCodeAt(trimmed.length - 2) let next = input[i + 1] let nextCode = next?.charCodeAt(0) // Do not add spaces for scientific notation, e.g.: `-3.4e-2` - if ((prev === 'e' || prev === 'E') && prevprevCode >= 48 && prevprevCode <= 57) { + if ((prev === 'e' || prev === 'E') && prevPrevCode >= 48 && prevPrevCode <= 57) { result += char continue } From fc2f78e20c8edf68a5a7a95a5fb4d2f96187c4dd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 16:20:04 +0200 Subject: [PATCH 18/19] use char codes directly --- .../tailwindcss/src/utils/math-operators.ts | 87 +++++++++++-------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index 09f7546db2ef..baa8cc29be67 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -1,3 +1,18 @@ +const LOWER_A = 0x61 +const LOWER_Z = 0x7a +const LOWER_E = 0x65 +const UPPER_E = 0x45 +const ZERO = 0x30 +const NINE = 0x39 +const ADD = 0x2b +const SUB = 0x2d +const MUL = 0x2a +const DIV = 0x2f +const OPEN_PAREN = 0x28 +const CLOSE_PAREN = 0x29 +const COMMA = 0x2c +const SPACE = 0x20 + const MATH_FUNCTIONS = [ 'calc', 'min', @@ -37,18 +52,17 @@ export function addWhitespaceAroundMathOperators(input: string) { let lastValuePos = null for (let i = 0; i < input.length; i++) { - let char = input[i] - let charCode = char.charCodeAt(0) + let char = input.charCodeAt(i) // Track if we see a number followed by a unit, then we know for sure that // this is not a function call. - if (charCode >= 48 && charCode <= 57) { + if (char >= ZERO && char <= NINE) { valuePos = i } // If we saw a number before, and we see normal a-z character, then we // assume this is a value such as `123px` - else if (valuePos !== null && charCode >= 97 && charCode <= 122) { + else if (valuePos !== null && char >= LOWER_A && char <= LOWER_Z) { valuePos = i } @@ -59,8 +73,8 @@ export function addWhitespaceAroundMathOperators(input: string) { } // Determine if we're inside a math function - if (char === '(') { - result += char + if (char === OPEN_PAREN) { + result += String.fromCharCode(char) // Scan backwards to determine the function name. This assumes math // functions are named with lowercase alphanumeric characters. @@ -69,9 +83,9 @@ export function addWhitespaceAroundMathOperators(input: string) { for (let j = i - 1; j >= 0; j--) { let inner = input.charCodeAt(j) - if (inner >= 48 && inner <= 57) { + if (inner >= ZERO && inner <= NINE) { start = j // 0-9 - } else if (inner >= 97 && inner <= 122) { + } else if (inner >= LOWER_A && inner <= LOWER_Z) { start = j // a-z } else { break @@ -100,85 +114,82 @@ export function addWhitespaceAroundMathOperators(input: string) { // We've exited the function so format according to the parent function's // type. - else if (char === ')') { - result += char + else if (char === CLOSE_PAREN) { + result += String.fromCharCode(char) formattable.shift() } // Add spaces after commas in math functions - else if (char === ',' && formattable[0]) { + else if (char === COMMA && formattable[0]) { result += `, ` continue } // Skip over consecutive whitespace - else if (char === ' ' && formattable[0] && result[result.length - 1] === ' ') { + else if (char === SPACE && formattable[0] && result.charCodeAt(result.length - 1) === SPACE) { continue } // Add whitespace around operators inside math functions - else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { + else if ((char === ADD || char === MUL || char === DIV || char === SUB) && formattable[0]) { let trimmed = result.trimEnd() - let prev = trimmed[trimmed.length - 1] - let prevCode = prev.charCodeAt(0) - let prevPrevCode = trimmed.charCodeAt(trimmed.length - 2) - - let next = input[i + 1] - let nextCode = next?.charCodeAt(0) + let prev = trimmed.charCodeAt(trimmed.length - 1) + let prevPrev = trimmed.charCodeAt(trimmed.length - 2) + let next = input.charCodeAt(i + 1) // Do not add spaces for scientific notation, e.g.: `-3.4e-2` - if ((prev === 'e' || prev === 'E') && prevPrevCode >= 48 && prevPrevCode <= 57) { - result += char + if ((prev === LOWER_E || prev === UPPER_E) && prevPrev >= ZERO && prevPrev <= NINE) { + result += String.fromCharCode(char) continue } // If we're preceded by an operator don't add spaces - else if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { - result += char + else if (prev === ADD || prev === MUL || prev === DIV || prev === SUB) { + result += String.fromCharCode(char) continue } // If we're at the beginning of an argument don't add spaces - else if (prev === '(' || prev === ',') { - result += char + else if (prev === OPEN_PAREN || prev === COMMA) { + result += String.fromCharCode(char) continue } // Add spaces only after the operator if we already have spaces before it - else if (input[i - 1] === ' ') { - result += `${char} ` + else if (input.charCodeAt(i - 1) === SPACE) { + result += `${String.fromCharCode(char)} ` } // Add spaces around the operator, if... else if ( // Previous is a digit - (prevCode >= 48 && prevCode <= 57) || + (prev >= ZERO && prev <= NINE) || // Next is a digit - (nextCode >= 48 && nextCode <= 57) || + (next >= ZERO && next <= NINE) || // Previous is end of a function call (or parenthesized expression) - prev === ')' || + prev === CLOSE_PAREN || // Next is start of a parenthesized expression - next === '(' || + next === OPEN_PAREN || // Next is an operator - next === '+' || - next === '*' || - next === '/' || - next === '-' || + next === ADD || + next === MUL || + next === DIV || + next === SUB || // Previous position was a value (+ unit) (lastValuePos !== null && lastValuePos === i - 1) ) { - result += ` ${char} ` + result += ` ${String.fromCharCode(char)} ` } // Everything else else { - result += char + result += String.fromCharCode(char) } } // Handle all other characters else { - result += char + result += String.fromCharCode(char) } } From 632a247f108e3b9bb2d8f2e012797aedde3ce483 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Jun 2025 16:29:39 +0200 Subject: [PATCH 19/19] use `input[i]` directly We are always dealing with the same `char` at the same position, so we can just use `input[i]` on its own. --- .../tailwindcss/src/utils/math-operators.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index baa8cc29be67..523fcce208cf 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -74,7 +74,7 @@ export function addWhitespaceAroundMathOperators(input: string) { // Determine if we're inside a math function if (char === OPEN_PAREN) { - result += String.fromCharCode(char) + result += input[i] // Scan backwards to determine the function name. This assumes math // functions are named with lowercase alphanumeric characters. @@ -115,7 +115,7 @@ export function addWhitespaceAroundMathOperators(input: string) { // We've exited the function so format according to the parent function's // type. else if (char === CLOSE_PAREN) { - result += String.fromCharCode(char) + result += input[i] formattable.shift() } @@ -139,25 +139,25 @@ export function addWhitespaceAroundMathOperators(input: string) { // Do not add spaces for scientific notation, e.g.: `-3.4e-2` if ((prev === LOWER_E || prev === UPPER_E) && prevPrev >= ZERO && prevPrev <= NINE) { - result += String.fromCharCode(char) + result += input[i] continue } // If we're preceded by an operator don't add spaces else if (prev === ADD || prev === MUL || prev === DIV || prev === SUB) { - result += String.fromCharCode(char) + result += input[i] continue } // If we're at the beginning of an argument don't add spaces else if (prev === OPEN_PAREN || prev === COMMA) { - result += String.fromCharCode(char) + result += input[i] continue } // Add spaces only after the operator if we already have spaces before it else if (input.charCodeAt(i - 1) === SPACE) { - result += `${String.fromCharCode(char)} ` + result += `${input[i]} ` } // Add spaces around the operator, if... @@ -178,18 +178,18 @@ export function addWhitespaceAroundMathOperators(input: string) { // Previous position was a value (+ unit) (lastValuePos !== null && lastValuePos === i - 1) ) { - result += ` ${String.fromCharCode(char)} ` + result += ` ${input[i]} ` } // Everything else else { - result += String.fromCharCode(char) + result += input[i] } } // Handle all other characters else { - result += String.fromCharCode(char) + result += input[i] } }