v3: Don’t break sibling-*() functions when used inside calc(…) (#19335)
The implementation of v3's math operator normalization uses a safe-list of function names. Need to add `sibling-index()` and `sibling-count()` to this list otherwise when used inside math functions like `calc()` they'll get spaces around the `-`.
This commit is contained in:
parent
7e2dd5383a
commit
e2b47e8a5e
4 changed files with 216 additions and 120 deletions
|
|
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Nothing yet!
|
||||
### Fixed
|
||||
|
||||
- Don’t break `sibling-*()` functions when used inside `calc(…)` ([#19335](https://github.com/tailwindlabs/tailwindcss/pull/19335))
|
||||
|
||||
## [3.4.18] - 2024-10-01
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { parseColor } from './color'
|
||||
import { addWhitespaceAroundMathOperators } from './math-operators'
|
||||
import { parseBoxShadowValue } from './parseBoxShadowValue'
|
||||
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ export function normalize(value, context = null, isRoot = true) {
|
|||
value = value.trim()
|
||||
}
|
||||
|
||||
value = normalizeMathOperatorSpacing(value)
|
||||
value = addWhitespaceAroundMathOperators(value)
|
||||
|
||||
return value
|
||||
}
|
||||
|
|
@ -109,122 +110,6 @@ export function normalizeAttributeSelectors(value) {
|
|||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Add spaces around operators inside math functions
|
||||
* like calc() that do not follow an operator, '(', or `,`.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeMathOperatorSpacing(value) {
|
||||
let preventFormattingInFunctions = ['theme']
|
||||
let preventFormattingKeywords = [
|
||||
'min-content',
|
||||
'max-content',
|
||||
'fit-content',
|
||||
|
||||
// Env
|
||||
'safe-area-inset-top',
|
||||
'safe-area-inset-right',
|
||||
'safe-area-inset-bottom',
|
||||
'safe-area-inset-left',
|
||||
|
||||
'titlebar-area-x',
|
||||
'titlebar-area-y',
|
||||
'titlebar-area-width',
|
||||
'titlebar-area-height',
|
||||
|
||||
'keyboard-inset-top',
|
||||
'keyboard-inset-right',
|
||||
'keyboard-inset-bottom',
|
||||
'keyboard-inset-left',
|
||||
'keyboard-inset-width',
|
||||
'keyboard-inset-height',
|
||||
|
||||
'radial-gradient',
|
||||
'linear-gradient',
|
||||
'conic-gradient',
|
||||
'repeating-radial-gradient',
|
||||
'repeating-linear-gradient',
|
||||
'repeating-conic-gradient',
|
||||
|
||||
'anchor-size',
|
||||
]
|
||||
|
||||
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
|
||||
let result = ''
|
||||
|
||||
function lastChar() {
|
||||
let char = result.trimEnd()
|
||||
return char[char.length - 1]
|
||||
}
|
||||
|
||||
for (let i = 0; i < match.length; i++) {
|
||||
function peek(word) {
|
||||
return word.split('').every((char, j) => match[i + j] === char)
|
||||
}
|
||||
|
||||
function consumeUntil(chars) {
|
||||
let minIndex = Infinity
|
||||
for (let char of chars) {
|
||||
let index = match.indexOf(char, i)
|
||||
if (index !== -1 && index < minIndex) {
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
let result = match.slice(i, minIndex)
|
||||
i += result.length - 1
|
||||
return result
|
||||
}
|
||||
|
||||
let char = match[i]
|
||||
|
||||
// Handle `var(--variable)`
|
||||
if (peek('var')) {
|
||||
// When we consume until `)`, then we are dealing with this scenario:
|
||||
// `var(--example)`
|
||||
//
|
||||
// When we consume until `,`, then we are dealing with this scenario:
|
||||
// `var(--example, 1rem)`
|
||||
//
|
||||
// In this case we do want to "format", the default value as well
|
||||
result += consumeUntil([')', ','])
|
||||
}
|
||||
|
||||
// Skip formatting of known keywords
|
||||
else if (preventFormattingKeywords.some((keyword) => peek(keyword))) {
|
||||
let keyword = preventFormattingKeywords.find((keyword) => peek(keyword))
|
||||
result += keyword
|
||||
i += keyword.length - 1
|
||||
}
|
||||
|
||||
// Skip formatting inside known functions
|
||||
else if (preventFormattingInFunctions.some((fn) => peek(fn))) {
|
||||
result += consumeUntil([')'])
|
||||
}
|
||||
|
||||
// Don't break CSS grid track names
|
||||
else if (peek('[')) {
|
||||
result += consumeUntil([']'])
|
||||
}
|
||||
|
||||
// Handle operators
|
||||
else if (
|
||||
['+', '-', '*', '/'].includes(char) &&
|
||||
!['(', '+', '-', '*', '/', ','].includes(lastChar())
|
||||
) {
|
||||
result += ` ${char} `
|
||||
} else {
|
||||
result += char
|
||||
}
|
||||
}
|
||||
|
||||
// Simplify multiple spaces
|
||||
return result.replace(/\s+/g, ' ')
|
||||
})
|
||||
}
|
||||
|
||||
export function url(value) {
|
||||
return value.startsWith('url(')
|
||||
}
|
||||
|
|
|
|||
205
src/util/math-operators.ts
Normal file
205
src/util/math-operators.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
const LOWER_A = 0x61
|
||||
const LOWER_Z = 0x7a
|
||||
const UPPER_A = 0x41
|
||||
const UPPER_Z = 0x5a
|
||||
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 PERCENT = 0x25
|
||||
|
||||
const MATH_FUNCTIONS = [
|
||||
'calc',
|
||||
'min',
|
||||
'max',
|
||||
'clamp',
|
||||
'mod',
|
||||
'rem',
|
||||
'sin',
|
||||
'cos',
|
||||
'tan',
|
||||
'asin',
|
||||
'acos',
|
||||
'atan',
|
||||
'atan2',
|
||||
'pow',
|
||||
'sqrt',
|
||||
'hypot',
|
||||
'log',
|
||||
'exp',
|
||||
'round',
|
||||
]
|
||||
|
||||
export function hasMathFn(input: string) {
|
||||
return input.indexOf('(') !== -1 && MATH_FUNCTIONS.some((fn) => input.includes(`${fn}(`))
|
||||
}
|
||||
|
||||
export function addWhitespaceAroundMathOperators(input: string) {
|
||||
// Bail early if there are no math functions in the input
|
||||
if (!MATH_FUNCTIONS.some((fn) => input.includes(fn))) {
|
||||
return input
|
||||
}
|
||||
|
||||
let result = ''
|
||||
let formattable: boolean[] = []
|
||||
|
||||
let valuePos = null
|
||||
let lastValuePos = null
|
||||
|
||||
for (let i = 0; i < input.length; 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 === PERCENT ||
|
||||
(char >= LOWER_A && char <= LOWER_Z) ||
|
||||
(char >= UPPER_A && char <= UPPER_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 === OPEN_PAREN) {
|
||||
result += input[i]
|
||||
|
||||
// Scan backwards to determine the function name. This assumes math
|
||||
// functions are named with lowercase alphanumeric characters.
|
||||
let start = i
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
let inner = input.charCodeAt(j)
|
||||
|
||||
if (inner >= ZERO && inner <= NINE) {
|
||||
start = j // 0-9
|
||||
} else if (inner >= LOWER_A && inner <= LOWER_Z) {
|
||||
start = j // a-z
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let fn = input.slice(start, i)
|
||||
|
||||
// This is a known math function so start formatting
|
||||
if (MATH_FUNCTIONS.includes(fn)) {
|
||||
formattable.unshift(true)
|
||||
continue
|
||||
}
|
||||
|
||||
// We've encountered nested parens inside a math function, record that and
|
||||
// keep formatting until we've closed all parens.
|
||||
else if (formattable[0] && fn === '') {
|
||||
formattable.unshift(true)
|
||||
continue
|
||||
}
|
||||
|
||||
// This is not a known math function so don't format it
|
||||
formattable.unshift(false)
|
||||
continue
|
||||
}
|
||||
|
||||
// We've exited the function so format according to the parent function's
|
||||
// type.
|
||||
else if (char === CLOSE_PAREN) {
|
||||
result += input[i]
|
||||
formattable.shift()
|
||||
}
|
||||
|
||||
// Add spaces after commas in math functions
|
||||
else if (char === COMMA && formattable[0]) {
|
||||
result += `, `
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip over consecutive whitespace
|
||||
else if (char === SPACE && formattable[0] && result.charCodeAt(result.length - 1) === SPACE) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add whitespace around operators inside math functions
|
||||
else if ((char === ADD || char === MUL || char === DIV || char === SUB) && formattable[0]) {
|
||||
let trimmed = result.trimEnd()
|
||||
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
|
||||
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 === OPEN_PAREN || prev === COMMA) {
|
||||
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 += `${input[i]} `
|
||||
}
|
||||
|
||||
// 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 += input[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Handle all other characters
|
||||
else {
|
||||
result += input[i]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ let table = [
|
|||
],
|
||||
['min(1+2)', 'min(1 + 2)'],
|
||||
['max(1+2)', 'max(1 + 2)'],
|
||||
['clamp(1+2,1+3,1+4)', 'clamp(1 + 2,1 + 3,1 + 4)'],
|
||||
['clamp(1+2,1+3,1+4)', 'clamp(1 + 2, 1 + 3, 1 + 4)'],
|
||||
['var(--heading-h1-font-size)', 'var(--heading-h1-font-size)'],
|
||||
['var(--my-var-with-more-than-3-words)', 'var(--my-var-with-more-than-3-words)'],
|
||||
['var(--width, calc(100%+1rem))', 'var(--width, calc(100% + 1rem))'],
|
||||
|
|
@ -69,7 +69,7 @@ let table = [
|
|||
['calc(theme(spacing.foo-bar))', 'calc(theme(spacing.foo-bar))'],
|
||||
|
||||
// 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)'],
|
||||
['clamp(-3px+4px,-3px+4px,-3px+4px)', 'clamp(-3px + 4px, -3px + 4px, -3px + 4px)'],
|
||||
|
||||
// Prevent formatting inside `var()` functions
|
||||
['calc(var(--foo-bar-bar)*2)', 'calc(var(--foo-bar-bar) * 2)'],
|
||||
|
|
@ -98,6 +98,10 @@ let table = [
|
|||
|
||||
// Prevent formatting functions that are not math functions
|
||||
['w-[calc(anchor-size(width)+8px)]', 'w-[calc(anchor-size(width) + 8px)]'],
|
||||
['w-[calc(sibling-index()*1%)]', 'w-[calc(sibling-index() * 1%)]'],
|
||||
['w-[calc(sibling-count()*1%)]', 'w-[calc(sibling-count() * 1%)]'],
|
||||
['w-[calc(--custom()*1%)]', 'w-[calc(--custom() * 1%)]'],
|
||||
['w-[calc(--custom-fn()*1%)]', 'w-[calc(--custom-fn() * 1%)]'],
|
||||
|
||||
// Misc
|
||||
['color(0_0_0/1.0)', 'color(0 0 0/1.0)'],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue