Generalize the walk implementation (#19126)
This PR generalizes the `walk` implementations we have. What's important
here is that we currently have multiple `walk` implementations, one for
the AST, one for the `SelectorParser`, one for the `ValueParser`.
Sometimes, we also need to go up the tree in a depth-first manner. For
that, we have `walkDepth` implementations.
The funny thing is, all these implementations are very very similar,
even the kinds of trees are very similar. They are just objects with
`nodes: []` as children.
So this PR introduces a generic `walk` function that can work on all of
these trees.
There are also some situations where you need to go down and back up the
tree. For this reason, we added an `enter` and `exit` phase:
```ts
walk(ast, {
enter(node, ctx) {},
exit(node, ctx) {},
})
```
This means that you don't need to `walk(ast)` and later `walkDepth(ast)`
in case you wanted to do something _after_ visiting all nodes.
The API of these walk functions also slightly changed to fix some
problems we've had before. One is the `replaceWith` function. You could
technically call it multiple times, but that doesn't make sense so
instead you always have to return an explicit `WalkAction`. The
possibilities are:
```ts
// The ones we already had
WalkAction.Continue // Continue walking as normal, the default behavior
WalkAction.Skip // Skip walking the `nodes` of the current node
WalkAction.Stop // Stop the entire walk
// The new ones
WalkAction.Replace(newNode) // Replace the current node, and continue walking the new node(s)
WalkAction.ReplaceSkip(newNode) // Replace the current node, but don't walk the new node(s)
WalkAction.ReplaceStop(newNode) // Replace the current node, but stop the entire walk
```
To make sure that we can walk in both directions, and to make sure we
have proper control over when to walk which nodes, the `walk` function
is implemented in an iterative manner using a stack instead of
recursion.
This also means that a `WalkAction.Stop` or `WalkAction.ReplaceStop`
will immediately stop the walk, without unwinding the entire call stack.
Some notes:
- The CSS AST does have `context` nodes, for this we can build up the
context lazily when we need it. I added a `cssContext(ctx)` that gives
you an enhanced context including the `context` object that you can read
information from.
- The second argument of the `walk` function can still be a normal
function, which is equivalent to `{ enter: fn }`.
Let's also take a look at some numbers. With this new implementation,
each `walk` is roughly ~1.3-1.5x faster than before. If you look at the
memory usage (especially in Bun) we go from `~2.2GB` peak memory usage,
to `~300mb` peak memory usage.
Some benchmarks on small and big trees (M1 Max):
<img width="2062" height="1438" alt="image"
src="https://github.com/user-attachments/assets/5ec8c22a-9de8-4e08-869a-18c0d30eb7e8"
/>
<img width="2062" height="1246" alt="image"
src="https://github.com/user-attachments/assets/e89d4b8e-29ca-4aee-8fd2-b7c043d3bbf4"
/>
We also ran some benchmarks on @thecrypticace's M3 Max:
<img width="1598" height="1452" alt="image"
src="https://github.com/user-attachments/assets/3b06b6fe-2497-4f24-a428-1a0e2af3896a"
/>
In node the memory difference isn't that big, but the performance itself
is still better:
<img width="2034" height="1586" alt="image"
src="https://github.com/user-attachments/assets/ef28ae14-b53e-4912-9621-531f3b02898f"
/>
In summary:
1. Single `walk` implementation for multiple use cases
2. Support for `enter` and `exit` phases
3. New `WalkAction` possibilities for better control
4. Overall better performance
5. ... and lower memory usage
## Test plan
1. All tests still pass (but had to adjust some of the APIs if `walk`
was used inside tests).
2. Added new tests for the `walk` implementation
3. Ran local benchmarks to verify the performance improvements
This commit is contained in:
parent
fc63ce7fbc
commit
acb27ef9e9
29 changed files with 2179 additions and 736 deletions
|
|
@ -5,8 +5,9 @@
|
|||
// Minor modifications have been made to work with the Tailwind CSS codebase
|
||||
|
||||
import * as path from 'node:path'
|
||||
import { toCss, walk } from '../../tailwindcss/src/ast'
|
||||
import { toCss } from '../../tailwindcss/src/ast'
|
||||
import { parse } from '../../tailwindcss/src/css-parser'
|
||||
import { walk } from '../../tailwindcss/src/walk'
|
||||
import { normalizePath } from './normalize-path'
|
||||
|
||||
const cssUrlRE =
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
|||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path'
|
||||
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
|
||||
import { walk, WalkAction } from '../../../../tailwindcss/src/walk'
|
||||
import * as version from '../../utils/version'
|
||||
|
||||
// Defaults in v4
|
||||
|
|
@ -117,7 +118,7 @@ function substituteFunctionsInValue(
|
|||
ast: ValueParser.ValueAstNode[],
|
||||
handle: (value: string, fallback?: string) => string | null,
|
||||
) {
|
||||
ValueParser.walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'function' && node.value === 'theme') {
|
||||
if (node.nodes.length < 1) return
|
||||
|
||||
|
|
@ -155,7 +156,7 @@ function substituteFunctionsInValue(
|
|||
fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path)
|
||||
if (replacement === null) return
|
||||
|
||||
replaceWith(ValueParser.parse(replacement))
|
||||
return WalkAction.Replace(ValueParser.parse(replacement))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { walk, WalkAction } from '../../../../tailwindcss/src/ast'
|
||||
import { cloneCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import type { Writable } from '../../../../tailwindcss/src/types'
|
||||
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
|
||||
import { walk, WalkAction } from '../../../../tailwindcss/src/walk'
|
||||
|
||||
export function migrateAutomaticVarInjection(
|
||||
designSystem: DesignSystem,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { isValidSpacingMultiplier } from '../../../../tailwindcss/src/utils/infe
|
|||
import { segment } from '../../../../tailwindcss/src/utils/segment'
|
||||
import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path'
|
||||
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
|
||||
import { walk, WalkAction } from '../../../../tailwindcss/src/walk'
|
||||
|
||||
export const enum Convert {
|
||||
All = 0,
|
||||
|
|
@ -27,7 +28,7 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals
|
|||
let themeModifierCount = 0
|
||||
|
||||
// Analyze AST
|
||||
ValueParser.walk(ast, (node) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'function') return
|
||||
if (node.value !== 'theme') return
|
||||
|
||||
|
|
@ -35,19 +36,19 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals
|
|||
themeUsageCount += 1
|
||||
|
||||
// Figure out if a modifier is used
|
||||
ValueParser.walk(node.nodes, (child) => {
|
||||
walk(node.nodes, (child) => {
|
||||
// If we see a `,`, it means that we have a fallback value
|
||||
if (child.kind === 'separator' && child.value.includes(',')) {
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
// If we see a `/`, we have a modifier
|
||||
else if (child.kind === 'word' && child.value === '/') {
|
||||
themeModifierCount += 1
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
return WalkAction.Skip
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -172,7 +173,7 @@ function substituteFunctionsInValue(
|
|||
ast: ValueParser.ValueAstNode[],
|
||||
handle: (value: string, fallback?: string) => string | null,
|
||||
) {
|
||||
ValueParser.walk(ast, (node, { parent, replaceWith }) => {
|
||||
walk(ast, (node, ctx) => {
|
||||
if (node.kind === 'function' && node.value === 'theme') {
|
||||
if (node.nodes.length < 1) return
|
||||
|
||||
|
|
@ -210,10 +211,10 @@ function substituteFunctionsInValue(
|
|||
fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path)
|
||||
if (replacement === null) return
|
||||
|
||||
if (parent) {
|
||||
let idx = parent.nodes.indexOf(node) - 1
|
||||
if (ctx.parent) {
|
||||
let idx = ctx.parent.nodes.indexOf(node) - 1
|
||||
while (idx !== -1) {
|
||||
let previous = parent.nodes[idx]
|
||||
let previous = ctx.parent.nodes[idx]
|
||||
// Skip the space separator
|
||||
if (previous.kind === 'separator' && previous.value.trim() === '') {
|
||||
idx -= 1
|
||||
|
|
@ -241,7 +242,7 @@ function substituteFunctionsInValue(
|
|||
}
|
||||
}
|
||||
|
||||
replaceWith(ValueParser.parse(replacement))
|
||||
return WalkAction.Replace(ValueParser.parse(replacement))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { walk, type AstNode } from '../../../../tailwindcss/src/ast'
|
||||
import { type AstNode } from '../../../../tailwindcss/src/ast'
|
||||
import { type Variant } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { walk } from '../../../../tailwindcss/src/walk'
|
||||
import * as version from '../../utils/version'
|
||||
|
||||
export function migrateVariantOrder(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Features } from '.'
|
||||
import { cloneAstNode, rule, toCss, walk, WalkAction, type AstNode } from './ast'
|
||||
import { cloneAstNode, rule, toCss, type AstNode } from './ast'
|
||||
import { compileCandidates } from './compile'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import type { SourceLocation } from './source-maps/source'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { segment } from './utils/segment'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
||||
let features = Features.None
|
||||
|
|
@ -25,7 +26,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
|||
let definitions = new DefaultMap(() => new Set<AstNode>())
|
||||
|
||||
// Collect all new `@utility` definitions and all `@apply` rules first
|
||||
walk([root], (node, { parent, path }) => {
|
||||
walk([root], (node, ctx) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
||||
// Do not allow `@apply` rules inside `@keyframes` rules.
|
||||
|
|
@ -61,16 +62,15 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
|||
if (node.name === '@apply') {
|
||||
// `@apply` cannot be top-level, so we need to have a parent such that we
|
||||
// can replace the `@apply` node with the actual utility classes later.
|
||||
if (parent === null) return
|
||||
if (ctx.parent === null) return
|
||||
|
||||
features |= Features.AtApply
|
||||
|
||||
parents.add(parent)
|
||||
parents.add(ctx.parent)
|
||||
|
||||
for (let dependency of resolveApplyDependencies(node, designSystem)) {
|
||||
// Mark every parent in the path as having a dependency to that utility.
|
||||
for (let parent of path) {
|
||||
if (parent === node) continue
|
||||
for (let parent of ctx.path()) {
|
||||
if (!parents.has(parent)) continue
|
||||
dependencies.get(parent).add(dependency)
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
|||
for (let parent of sorted) {
|
||||
if (!('nodes' in parent)) continue
|
||||
|
||||
walk(parent.nodes, (child, { replaceWith }) => {
|
||||
walk(parent.nodes, (child) => {
|
||||
if (child.kind !== 'at-rule' || child.name !== '@apply') return
|
||||
|
||||
let parts = child.params.split(/(\s+)/g)
|
||||
|
|
@ -291,7 +291,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
|||
}
|
||||
}
|
||||
|
||||
replaceWith(newNodes)
|
||||
return WalkAction.Replace(newNodes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { expect, it } from 'vitest'
|
|||
import {
|
||||
atRule,
|
||||
context,
|
||||
cssContext,
|
||||
decl,
|
||||
optimizeAst,
|
||||
styleRule,
|
||||
toCss,
|
||||
walk,
|
||||
WalkAction,
|
||||
type AstNode,
|
||||
} from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
import { buildDesignSystem } from './design-system'
|
||||
import { Theme } from './theme'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
const css = String.raw
|
||||
const defaultDesignSystem = buildDesignSystem(new Theme())
|
||||
|
|
@ -31,7 +31,7 @@ it('should pretty print an AST', () => {
|
|||
})
|
||||
|
||||
it('allows the placement of context nodes', () => {
|
||||
const ast = [
|
||||
let ast: AstNode[] = [
|
||||
styleRule('.foo', [decl('color', 'red')]),
|
||||
context({ context: 'a' }, [
|
||||
styleRule('.bar', [
|
||||
|
|
@ -48,17 +48,18 @@ it('allows the placement of context nodes', () => {
|
|||
let blueContext
|
||||
let greenContext
|
||||
|
||||
walk(ast, (node, { context }) => {
|
||||
walk(ast, (node, _ctx) => {
|
||||
if (node.kind !== 'declaration') return
|
||||
let ctx = cssContext(_ctx)
|
||||
switch (node.value) {
|
||||
case 'red':
|
||||
redContext = context
|
||||
redContext = ctx.context
|
||||
break
|
||||
case 'blue':
|
||||
blueContext = context
|
||||
blueContext = ctx.context
|
||||
break
|
||||
case 'green':
|
||||
greenContext = context
|
||||
greenContext = ctx.context
|
||||
break
|
||||
}
|
||||
})
|
||||
|
|
@ -292,25 +293,25 @@ it('should not emit exact duplicate declarations in the same rule', () => {
|
|||
it('should only visit children once when calling `replaceWith` with single element array', () => {
|
||||
let visited = new Set()
|
||||
|
||||
let ast = [
|
||||
let ast: AstNode[] = [
|
||||
atRule('@media', '', [styleRule('.foo', [decl('color', 'blue')])]),
|
||||
styleRule('.bar', [decl('color', 'blue')]),
|
||||
]
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (visited.has(node)) {
|
||||
throw new Error('Visited node twice')
|
||||
}
|
||||
visited.add(node)
|
||||
|
||||
if (node.kind === 'at-rule') replaceWith(node.nodes)
|
||||
if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes)
|
||||
})
|
||||
})
|
||||
|
||||
it('should only visit children once when calling `replaceWith` with multi-element array', () => {
|
||||
let visited = new Set()
|
||||
|
||||
let ast = [
|
||||
let ast: AstNode[] = [
|
||||
atRule('@media', '', [
|
||||
context({}, [
|
||||
styleRule('.foo', [decl('color', 'red')]),
|
||||
|
|
@ -320,19 +321,20 @@ it('should only visit children once when calling `replaceWith` with multi-elemen
|
|||
styleRule('.bar', [decl('color', 'green')]),
|
||||
]
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
let key = id(node)
|
||||
if (visited.has(key)) {
|
||||
throw new Error('Visited node twice')
|
||||
}
|
||||
visited.add(key)
|
||||
|
||||
if (node.kind === 'at-rule') replaceWith(node.nodes)
|
||||
if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes)
|
||||
})
|
||||
|
||||
expect(visited).toMatchInlineSnapshot(`
|
||||
Set {
|
||||
"@media ",
|
||||
"<context>",
|
||||
".foo",
|
||||
"color: red",
|
||||
".baz",
|
||||
|
|
@ -348,14 +350,13 @@ it('should never visit children when calling `replaceWith` with `WalkAction.Skip
|
|||
|
||||
let inner = styleRule('.foo', [decl('color', 'blue')])
|
||||
|
||||
let ast = [atRule('@media', '', [inner]), styleRule('.bar', [decl('color', 'blue')])]
|
||||
let ast: AstNode[] = [atRule('@media', '', [inner]), styleRule('.bar', [decl('color', 'blue')])]
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.add(node)
|
||||
|
||||
if (node.kind === 'at-rule') {
|
||||
replaceWith(node.nodes)
|
||||
return WalkAction.Skip
|
||||
return WalkAction.ReplaceSkip(node.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -413,11 +414,10 @@ it('should skip the correct number of children based on the replaced children no
|
|||
decl('--index', '4'),
|
||||
]
|
||||
let visited: string[] = []
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.push(id(node))
|
||||
if (node.kind === 'declaration' && node.value === '2') {
|
||||
replaceWith([])
|
||||
return WalkAction.Skip
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -441,11 +441,10 @@ it('should skip the correct number of children based on the replaced children no
|
|||
decl('--index', '4'),
|
||||
]
|
||||
let visited: string[] = []
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.push(id(node))
|
||||
if (node.kind === 'declaration' && node.value === '2') {
|
||||
replaceWith([])
|
||||
return WalkAction.Continue
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -469,11 +468,10 @@ it('should skip the correct number of children based on the replaced children no
|
|||
decl('--index', '4'),
|
||||
]
|
||||
let visited: string[] = []
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.push(id(node))
|
||||
if (node.kind === 'declaration' && node.value === '2') {
|
||||
replaceWith([decl('--index', '2.1')])
|
||||
return WalkAction.Skip
|
||||
return WalkAction.ReplaceSkip([decl('--index', '2.1')])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -497,11 +495,10 @@ it('should skip the correct number of children based on the replaced children no
|
|||
decl('--index', '4'),
|
||||
]
|
||||
let visited: string[] = []
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.push(id(node))
|
||||
if (node.kind === 'declaration' && node.value === '2') {
|
||||
replaceWith([decl('--index', '2.1')])
|
||||
return WalkAction.Continue
|
||||
return WalkAction.Replace([decl('--index', '2.1')])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -526,11 +523,10 @@ it('should skip the correct number of children based on the replaced children no
|
|||
decl('--index', '4'),
|
||||
]
|
||||
let visited: string[] = []
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.push(id(node))
|
||||
if (node.kind === 'declaration' && node.value === '2') {
|
||||
replaceWith([decl('--index', '2.1'), decl('--index', '2.2')])
|
||||
return WalkAction.Skip
|
||||
return WalkAction.ReplaceSkip([decl('--index', '2.1'), decl('--index', '2.2')])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -554,11 +550,10 @@ it('should skip the correct number of children based on the replaced children no
|
|||
decl('--index', '4'),
|
||||
]
|
||||
let visited: string[] = []
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
visited.push(id(node))
|
||||
if (node.kind === 'declaration' && node.value === '2') {
|
||||
replaceWith([decl('--index', '2.1'), decl('--index', '2.2')])
|
||||
return WalkAction.Continue
|
||||
return WalkAction.Replace([decl('--index', '2.1'), decl('--index', '2.2')])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Theme, ThemeOptions } from './theme'
|
|||
import { DefaultMap } from './utils/default-map'
|
||||
import { extractUsedVariables } from './utils/variables'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction, type VisitContext } from './walk'
|
||||
|
||||
const AT_SIGN = 0x40
|
||||
|
||||
|
|
@ -184,157 +185,33 @@ export function cloneAstNode<T extends AstNode>(node: T): T {
|
|||
}
|
||||
}
|
||||
|
||||
export const enum WalkAction {
|
||||
/** Continue walking, which is the default */
|
||||
Continue,
|
||||
|
||||
/** Skip visiting the children of this node */
|
||||
Skip,
|
||||
|
||||
/** Stop the walk entirely */
|
||||
Stop,
|
||||
}
|
||||
|
||||
export function walk(
|
||||
ast: AstNode[],
|
||||
visit: (
|
||||
node: AstNode,
|
||||
utils: {
|
||||
parent: AstNode | null
|
||||
replaceWith(newNode: AstNode | AstNode[]): void
|
||||
context: Record<string, string | boolean>
|
||||
path: AstNode[]
|
||||
},
|
||||
) => void | WalkAction,
|
||||
path: AstNode[] = [],
|
||||
context: Record<string, string | boolean> = {},
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
let parent = path[path.length - 1] ?? null
|
||||
|
||||
// We want context nodes to be transparent in walks. This means that
|
||||
// whenever we encounter one, we immediately walk through its children and
|
||||
// furthermore we also don't update the parent.
|
||||
if (node.kind === 'context') {
|
||||
if (walk(node.nodes, visit, path, { ...context, ...node.context }) === WalkAction.Stop) {
|
||||
return WalkAction.Stop
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
path.push(node)
|
||||
let replacedNode = false
|
||||
let replacedNodeOffset = 0
|
||||
let status =
|
||||
visit(node, {
|
||||
parent,
|
||||
context,
|
||||
path,
|
||||
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
|
||||
replacedNodeOffset = 1
|
||||
}
|
||||
},
|
||||
}) ?? WalkAction.Continue
|
||||
path.pop()
|
||||
|
||||
// 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 === WalkAction.Continue) {
|
||||
i--
|
||||
} else {
|
||||
i += replacedNodeOffset - 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Stop the walk entirely
|
||||
if (status === WalkAction.Stop) return WalkAction.Stop
|
||||
|
||||
// Skip visiting the children of this node
|
||||
if (status === WalkAction.Skip) continue
|
||||
|
||||
if ('nodes' in node) {
|
||||
path.push(node)
|
||||
let result = walk(node.nodes, visit, path, context)
|
||||
path.pop()
|
||||
|
||||
if (result === WalkAction.Stop) {
|
||||
return WalkAction.Stop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is a depth-first traversal of the AST
|
||||
export function walkDepth(
|
||||
ast: AstNode[],
|
||||
visit: (
|
||||
node: AstNode,
|
||||
utils: {
|
||||
parent: AstNode | null
|
||||
path: AstNode[]
|
||||
context: Record<string, string | boolean>
|
||||
replaceWith(newNode: AstNode[]): void
|
||||
},
|
||||
) => void,
|
||||
path: AstNode[] = [],
|
||||
context: Record<string, string | boolean> = {},
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
let parent = path[path.length - 1] ?? null
|
||||
|
||||
if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
path.push(node)
|
||||
walkDepth(node.nodes, visit, path, context)
|
||||
path.pop()
|
||||
} else if (node.kind === 'context') {
|
||||
walkDepth(node.nodes, visit, path, { ...context, ...node.context })
|
||||
continue
|
||||
}
|
||||
|
||||
path.push(node)
|
||||
visit(node, {
|
||||
parent,
|
||||
context,
|
||||
path,
|
||||
replaceWith(newNode) {
|
||||
if (Array.isArray(newNode)) {
|
||||
if (newNode.length === 0) {
|
||||
ast.splice(i, 1)
|
||||
} else if (newNode.length === 1) {
|
||||
ast[i] = newNode[0]
|
||||
} else {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
}
|
||||
} else {
|
||||
ast[i] = newNode
|
||||
export function cssContext(
|
||||
ctx: VisitContext<AstNode>,
|
||||
): VisitContext<AstNode> & { context: Record<string, string | boolean> } {
|
||||
return {
|
||||
depth: ctx.depth,
|
||||
get context() {
|
||||
let context: Record<string, string | boolean> = {}
|
||||
for (let child of ctx.path()) {
|
||||
if (child.kind === 'context') {
|
||||
Object.assign(context, child.context)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip over the newly inserted nodes (being depth-first it doesn't make sense to visit them)
|
||||
i += newNode.length - 1
|
||||
},
|
||||
})
|
||||
path.pop()
|
||||
// Once computed, we never need to compute this again
|
||||
Object.defineProperty(this, 'context', { value: context })
|
||||
return context
|
||||
},
|
||||
get parent() {
|
||||
let parent = (this.path().pop() as Extract<AstNode, { nodes: AstNode[] }>) ?? null
|
||||
|
||||
// Once computed, we never need to compute this again
|
||||
Object.defineProperty(this, 'parent', { value: parent })
|
||||
return parent
|
||||
},
|
||||
path() {
|
||||
return ctx.path().filter((n) => n.kind !== 'context')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -642,12 +519,12 @@ export function optimizeAst(
|
|||
|
||||
let ast = ValueParser.parse(declaration.value)
|
||||
let requiresPolyfill = false
|
||||
ValueParser.walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'function' || node.value !== 'color-mix') return
|
||||
|
||||
let containsUnresolvableVars = false
|
||||
let containsCurrentcolor = false
|
||||
ValueParser.walk(node.nodes, (node, { replaceWith }) => {
|
||||
walk(node.nodes, (node) => {
|
||||
if (node.kind == 'word' && node.value.toLowerCase() === 'currentcolor') {
|
||||
containsCurrentcolor = true
|
||||
requiresPolyfill = true
|
||||
|
|
@ -691,7 +568,7 @@ export function optimizeAst(
|
|||
}
|
||||
} while (varNode)
|
||||
|
||||
replaceWith({ kind: 'word', value: inlinedColor })
|
||||
return WalkAction.Replace({ kind: 'word', value: inlinedColor } as const)
|
||||
})
|
||||
|
||||
if (containsUnresolvableVars || containsCurrentcolor) {
|
||||
|
|
@ -702,7 +579,7 @@ export function optimizeAst(
|
|||
let firstColorValue =
|
||||
node.nodes.length > separatorIndex ? node.nodes[separatorIndex + 1] : null
|
||||
if (!firstColorValue) return
|
||||
replaceWith(firstColorValue)
|
||||
return WalkAction.Replace(firstColorValue)
|
||||
} else if (requiresPolyfill) {
|
||||
// Change the colorspace to `srgb` since the fallback values should not be represented as
|
||||
// `oklab(…)` functions again as their support in Safari <16 is very limited.
|
||||
|
|
@ -1005,9 +882,10 @@ export function toCss(ast: AstNode[], track?: boolean) {
|
|||
|
||||
function findNode(ast: AstNode[], fn: (node: AstNode) => boolean): AstNode[] | null {
|
||||
let foundPath: AstNode[] = []
|
||||
walk(ast, (node, { path }) => {
|
||||
walk(ast, (node, ctx) => {
|
||||
if (fn(node)) {
|
||||
foundPath = [...path]
|
||||
foundPath = ctx.path()
|
||||
foundPath.push(node)
|
||||
return WalkAction.Stop
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Features } from '.'
|
||||
import { atRule, context, walk, WalkAction, type AstNode } from './ast'
|
||||
import { atRule, context, type AstNode } from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
type LoadStylesheet = (
|
||||
id: string,
|
||||
|
|
@ -22,7 +23,7 @@ export async function substituteAtImports(
|
|||
let features = Features.None
|
||||
let promises: Promise<void>[] = []
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'at-rule' && (node.name === '@import' || node.name === '@reference')) {
|
||||
let parsed = parseImportParams(ValueParser.parse(node.params))
|
||||
if (parsed === null) return
|
||||
|
|
@ -66,11 +67,9 @@ export async function substituteAtImports(
|
|||
})(),
|
||||
)
|
||||
|
||||
replaceWith(contextNode)
|
||||
|
||||
// The resolved Stylesheets already have their transitive @imports
|
||||
// resolved, so we can skip walking them.
|
||||
return WalkAction.Skip
|
||||
return WalkAction.ReplaceSkip(contextNode)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { DefaultMap } from './utils/default-map'
|
|||
import { isValidArbitrary } from './utils/is-valid-arbitrary'
|
||||
import { segment } from './utils/segment'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
const COLON = 0x3a
|
||||
const DASH = 0x2d
|
||||
|
|
@ -1025,8 +1026,8 @@ const printArbitraryValueCache = new DefaultMap<string, string>((input) => {
|
|||
|
||||
let drop = new Set<ValueParser.ValueAstNode>()
|
||||
|
||||
ValueParser.walk(ast, (node, { parent }) => {
|
||||
let parentArray = parent === null ? ast : (parent.nodes ?? [])
|
||||
walk(ast, (node, ctx) => {
|
||||
let parentArray = ctx.parent === null ? ast : (ctx.parent.nodes ?? [])
|
||||
|
||||
// Handle operators (e.g.: inside of `calc(…)`)
|
||||
if (
|
||||
|
|
@ -1064,10 +1065,10 @@ const printArbitraryValueCache = new DefaultMap<string, string>((input) => {
|
|||
})
|
||||
|
||||
if (drop.size > 0) {
|
||||
ValueParser.walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (drop.has(node)) {
|
||||
drop.delete(node)
|
||||
replaceWith([])
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { replaceObject } from './utils/replace-object'
|
|||
import { segment } from './utils/segment'
|
||||
import { toKeyPath } from './utils/to-key-path'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
export interface CanonicalizeOptions {
|
||||
/**
|
||||
|
|
@ -289,7 +290,7 @@ const converterCache = new DefaultMap((ds: DesignSystem) => {
|
|||
let themeModifierCount = 0
|
||||
|
||||
// Analyze AST
|
||||
ValueParser.walk(ast, (node) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'function') return
|
||||
if (node.value !== 'theme') return
|
||||
|
||||
|
|
@ -297,19 +298,19 @@ const converterCache = new DefaultMap((ds: DesignSystem) => {
|
|||
themeUsageCount += 1
|
||||
|
||||
// Figure out if a modifier is used
|
||||
ValueParser.walk(node.nodes, (child) => {
|
||||
walk(node.nodes, (child) => {
|
||||
// If we see a `,`, it means that we have a fallback value
|
||||
if (child.kind === 'separator' && child.value.includes(',')) {
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
// If we see a `/`, we have a modifier
|
||||
else if (child.kind === 'word' && child.value === '/') {
|
||||
themeModifierCount += 1
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
return WalkAction.Skip
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -434,7 +435,7 @@ function substituteFunctionsInValue(
|
|||
ast: ValueParser.ValueAstNode[],
|
||||
handle: (value: string, fallback?: string) => string | null,
|
||||
) {
|
||||
ValueParser.walk(ast, (node, { parent, replaceWith }) => {
|
||||
walk(ast, (node, ctx) => {
|
||||
if (node.kind === 'function' && node.value === 'theme') {
|
||||
if (node.nodes.length < 1) return
|
||||
|
||||
|
|
@ -472,10 +473,10 @@ function substituteFunctionsInValue(
|
|||
fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path)
|
||||
if (replacement === null) return
|
||||
|
||||
if (parent) {
|
||||
let idx = parent.nodes.indexOf(node) - 1
|
||||
if (ctx.parent) {
|
||||
let idx = ctx.parent.nodes.indexOf(node) - 1
|
||||
while (idx !== -1) {
|
||||
let previous = parent.nodes[idx]
|
||||
let previous = ctx.parent.nodes[idx]
|
||||
// Skip the space separator
|
||||
if (previous.kind === 'separator' && previous.value.trim() === '') {
|
||||
idx -= 1
|
||||
|
|
@ -503,7 +504,7 @@ function substituteFunctionsInValue(
|
|||
}
|
||||
}
|
||||
|
||||
replaceWith(ValueParser.parse(replacement))
|
||||
return WalkAction.Replace(ValueParser.parse(replacement))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -780,7 +781,7 @@ function allVariablesAreUsed(
|
|||
.join('\n')
|
||||
|
||||
let isSafeMigration = true
|
||||
ValueParser.walk(ValueParser.parse(value), (node) => {
|
||||
walk(ValueParser.parse(value), (node) => {
|
||||
if (node.kind === 'function' && node.value === 'var') {
|
||||
let variable = node.nodes[0].value
|
||||
let r = new RegExp(`var\\(${variable}[,)]\\s*`, 'g')
|
||||
|
|
@ -792,7 +793,7 @@ function allVariablesAreUsed(
|
|||
replacementAsCss.includes(`${variable}:`)
|
||||
) {
|
||||
isSafeMigration = false
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.Stop
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -1240,16 +1241,16 @@ function modernizeArbitraryValuesVariant(
|
|||
|
||||
let parsed = ValueParser.parse(SelectorParser.toCss(ast))
|
||||
let containsNot = false
|
||||
ValueParser.walk(parsed, (node, { replaceWith }) => {
|
||||
walk(parsed, (node) => {
|
||||
if (node.kind === 'word' && node.value === 'not') {
|
||||
containsNot = true
|
||||
replaceWith([])
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
})
|
||||
|
||||
// Remove unnecessary whitespace
|
||||
parsed = ValueParser.parse(ValueParser.toCss(parsed))
|
||||
ValueParser.walk(parsed, (node) => {
|
||||
walk(parsed, (node) => {
|
||||
if (node.kind === 'separator' && node.value !== ' ' && node.value.trim() === '') {
|
||||
// node.value contains at least 2 spaces. Normalize it to a single
|
||||
// space.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Features } from '..'
|
||||
import { styleRule, toCss, walk, WalkAction, type AstNode } from '../ast'
|
||||
import { cssContext, styleRule, toCss, type AstNode } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { SourceLocation } from '../source-maps/source'
|
||||
import { segment } from '../utils/segment'
|
||||
import { walk, WalkAction } from '../walk'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
|
||||
import { createCompatConfig } from './config/create-compat-config'
|
||||
|
|
@ -50,12 +51,13 @@ export async function applyCompatibilityHooks({
|
|||
src: SourceLocation | undefined
|
||||
}[] = []
|
||||
|
||||
walk(ast, (node, { parent, replaceWith, context }) => {
|
||||
walk(ast, (node, _ctx) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
let ctx = cssContext(_ctx)
|
||||
|
||||
// Collect paths from `@plugin` at-rules
|
||||
if (node.name === '@plugin') {
|
||||
if (parent !== null) {
|
||||
if (ctx.parent !== null) {
|
||||
throw new Error('`@plugin` cannot be nested.')
|
||||
}
|
||||
|
||||
|
|
@ -110,16 +112,15 @@ export async function applyCompatibilityHooks({
|
|||
pluginPaths.push([
|
||||
{
|
||||
id: pluginPath,
|
||||
base: context.base as string,
|
||||
reference: !!context.reference,
|
||||
base: ctx.context.base as string,
|
||||
reference: !!ctx.context.reference,
|
||||
src: node.src,
|
||||
},
|
||||
Object.keys(options).length > 0 ? options : null,
|
||||
])
|
||||
|
||||
replaceWith([])
|
||||
features |= Features.JsPluginCompat
|
||||
return
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
|
||||
// Collect paths from `@config` at-rules
|
||||
|
|
@ -128,19 +129,18 @@ export async function applyCompatibilityHooks({
|
|||
throw new Error('`@config` cannot have a body.')
|
||||
}
|
||||
|
||||
if (parent !== null) {
|
||||
if (ctx.parent !== null) {
|
||||
throw new Error('`@config` cannot be nested.')
|
||||
}
|
||||
|
||||
configPaths.push({
|
||||
id: node.params.slice(1, -1),
|
||||
base: context.base as string,
|
||||
reference: !!context.reference,
|
||||
base: ctx.context.base as string,
|
||||
reference: !!ctx.context.reference,
|
||||
src: node.src,
|
||||
})
|
||||
replaceWith([])
|
||||
features |= Features.JsPluginCompat
|
||||
return
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -386,18 +386,18 @@ function upgradeToFullPluginSupport({
|
|||
if (typeof resolvedConfig.important === 'string') {
|
||||
let wrappingSelector = resolvedConfig.important
|
||||
|
||||
walk(ast, (node, { replaceWith, parent }) => {
|
||||
walk(ast, (node, _ctx) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
if (node.name !== '@tailwind' || node.params !== 'utilities') return
|
||||
|
||||
let ctx = cssContext(_ctx)
|
||||
|
||||
// The AST node was already manually wrapped so there's nothing to do
|
||||
if (parent?.kind === 'rule' && parent.selector === wrappingSelector) {
|
||||
if (ctx.parent?.kind === 'rule' && ctx.parent.selector === wrappingSelector) {
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
replaceWith(styleRule(wrappingSelector, [node]))
|
||||
|
||||
return WalkAction.Stop
|
||||
return WalkAction.ReplaceStop(styleRule(wrappingSelector, [node]))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Features } from '..'
|
||||
import { substituteAtApply } from '../apply'
|
||||
import { atRule, cloneAstNode, decl, rule, walk, type AstNode } from '../ast'
|
||||
import { atRule, cloneAstNode, decl, rule, type AstNode } from '../ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate'
|
||||
import { substituteFunctions } from '../css-functions'
|
||||
import * as CSS from '../css-parser'
|
||||
|
|
@ -14,6 +14,7 @@ import { inferDataType } from '../utils/infer-data-type'
|
|||
import { segment } from '../utils/segment'
|
||||
import { toKeyPath } from '../utils/to-key-path'
|
||||
import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtSlot } from '../variants'
|
||||
import { walk, WalkAction } from '../walk'
|
||||
import type { ResolvedConfig, UserConfig } from './config/types'
|
||||
import { createThemeFn } from './plugin-functions'
|
||||
|
||||
|
|
@ -281,7 +282,7 @@ export function buildPluginApi({
|
|||
let selectorAst = SelectorParser.parse(name)
|
||||
let foundValidUtility = false
|
||||
|
||||
SelectorParser.walk(selectorAst, (node) => {
|
||||
walk(selectorAst, (node) => {
|
||||
if (
|
||||
node.kind === 'selector' &&
|
||||
node.value[0] === '.' &&
|
||||
|
|
@ -301,7 +302,7 @@ export function buildPluginApi({
|
|||
}
|
||||
|
||||
if (node.kind === 'function' && node.value === ':not') {
|
||||
return SelectorParser.SelectorWalkAction.Skip
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -318,7 +319,7 @@ export function buildPluginApi({
|
|||
walk(ast, (node) => {
|
||||
if (node.kind === 'rule') {
|
||||
let selectorAst = SelectorParser.parse(node.selector)
|
||||
SelectorParser.walk(selectorAst, (node) => {
|
||||
walk(selectorAst, (node) => {
|
||||
if (node.kind === 'selector' && node.value[0] === '.') {
|
||||
node.value = `.${designSystem.theme.prefix}\\:${node.value.slice(1)}`
|
||||
}
|
||||
|
|
@ -612,7 +613,7 @@ function replaceNestedClassNameReferences(
|
|||
walk(ast, (node) => {
|
||||
if (node.kind === 'rule') {
|
||||
let selectorAst = SelectorParser.parse(node.selector)
|
||||
SelectorParser.walk(selectorAst, (node) => {
|
||||
walk(selectorAst, (node) => {
|
||||
if (node.kind === 'selector' && node.value === `.${utilityName}`) {
|
||||
node.value = `.${escape(rawCandidate)}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
atRule,
|
||||
decl,
|
||||
rule,
|
||||
walk,
|
||||
WalkAction,
|
||||
type AstNode,
|
||||
type Rule,
|
||||
type StyleRule,
|
||||
} from './ast'
|
||||
import { atRule, decl, rule, type AstNode, type Rule, type StyleRule } from './ast'
|
||||
import { type Candidate, type Variant } from './candidate'
|
||||
import { CompileAstFlags, type DesignSystem } from './design-system'
|
||||
import GLOBAL_PROPERTY_ORDER from './property-order'
|
||||
|
|
@ -15,6 +6,7 @@ import { asColor, type Utility } from './utilities'
|
|||
import { compare } from './utils/compare'
|
||||
import { escape } from './utils/escape'
|
||||
import type { Variants } from './variants'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
export function compileCandidates(
|
||||
rawCandidates: Iterable<string>,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { dimensions } from './utils/dimensions'
|
||||
import { isLength } from './utils/infer-data-type'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
// Assumption: We already assume that we receive somewhat valid `calc()`
|
||||
// expressions. So we will see `calc(1 + 1)` and not `calc(1+1)`
|
||||
|
|
@ -8,106 +9,106 @@ export function constantFoldDeclaration(input: string, rem: number | null = null
|
|||
let folded = false
|
||||
let valueAst = ValueParser.parse(input)
|
||||
|
||||
ValueParser.walkDepth(valueAst, (valueNode, { replaceWith }) => {
|
||||
// Canonicalize dimensions to their simplest form. This includes:
|
||||
// - Convert `-0`, `+0`, `0.0`, … to `0`
|
||||
// - Convert `-0px`, `+0em`, `0.0rem`, … to `0`
|
||||
// - Convert units to an equivalent unit
|
||||
if (
|
||||
valueNode.kind === 'word' &&
|
||||
valueNode.value !== '0' // Already `0`, nothing to do
|
||||
) {
|
||||
let canonical = canonicalizeDimension(valueNode.value, rem)
|
||||
if (canonical === null) return // Couldn't be canonicalized, nothing to do
|
||||
if (canonical === valueNode.value) return // Already in canonical form, nothing to do
|
||||
|
||||
folded = true
|
||||
replaceWith(ValueParser.word(canonical))
|
||||
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?
|
||||
walk(valueAst, {
|
||||
exit(valueNode) {
|
||||
// Canonicalize dimensions to their simplest form. This includes:
|
||||
// - Convert `-0`, `+0`, `0.0`, … to `0`
|
||||
// - Convert `-0px`, `+0em`, `0.0rem`, … to `0`
|
||||
// - Convert units to an equivalent unit
|
||||
if (
|
||||
operator === '*' &&
|
||||
((lhs?.[0] === 0 && lhs?.[1] === null) || // 0 * something
|
||||
(rhs?.[0] === 0 && rhs?.[1] === null)) // something * 0
|
||||
valueNode.kind === 'word' &&
|
||||
valueNode.value !== '0' // Already `0`, nothing to do
|
||||
) {
|
||||
let canonical = canonicalizeDimension(valueNode.value, rem)
|
||||
if (canonical === null) return // Couldn't be canonicalized, nothing to do
|
||||
if (canonical === valueNode.value) return // Already in canonical form, nothing to do
|
||||
|
||||
folded = true
|
||||
replaceWith(ValueParser.word('0'))
|
||||
return
|
||||
return WalkAction.ReplaceSkip(ValueParser.word(canonical))
|
||||
}
|
||||
|
||||
// We're not dealing with dimensions, so we can't fold this
|
||||
if (lhs === null || rhs === null) {
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
return WalkAction.ReplaceSkip(ValueParser.word('0'))
|
||||
}
|
||||
|
||||
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
|
||||
// We're not dealing with dimensions, so we can't fold this
|
||||
if (lhs === null || rhs === null) {
|
||||
return
|
||||
}
|
||||
|
||||
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] ?? ''}`))
|
||||
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
|
||||
return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] * rhs[0]}${lhs[1] ?? ''}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
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] ?? ''}`))
|
||||
case '+': {
|
||||
if (
|
||||
lhs[1] === rhs[1] // Same unit or unitless, e.g.: `1rem + 2rem`, `8 + 6`
|
||||
) {
|
||||
folded = true
|
||||
return WalkAction.ReplaceSkip(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
|
||||
return WalkAction.ReplaceSkip(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
|
||||
return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] / rhs[0]}${lhs[1] ?? ''}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return folded ? ValueParser.toCss(valueAst) : input
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Features } from '.'
|
||||
import { walk, type AstNode } from './ast'
|
||||
import { type AstNode } from './ast'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import { withAlpha } from './utilities'
|
||||
import { segment } from './utils/segment'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
const CSS_FUNCTIONS: Record<
|
||||
string,
|
||||
|
|
@ -187,7 +188,7 @@ export function substituteFunctionsInValue(
|
|||
designSystem: DesignSystem,
|
||||
): string {
|
||||
let ast = ValueParser.parse(value)
|
||||
ValueParser.walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'function' && node.value in CSS_FUNCTIONS) {
|
||||
let args = segment(ValueParser.toCss(node.nodes).trim(), ',').map((x) => x.trim())
|
||||
let result = CSS_FUNCTIONS[node.value as keyof typeof CSS_FUNCTIONS](
|
||||
|
|
@ -195,7 +196,7 @@ export function substituteFunctionsInValue(
|
|||
source,
|
||||
...args,
|
||||
)
|
||||
return replaceWith(ValueParser.parse(result))
|
||||
return WalkAction.Replace(ValueParser.parse(result))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -223,7 +224,7 @@ function eventuallyUnquote(value: string) {
|
|||
}
|
||||
|
||||
function injectFallbackForInitialFallback(ast: ValueParser.ValueAstNode[], fallback: string): void {
|
||||
ValueParser.walk(ast, (node) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'function') return
|
||||
if (node.value !== 'var' && node.value !== 'theme' && node.value !== '--theme') return
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import {
|
|||
comment,
|
||||
context,
|
||||
context as contextNode,
|
||||
cssContext,
|
||||
decl,
|
||||
optimizeAst,
|
||||
rule,
|
||||
styleRule,
|
||||
toCss,
|
||||
walk,
|
||||
WalkAction,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Context,
|
||||
|
|
@ -34,6 +33,7 @@ import { escape, unescape } from './utils/escape'
|
|||
import { segment } from './utils/segment'
|
||||
import { topologicalSort } from './utils/topological-sort'
|
||||
import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtVariant } from './variants'
|
||||
import { walk, WalkAction } from './walk'
|
||||
export type Config = UserConfig
|
||||
|
||||
const IS_VALID_PREFIX = /^[a-z]+$/
|
||||
|
|
@ -166,8 +166,9 @@ async function parseCss(
|
|||
let root = null as Root
|
||||
|
||||
// Handle at-rules
|
||||
walk(ast, (node, { parent, replaceWith, context }) => {
|
||||
walk(ast, (node, _ctx) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
let ctx = cssContext(_ctx)
|
||||
|
||||
// Find `@tailwind utilities` so that we can later replace it with the
|
||||
// actual generated utility class CSS.
|
||||
|
|
@ -177,16 +178,14 @@ async function parseCss(
|
|||
) {
|
||||
// Any additional `@tailwind utilities` nodes can be removed
|
||||
if (utilitiesNode !== null) {
|
||||
replaceWith([])
|
||||
return
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
|
||||
// When inside `@reference` we should treat `@tailwind utilities` as if
|
||||
// it wasn't there in the first place. This should also let `build()`
|
||||
// return the cached static AST.
|
||||
if (context.reference) {
|
||||
replaceWith([])
|
||||
return
|
||||
if (ctx.context.reference) {
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
|
||||
let params = segment(node.params, ' ')
|
||||
|
|
@ -210,7 +209,7 @@ async function parseCss(
|
|||
}
|
||||
|
||||
root = {
|
||||
base: (context.sourceBase as string) ?? (context.base as string),
|
||||
base: (ctx.context.sourceBase as string) ?? (ctx.context.base as string),
|
||||
pattern: path.slice(1, -1),
|
||||
}
|
||||
}
|
||||
|
|
@ -222,7 +221,7 @@ async function parseCss(
|
|||
|
||||
// Collect custom `@utility` at-rules
|
||||
if (node.name === '@utility') {
|
||||
if (parent !== null) {
|
||||
if (ctx.parent !== null) {
|
||||
throw new Error('`@utility` cannot be nested.')
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +259,7 @@ async function parseCss(
|
|||
throw new Error('`@source` cannot have a body.')
|
||||
}
|
||||
|
||||
if (parent !== null) {
|
||||
if (ctx.parent !== null) {
|
||||
throw new Error('`@source` cannot be nested.')
|
||||
}
|
||||
|
||||
|
|
@ -298,20 +297,20 @@ async function parseCss(
|
|||
}
|
||||
} else {
|
||||
sources.push({
|
||||
base: context.base as string,
|
||||
base: ctx.context.base as string,
|
||||
pattern: source,
|
||||
negated: not,
|
||||
})
|
||||
}
|
||||
replaceWith([])
|
||||
return
|
||||
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
|
||||
// Apply `@variant` at-rules
|
||||
if (node.name === '@variant') {
|
||||
// Legacy `@variant` at-rules containing `@slot` or without a body should
|
||||
// be considered a `@custom-variant` at-rule.
|
||||
if (parent === null) {
|
||||
if (ctx.parent === null) {
|
||||
// Body-less `@variant`, e.g.: `@variant foo (…);`
|
||||
if (node.nodes.length === 0) {
|
||||
node.name = '@custom-variant'
|
||||
|
|
@ -350,13 +349,10 @@ async function parseCss(
|
|||
|
||||
// Register custom variants from `@custom-variant` at-rules
|
||||
if (node.name === '@custom-variant') {
|
||||
if (parent !== null) {
|
||||
if (ctx.parent !== null) {
|
||||
throw new Error('`@custom-variant` cannot be nested.')
|
||||
}
|
||||
|
||||
// Remove `@custom-variant` at-rule so it's not included in the compiled CSS
|
||||
replaceWith([])
|
||||
|
||||
let [name, selector] = segment(node.params, ' ')
|
||||
|
||||
if (!IS_VALID_VARIANT_NAME.test(name)) {
|
||||
|
|
@ -417,8 +413,6 @@ async function parseCss(
|
|||
)
|
||||
})
|
||||
customVariantDependencies.set(name, new Set<string>())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Variants without a selector, but with a body:
|
||||
|
|
@ -448,9 +442,10 @@ async function parseCss(
|
|||
designSystem.variants.fromAst(name, node.nodes, designSystem)
|
||||
})
|
||||
customVariantDependencies.set(name, dependencies)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Remove `@custom-variant` at-rule so it's not included in the compiled CSS
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
|
||||
if (node.name === '@media') {
|
||||
|
|
@ -462,13 +457,14 @@ async function parseCss(
|
|||
if (param.startsWith('source(')) {
|
||||
let path = param.slice(7, -1)
|
||||
|
||||
walk(node.nodes, (child, { replaceWith }) => {
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'at-rule') return
|
||||
|
||||
if (child.name === '@tailwind' && child.params === 'utilities') {
|
||||
child.params += ` source(${path})`
|
||||
replaceWith([contextNode({ sourceBase: context.base }, [child])])
|
||||
return WalkAction.Stop
|
||||
return WalkAction.ReplaceStop([
|
||||
contextNode({ sourceBase: ctx.context.base }, [child]),
|
||||
])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -483,6 +479,7 @@ async function parseCss(
|
|||
let hasReference = themeParams.includes('reference')
|
||||
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind === 'context') return
|
||||
if (child.kind !== 'at-rule') {
|
||||
if (hasReference) {
|
||||
throw new Error(
|
||||
|
|
@ -535,8 +532,10 @@ async function parseCss(
|
|||
if (unknownParams.length > 0) {
|
||||
node.params = unknownParams.join(' ')
|
||||
} else if (params.length > 0) {
|
||||
replaceWith(node.nodes)
|
||||
return WalkAction.Replace(node.nodes)
|
||||
}
|
||||
|
||||
return WalkAction.Continue
|
||||
}
|
||||
|
||||
// Handle `@theme`
|
||||
|
|
@ -545,7 +544,7 @@ async function parseCss(
|
|||
|
||||
features |= Features.AtTheme
|
||||
|
||||
if (context.reference) {
|
||||
if (ctx.context.reference) {
|
||||
themeOptions |= ThemeOptions.REFERENCE
|
||||
}
|
||||
|
||||
|
|
@ -589,11 +588,10 @@ async function parseCss(
|
|||
if (!firstThemeRule) {
|
||||
firstThemeRule = styleRule(':root, :host', [])
|
||||
firstThemeRule.src = node.src
|
||||
replaceWith([firstThemeRule])
|
||||
return WalkAction.ReplaceSkip(firstThemeRule)
|
||||
} else {
|
||||
replaceWith([])
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -685,11 +683,11 @@ async function parseCss(
|
|||
|
||||
// Remove `@utility`, we couldn't replace it before yet because we had to
|
||||
// handle the nested `@apply` at-rules first.
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
||||
if (node.name === '@utility') {
|
||||
replaceWith([])
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
|
||||
// The `@utility` has to be top-level, therefore we don't have to traverse
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { styleRule, walkDepth } from './ast'
|
||||
import { styleRule } from './ast'
|
||||
import { applyVariant } from './compile'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import { compare } from './utils/compare'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { walk } from './walk'
|
||||
export { canonicalizeCandidates, type CanonicalizeOptions } from './canonicalize-candidates'
|
||||
|
||||
interface ClassMetadata {
|
||||
|
|
@ -180,42 +181,47 @@ export function getVariants(design: DesignSystem) {
|
|||
|
||||
// Produce v3-style selector strings in the face of nested rules
|
||||
// this is more visible for things like group-*, not-*, etc…
|
||||
walkDepth(node.nodes, (node, { path }) => {
|
||||
if (node.kind !== 'rule' && node.kind !== 'at-rule') return
|
||||
if (node.nodes.length > 0) return
|
||||
walk(node.nodes, {
|
||||
exit(node, ctx) {
|
||||
if (node.kind !== 'rule' && node.kind !== 'at-rule') return
|
||||
if (node.nodes.length > 0) return
|
||||
|
||||
// Sort at-rules before style rules
|
||||
path.sort((a, b) => {
|
||||
let aIsAtRule = a.kind === 'at-rule'
|
||||
let bIsAtRule = b.kind === 'at-rule'
|
||||
let path = ctx.path()
|
||||
path.push(node)
|
||||
|
||||
if (aIsAtRule && !bIsAtRule) return -1
|
||||
if (!aIsAtRule && bIsAtRule) return 1
|
||||
// Sort at-rules before style rules
|
||||
path.sort((a, b) => {
|
||||
let aIsAtRule = a.kind === 'at-rule'
|
||||
let bIsAtRule = b.kind === 'at-rule'
|
||||
|
||||
return 0
|
||||
})
|
||||
if (aIsAtRule && !bIsAtRule) return -1
|
||||
if (!aIsAtRule && bIsAtRule) return 1
|
||||
|
||||
// A list of the selectors / at rules encountered to get to this point
|
||||
let group = path.flatMap((node) => {
|
||||
if (node.kind === 'rule') {
|
||||
return node.selector === '&' ? [] : [node.selector]
|
||||
return 0
|
||||
})
|
||||
|
||||
// A list of the selectors / at rules encountered to get to this point
|
||||
let group = path.flatMap((node) => {
|
||||
if (node.kind === 'rule') {
|
||||
return node.selector === '&' ? [] : [node.selector]
|
||||
}
|
||||
|
||||
if (node.kind === 'at-rule') {
|
||||
return [`${node.name} ${node.params}`]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
// Build a v3-style nested selector
|
||||
let selector = ''
|
||||
|
||||
for (let i = group.length - 1; i >= 0; i--) {
|
||||
selector = selector === '' ? group[i] : `${group[i]} { ${selector} }`
|
||||
}
|
||||
|
||||
if (node.kind === 'at-rule') {
|
||||
return [`${node.name} ${node.params}`]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
// Build a v3-style nested selector
|
||||
let selector = ''
|
||||
|
||||
for (let i = group.length - 1; i >= 0; i--) {
|
||||
selector = selector === '' ? group[i] : `${group[i]} { ${selector} }`
|
||||
}
|
||||
|
||||
selectors.push(selector)
|
||||
selectors.push(selector)
|
||||
},
|
||||
})
|
||||
|
||||
return selectors
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { parse, toCss, walk } from './selector-parser'
|
||||
import { parse, toCss } from './selector-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
describe('parse', () => {
|
||||
it('should parse a simple selector', () => {
|
||||
|
|
@ -194,9 +195,9 @@ describe('toCss', () => {
|
|||
describe('walk', () => {
|
||||
it('can be used to replace a function call', () => {
|
||||
const ast = parse('.foo:hover:not(.bar:focus)')
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'function' && node.value === ':not') {
|
||||
replaceWith({ kind: 'selector', value: '.inverted-bar' })
|
||||
return WalkAction.Replace({ kind: 'selector', value: '.inverted-bar' } as const)
|
||||
}
|
||||
})
|
||||
expect(toCss(ast)).toBe('.foo:hover.inverted-bar')
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export type SelectorAstNode =
|
|||
| SelectorNode
|
||||
| SelectorSeparatorNode
|
||||
| SelectorValueNode
|
||||
type SelectorParentNode = SelectorFunctionNode | null
|
||||
|
||||
function combinator(value: string): SelectorCombinatorNode {
|
||||
return {
|
||||
|
|
@ -68,83 +67,6 @@ function value(value: string): SelectorValueNode {
|
|||
}
|
||||
}
|
||||
|
||||
export const enum SelectorWalkAction {
|
||||
/** Continue walking, which is the default */
|
||||
Continue,
|
||||
|
||||
/** Skip visiting the children of this node */
|
||||
Skip,
|
||||
|
||||
/** Stop the walk entirely */
|
||||
Stop,
|
||||
}
|
||||
|
||||
export function walk(
|
||||
ast: SelectorAstNode[],
|
||||
visit: (
|
||||
node: SelectorAstNode,
|
||||
utils: {
|
||||
parent: SelectorParentNode
|
||||
replaceWith(newNode: SelectorAstNode | SelectorAstNode[]): void
|
||||
},
|
||||
) => void | SelectorWalkAction,
|
||||
parent: SelectorParentNode = null,
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
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
|
||||
replacedNodeOffset = 1
|
||||
}
|
||||
},
|
||||
}) ?? SelectorWalkAction.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 === SelectorWalkAction.Continue) {
|
||||
i--
|
||||
} else {
|
||||
i += replacedNodeOffset - 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Stop the walk entirely
|
||||
if (status === SelectorWalkAction.Stop) return SelectorWalkAction.Stop
|
||||
|
||||
// Skip visiting the children of this node
|
||||
if (status === SelectorWalkAction.Skip) continue
|
||||
|
||||
if (node.kind === 'function') {
|
||||
if (walk(node.nodes, visit, node) === SelectorWalkAction.Stop) {
|
||||
return SelectorWalkAction.Stop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toCss(ast: SelectorAstNode[]) {
|
||||
let css = ''
|
||||
for (const node of ast) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { substituteAtApply } from './apply'
|
||||
import { atRule, styleRule, toCss, walk, type AstNode } from './ast'
|
||||
import { atRule, styleRule, toCss, type AstNode } from './ast'
|
||||
import { printArbitraryValue } from './candidate'
|
||||
import { constantFoldDeclaration } from './constant-fold-declaration'
|
||||
import { CompileAstFlags, type DesignSystem } from './design-system'
|
||||
|
|
@ -8,6 +8,7 @@ import { ThemeOptions } from './theme'
|
|||
import { DefaultMap } from './utils/default-map'
|
||||
import { isValidSpacingMultiplier } from './utils/infer-data-type'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
const FLOATING_POINT_PERCENTAGE = /\d*\.\d+(?:[eE][+-]?\d+)?%/g
|
||||
|
||||
|
|
@ -69,11 +70,11 @@ export const computeUtilitySignature = new DefaultMap((options: SignatureOptions
|
|||
// Optimize the AST. This is needed such that any internal intermediate
|
||||
// nodes are gone. This will also cleanup declaration nodes with undefined
|
||||
// values or `--tw-sort` declarations.
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
// Optimize declarations
|
||||
if (node.kind === 'declaration') {
|
||||
if (node.value === undefined || node.property === '--tw-sort') {
|
||||
replaceWith([])
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
|
||||
// Normalize percentages by removing unnecessary dots and zeros.
|
||||
|
|
@ -90,17 +91,17 @@ export const computeUtilitySignature = new DefaultMap((options: SignatureOptions
|
|||
|
||||
// Replace special nodes with its children
|
||||
else if (node.kind === 'context' || node.kind === 'at-root') {
|
||||
replaceWith(node.nodes)
|
||||
return WalkAction.Replace(node.nodes)
|
||||
}
|
||||
|
||||
// Remove comments
|
||||
else if (node.kind === 'comment') {
|
||||
replaceWith([])
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
|
||||
// Remove at-rules that are not needed for the signature
|
||||
else if (node.kind === 'at-rule' && node.name === '@property') {
|
||||
replaceWith([])
|
||||
return WalkAction.Replace([])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -151,7 +152,7 @@ export const computeUtilitySignature = new DefaultMap((options: SignatureOptions
|
|||
let valueAst = ValueParser.parse(node.value)
|
||||
|
||||
let seen = new Set<string>()
|
||||
ValueParser.walk(valueAst, (valueNode, { replaceWith }) => {
|
||||
walk(valueAst, (valueNode) => {
|
||||
if (valueNode.kind !== 'function') return
|
||||
if (valueNode.value !== 'var') return
|
||||
|
||||
|
|
@ -204,7 +205,7 @@ export const computeUtilitySignature = new DefaultMap((options: SignatureOptions
|
|||
let constructedValue = `${valueNode.nodes[0].value},${variableValue}`
|
||||
if (nodeAsString === constructedValue) {
|
||||
changed = true
|
||||
replaceWith(ValueParser.parse(variableValue))
|
||||
return WalkAction.Replace(ValueParser.parse(variableValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,7 +329,7 @@ export const computeVariantSignature = new DefaultMap((options: SignatureOptions
|
|||
else if (node.kind === 'rule') {
|
||||
let selectorAst = SelectorParser.parse(node.selector)
|
||||
let changed = false
|
||||
SelectorParser.walk(selectorAst, (node, { replaceWith }) => {
|
||||
walk(selectorAst, (node) => {
|
||||
if (node.kind === 'separator' && node.value !== ' ') {
|
||||
node.value = node.value.trim()
|
||||
changed = true
|
||||
|
|
@ -342,7 +343,7 @@ export const computeVariantSignature = new DefaultMap((options: SignatureOptions
|
|||
// E.g.: `:is(.foo)` → `.foo`
|
||||
if (node.nodes.length === 1) {
|
||||
changed = true
|
||||
replaceWith(node.nodes)
|
||||
return WalkAction.Replace(node.nodes)
|
||||
}
|
||||
|
||||
// A selector with the universal selector `*` followed by a pseudo
|
||||
|
|
@ -355,7 +356,7 @@ export const computeVariantSignature = new DefaultMap((options: SignatureOptions
|
|||
node.nodes[1].value[0] === ':'
|
||||
) {
|
||||
changed = true
|
||||
replaceWith(node.nodes[1])
|
||||
return WalkAction.Replace(node.nodes[1])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { walk, type AstNode } from '../ast'
|
||||
import { type AstNode } from '../ast'
|
||||
import { DefaultMap } from '../utils/default-map'
|
||||
import { walk } from '../walk'
|
||||
import { createLineTable, type LineTable, type Position } from './line-table'
|
||||
import type { Source } from './source'
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
decl,
|
||||
rule,
|
||||
styleRule,
|
||||
walk,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Declaration,
|
||||
|
|
@ -27,6 +26,7 @@ import {
|
|||
import { replaceShadowColors } from './utils/replace-shadow-colors'
|
||||
import { segment } from './utils/segment'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
const IS_VALID_STATIC_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*$/
|
||||
const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/
|
||||
|
|
@ -5894,7 +5894,7 @@ export function createCssUtility(node: AtRule) {
|
|||
//
|
||||
// Once Prettier / Biome handle these better (e.g.: not crashing without
|
||||
// `\\*` or not inserting whitespace) then most of these can go away.
|
||||
ValueParser.walk(declarationValueAst, (fn) => {
|
||||
walk(declarationValueAst, (fn) => {
|
||||
if (fn.kind !== 'function') return
|
||||
|
||||
// Track usage of `--spacing(…)`
|
||||
|
|
@ -5904,7 +5904,7 @@ export function createCssUtility(node: AtRule) {
|
|||
// using the full `--spacing` theme scale.
|
||||
!(storage['--modifier'].usedSpacingNumber && storage['--value'].usedSpacingNumber)
|
||||
) {
|
||||
ValueParser.walk(fn.nodes, (node) => {
|
||||
walk(fn.nodes, (node) => {
|
||||
if (node.kind !== 'function') return
|
||||
if (node.value !== '--value' && node.value !== '--modifier') return
|
||||
const key = node.value
|
||||
|
|
@ -5923,12 +5923,12 @@ export function createCssUtility(node: AtRule) {
|
|||
storage['--modifier'].usedSpacingNumber &&
|
||||
storage['--value'].usedSpacingNumber
|
||||
) {
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.Stop
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return ValueParser.ValueWalkAction.Continue
|
||||
return WalkAction.Continue
|
||||
}
|
||||
|
||||
if (fn.value !== '--value' && fn.value !== '--modifier') return
|
||||
|
|
@ -5987,9 +5987,9 @@ export function createCssUtility(node: AtRule) {
|
|||
let dataType = node.value
|
||||
let copy = structuredClone(fn)
|
||||
let sentinelValue = '¶'
|
||||
ValueParser.walk(copy.nodes, (node, { replaceWith }) => {
|
||||
walk(copy.nodes, (node) => {
|
||||
if (node.kind === 'word' && node.value === dataType) {
|
||||
replaceWith({ kind: 'word', value: sentinelValue })
|
||||
return WalkAction.ReplaceSkip({ kind: 'word', value: sentinelValue } as const)
|
||||
}
|
||||
})
|
||||
let underline = '^'.repeat(ValueParser.toCss([node]).length)
|
||||
|
|
@ -6048,66 +6048,68 @@ export function createCssUtility(node: AtRule) {
|
|||
// Whether `--value(ratio)` was resolved
|
||||
let resolvedRatioValue = false
|
||||
|
||||
walk([atRule], (node, { parent, replaceWith: replaceDeclarationWith }) => {
|
||||
walk<AstNode>([atRule], (node, ctx) => {
|
||||
let parent = ctx.parent
|
||||
if (parent?.kind !== 'rule' && parent?.kind !== 'at-rule') return
|
||||
if (node.kind !== 'declaration') return
|
||||
if (!node.value) return
|
||||
|
||||
let shouldRemoveDeclaration = false
|
||||
|
||||
let valueAst = ValueParser.parse(node.value)
|
||||
let result =
|
||||
ValueParser.walk(valueAst, (valueNode, { replaceWith }) => {
|
||||
if (valueNode.kind !== 'function') return
|
||||
walk(valueAst, (valueNode) => {
|
||||
if (valueNode.kind !== 'function') return
|
||||
|
||||
// Value function, e.g.: `--value(integer)`
|
||||
if (valueNode.value === '--value') {
|
||||
usedValueFn = true
|
||||
// Value function, e.g.: `--value(integer)`
|
||||
if (valueNode.value === '--value') {
|
||||
usedValueFn = true
|
||||
|
||||
let resolved = resolveValueFunction(value, valueNode, designSystem)
|
||||
if (resolved) {
|
||||
resolvedValueFn = true
|
||||
if (resolved.ratio) {
|
||||
resolvedRatioValue = true
|
||||
} else {
|
||||
resolvedDeclarations.set(node, parent)
|
||||
}
|
||||
replaceWith(resolved.nodes)
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
let resolved = resolveValueFunction(value, valueNode, designSystem)
|
||||
if (resolved) {
|
||||
resolvedValueFn = true
|
||||
if (resolved.ratio) {
|
||||
resolvedRatioValue = true
|
||||
} else {
|
||||
resolvedDeclarations.set(node, parent)
|
||||
}
|
||||
|
||||
// Drop the declaration in case we couldn't resolve the value
|
||||
usedValueFn ||= false
|
||||
replaceDeclarationWith([])
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
return WalkAction.ReplaceSkip(resolved.nodes)
|
||||
}
|
||||
|
||||
// Modifier function, e.g.: `--modifier(integer)`
|
||||
else if (valueNode.value === '--modifier') {
|
||||
// If there is no modifier present in the candidate, then the
|
||||
// declaration can be removed.
|
||||
if (modifier === null) {
|
||||
replaceDeclarationWith([])
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
}
|
||||
// Drop the declaration in case we couldn't resolve the value
|
||||
usedValueFn ||= false
|
||||
shouldRemoveDeclaration = true
|
||||
return WalkAction.Stop
|
||||
}
|
||||
|
||||
usedModifierFn = true
|
||||
|
||||
let replacement = resolveValueFunction(modifier, valueNode, designSystem)
|
||||
if (replacement) {
|
||||
resolvedModifierFn = true
|
||||
replaceWith(replacement.nodes)
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
}
|
||||
|
||||
// Drop the declaration in case we couldn't resolve the value
|
||||
usedModifierFn ||= false
|
||||
replaceDeclarationWith([])
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
// Modifier function, e.g.: `--modifier(integer)`
|
||||
else if (valueNode.value === '--modifier') {
|
||||
// If there is no modifier present in the candidate, then the
|
||||
// declaration can be removed.
|
||||
if (modifier === null) {
|
||||
shouldRemoveDeclaration = true
|
||||
return WalkAction.Stop
|
||||
}
|
||||
}) ?? ValueParser.ValueWalkAction.Continue
|
||||
|
||||
if (result === ValueParser.ValueWalkAction.Continue) {
|
||||
node.value = ValueParser.toCss(valueAst)
|
||||
usedModifierFn = true
|
||||
|
||||
let replacement = resolveValueFunction(modifier, valueNode, designSystem)
|
||||
if (replacement) {
|
||||
resolvedModifierFn = true
|
||||
return WalkAction.ReplaceSkip(replacement.nodes)
|
||||
}
|
||||
|
||||
// Drop the declaration in case we couldn't resolve the value
|
||||
usedModifierFn ||= false
|
||||
shouldRemoveDeclaration = true
|
||||
return WalkAction.Stop
|
||||
}
|
||||
})
|
||||
|
||||
if (shouldRemoveDeclaration) {
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
|
||||
node.value = ValueParser.toCss(valueAst)
|
||||
})
|
||||
|
||||
// Used `--value(…)` but nothing resolved
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import * as ValueParser from '../value-parser'
|
||||
import { walk, WalkAction } from '../walk'
|
||||
|
||||
export function extractUsedVariables(raw: string): string[] {
|
||||
let variables: string[] = []
|
||||
ValueParser.walk(ValueParser.parse(raw), (node) => {
|
||||
walk(ValueParser.parse(raw), (node) => {
|
||||
if (node.kind !== 'function' || node.value !== 'var') return
|
||||
|
||||
ValueParser.walk(node.nodes, (child) => {
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'word' || child.value[0] !== '-' || child.value[1] !== '-') return
|
||||
|
||||
variables.push(child.value)
|
||||
})
|
||||
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
return WalkAction.Skip
|
||||
})
|
||||
return variables
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { parse, toCss, walk } from './value-parser'
|
||||
import { parse, toCss } from './value-parser'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
describe('parse', () => {
|
||||
it('should parse a value', () => {
|
||||
|
|
@ -207,9 +208,9 @@ describe('walk', () => {
|
|||
it('can be used to replace a function call', () => {
|
||||
const ast = parse('(min-width: 600px) and (max-width: theme(lg))')
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'function' && node.value === 'theme') {
|
||||
replaceWith({ kind: 'word', value: '64rem' })
|
||||
return WalkAction.Replace({ kind: 'word', value: '64rem' } as const)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ export type ValueSeparatorNode = {
|
|||
}
|
||||
|
||||
export type ValueAstNode = ValueWordNode | ValueFunctionNode | ValueSeparatorNode
|
||||
type ValueParentNode = ValueFunctionNode | null
|
||||
|
||||
export function word(value: string): ValueWordNode {
|
||||
return {
|
||||
|
|
@ -39,147 +38,6 @@ function separator(value: string): ValueSeparatorNode {
|
|||
}
|
||||
}
|
||||
|
||||
export const enum ValueWalkAction {
|
||||
/** Continue walking, which is the default */
|
||||
Continue,
|
||||
|
||||
/** Skip visiting the children of this node */
|
||||
Skip,
|
||||
|
||||
/** Stop the walk entirely */
|
||||
Stop,
|
||||
}
|
||||
|
||||
export function walk(
|
||||
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]
|
||||
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
|
||||
|
||||
if (node.kind === 'function') {
|
||||
if (walk(node.nodes, visit, node) === ValueWalkAction.Stop) {
|
||||
return ValueWalkAction.Stop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { Features } from '.'
|
||||
import {
|
||||
WalkAction,
|
||||
atRoot,
|
||||
atRule,
|
||||
cloneAstNode,
|
||||
decl,
|
||||
rule,
|
||||
styleRule,
|
||||
walk,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Rule,
|
||||
|
|
@ -21,6 +19,7 @@ import { compareBreakpoints } from './utils/compare-breakpoints'
|
|||
import { DefaultMap } from './utils/default-map'
|
||||
import { isPositiveInteger } from './utils/infer-data-type'
|
||||
import { segment } from './utils/segment'
|
||||
import { walk, WalkAction } from './walk'
|
||||
|
||||
export const IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*(?<![_-])$/
|
||||
|
||||
|
|
@ -449,7 +448,7 @@ export function createVariants(theme: Theme): Variants {
|
|||
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
walk([ruleNode], (node, ctx) => {
|
||||
if (node.kind !== 'rule' && node.kind !== 'at-rule') return WalkAction.Continue
|
||||
if (node.nodes.length > 0) return WalkAction.Continue
|
||||
|
||||
|
|
@ -457,11 +456,14 @@ export function createVariants(theme: Theme): Variants {
|
|||
let atRules: AtRule[] = []
|
||||
let styleRules: StyleRule[] = []
|
||||
|
||||
for (let parent of path) {
|
||||
if (parent.kind === 'at-rule') {
|
||||
atRules.push(parent)
|
||||
} else if (parent.kind === 'rule') {
|
||||
styleRules.push(parent)
|
||||
let path = ctx.path()
|
||||
path.push(node)
|
||||
|
||||
for (let node of path) {
|
||||
if (node.kind === 'at-rule') {
|
||||
atRules.push(node)
|
||||
} else if (node.kind === 'rule') {
|
||||
styleRules.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -525,11 +527,11 @@ export function createVariants(theme: Theme): Variants {
|
|||
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
walk([ruleNode], (node, ctx) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
for (let parent of ctx.path()) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
|
||||
didApply = false
|
||||
|
|
@ -577,11 +579,11 @@ export function createVariants(theme: Theme): Variants {
|
|||
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
walk([ruleNode], (node, ctx) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
for (let parent of ctx.path()) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
|
||||
didApply = false
|
||||
|
|
@ -725,11 +727,11 @@ export function createVariants(theme: Theme): Variants {
|
|||
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
walk([ruleNode], (node, ctx) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
for (let parent of ctx.path()) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
|
||||
didApply = false
|
||||
|
|
@ -760,11 +762,11 @@ export function createVariants(theme: Theme): Variants {
|
|||
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
walk([ruleNode], (node, ctx) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
|
||||
// Throw out any candidates with variants using nested style rules
|
||||
for (let parent of path.slice(0, -1)) {
|
||||
for (let parent of ctx.path()) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
|
||||
didApply = false
|
||||
|
|
@ -1191,10 +1193,10 @@ function quoteAttributeValue(input: string) {
|
|||
}
|
||||
|
||||
export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
walk(ast, (node) => {
|
||||
// Replace `@slot` with rule nodes
|
||||
if (node.kind === 'at-rule' && node.name === '@slot') {
|
||||
replaceWith(nodes)
|
||||
return WalkAction.Replace(nodes)
|
||||
}
|
||||
|
||||
// Wrap `@keyframes` and `@property` in `AtRoot` nodes
|
||||
|
|
@ -1207,7 +1209,7 @@ export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {
|
|||
|
||||
export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem): Features {
|
||||
let features = Features.None
|
||||
walk(ast, (variantNode, { replaceWith }) => {
|
||||
walk(ast, (variantNode) => {
|
||||
if (variantNode.kind !== 'at-rule' || variantNode.name !== '@variant') return
|
||||
|
||||
// Starting with the `&` rule node
|
||||
|
|
@ -1226,8 +1228,8 @@ export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem):
|
|||
}
|
||||
|
||||
// Update the variant at-rule node, to be the `&` rule node
|
||||
replaceWith(node)
|
||||
features |= Features.Variants
|
||||
return WalkAction.Replace(node)
|
||||
})
|
||||
return features
|
||||
}
|
||||
|
|
|
|||
1596
packages/tailwindcss/src/walk.test.ts
Normal file
1596
packages/tailwindcss/src/walk.test.ts
Normal file
File diff suppressed because it is too large
Load diff
181
packages/tailwindcss/src/walk.ts
Normal file
181
packages/tailwindcss/src/walk.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
const enum WalkKind {
|
||||
Continue,
|
||||
Skip,
|
||||
Stop,
|
||||
Replace,
|
||||
ReplaceSkip,
|
||||
ReplaceStop,
|
||||
}
|
||||
|
||||
export const WalkAction = {
|
||||
Continue: { kind: WalkKind.Continue } as const,
|
||||
Skip: { kind: WalkKind.Skip } as const,
|
||||
Stop: { kind: WalkKind.Stop } as const,
|
||||
Replace: <T>(nodes: T | T[]) =>
|
||||
({ kind: WalkKind.Replace, nodes: Array.isArray(nodes) ? nodes : [nodes] }) as const,
|
||||
ReplaceSkip: <T>(nodes: T | T[]) =>
|
||||
({ kind: WalkKind.ReplaceSkip, nodes: Array.isArray(nodes) ? nodes : [nodes] }) as const,
|
||||
ReplaceStop: <T>(nodes: T | T[]) =>
|
||||
({ kind: WalkKind.ReplaceStop, nodes: Array.isArray(nodes) ? nodes : [nodes] }) as const,
|
||||
} as const
|
||||
|
||||
type WalkAction = typeof WalkAction
|
||||
type WalkResult<T> =
|
||||
| WalkAction['Continue']
|
||||
| WalkAction['Skip']
|
||||
| WalkAction['Stop']
|
||||
| ReturnType<typeof WalkAction.Replace<T>>
|
||||
| ReturnType<typeof WalkAction.ReplaceSkip<T>>
|
||||
| ReturnType<typeof WalkAction.ReplaceStop<T>>
|
||||
|
||||
type EnterResult<T> = WalkResult<T>
|
||||
type ExitResult<T> = Exclude<WalkResult<T>, { kind: WalkKind.Skip }>
|
||||
|
||||
type Parent<T> = T & { nodes: T[] }
|
||||
|
||||
export interface VisitContext<T> {
|
||||
parent: Parent<T> | null
|
||||
depth: number
|
||||
path: () => T[]
|
||||
}
|
||||
|
||||
export function walk<T extends object>(
|
||||
ast: T[],
|
||||
hooks:
|
||||
| ((node: T, ctx: VisitContext<T>) => EnterResult<T> | void) // Old API, enter only
|
||||
| {
|
||||
enter?: (node: T, ctx: VisitContext<T>) => EnterResult<T> | void
|
||||
exit?: (node: T, ctx: VisitContext<T>) => ExitResult<T> | void
|
||||
},
|
||||
): void {
|
||||
if (typeof hooks === 'function') walkImplementation(ast, hooks)
|
||||
else walkImplementation(ast, hooks.enter, hooks.exit)
|
||||
}
|
||||
|
||||
function walkImplementation<T extends { nodes?: T[] }>(
|
||||
ast: T[],
|
||||
enter: (node: T, ctx: VisitContext<T>) => EnterResult<T> | void = () => WalkAction.Continue,
|
||||
exit: (node: T, ctx: VisitContext<T>) => ExitResult<T> | void = () => WalkAction.Continue,
|
||||
) {
|
||||
let stack: [nodes: T[], offset: number, parent: Parent<T> | null][] = [[ast, 0, null]]
|
||||
let ctx: VisitContext<T> = {
|
||||
parent: null,
|
||||
depth: 0,
|
||||
path() {
|
||||
let path: T[] = []
|
||||
|
||||
for (let i = 1; i < stack.length; i++) {
|
||||
let parent = stack[i][2]
|
||||
if (parent) path.push(parent)
|
||||
}
|
||||
|
||||
return path
|
||||
},
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
let depth = stack.length - 1
|
||||
let frame = stack[depth]
|
||||
let nodes = frame[0]
|
||||
let offset = frame[1]
|
||||
let parent = frame[2]
|
||||
|
||||
// Done with this level
|
||||
if (offset >= nodes.length) {
|
||||
stack.pop()
|
||||
continue
|
||||
}
|
||||
|
||||
ctx.parent = parent
|
||||
ctx.depth = depth
|
||||
|
||||
// Enter phase (offsets are positive)
|
||||
if (offset >= 0) {
|
||||
let node = nodes[offset]
|
||||
let result = enter(node, ctx) ?? WalkAction.Continue
|
||||
|
||||
switch (result.kind) {
|
||||
case WalkKind.Continue: {
|
||||
if (node.nodes && node.nodes.length > 0) {
|
||||
stack.push([node.nodes, 0, node as Parent<T>])
|
||||
}
|
||||
|
||||
frame[1] = ~offset // Prepare for exit phase, same offset
|
||||
continue
|
||||
}
|
||||
|
||||
case WalkKind.Stop:
|
||||
return // Stop immediately
|
||||
|
||||
case WalkKind.Skip: {
|
||||
frame[1] = ~offset // Prepare for exit phase, same offset
|
||||
continue
|
||||
}
|
||||
|
||||
case WalkKind.Replace: {
|
||||
nodes.splice(offset, 1, ...result.nodes)
|
||||
continue // Re-process at same offset
|
||||
}
|
||||
|
||||
case WalkKind.ReplaceStop: {
|
||||
nodes.splice(offset, 1, ...result.nodes)
|
||||
return // Stop immediately
|
||||
}
|
||||
|
||||
case WalkKind.ReplaceSkip: {
|
||||
nodes.splice(offset, 1, ...result.nodes)
|
||||
frame[1] += result.nodes.length // Advance to next sibling past replacements
|
||||
continue
|
||||
}
|
||||
|
||||
default: {
|
||||
result satisfies never
|
||||
throw new Error(
|
||||
// @ts-expect-error enterResult.kind may be invalid
|
||||
`Invalid \`WalkAction.${WalkKind[result.kind] ?? `Unknown(${result.kind})`}\` in enter.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exit phase for nodes[~offset]
|
||||
let index = ~offset // Two's complement to get original offset
|
||||
let node = nodes[index]
|
||||
|
||||
let result = exit(node, ctx) ?? WalkAction.Continue
|
||||
|
||||
switch (result.kind) {
|
||||
case WalkKind.Continue:
|
||||
frame[1] = index + 1 // Advance to next sibling
|
||||
continue
|
||||
|
||||
case WalkKind.Stop:
|
||||
return // Stop immediately
|
||||
|
||||
case WalkKind.Replace: {
|
||||
nodes.splice(index, 1, ...result.nodes)
|
||||
frame[1] = index + result.nodes.length // Advance to next sibling past replacements
|
||||
continue
|
||||
}
|
||||
|
||||
case WalkKind.ReplaceStop: {
|
||||
nodes.splice(index, 1, ...result.nodes)
|
||||
return // Stop immediately
|
||||
}
|
||||
|
||||
case WalkKind.ReplaceSkip: {
|
||||
nodes.splice(index, 1, ...result.nodes)
|
||||
frame[1] = index + result.nodes.length // Advance to next sibling past replacements
|
||||
continue
|
||||
}
|
||||
|
||||
default: {
|
||||
result satisfies never
|
||||
throw new Error(
|
||||
// @ts-expect-error `result.kind` could still be filled with an invalid value
|
||||
`Invalid \`WalkAction.${WalkKind[result.kind] ?? `Unknown(${result.kind})`}\` in exit.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue