Skip to content

Commit a4e5075

Browse files
authored
breaking: wrap declaration values in a Value node (#97)
BREAKING CHANGES: - Declaration.value now returns VALUE node instead of raw string - Declaration.values property removed - Declaration.children now returns [VALUE] instead of value tokens - parse_value() function returns CSSNode instead of CSSNode[] Add VALUE node type (19) to wrap all declaration value tokens. Each declaration now has a single VALUE child node that contains the individual value tokens (IDENTIFIER, DIMENSION, etc.) as its children. Implementation changes: - Add VALUE constant (19) in arena.ts - Update NODES_PER_KB from 270 to 325 (+20% for VALUE wrappers) - Modify CSSNode.value getter to return VALUE node for declarations - Remove CSSNode.values getter (redundant) - Update ValueParser.parse_value() to return single VALUE node - Update DeclarationParser to create VALUE node for empty values - Export VALUE from constants.ts - Update 1068+ tests to use new API Migration guide: OLD: const text = declaration.value // "10px 20px" OLD: const tokens = declaration.values // [DIMENSION, DIMENSION] NEW: const valueNode = declaration.value // VALUE node NEW: const text = valueNode.text // "10px 20px" NEW: const tokens = valueNode.children // [DIMENSION, DIMENSION]
1 parent ab661d7 commit a4e5075

12 files changed

+314
-277
lines changed

src/api.test.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ describe('CSSNode', () => {
162162

163163
test('should work for other node types that use value field', () => {
164164
const source = 'body { color: red; }'
165-
const root = parse(source)
165+
const root = parse(source, { parse_values: false })
166166
const rule = root.first_child!
167167
const selector = rule.first_child!
168168
const block = selector.next_sibling!
@@ -509,7 +509,7 @@ describe('CSSNode', () => {
509509
const rule = root.first_child!
510510
const block = rule.block!
511511
const decl = block.first_child!
512-
const value = decl.first_child!
512+
const value = decl.first_child!.first_child!
513513

514514
expect(value.type_name).toBe('Identifier')
515515
})
@@ -520,7 +520,7 @@ describe('CSSNode', () => {
520520
const rule = root.first_child!
521521
const block = rule.block!
522522
const decl = block.first_child!
523-
const value = decl.first_child!
523+
const value = decl.first_child!.first_child!
524524

525525
expect(value.type_name).toBe('Number')
526526
})
@@ -531,7 +531,7 @@ describe('CSSNode', () => {
531531
const rule = root.first_child!
532532
const block = rule.block!
533533
const decl = block.first_child!
534-
const value = decl.first_child!
534+
const value = decl.first_child!.first_child!
535535

536536
expect(value.type_name).toBe('Dimension')
537537
})
@@ -542,7 +542,7 @@ describe('CSSNode', () => {
542542
const rule = root.first_child!
543543
const block = rule.block!
544544
const decl = block.first_child!
545-
const value = decl.first_child!
545+
const value = decl.first_child!.first_child!
546546

547547
expect(value.type_name).toBe('String')
548548
})
@@ -553,7 +553,7 @@ describe('CSSNode', () => {
553553
const rule = root.first_child!
554554
const block = rule.block!
555555
const decl = block.first_child!
556-
const value = decl.first_child!
556+
const value = decl.first_child!.first_child!
557557

558558
expect(value.type_name).toBe('Hash')
559559
})
@@ -564,7 +564,7 @@ describe('CSSNode', () => {
564564
const rule = root.first_child!
565565
const block = rule.block!
566566
const decl = block.first_child!
567-
const value = decl.first_child!
567+
const value = decl.first_child!.first_child!
568568

569569
expect(value.type_name).toBe('Function')
570570
})
@@ -1028,12 +1028,13 @@ describe('CSSNode', () => {
10281028

10291029
const deep = decl.clone()
10301030

1031-
expect(deep.children.length).toBe(2)
1032-
expect(deep.children[0].type).toBe(DIMENSION)
1033-
expect(deep.children[0].value).toBe(10)
1034-
expect(deep.children[0].unit).toBe('px')
1035-
expect(deep.children[1].value).toBe(20)
1036-
expect(deep.children[1].unit).toBe('px')
1031+
expect(deep.children.length).toBe(1) // VALUE node
1032+
expect(deep.children[0].children.length).toBe(2)
1033+
expect(deep.children[0].children[0].type).toBe(DIMENSION)
1034+
expect(deep.children[0].children[0].value).toBe(10)
1035+
expect(deep.children[0].children[0].unit).toBe('px')
1036+
expect(deep.children[0].children[1].value).toBe(20)
1037+
expect(deep.children[0].children[1].unit).toBe('px')
10371038
})
10381039

10391040
test('collects multiple children correctly', () => {
@@ -1042,11 +1043,12 @@ describe('CSSNode', () => {
10421043

10431044
const clone = decl.clone()
10441045

1045-
expect(clone.children.length).toBe(4)
1046-
expect(clone.children[0].value).toBe(10)
1047-
expect(clone.children[1].value).toBe(20)
1048-
expect(clone.children[2].value).toBe(30)
1049-
expect(clone.children[3].value).toBe(40)
1046+
expect(clone.children.length).toBe(1) // VALUE node
1047+
expect(clone.children[0].children.length).toBe(4)
1048+
expect(clone.children[0].children[0].value).toBe(10)
1049+
expect(clone.children[0].children[1].value).toBe(20)
1050+
expect(clone.children[0].children[2].value).toBe(30)
1051+
expect(clone.children[0].children[3].value).toBe(40)
10501052
})
10511053

10521054
test('handles nested children', () => {
@@ -1055,11 +1057,12 @@ describe('CSSNode', () => {
10551057

10561058
const clone = decl.clone()
10571059

1058-
expect(clone.children.length).toBe(1)
1059-
expect(clone.children[0].type).toBe(FUNCTION)
1060-
expect(clone.children[0].name).toBe('calc')
1060+
expect(clone.children.length).toBe(1) // VALUE node
1061+
expect(clone.children[0].children.length).toBe(1)
1062+
expect(clone.children[0].children[0].type).toBe(FUNCTION)
1063+
expect(clone.children[0].children[0].name).toBe('calc')
10611064
// Function should have nested children
1062-
expect(clone.children[0].children.length).toBeGreaterThan(0)
1065+
expect(clone.children[0].children[0].children.length).toBeGreaterThan(0)
10631066
})
10641067
})
10651068

@@ -1093,7 +1096,7 @@ describe('CSSNode', () => {
10931096
test('extracts dimension value with unit', () => {
10941097
const ast = parse('div { width: 100px; }')
10951098
const decl = ast.first_child!.block!.first_child!
1096-
const dimension = decl.first_child!
1099+
const dimension = decl.first_child!.first_child!
10971100

10981101
const clone = dimension.clone({ deep: false })
10991102

@@ -1106,7 +1109,7 @@ describe('CSSNode', () => {
11061109
test('extracts number value', () => {
11071110
const ast = parse('div { opacity: 0.5; }')
11081111
const decl = ast.first_child!.block!.first_child!
1109-
const number = decl.first_child!
1112+
const number = decl.first_child!.first_child!
11101113

11111114
const clone = number.clone({ deep: false })
11121115

@@ -1213,10 +1216,12 @@ describe('CSSNode', () => {
12131216

12141217
const clone = decl.clone({ locations: true })
12151218

1216-
expect(clone.children[0].line).toBeDefined()
1219+
expect(clone.children[0].line).toBeDefined() // VALUE node
12171220
expect(clone.children[0].column).toBeDefined()
1218-
expect(clone.children[1].line).toBeDefined()
1219-
expect(clone.children[1].column).toBeDefined()
1221+
expect(clone.children[0].children[0].line).toBeDefined() // First dimension
1222+
expect(clone.children[0].children[0].column).toBeDefined()
1223+
expect(clone.children[0].children[1].line).toBeDefined() // Second dimension
1224+
expect(clone.children[0].children[1].column).toBeDefined()
12201225
})
12211226
})
12221227
})

src/arena.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const FUNCTION = 15 // function: calc(), var()
6262
export const OPERATOR = 16 // operator: +, -, *, /, comma
6363
export const PARENTHESIS = 17 // parenthesized expression: (100% - 50px)
6464
export const URL = 18 // URL: url("file.css"), url(image.png), used in values and @import
65+
export const VALUE = 19 // Wrapper for declaration values
6566

6667
// Selector node type constants (for detailed selector parsing)
6768
export const SELECTOR_LIST = 20 // comma-separated selectors
@@ -125,7 +126,9 @@ export class CSSDataArena {
125126
private static readonly GROWTH_FACTOR = 1.3
126127

127128
// Estimated nodes per KB of CSS (based on real-world data)
128-
private static readonly NODES_PER_KB = 270
129+
// Increased from 270 to 325 to account for VALUE wrapper nodes
130+
// (~20% of nodes are declarations, +1 VALUE node per declaration = +20% nodes)
131+
private static readonly NODES_PER_KB = 325
129132

130133
// Buffer to avoid frequent growth (15%)
131134
private static readonly CAPACITY_BUFFER = 1.2

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
OPERATOR,
1919
PARENTHESIS,
2020
URL,
21+
VALUE,
2122
SELECTOR_LIST,
2223
TYPE_SELECTOR,
2324
CLASS_SELECTOR,
@@ -69,6 +70,7 @@ export {
6970
OPERATOR,
7071
PARENTHESIS,
7172
URL,
73+
VALUE,
7274
SELECTOR_LIST,
7375
TYPE_SELECTOR,
7476
CLASS_SELECTOR,
@@ -123,6 +125,7 @@ export const NODE_TYPES = {
123125
OPERATOR,
124126
PARENTHESIS,
125127
URL,
128+
VALUE,
126129
// Selector nodes
127130
SELECTOR_LIST,
128131
TYPE_SELECTOR,

src/css-node.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
OPERATOR,
1818
PARENTHESIS,
1919
URL,
20+
VALUE,
2021
SELECTOR_LIST,
2122
TYPE_SELECTOR,
2223
CLASS_SELECTOR,
@@ -67,6 +68,7 @@ export const TYPE_NAMES = {
6768
[OPERATOR]: 'Operator',
6869
[PARENTHESIS]: 'Parentheses',
6970
[URL]: 'Url',
71+
[VALUE]: 'Value',
7072
[SELECTOR_LIST]: 'SelectorList',
7173
[TYPE_SELECTOR]: 'TypeSelector',
7274
[CLASS_SELECTOR]: 'ClassSelector',
@@ -110,6 +112,7 @@ export type CSSNodeType =
110112
| typeof OPERATOR
111113
| typeof PARENTHESIS
112114
| typeof URL
115+
| typeof VALUE
113116
| typeof SELECTOR_LIST
114117
| typeof TYPE_SELECTOR
115118
| typeof CLASS_SELECTOR
@@ -240,9 +243,15 @@ export class CSSNode {
240243
* For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
241244
* For URL nodes with unquoted URL: returns the URL content without quotes
242245
*/
243-
get value(): string | number | null {
246+
get value(): CSSNode | string | number | null {
244247
let { type, text } = this
245248

249+
// For DECLARATION nodes with parsed values, return the VALUE node
250+
// For DECLARATION nodes without parsed values, fall through to get raw text
251+
if (type === DECLARATION && this.first_child) {
252+
return this.first_child // VALUE node (when parse_values=true)
253+
}
254+
246255
if (type === DIMENSION) {
247256
return parse_dimension(text).value
248257
}
@@ -427,19 +436,6 @@ export class CSSNode {
427436
return true
428437
}
429438

430-
// --- Value Node Access (for declarations) ---
431-
432-
/** Get array of parsed value nodes (for declarations only) */
433-
get values(): CSSNode[] {
434-
let result: CSSNode[] = []
435-
let child = this.first_child
436-
while (child) {
437-
result.push(child)
438-
child = child.next_sibling
439-
}
440-
return result
441-
}
442-
443439
/** Get start line number */
444440
get line(): number {
445441
return this.arena.get_start_line(this.index)

src/parse-atrule-prelude.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
IDENTIFIER,
1414
PRELUDE_OPERATOR,
1515
URL,
16-
FUNCTION,
1716
DIMENSION,
1817
FEATURE_RANGE,
1918
} from './arena'

0 commit comments

Comments
 (0)