Skip to content

Commit 0adcef7

Browse files
authored
feat: expose parse_declaration (#72)
`du -sh dist/` before: 228K after: 240K
1 parent 03969db commit 0adcef7

File tree

8 files changed

+560
-70
lines changed

8 files changed

+560
-70
lines changed

API.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,35 @@ for (const part of selector.first_child) {
529529
530530
---
531531
532+
## `parse_declaration(source)`
533+
534+
Parse a CSS declaration string into a detailed AST.
535+
536+
```typescript
537+
function parse_declaration(source: string): CSSNode
538+
```
539+
540+
**Example:**
541+
542+
```typescript
543+
import { parse_declaration } from '@projectwallace/css-parser'
544+
545+
const decl = parse_declaration('color: red !important')
546+
547+
console.log(decl.type) // DECLARATION
548+
console.log(decl.name) // "color"
549+
console.log(decl.value) // "red"
550+
console.log(decl.is_important) // true
551+
552+
// Iterate over value nodes
553+
for (const valueNode of decl.children) {
554+
console.log(valueNode.type, valueNode.text)
555+
}
556+
// IDENTIFIER "red"
557+
```
558+
559+
---
560+
532561
## `parse_atrule_prelude(at_rule_name, prelude)`
533562
534563
Parse an at-rule prelude into structured nodes.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
"types": "./dist/parse-atrule-prelude.d.ts",
3636
"import": "./dist/parse-atrule-prelude.js"
3737
},
38+
"./parse-declaration": {
39+
"types": "./dist/parse-declaration.d.ts",
40+
"import": "./dist/parse-declaration.js"
41+
},
3842
"./parse-value": {
3943
"types": "./dist/parse-value.d.ts",
4044
"import": "./dist/parse-value.js"

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export { parse } from './parse'
55
export { parse_selector } from './parse-selector'
66
export { parse_atrule_prelude } from './parse-atrule-prelude'
7+
export { parse_declaration } from './parse-declaration'
78
export { parse_value } from './parse-value'
89
export { tokenize } from './tokenize'
910
export { walk, traverse, SKIP, BREAK } from './walk'

src/parse-declaration.test.ts

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import { describe, test, expect } from 'vitest'
2+
import { parse_declaration } from './parse-declaration'
3+
import { DECLARATION, IDENTIFIER, DIMENSION, NUMBER, FUNCTION } from './arena'
4+
5+
describe('parse_declaration', () => {
6+
describe('Location Tracking', () => {
7+
test('line and column for declaration starting at column 1', () => {
8+
const node = parse_declaration('color: red')
9+
expect(node.line).toBe(1)
10+
expect(node.column).toBe(1)
11+
expect(node.start).toBe(0)
12+
expect(node.length).toBe(10)
13+
})
14+
15+
test('line and column with leading whitespace', () => {
16+
const node = parse_declaration(' color: red')
17+
expect(node.line).toBe(1)
18+
expect(node.column).toBe(3) // Points to 'c' in 'color'
19+
expect(node.start).toBe(2)
20+
})
21+
22+
test('line and column for multi-line input', () => {
23+
const node = parse_declaration('\n margin: 10px')
24+
expect(node.line).toBe(2)
25+
expect(node.column).toBe(3)
26+
})
27+
28+
test('start and length for simple declaration', () => {
29+
const node = parse_declaration('color: red')
30+
expect(node.start).toBe(0)
31+
expect(node.length).toBe(10)
32+
expect(node.end).toBe(10)
33+
})
34+
35+
test('start and length with semicolon', () => {
36+
const node = parse_declaration('color: red;')
37+
expect(node.start).toBe(0)
38+
expect(node.length).toBe(11)
39+
expect(node.end).toBe(11)
40+
})
41+
42+
test('value nodes have correct line/column', () => {
43+
const node = parse_declaration('color: red blue')
44+
const [value1, value2] = node.children
45+
expect(value1.line).toBe(1)
46+
expect(value1.column).toBe(8) // Position of 'red'
47+
expect(value2.line).toBe(1)
48+
expect(value2.column).toBe(12) // Position of 'blue'
49+
})
50+
51+
test('value nodes on multi-line have correct positions', () => {
52+
const node = parse_declaration('margin:\n 10px 20px')
53+
const [value1, value2] = node.children
54+
expect(value1.line).toBe(2)
55+
expect(value1.column).toBe(3) // Position of '10px'
56+
expect(value2.line).toBe(2)
57+
expect(value2.column).toBe(8) // Position of '20px'
58+
})
59+
})
60+
61+
describe('Basic Properties', () => {
62+
test('simple declaration', () => {
63+
const node = parse_declaration('color: red')
64+
expect(node.type).toBe(DECLARATION)
65+
expect(node.name).toBe('color')
66+
expect(node.value).toBe('red')
67+
expect(node.is_important).toBe(false)
68+
})
69+
70+
test('declaration with semicolon', () => {
71+
const node = parse_declaration('color: red;')
72+
expect(node.type).toBe(DECLARATION)
73+
expect(node.name).toBe('color')
74+
expect(node.value).toBe('red')
75+
})
76+
77+
test('declaration without semicolon', () => {
78+
const node = parse_declaration('color: red')
79+
expect(node.type).toBe(DECLARATION)
80+
expect(node.name).toBe('color')
81+
expect(node.value).toBe('red')
82+
})
83+
84+
test('declaration with whitespace variations', () => {
85+
const node = parse_declaration('color : red')
86+
expect(node.name).toBe('color')
87+
expect(node.value).toBe('red')
88+
})
89+
90+
test('declaration with leading and trailing whitespace', () => {
91+
const node = parse_declaration(' color: red ')
92+
expect(node.name).toBe('color')
93+
expect(node.value).toBe('red')
94+
})
95+
96+
test('empty value', () => {
97+
const node = parse_declaration('color:')
98+
expect(node.name).toBe('color')
99+
// Empty values return null (consistent with main parser)
100+
expect(node.value).toBe(null)
101+
expect(node.children).toHaveLength(0)
102+
})
103+
104+
test('empty value with semicolon', () => {
105+
const node = parse_declaration('color:;')
106+
expect(node.name).toBe('color')
107+
// Empty values return null (consistent with main parser)
108+
expect(node.value).toBe(null)
109+
})
110+
})
111+
112+
describe('!important Flag', () => {
113+
test('declaration with !important', () => {
114+
const node = parse_declaration('color: red !important')
115+
expect(node.name).toBe('color')
116+
expect(node.value).toBe('red')
117+
expect(node.is_important).toBe(true)
118+
})
119+
120+
test('declaration with !important and semicolon', () => {
121+
const node = parse_declaration('color: red !important;')
122+
expect(node.name).toBe('color')
123+
expect(node.value).toBe('red')
124+
expect(node.is_important).toBe(true)
125+
})
126+
127+
test('historic !ie variant', () => {
128+
const node = parse_declaration('color: red !ie')
129+
expect(node.name).toBe('color')
130+
expect(node.value).toBe('red')
131+
expect(node.is_important).toBe(true)
132+
})
133+
134+
test('any identifier after ! is treated as important', () => {
135+
const node = parse_declaration('color: red !foo')
136+
expect(node.name).toBe('color')
137+
expect(node.value).toBe('red')
138+
expect(node.is_important).toBe(true)
139+
})
140+
141+
test('!important with no spaces', () => {
142+
const node = parse_declaration('color: red!important')
143+
expect(node.is_important).toBe(true)
144+
})
145+
})
146+
147+
describe('Vendor Prefixes', () => {
148+
test('-webkit- vendor prefix', () => {
149+
const node = parse_declaration('-webkit-transform: rotate(45deg)')
150+
expect(node.name).toBe('-webkit-transform')
151+
expect(node.is_vendor_prefixed).toBe(true)
152+
})
153+
154+
test('-moz- vendor prefix', () => {
155+
const node = parse_declaration('-moz-appearance: none')
156+
expect(node.name).toBe('-moz-appearance')
157+
expect(node.is_vendor_prefixed).toBe(true)
158+
})
159+
160+
test('-ms- vendor prefix', () => {
161+
const node = parse_declaration('-ms-filter: blur(5px)')
162+
expect(node.name).toBe('-ms-filter')
163+
expect(node.is_vendor_prefixed).toBe(true)
164+
})
165+
166+
test('-o- vendor prefix', () => {
167+
const node = parse_declaration('-o-transition: all 0.3s')
168+
expect(node.name).toBe('-o-transition')
169+
expect(node.is_vendor_prefixed).toBe(true)
170+
})
171+
172+
test('non-prefixed property', () => {
173+
const node = parse_declaration('transform: rotate(45deg)')
174+
expect(node.name).toBe('transform')
175+
expect(node.is_vendor_prefixed).toBe(false)
176+
})
177+
178+
test('custom property is not vendor prefixed', () => {
179+
const node = parse_declaration('--custom-color: blue')
180+
expect(node.name).toBe('--custom-color')
181+
expect(node.is_vendor_prefixed).toBe(false)
182+
})
183+
})
184+
185+
describe('Value Parsing', () => {
186+
test('identifier value', () => {
187+
const node = parse_declaration('display: flex')
188+
expect(node.children).toHaveLength(1)
189+
expect(node.children[0].type).toBe(IDENTIFIER)
190+
expect(node.children[0].text).toBe('flex')
191+
})
192+
193+
test('number value', () => {
194+
const node = parse_declaration('opacity: 0.5')
195+
expect(node.children).toHaveLength(1)
196+
expect(node.children[0].type).toBe(NUMBER)
197+
expect(node.children[0].value).toBe(0.5)
198+
})
199+
200+
test('dimension value', () => {
201+
const node = parse_declaration('width: 100px')
202+
expect(node.children).toHaveLength(1)
203+
expect(node.children[0].type).toBe(DIMENSION)
204+
expect(node.children[0].value).toBe(100)
205+
expect(node.children[0].unit).toBe('px')
206+
})
207+
208+
test('multiple values', () => {
209+
const node = parse_declaration('margin: 10px 20px 30px 40px')
210+
expect(node.children).toHaveLength(4)
211+
expect(node.children[0].type).toBe(DIMENSION)
212+
expect(node.children[0].text).toBe('10px')
213+
expect(node.children[1].text).toBe('20px')
214+
expect(node.children[2].text).toBe('30px')
215+
expect(node.children[3].text).toBe('40px')
216+
})
217+
218+
test('function value', () => {
219+
const node = parse_declaration('transform: rotate(45deg)')
220+
expect(node.children).toHaveLength(1)
221+
expect(node.children[0].type).toBe(FUNCTION)
222+
expect(node.children[0].name).toBe('rotate')
223+
})
224+
225+
test('nested functions', () => {
226+
const node = parse_declaration('width: calc(100% - 20px)')
227+
expect(node.children).toHaveLength(1)
228+
expect(node.children[0].type).toBe(FUNCTION)
229+
expect(node.children[0].name).toBe('calc')
230+
expect(node.children[0].children.length).toBeGreaterThan(0)
231+
})
232+
233+
test('complex value with multiple functions', () => {
234+
const node = parse_declaration('background: linear-gradient(to bottom, red, blue)')
235+
expect(node.children).toHaveLength(1)
236+
expect(node.children[0].type).toBe(FUNCTION)
237+
expect(node.children[0].name).toBe('linear-gradient')
238+
})
239+
240+
test('CSS variable', () => {
241+
const node = parse_declaration('color: var(--primary-color)')
242+
expect(node.children).toHaveLength(1)
243+
expect(node.children[0].type).toBe(FUNCTION)
244+
expect(node.children[0].name).toBe('var')
245+
})
246+
})
247+
248+
describe('Edge Cases', () => {
249+
test('invalid input - no colon', () => {
250+
const node = parse_declaration('not-a-declaration')
251+
expect(node.type).toBe(DECLARATION)
252+
// Should return empty declaration node
253+
expect(node.start).toBe(0)
254+
expect(node.length).toBe(0)
255+
})
256+
257+
test('empty string', () => {
258+
const node = parse_declaration('')
259+
expect(node.type).toBe(DECLARATION)
260+
expect(node.start).toBe(0)
261+
expect(node.length).toBe(0)
262+
})
263+
264+
test('only property name - no colon', () => {
265+
const node = parse_declaration('color')
266+
expect(node.type).toBe(DECLARATION)
267+
expect(node.start).toBe(0)
268+
expect(node.length).toBe(0)
269+
})
270+
271+
test('property with colon but value with invalid token', () => {
272+
const node = parse_declaration('color: red')
273+
expect(node.name).toBe('color')
274+
expect(node.value).toBe('red')
275+
})
276+
})
277+
278+
describe('CSSNode API', () => {
279+
test('node.type is DECLARATION', () => {
280+
const node = parse_declaration('color: red')
281+
expect(node.type).toBe(DECLARATION)
282+
})
283+
284+
test('node.name returns property name', () => {
285+
const node = parse_declaration('background-color: blue')
286+
expect(node.name).toBe('background-color')
287+
})
288+
289+
test('node.value returns raw value string', () => {
290+
const node = parse_declaration('margin: 10px 20px')
291+
expect(node.value).toBe('10px 20px')
292+
})
293+
294+
test('node.is_important returns boolean', () => {
295+
const node1 = parse_declaration('color: red')
296+
expect(node1.is_important).toBe(false)
297+
298+
const node2 = parse_declaration('color: red !important')
299+
expect(node2.is_important).toBe(true)
300+
})
301+
302+
test('node.is_vendor_prefixed returns boolean', () => {
303+
const node1 = parse_declaration('transform: none')
304+
expect(node1.is_vendor_prefixed).toBe(false)
305+
306+
const node2 = parse_declaration('-webkit-transform: none')
307+
expect(node2.is_vendor_prefixed).toBe(true)
308+
})
309+
310+
test('node.children returns value nodes', () => {
311+
const node = parse_declaration('margin: 10px 20px')
312+
expect(node.children).toHaveLength(2)
313+
expect(node.children[0].type).toBe(DIMENSION)
314+
expect(node.children[1].type).toBe(DIMENSION)
315+
})
316+
317+
test('node.text returns full declaration text', () => {
318+
const node = parse_declaration('color: red')
319+
expect(node.text).toBe('color: red')
320+
})
321+
322+
test('node location properties', () => {
323+
const node = parse_declaration('color: red')
324+
expect(node.line).toBe(1)
325+
expect(node.column).toBe(1)
326+
expect(node.start).toBe(0)
327+
expect(node.length).toBe(10)
328+
expect(node.end).toBe(10)
329+
})
330+
})
331+
})

0 commit comments

Comments
 (0)