Skip to content

Commit a84e52a

Browse files
Move value parser into tailwindcss root (tailwindlabs#14236)
This PR is moving content from `packages/tailwindcss/src/value-parser/*.ts` into `packages/tailwindcss/src/value-parser.ts` to simplify the file structure.
1 parent fcc0782 commit a84e52a

File tree

6 files changed

+265
-271
lines changed

6 files changed

+265
-271
lines changed

packages/tailwindcss/src/functions.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { walk, type AstNode } from './ast'
22
import type { PluginAPI } from './plugin-api'
33
import { withAlpha } from './utilities'
4-
import {
5-
toCss as toValueCss,
6-
walk as walkValues,
7-
type AstNode as ValueAstNode,
8-
} from './value-parser/ast'
9-
import * as ValueParser from './value-parser/parser'
4+
import * as ValueParser from './value-parser'
5+
import { type ValueAstNode } from './value-parser'
106

117
export const THEME_FUNCTION_INVOCATION = 'theme('
128

@@ -33,7 +29,7 @@ export function substituteFunctions(ast: AstNode[], pluginApi: PluginAPI) {
3329

3430
export function substituteFunctionsInValue(value: string, pluginApi: PluginAPI): string {
3531
let ast = ValueParser.parse(value)
36-
walkValues(ast, (node, { replaceWith }) => {
32+
ValueParser.walk(ast, (node, { replaceWith }) => {
3733
if (node.kind === 'function' && node.value === 'theme') {
3834
if (node.nodes.length < 1) {
3935
throw new Error(
@@ -73,7 +69,7 @@ export function substituteFunctionsInValue(value: string, pluginApi: PluginAPI):
7369
}
7470
})
7571

76-
return toValueCss(ast)
72+
return ValueParser.toCss(ast)
7773
}
7874

7975
function cssThemeFn(
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { parse, toCss, walk } from './value-parser'
3+
4+
describe('parse', () => {
5+
it('should parse a value', () => {
6+
expect(parse('123px')).toEqual([{ kind: 'word', value: '123px' }])
7+
})
8+
9+
it('should parse a string value', () => {
10+
expect(parse("'hello world'")).toEqual([{ kind: 'word', value: "'hello world'" }])
11+
})
12+
13+
it('should parse a list', () => {
14+
expect(parse('hello world')).toEqual([
15+
{ kind: 'word', value: 'hello' },
16+
{ kind: 'separator', value: ' ' },
17+
{ kind: 'word', value: 'world' },
18+
])
19+
})
20+
21+
it('should parse a string containing parentheses', () => {
22+
expect(parse("'hello ( world )'")).toEqual([{ kind: 'word', value: "'hello ( world )'" }])
23+
})
24+
25+
it('should parse a function with no arguments', () => {
26+
expect(parse('theme()')).toEqual([{ kind: 'function', value: 'theme', nodes: [] }])
27+
})
28+
29+
it('should parse a function with a single argument', () => {
30+
expect(parse('theme(foo)')).toEqual([
31+
{ kind: 'function', value: 'theme', nodes: [{ kind: 'word', value: 'foo' }] },
32+
])
33+
})
34+
35+
it('should parse a function with a single string argument', () => {
36+
expect(parse("theme('foo')")).toEqual([
37+
{ kind: 'function', value: 'theme', nodes: [{ kind: 'word', value: "'foo'" }] },
38+
])
39+
})
40+
41+
it('should parse a function with multiple arguments', () => {
42+
expect(parse('theme(foo, bar)')).toEqual([
43+
{
44+
kind: 'function',
45+
value: 'theme',
46+
nodes: [
47+
{ kind: 'word', value: 'foo' },
48+
{ kind: 'separator', value: ', ' },
49+
{ kind: 'word', value: 'bar' },
50+
],
51+
},
52+
])
53+
})
54+
55+
it('should parse a function with nested arguments', () => {
56+
expect(parse('theme(foo, theme(bar))')).toEqual([
57+
{
58+
kind: 'function',
59+
value: 'theme',
60+
nodes: [
61+
{ kind: 'word', value: 'foo' },
62+
{ kind: 'separator', value: ', ' },
63+
{ kind: 'function', value: 'theme', nodes: [{ kind: 'word', value: 'bar' }] },
64+
],
65+
},
66+
])
67+
})
68+
69+
it('should handle calculations', () => {
70+
expect(parse('calc((1 + 2) * 3)')).toEqual([
71+
{
72+
kind: 'function',
73+
value: 'calc',
74+
nodes: [
75+
{
76+
kind: 'function',
77+
value: '',
78+
nodes: [
79+
{ kind: 'word', value: '1' },
80+
{ kind: 'separator', value: ' ' },
81+
{ kind: 'word', value: '+' },
82+
{ kind: 'separator', value: ' ' },
83+
{ kind: 'word', value: '2' },
84+
],
85+
},
86+
{ kind: 'separator', value: ' ' },
87+
{ kind: 'word', value: '*' },
88+
{ kind: 'separator', value: ' ' },
89+
{ kind: 'word', value: '3' },
90+
],
91+
},
92+
])
93+
})
94+
95+
it('should handle media query params with functions', () => {
96+
expect(parse('(min-width: 600px) and (max-width:theme(colors.red.500))')).toEqual([
97+
{
98+
kind: 'function',
99+
value: '',
100+
nodes: [
101+
{ kind: 'word', value: 'min-width' },
102+
{ kind: 'separator', value: ': ' },
103+
{ kind: 'word', value: '600px' },
104+
],
105+
},
106+
{ kind: 'separator', value: ' ' },
107+
{ kind: 'word', value: 'and' },
108+
{ kind: 'separator', value: ' ' },
109+
{
110+
kind: 'function',
111+
value: '',
112+
nodes: [
113+
{ kind: 'word', value: 'max-width' },
114+
{ kind: 'separator', value: ':' },
115+
{ kind: 'function', value: 'theme', nodes: [{ kind: 'word', value: 'colors.red.500' }] },
116+
],
117+
},
118+
])
119+
})
120+
})
121+
122+
describe('toCss', () => {
123+
it('should pretty print calculations', () => {
124+
expect(toCss(parse('calc((1 + 2) * 3)'))).toBe('calc((1 + 2) * 3)')
125+
})
126+
127+
it('should pretty print nested function calls', () => {
128+
expect(toCss(parse('theme(foo, theme(bar))'))).toBe('theme(foo, theme(bar))')
129+
})
130+
131+
it('should pretty print media query params with functions', () => {
132+
expect(toCss(parse('(min-width: 600px) and (max-width:theme(colors.red.500))'))).toBe(
133+
'(min-width: 600px) and (max-width:theme(colors.red.500))',
134+
)
135+
})
136+
137+
it('preserves multiple spaces', () => {
138+
expect(toCss(parse('foo( bar )'))).toBe('foo( bar )')
139+
})
140+
})
141+
142+
describe('walk', () => {
143+
it('can be used to replace a function call', () => {
144+
const ast = parse('(min-width: 600px) and (max-width: theme(lg))')
145+
146+
walk(ast, (node, { replaceWith }) => {
147+
if (node.kind === 'function' && node.value === 'theme') {
148+
replaceWith({ kind: 'word', value: '64rem' })
149+
}
150+
})
151+
152+
expect(toCss(ast)).toBe('(min-width: 600px) and (max-width: 64rem)')
153+
})
154+
})

packages/tailwindcss/src/value-parser/parser.ts renamed to packages/tailwindcss/src/value-parser.ts

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,107 @@
1-
import { fun, separator, word, type AstNode, type FunctionNode } from './ast'
1+
export type ValueWordNode = {
2+
kind: 'word'
3+
value: string
4+
}
5+
6+
export type ValueFunctionNode = {
7+
kind: 'function'
8+
value: string
9+
nodes: ValueAstNode[]
10+
}
11+
12+
export type ValueSeparatorNode = {
13+
kind: 'separator'
14+
value: string
15+
}
16+
17+
export type ValueAstNode = ValueWordNode | ValueFunctionNode | ValueSeparatorNode
18+
19+
function word(value: string): ValueWordNode {
20+
return {
21+
kind: 'word',
22+
value,
23+
}
24+
}
25+
26+
function fun(value: string, nodes: ValueAstNode[]): ValueFunctionNode {
27+
return {
28+
kind: 'function',
29+
value: value,
30+
nodes,
31+
}
32+
}
33+
34+
function separator(value: string): ValueSeparatorNode {
35+
return {
36+
kind: 'separator',
37+
value,
38+
}
39+
}
40+
41+
enum ValueWalkAction {
42+
/** Continue walking, which is the default */
43+
Continue,
44+
45+
/** Skip visiting the children of this node */
46+
Skip,
47+
48+
/** Stop the walk entirely */
49+
Stop,
50+
}
51+
52+
export function walk(
53+
ast: ValueAstNode[],
54+
visit: (
55+
node: ValueAstNode,
56+
utils: {
57+
parent: ValueAstNode | null
58+
replaceWith(newNode: ValueAstNode | ValueAstNode[]): void
59+
},
60+
) => void | ValueWalkAction,
61+
parent: ValueAstNode | null = null,
62+
) {
63+
for (let i = 0; i < ast.length; i++) {
64+
let node = ast[i]
65+
let status =
66+
visit(node, {
67+
parent,
68+
replaceWith(newNode) {
69+
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
70+
// We want to visit the newly replaced node(s), which start at the
71+
// current index (i). By decrementing the index here, the next loop
72+
// will process this position (containing the replaced node) again.
73+
i--
74+
},
75+
}) ?? ValueWalkAction.Continue
76+
77+
// Stop the walk entirely
78+
if (status === ValueWalkAction.Stop) return
79+
80+
// Skip visiting the children of this node
81+
if (status === ValueWalkAction.Skip) continue
82+
83+
if (node.kind === 'function') {
84+
walk(node.nodes, visit, node)
85+
}
86+
}
87+
}
88+
89+
export function toCss(ast: ValueAstNode[]) {
90+
let css = ''
91+
for (const node of ast) {
92+
switch (node.kind) {
93+
case 'word':
94+
case 'separator': {
95+
css += node.value
96+
break
97+
}
98+
case 'function': {
99+
css += node.value + '(' + toCss(node.nodes) + ')'
100+
}
101+
}
102+
}
103+
return css
104+
}
2105

3106
const BACKSLASH = 0x5c
4107
const CLOSE_PAREN = 0x29
@@ -12,11 +115,11 @@ const SPACE = 0x20
12115
export function parse(input: string) {
13116
input = input.replaceAll('\r\n', '\n')
14117

15-
let ast: AstNode[] = []
118+
let ast: ValueAstNode[] = []
16119

17-
let stack: (FunctionNode | null)[] = []
120+
let stack: (ValueFunctionNode | null)[] = []
18121

19-
let parent = null as FunctionNode | null
122+
let parent = null as ValueFunctionNode | null
20123

21124
let buffer = ''
22125

packages/tailwindcss/src/value-parser/ast.test.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)