Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cleanup: do not hardcode dashed ident information
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.
  • Loading branch information
RobinMalfait committed Jun 10, 2025
commit 02ef291183ccd58f2d2d9aa96564aaadc0a7d8ac
112 changes: 46 additions & 66 deletions packages/tailwindcss/src/utils/math-operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}(`))
}
Expand All @@ -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 === '(') {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}