Canonicalization constant folding and handling zeros (#19095)
The main goal of this PR was to support canonicalization of zero like values. We essentially want to canonicalize `-mt-0` as `mt-0`, but also `mt-[0px]`, `mt-[0rem]`, and other length-like units to just `mt-0`. To do this, we had to handle 2 things: 1. We introduced some more constant folding, including making `0px` and `0rem` fold to `0`. We only do this for length units. We also normalize `-0`, `+0`, `-0.0` and so on to `0`. 2. While pre-computing utilities in our lookup table, we make sure that we prefer `mt-0` over `-mt-0` if both result in the same signature. Moved some of the constant folding logic into its own function and added a bunch of separate tests for it. ## Test plan Added more unit tests where we normalize different zero-like values to `0`. Running the canonicalization logic: ```js designSystem.canonicalizeCandidates([ '-m-0', '-m-[-0px]', '-m-[-0rem]', '-m-[0px]', '-m-[0rem]', 'm-0', 'm-[-0px]', 'm-[-0rem]', 'm-[0px]', 'm-[0rem]', 'm-[calc(var(--spacing)*0)]', 'm-[--spacing(0)]', 'm-[--spacing(0.0)]', 'm-[+0]', 'm-[-0]', '-m-[-0]', '-m-[+0]', ]) // → ['m-0'] ``` --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
3aadba7cc1
commit
01d1e98259
13 changed files with 362 additions and 55 deletions
|
|
@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094))
|
||||
- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097))
|
||||
- Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#19100](https://github.com/tailwindlabs/tailwindcss/pull/19100))
|
||||
- Upgrade: Canonicalize utilities containing `0` values ([#19095](https://github.com/tailwindlabs/tailwindcss/pull/19095))
|
||||
|
||||
## [4.1.14] - 2025-10-01
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals
|
|||
}
|
||||
|
||||
// If we see a `/`, we have a modifier
|
||||
else if (child.kind === 'separator' && child.value.trim() === '/') {
|
||||
else if (child.kind === 'word' && child.value === '/') {
|
||||
themeModifierCount += 1
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
// handle the `0px * -1` case which translates to `0px` not `-0px`.
|
||||
//
|
||||
// This translation is actually fine, because now, we will prefer the
|
||||
// non-negative version first so we can replace `-mt-[0px]` with `mt-[0px]`.
|
||||
['mt-[0px]', 'mt-[0px]'],
|
||||
['-mt-[0px]', 'mt-[0px]'],
|
||||
// non-negative version first so we can replace `-mt-[0px]` with `mt-0`.
|
||||
['mt-[0px]', 'mt-0'],
|
||||
['-mt-[0px]', 'mt-0'],
|
||||
|
||||
// Shorthand CSS Variables should be converted to the new syntax, even if
|
||||
// the fallback contains functions. The fallback should also be migrated to
|
||||
|
|
|
|||
|
|
@ -1049,13 +1049,6 @@ const printArbitraryValueCache = new DefaultMap<string, string>((input) => {
|
|||
drop.add(next)
|
||||
}
|
||||
|
||||
// The value parser handles `/` as a separator in some scenarios. E.g.:
|
||||
// `theme(colors.red/50%)`. Because of this, we have to handle this case
|
||||
// separately.
|
||||
else if (node.kind === 'separator' && node.value.trim() === '/') {
|
||||
node.value = '/'
|
||||
}
|
||||
|
||||
// Leading and trailing whitespace
|
||||
else if (node.kind === 'separator' && node.value.length > 0 && node.value.trim() === '') {
|
||||
if (parentArray[0] === node || parentArray[parentArray.length - 1] === node) {
|
||||
|
|
|
|||
|
|
@ -675,6 +675,30 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
['grid-cols-[subgrid]', 'grid-cols-subgrid'],
|
||||
['grid-rows-[subgrid]', 'grid-rows-subgrid'],
|
||||
|
||||
// Handle zeroes
|
||||
['m-[0]', 'm-0'],
|
||||
['m-[0px]', 'm-0'],
|
||||
['m-[0rem]', 'm-0'],
|
||||
|
||||
['-m-[0]', 'm-0'],
|
||||
['-m-[0px]', 'm-0'],
|
||||
['-m-[0rem]', 'm-0'],
|
||||
|
||||
['m-[-0]', 'm-0'],
|
||||
['m-[-0px]', 'm-0'],
|
||||
['m-[-0rem]', 'm-0'],
|
||||
|
||||
['-m-[-0]', 'm-0'],
|
||||
['-m-[-0px]', 'm-0'],
|
||||
['-m-[-0rem]', 'm-0'],
|
||||
|
||||
['[margin:0]', 'm-0'],
|
||||
['[margin:-0]', 'm-0'],
|
||||
['[margin:0px]', 'm-0'],
|
||||
|
||||
// Not a length-unit, can't safely constant fold
|
||||
['[margin:0%]', 'm-[0%]'],
|
||||
|
||||
// Only 50-200% (inclusive) are valid:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch#percentage
|
||||
['font-stretch-[50%]', 'font-stretch-50%'],
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ const converterCache = new DefaultMap((ds: DesignSystem) => {
|
|||
}
|
||||
|
||||
// If we see a `/`, we have a modifier
|
||||
else if (child.kind === 'separator' && child.value.trim() === '/') {
|
||||
else if (child.kind === 'word' && child.value === '/') {
|
||||
themeModifierCount += 1
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
}
|
||||
|
|
|
|||
94
packages/tailwindcss/src/constant-fold-declaration.test.ts
Normal file
94
packages/tailwindcss/src/constant-fold-declaration.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import { constantFoldDeclaration } from './constant-fold-declaration'
|
||||
|
||||
it.each([
|
||||
// Simple expression
|
||||
['calc(1 + 1)', '2'],
|
||||
['calc(3 - 2)', '1'],
|
||||
['calc(2 * 3)', '6'],
|
||||
['calc(8 / 2)', '4'],
|
||||
|
||||
// Nested
|
||||
['calc(1 + calc(1 + 1))', '3'],
|
||||
['calc(3 - calc(1 + 2))', '0'],
|
||||
['calc(2 * calc(1 + 3))', '8'],
|
||||
['calc(8 / calc(2 + 2))', '2'],
|
||||
['calc(1 + (1 + 1))', '3'],
|
||||
['calc(3 - (1 + 2))', '0'],
|
||||
['calc(2 * (1 + 3))', '8'],
|
||||
['calc(8 / (2 + 2))', '2'],
|
||||
|
||||
// With units
|
||||
['calc(1rem * 2)', '2rem'],
|
||||
['calc(2rem - 0.5rem)', '1.5rem'],
|
||||
['calc(3rem * 6)', '18rem'],
|
||||
['calc(5rem / 2)', '2.5rem'],
|
||||
|
||||
// Nested partial evaluation
|
||||
['calc(calc(1 + 2) + 2rem)', 'calc(3 + 2rem)'],
|
||||
|
||||
// Evaluation only handles two operands right now, this can change in the future
|
||||
['calc(1 + 2 + 3)', 'calc(1 + 2 + 3)'],
|
||||
])('should constant fold `%s` into `%s`', (input, expected) => {
|
||||
expect(constantFoldDeclaration(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['calc(1rem * 2%)'],
|
||||
['calc(1rem * 2px)'],
|
||||
['calc(2rem - 6)'],
|
||||
['calc(3rem * 3dvw)'],
|
||||
['calc(3rem * 2dvh)'],
|
||||
['calc(5rem / 17px)'],
|
||||
])('should not constant fold different units `%s`', (input) => {
|
||||
expect(constantFoldDeclaration(input)).toBe(input)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['calc(0 * 100vw)'],
|
||||
['calc(0 * calc(1 * 2))'],
|
||||
['calc(0 * var(--foo))'],
|
||||
['calc(0 * calc(var(--spacing) * 32))'],
|
||||
|
||||
['calc(100vw * 0)'],
|
||||
['calc(calc(1 * 2) * 0)'],
|
||||
['calc(var(--foo) * 0)'],
|
||||
['calc(calc(var(--spacing, 0.25rem) * 32) * 0)'],
|
||||
['calc(var(--spacing, 0.25rem) * -0)'],
|
||||
['calc(-0px * -1)'],
|
||||
|
||||
// Zeroes
|
||||
['0px'],
|
||||
['0rem'],
|
||||
['0em'],
|
||||
['0dvh'],
|
||||
['-0'],
|
||||
['+0'],
|
||||
['-0.0rem'],
|
||||
['+0.00rem'],
|
||||
])('should constant fold `%s` to `0`', (input) => {
|
||||
expect(constantFoldDeclaration(input)).toBe('0')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['0deg', '0deg'],
|
||||
['0rad', '0rad'],
|
||||
['0%', '0%'],
|
||||
['0turn', '0turn'],
|
||||
['0fr', '0fr'],
|
||||
['0ms', '0ms'],
|
||||
['0s', '0s'],
|
||||
['-0.0deg', '0deg'],
|
||||
['-0.0rad', '0rad'],
|
||||
['-0.0%', '0%'],
|
||||
['-0.0turn', '0turn'],
|
||||
['-0.0fr', '0fr'],
|
||||
['-0.0ms', '0ms'],
|
||||
['-0.0s', '0s'],
|
||||
])('should not fold non-foldable units to `0`. Constant fold `%s` into `%s`', (input, expected) => {
|
||||
expect(constantFoldDeclaration(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('should not constant fold when dividing by `0`', () => {
|
||||
expect(constantFoldDeclaration('calc(123rem / 0)')).toBe('calc(123rem / 0)')
|
||||
})
|
||||
126
packages/tailwindcss/src/constant-fold-declaration.ts
Normal file
126
packages/tailwindcss/src/constant-fold-declaration.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { dimensions } from './utils/dimensions'
|
||||
import { isLength } from './utils/infer-data-type'
|
||||
import * as ValueParser from './value-parser'
|
||||
|
||||
// Assumption: We already assume that we receive somewhat valid `calc()`
|
||||
// expressions. So we will see `calc(1 + 1)` and not `calc(1+1)`
|
||||
export function constantFoldDeclaration(input: string): string {
|
||||
let folded = false
|
||||
let valueAst = ValueParser.parse(input)
|
||||
|
||||
ValueParser.walkDepth(valueAst, (valueNode, { replaceWith }) => {
|
||||
// Convert `-0`, `+0`, `0.0`, … to `0`
|
||||
// Convert `-0px`, `+0em`, `0.0rem`, … to `0`
|
||||
if (
|
||||
valueNode.kind === 'word' &&
|
||||
valueNode.value !== '0' && // Already `0`, nothing to do
|
||||
((valueNode.value[0] === '-' && valueNode.value[1] === '0') || // `-0…`
|
||||
(valueNode.value[0] === '+' && valueNode.value[1] === '0') || // `+0…`
|
||||
valueNode.value[0] === '0') // `0…`
|
||||
) {
|
||||
let dimension = dimensions.get(valueNode.value)
|
||||
if (dimension === null) return // This shouldn't happen
|
||||
|
||||
if (dimension[0] !== 0) return // Not a zero value, nothing to do
|
||||
|
||||
// Replace length units with just `0`
|
||||
if (dimension[1] === null || isLength(valueNode.value)) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word('0'))
|
||||
return
|
||||
}
|
||||
|
||||
// Replace other units with `0<unit>`, e.g. `0%`, `0fr`, `0s`, …
|
||||
else if (valueNode.value !== `0${dimension[1]}`) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word(`0${dimension[1]}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Constant fold `calc()` expressions with two operands and one operator
|
||||
else if (
|
||||
valueNode.kind === 'function' &&
|
||||
(valueNode.value === 'calc' || valueNode.value === '')
|
||||
) {
|
||||
// [
|
||||
// { kind: 'word', value: '0.25rem' }, 0
|
||||
// { kind: 'separator', value: ' ' }, 1
|
||||
// { kind: 'word', value: '*' }, 2
|
||||
// { kind: 'separator', value: ' ' }, 3
|
||||
// { kind: 'word', value: '256' } 4
|
||||
// ]
|
||||
if (valueNode.nodes.length !== 5) return
|
||||
|
||||
let lhs = dimensions.get(valueNode.nodes[0].value)
|
||||
let operator = valueNode.nodes[2].value
|
||||
let rhs = dimensions.get(valueNode.nodes[4].value)
|
||||
|
||||
// Nullify entire expression when multiplying by `0`, e.g.: `calc(0 * 100vw)` -> `0`
|
||||
//
|
||||
// TODO: Ensure it's safe to do so based on the data types?
|
||||
if (
|
||||
operator === '*' &&
|
||||
((lhs?.[0] === 0 && lhs?.[1] === null) || // 0 * something
|
||||
(rhs?.[0] === 0 && rhs?.[1] === null)) // something * 0
|
||||
) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word('0'))
|
||||
return
|
||||
}
|
||||
|
||||
// We're not dealing with dimensions, so we can't fold this
|
||||
if (lhs === null || rhs === null) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (operator) {
|
||||
case '*': {
|
||||
if (
|
||||
lhs[1] === rhs[1] || // Same Units, e.g.: `1rem * 2rem`, `8 * 6`
|
||||
(lhs[1] === null && rhs[1] !== null) || // Unitless * Unit, e.g.: `2 * 1rem`
|
||||
(lhs[1] !== null && rhs[1] === null) // Unit * Unitless, e.g.: `1rem * 2`
|
||||
) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word(`${lhs[0] * rhs[0]}${lhs[1] ?? ''}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case '+': {
|
||||
if (
|
||||
lhs[1] === rhs[1] // Same unit or unitless, e.g.: `1rem + 2rem`, `8 + 6`
|
||||
) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word(`${lhs[0] + rhs[0]}${lhs[1] ?? ''}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case '-': {
|
||||
if (
|
||||
lhs[1] === rhs[1] // Same unit or unitless, e.g.: `2rem - 1rem`, `8 - 6`
|
||||
) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word(`${lhs[0] - rhs[0]}${lhs[1] ?? ''}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case '/': {
|
||||
if (
|
||||
rhs[0] !== 0 && // Don't divide by zero
|
||||
((lhs[1] === null && rhs[1] === null) || // Unitless / Unitless, e.g.: `8 / 2`
|
||||
(lhs[1] !== null && rhs[1] === null)) // Unit / Unitless, e.g.: `1rem / 2`
|
||||
) {
|
||||
folded = true
|
||||
replaceWith(ValueParser.word(`${lhs[0] / rhs[0]}${lhs[1] ?? ''}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return folded ? ValueParser.toCss(valueAst) : input
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { substituteAtApply } from './apply'
|
||||
import { atRule, styleRule, toCss, walk, type AstNode } from './ast'
|
||||
import { printArbitraryValue } from './candidate'
|
||||
import { constantFoldDeclaration } from './constant-fold-declaration'
|
||||
import { CompileAstFlags, type DesignSystem } from './design-system'
|
||||
import * as SelectorParser from './selector-parser'
|
||||
import { ThemeOptions } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { dimensions } from './utils/dimensions'
|
||||
import { isValidSpacingMultiplier } from './utils/infer-data-type'
|
||||
import * as ValueParser from './value-parser'
|
||||
|
||||
|
|
@ -208,39 +208,7 @@ export const computeUtilitySignature = new DefaultMap<
|
|||
// → `calc(0.25rem * 4)` ← this is the case we will see
|
||||
// after inlining the variable
|
||||
// → `1rem`
|
||||
if (node.value.includes('calc')) {
|
||||
let folded = false
|
||||
let valueAst = ValueParser.parse(node.value)
|
||||
ValueParser.walk(valueAst, (valueNode, { replaceWith }) => {
|
||||
if (valueNode.kind !== 'function') return
|
||||
if (valueNode.value !== 'calc') return
|
||||
|
||||
// [
|
||||
// { kind: 'word', value: '0.25rem' }, 0
|
||||
// { kind: 'separator', value: ' ' }, 1
|
||||
// { kind: 'word', value: '*' }, 2
|
||||
// { kind: 'separator', value: ' ' }, 3
|
||||
// { kind: 'word', value: '256' } 4
|
||||
// ]
|
||||
if (valueNode.nodes.length !== 5) return
|
||||
if (valueNode.nodes[2].kind !== 'word' && valueNode.nodes[2].value !== '*') return
|
||||
|
||||
let parsed = dimensions.get(valueNode.nodes[0].value)
|
||||
if (parsed === null) return
|
||||
|
||||
let [value, unit] = parsed
|
||||
|
||||
let multiplier = Number(valueNode.nodes[4].value)
|
||||
if (Number.isNaN(multiplier)) return
|
||||
|
||||
folded = true
|
||||
replaceWith(ValueParser.parse(`${value * multiplier}${unit}`))
|
||||
})
|
||||
|
||||
if (folded) {
|
||||
node.value = ValueParser.toCss(valueAst)
|
||||
}
|
||||
}
|
||||
node.value = constantFoldDeclaration(node.value)
|
||||
|
||||
// We will normalize the `node.value`, this is the same kind of logic
|
||||
// we use when printing arbitrary values. It will remove unnecessary
|
||||
|
|
@ -277,6 +245,17 @@ export const preComputedUtilities = new DefaultMap<DesignSystem, DefaultMap<stri
|
|||
for (let [className, meta] of ds.getClassList()) {
|
||||
let signature = signatures.get(className)
|
||||
if (typeof signature !== 'string') continue
|
||||
|
||||
// Skip the utility if `-{utility}-0` has the same signature as
|
||||
// `{utility}-0` (its positive version). This will prefer positive values
|
||||
// over negative values.
|
||||
if (className[0] === '-' && className.endsWith('-0')) {
|
||||
let positiveSignature = signatures.get(className.slice(1))
|
||||
if (typeof positiveSignature === 'string' && signature === positiveSignature) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
lookup.get(signature).push(className)
|
||||
|
||||
for (let modifier of meta.modifiers) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
|
||||
|
||||
const DIMENSION_REGEX = /^(?<value>-?(?:\d*\.)?\d+)(?<unit>[a-z]+|%)$/i
|
||||
const DIMENSION_REGEX = /^(?<value>[-+]?(?:\d*\.)?\d+)(?<unit>[a-z]+|%)?$/i
|
||||
|
||||
// Parse a dimension such as `64rem` into `[64, 'rem']`.
|
||||
export const dimensions = new DefaultMap((input) => {
|
||||
|
|
@ -10,11 +10,11 @@ export const dimensions = new DefaultMap((input) => {
|
|||
let value = match.groups?.value
|
||||
if (value === undefined) return null
|
||||
|
||||
let unit = match.groups?.unit
|
||||
if (unit === undefined) return null
|
||||
|
||||
let valueAsNumber = Number(value)
|
||||
if (Number.isNaN(valueAsNumber)) return null
|
||||
|
||||
let unit = match.groups?.unit
|
||||
if (unit === undefined) return [valueAsNumber, null] as const
|
||||
|
||||
return [valueAsNumber, unit] as const
|
||||
})
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ const LENGTH_UNITS = [
|
|||
|
||||
const IS_LENGTH = new RegExp(`^${HAS_NUMBER.source}(${LENGTH_UNITS.join('|')})$`)
|
||||
|
||||
function isLength(value: string): boolean {
|
||||
export function isLength(value: string): boolean {
|
||||
return IS_LENGTH.test(value) || hasMathFn(value)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ describe('parse', () => {
|
|||
value: 'theme',
|
||||
nodes: [
|
||||
{ kind: 'word', value: 'colors.red.500' },
|
||||
{ kind: 'separator', value: '/' },
|
||||
{ kind: 'word', value: '/' },
|
||||
{ kind: 'function', value: 'var', nodes: [{ kind: 'word', value: '--opacity' }] },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export type ValueSeparatorNode = {
|
|||
export type ValueAstNode = ValueWordNode | ValueFunctionNode | ValueSeparatorNode
|
||||
type ValueParentNode = ValueFunctionNode | null
|
||||
|
||||
function word(value: string): ValueWordNode {
|
||||
export function word(value: string): ValueWordNode {
|
||||
return {
|
||||
kind: 'word',
|
||||
value,
|
||||
|
|
@ -115,6 +115,71 @@ export function walk(
|
|||
}
|
||||
}
|
||||
|
||||
export function walkDepth(
|
||||
ast: ValueAstNode[],
|
||||
visit: (
|
||||
node: ValueAstNode,
|
||||
utils: {
|
||||
parent: ValueParentNode
|
||||
replaceWith(newNode: ValueAstNode | ValueAstNode[]): void
|
||||
},
|
||||
) => void | ValueWalkAction,
|
||||
parent: ValueParentNode = null,
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
if (node.kind === 'function') {
|
||||
if (walkDepth(node.nodes, visit, node) === ValueWalkAction.Stop) {
|
||||
return ValueWalkAction.Stop
|
||||
}
|
||||
}
|
||||
|
||||
let replacedNode = false
|
||||
let replacedNodeOffset = 0
|
||||
let status =
|
||||
visit(node, {
|
||||
parent,
|
||||
replaceWith(newNode) {
|
||||
if (replacedNode) return
|
||||
replacedNode = true
|
||||
|
||||
if (Array.isArray(newNode)) {
|
||||
if (newNode.length === 0) {
|
||||
ast.splice(i, 1)
|
||||
replacedNodeOffset = 0
|
||||
} else if (newNode.length === 1) {
|
||||
ast[i] = newNode[0]
|
||||
replacedNodeOffset = 1
|
||||
} else {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
replacedNodeOffset = newNode.length
|
||||
}
|
||||
} else {
|
||||
ast[i] = newNode
|
||||
}
|
||||
},
|
||||
}) ?? ValueWalkAction.Continue
|
||||
|
||||
// We want to visit or skip the newly replaced node(s), which start at the
|
||||
// current index (i). By decrementing the index here, the next loop will
|
||||
// process this position (containing the replaced node) again.
|
||||
if (replacedNode) {
|
||||
if (status === ValueWalkAction.Continue) {
|
||||
i--
|
||||
} else {
|
||||
i += replacedNodeOffset - 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Stop the walk entirely
|
||||
if (status === ValueWalkAction.Stop) return ValueWalkAction.Stop
|
||||
|
||||
// Skip visiting the children of this node
|
||||
if (status === ValueWalkAction.Skip) continue
|
||||
}
|
||||
}
|
||||
|
||||
export function toCss(ast: ValueAstNode[]) {
|
||||
let css = ''
|
||||
for (const node of ast) {
|
||||
|
|
@ -172,6 +237,33 @@ export function parse(input: string) {
|
|||
break
|
||||
}
|
||||
|
||||
// Typically for math operators, they have to have spaces around them. But
|
||||
// there are situations in `theme(colors.red.500/10)` where we use `/`
|
||||
// without spaces. Let's make sure this is a separate word as well.
|
||||
case SLASH: {
|
||||
// 1. Handle everything before the separator as a word
|
||||
// Handle everything before the closing paren as a word
|
||||
if (buffer.length > 0) {
|
||||
let node = word(buffer)
|
||||
if (parent) {
|
||||
parent.nodes.push(node)
|
||||
} else {
|
||||
ast.push(node)
|
||||
}
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
// 2. Track the `/` as a word on its own
|
||||
let node = word(input[i])
|
||||
if (parent) {
|
||||
parent.nodes.push(node)
|
||||
} else {
|
||||
ast.push(node)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Space and commas are bundled into separators
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -186,7 +278,6 @@ export function parse(input: string) {
|
|||
case GREATER_THAN:
|
||||
case LESS_THAN:
|
||||
case NEWLINE:
|
||||
case SLASH:
|
||||
case SPACE:
|
||||
case TAB: {
|
||||
// 1. Handle everything before the separator as a word
|
||||
|
|
@ -213,7 +304,6 @@ export function parse(input: string) {
|
|||
peekChar !== GREATER_THAN &&
|
||||
peekChar !== LESS_THAN &&
|
||||
peekChar !== NEWLINE &&
|
||||
peekChar !== SLASH &&
|
||||
peekChar !== SPACE &&
|
||||
peekChar !== TAB
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue