Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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

Expand Down
24 changes: 23 additions & 1 deletion packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ 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))',
],
['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)'],

Expand All @@ -93,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))',
Expand Down Expand Up @@ -122,7 +144,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))'],
Expand Down
92 changes: 56 additions & 36 deletions packages/tailwindcss/src/utils/math-operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ const MATH_FUNCTIONS = [
'round',
]

const KNOWN_DASHED_FUNCTIONS = ['anchor-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}(`))
}
Expand All @@ -33,21 +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 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 === '(') {
Expand Down Expand Up @@ -111,9 +120,20 @@ 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
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
}
Expand All @@ -129,27 +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 `to-zero` when in a math function.
//
// This is specifically to handle this value in the round(…) function:
//
// ```
// round(to-zero, 1px)
// ^^^^^^^
// ```
//
// 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)) {
let start = i
i += 7
result += input.slice(start, i + 1)
// Everything else
else {
result += char
}
}

// Handle all other characters
Expand All @@ -158,9 +182,5 @@ export function addWhitespaceAroundMathOperators(input: string) {
}
}

if (hasKnownFunctions) {
return result.replace(/\$(\d+)\$/g, (fn, idx) => KNOWN_DASHED_FUNCTIONS[idx] ?? fn)
}

return result
}