mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
fix: don't break CSS keywords when formatting math expressions (#18220)
Fixes #18219 ## Summary In an arbitrary value, if there's a non-numeric character both before and after a hyphen, there's no need for a space. ## Test plan `decodeArbitraryValue` will correctly format special CSS values like `fit-content`. I believe spaces are only necessary if there's a digit either before or after the hyphen. ```js decodeArbitraryValue('min(fit-content,calc(100dvh-4rem))') ``` This way, the result of the following arbitrary value will also be correct: ```html <div class="min-h-[min(fit-content,calc(100dvh-4rem))]"></div> ``` ```css .min-h-\[min\(fit-content\,calc\(100dvh-4rem\)\)\] { min-height: min(fit-content, calc(100dvh - 4rem)); } ``` --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
da0895655e
commit
aa817fb6de
@ -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
|
||||
|
||||
|
||||
@ -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)'],
|
||||
|
||||
@ -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))',
|
||||
@ -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))'],
|
||||
|
||||
@ -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',
|
||||
@ -20,9 +35,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}(`))
|
||||
}
|
||||
@ -33,25 +45,36 @@ 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 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 (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 && char >= LOWER_A && char <= LOWER_Z) {
|
||||
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 === '(') {
|
||||
result += char
|
||||
if (char === OPEN_PAREN) {
|
||||
result += input[i]
|
||||
|
||||
// Scan backwards to determine the function name. This assumes math
|
||||
// functions are named with lowercase alphanumeric characters.
|
||||
@ -60,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
|
||||
@ -91,76 +114,84 @@ 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 += input[i]
|
||||
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 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 === LOWER_E || prev === UPPER_E) && prevPrev >= ZERO && prevPrev <= NINE) {
|
||||
result += input[i]
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're preceded by an operator don't add spaces
|
||||
if (prev === '+' || prev === '*' || prev === '/' || prev === '-') {
|
||||
result += char
|
||||
else if (prev === ADD || prev === MUL || prev === DIV || prev === SUB) {
|
||||
result += input[i]
|
||||
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 += input[i]
|
||||
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 += `${input[i]} `
|
||||
}
|
||||
|
||||
// Add spaces around the operator
|
||||
// Add spaces around the operator, if...
|
||||
else if (
|
||||
// Previous is a digit
|
||||
(prev >= ZERO && prev <= NINE) ||
|
||||
// Next is a digit
|
||||
(next >= ZERO && next <= NINE) ||
|
||||
// Previous is end of a function call (or parenthesized expression)
|
||||
prev === CLOSE_PAREN ||
|
||||
// Next is start of a parenthesized expression
|
||||
next === OPEN_PAREN ||
|
||||
// Next is an operator
|
||||
next === ADD ||
|
||||
next === MUL ||
|
||||
next === DIV ||
|
||||
next === SUB ||
|
||||
// Previous position was a value (+ unit)
|
||||
(lastValuePos !== null && lastValuePos === i - 1)
|
||||
) {
|
||||
result += ` ${input[i]} `
|
||||
}
|
||||
|
||||
// Everything else
|
||||
else {
|
||||
result += ` ${char} `
|
||||
result += input[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Handle all other characters
|
||||
else {
|
||||
result += char
|
||||
result += input[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (hasKnownFunctions) {
|
||||
return result.replace(/\$(\d+)\$/g, (fn, idx) => KNOWN_DASHED_FUNCTIONS[idx] ?? fn)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user