From 5b1d012e8406adc28d3d41b0ccf94733eba1db47 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Fri, 2 Jan 2026 17:25:37 +0100 Subject: [PATCH 1/3] breaking: wrap declaration values in a Value node --- src/arena.ts | 5 +- src/constants.ts | 3 + src/css-node.ts | 23 ++- src/parse-declaration.test.ts | 100 ++++++------ src/parse-declaration.ts | 12 +- src/parse-value.test.ts | 292 +++++++++++++++++----------------- src/parse-value.ts | 44 +++-- src/walk.test.ts | 14 +- 8 files changed, 263 insertions(+), 230 deletions(-) diff --git a/src/arena.ts b/src/arena.ts index 7c4d37a..147be85 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -62,6 +62,7 @@ export const FUNCTION = 15 // function: calc(), var() export const OPERATOR = 16 // operator: +, -, *, /, comma export const PARENTHESIS = 17 // parenthesized expression: (100% - 50px) export const URL = 18 // URL: url("file.css"), url(image.png), used in values and @import +export const VALUE = 19 // Wrapper for declaration values // Selector node type constants (for detailed selector parsing) export const SELECTOR_LIST = 20 // comma-separated selectors @@ -125,7 +126,9 @@ export class CSSDataArena { private static readonly GROWTH_FACTOR = 1.3 // Estimated nodes per KB of CSS (based on real-world data) - private static readonly NODES_PER_KB = 270 + // Increased from 270 to 325 to account for VALUE wrapper nodes + // (~20% of nodes are declarations, +1 VALUE node per declaration = +20% nodes) + private static readonly NODES_PER_KB = 325 // Buffer to avoid frequent growth (15%) private static readonly CAPACITY_BUFFER = 1.2 diff --git a/src/constants.ts b/src/constants.ts index f4ee4df..5f4090f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,6 +18,7 @@ import { OPERATOR, PARENTHESIS, URL, + VALUE, SELECTOR_LIST, TYPE_SELECTOR, CLASS_SELECTOR, @@ -69,6 +70,7 @@ export { OPERATOR, PARENTHESIS, URL, + VALUE, SELECTOR_LIST, TYPE_SELECTOR, CLASS_SELECTOR, @@ -123,6 +125,7 @@ export const NODE_TYPES = { OPERATOR, PARENTHESIS, URL, + VALUE, // Selector nodes SELECTOR_LIST, TYPE_SELECTOR, diff --git a/src/css-node.ts b/src/css-node.ts index 9c0d0a3..ab417ce 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -17,6 +17,7 @@ import { OPERATOR, PARENTHESIS, URL, + VALUE, SELECTOR_LIST, TYPE_SELECTOR, CLASS_SELECTOR, @@ -67,6 +68,7 @@ export const TYPE_NAMES = { [OPERATOR]: 'Operator', [PARENTHESIS]: 'Parentheses', [URL]: 'Url', + [VALUE]: 'Value', [SELECTOR_LIST]: 'SelectorList', [TYPE_SELECTOR]: 'TypeSelector', [CLASS_SELECTOR]: 'ClassSelector', @@ -110,6 +112,7 @@ export type CSSNodeType = | typeof OPERATOR | typeof PARENTHESIS | typeof URL + | typeof VALUE | typeof SELECTOR_LIST | typeof TYPE_SELECTOR | typeof CLASS_SELECTOR @@ -240,9 +243,14 @@ export class CSSNode { * For URL nodes with quoted string: returns the string with quotes (consistent with STRING node) * For URL nodes with unquoted URL: returns the URL content without quotes */ - get value(): string | number | null { + get value(): CSSNode | string | number | null { let { type, text } = this + // For DECLARATION nodes, return the VALUE node + if (type === DECLARATION) { + return this.first_child // VALUE node + } + if (type === DIMENSION) { return parse_dimension(text).value } @@ -427,19 +435,6 @@ export class CSSNode { return true } - // --- Value Node Access (for declarations) --- - - /** Get array of parsed value nodes (for declarations only) */ - get values(): CSSNode[] { - let result: CSSNode[] = [] - let child = this.first_child - while (child) { - result.push(child) - child = child.next_sibling - } - return result - } - /** Get start line number */ get line(): number { return this.arena.get_start_line(this.index) diff --git a/src/parse-declaration.test.ts b/src/parse-declaration.test.ts index b83a1cc..d60866a 100644 --- a/src/parse-declaration.test.ts +++ b/src/parse-declaration.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest' import { parse_declaration } from './parse-declaration' -import { DECLARATION, IDENTIFIER, DIMENSION, NUMBER, FUNCTION } from './arena' +import { DECLARATION, IDENTIFIER, DIMENSION, NUMBER, FUNCTION, VALUE } from './arena' describe('parse_declaration', () => { describe('Location Tracking', () => { @@ -41,7 +41,7 @@ describe('parse_declaration', () => { test('value nodes have correct line/column', () => { const node = parse_declaration('color: red blue') - const [value1, value2] = node.children + const [value1, value2] = node.value.children expect(value1.line).toBe(1) expect(value1.column).toBe(8) // Position of 'red' expect(value2.line).toBe(1) @@ -50,7 +50,7 @@ describe('parse_declaration', () => { test('value nodes on multi-line have correct positions', () => { const node = parse_declaration('margin:\n 10px 20px') - const [value1, value2] = node.children + const [value1, value2] = node.value.children expect(value1.line).toBe(2) expect(value1.column).toBe(3) // Position of '10px' expect(value2.line).toBe(2) @@ -63,7 +63,7 @@ describe('parse_declaration', () => { const node = parse_declaration('color: red') expect(node.type).toBe(DECLARATION) expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') expect(node.is_important).toBe(false) }) @@ -71,41 +71,41 @@ describe('parse_declaration', () => { const node = parse_declaration('color: red;') expect(node.type).toBe(DECLARATION) expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') }) test('declaration without semicolon', () => { const node = parse_declaration('color: red') expect(node.type).toBe(DECLARATION) expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') }) test('declaration with whitespace variations', () => { const node = parse_declaration('color : red') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') }) test('declaration with leading and trailing whitespace', () => { const node = parse_declaration(' color: red ') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') }) test('empty value', () => { const node = parse_declaration('color:') expect(node.name).toBe('color') // Empty values return null (consistent with main parser) - expect(node.value).toBe(null) - expect(node.children).toHaveLength(0) + expect(node.value.text).toBe("") + expect(node.value.children).toHaveLength(0) }) test('empty value with semicolon', () => { const node = parse_declaration('color:;') expect(node.name).toBe('color') // Empty values return null (consistent with main parser) - expect(node.value).toBe(null) + expect(node.value.text).toBe("") }) }) @@ -113,28 +113,28 @@ describe('parse_declaration', () => { test('declaration with !important', () => { const node = parse_declaration('color: red !important') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') expect(node.is_important).toBe(true) }) test('declaration with !important and semicolon', () => { const node = parse_declaration('color: red !important;') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') expect(node.is_important).toBe(true) }) test('historic !ie variant', () => { const node = parse_declaration('color: red !ie') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') expect(node.is_important).toBe(true) }) test('any identifier after ! is treated as important', () => { const node = parse_declaration('color: red !foo') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') expect(node.is_important).toBe(true) }) @@ -193,7 +193,7 @@ describe('parse_declaration', () => { test('value\\9', () => { const node = parse_declaration('property: value\\9') - expect(node.value).toBe('value\\9') + expect(node.value.text).toBe('value\\9') expect(node.is_browserhack).toBe(false) }) @@ -216,63 +216,63 @@ describe('parse_declaration', () => { describe('Value Parsing', () => { test('identifier value', () => { const node = parse_declaration('display: flex') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(IDENTIFIER) - expect(node.children[0].text).toBe('flex') + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(IDENTIFIER) + expect(node.value.children[0].text).toBe('flex') }) test('number value', () => { const node = parse_declaration('opacity: 0.5') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(NUMBER) - expect(node.children[0].value).toBe(0.5) + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(NUMBER) + expect(node.value.children[0].value).toBe(0.5) }) test('dimension value', () => { const node = parse_declaration('width: 100px') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(DIMENSION) - expect(node.children[0].value).toBe(100) - expect(node.children[0].unit).toBe('px') + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(DIMENSION) + expect(node.value.children[0].value).toBe(100) + expect(node.value.children[0].unit).toBe('px') }) test('multiple values', () => { const node = parse_declaration('margin: 10px 20px 30px 40px') - expect(node.children).toHaveLength(4) - expect(node.children[0].type).toBe(DIMENSION) - expect(node.children[0].text).toBe('10px') - expect(node.children[1].text).toBe('20px') - expect(node.children[2].text).toBe('30px') - expect(node.children[3].text).toBe('40px') + expect(node.value.children).toHaveLength(4) + expect(node.value.children[0].type).toBe(DIMENSION) + expect(node.value.children[0].text).toBe('10px') + expect(node.value.children[1].text).toBe('20px') + expect(node.value.children[2].text).toBe('30px') + expect(node.value.children[3].text).toBe('40px') }) test('function value', () => { const node = parse_declaration('transform: rotate(45deg)') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(FUNCTION) - expect(node.children[0].name).toBe('rotate') + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(FUNCTION) + expect(node.value.children[0].name).toBe('rotate') }) test('nested functions', () => { const node = parse_declaration('width: calc(100% - 20px)') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(FUNCTION) - expect(node.children[0].name).toBe('calc') - expect(node.children[0].children.length).toBeGreaterThan(0) + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(FUNCTION) + expect(node.value.children[0].name).toBe('calc') + expect(node.value.children[0].children.length).toBeGreaterThan(0) }) test('complex value with multiple functions', () => { const node = parse_declaration('background: linear-gradient(to bottom, red, blue)') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(FUNCTION) - expect(node.children[0].name).toBe('linear-gradient') + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(FUNCTION) + expect(node.value.children[0].name).toBe('linear-gradient') }) test('CSS variable', () => { const node = parse_declaration('color: var(--primary-color)') - expect(node.children).toHaveLength(1) - expect(node.children[0].type).toBe(FUNCTION) - expect(node.children[0].name).toBe('var') + expect(node.value.children).toHaveLength(1) + expect(node.value.children[0].type).toBe(FUNCTION) + expect(node.value.children[0].name).toBe('var') }) }) @@ -302,7 +302,7 @@ describe('parse_declaration', () => { test('property with colon but value with invalid token', () => { const node = parse_declaration('color: red') expect(node.name).toBe('color') - expect(node.value).toBe('red') + expect(node.value.text).toBe('red') }) }) @@ -319,7 +319,7 @@ describe('parse_declaration', () => { test('node.value returns raw value string', () => { const node = parse_declaration('margin: 10px 20px') - expect(node.value).toBe('10px 20px') + expect(node.value.text).toBe('10px 20px') }) test('node.is_important returns boolean', () => { @@ -340,9 +340,9 @@ describe('parse_declaration', () => { test('node.children returns value nodes', () => { const node = parse_declaration('margin: 10px 20px') - expect(node.children).toHaveLength(2) - expect(node.children[0].type).toBe(DIMENSION) - expect(node.children[1].type).toBe(DIMENSION) + expect(node.value.children).toHaveLength(2) + expect(node.value.children[0].type).toBe(DIMENSION) + expect(node.value.children[1].type).toBe(DIMENSION) }) test('node.text returns full declaration text', () => { diff --git a/src/parse-declaration.ts b/src/parse-declaration.ts index a7ae20e..7577d27 100644 --- a/src/parse-declaration.ts +++ b/src/parse-declaration.ts @@ -213,15 +213,21 @@ export class DeclarationParser { // Parse value into structured nodes (only if enabled) if (this.value_parser) { // CRITICAL: Pass value_start_line and value_start_column to value parser - let valueNodes = this.value_parser.parse_value(value_start, trimmed[1], value_start_line, value_start_column) + let valueNode = this.value_parser.parse_value(value_start, trimmed[1], value_start_line, value_start_column) - // Link value nodes as children of the declaration - this.arena.append_children(declaration, valueNodes) + // Link VALUE node as single child of the declaration + this.arena.append_children(declaration, [valueNode]) } } else { // Empty value - set zero-length value field so node.value returns "" instead of null this.arena.set_value_start_delta(declaration, value_start - prop_start) this.arena.set_value_length(declaration, 0) + + // Create empty VALUE node for consistency + if (this.value_parser) { + let valueNode = this.value_parser.parse_value(value_start, value_start, value_start_line, value_start_column) + this.arena.append_children(declaration, [valueNode]) + } } // Set !important flag if found diff --git a/src/parse-value.test.ts b/src/parse-value.test.ts index 0041f5d..cf277f5 100644 --- a/src/parse-value.test.ts +++ b/src/parse-value.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { parse } from './parse' -import { IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL } from './arena' +import { IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL, VALUE } from './arena' describe('Value Node Types', () => { // Helper to get first value node from a declaration @@ -8,7 +8,7 @@ describe('Value Node Types', () => { const root = parse(css) const rule = root.first_child const decl = rule?.first_child?.next_sibling?.first_child // selector → block → declaration - return decl?.values[0] + return decl?.value.children[0] } describe('Locations', () => { @@ -154,7 +154,7 @@ describe('Value Node Types', () => { it('should have correct offset and length', () => { const root = parse('div { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const comma = decl?.values[1] + const comma = decl?.value.children[1] expect(comma?.start).toBe(24) expect(comma?.length).toBe(1) expect(comma?.end).toBe(25) @@ -165,7 +165,7 @@ describe('Value Node Types', () => { it('should have correct line and column on line 2', () => { const root = parse('div {\n font-family: Arial, sans-serif;\n}') const decl = root.first_child?.first_child?.next_sibling?.first_child - const comma = decl?.values[1] + const comma = decl?.value.children[1] expect(comma?.start).toBe(26) expect(comma?.length).toBe(1) expect(comma?.end).toBe(27) @@ -177,7 +177,7 @@ describe('Value Node Types', () => { describe('PARENTHESIS', () => { it('should have correct offset and length', () => { const root = parse('div { width: calc((100% - 50px) / 2); }') - const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] const paren = func?.children[0] expect(paren?.start).toBe(18) expect(paren?.length).toBe(13) @@ -188,7 +188,7 @@ describe('Value Node Types', () => { it('should have correct line and column on line 2', () => { const root = parse('div {\n width: calc((100% - 50px) / 2);\n}') - const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] const paren = func?.children[0] expect(paren?.start).toBe(20) expect(paren?.length).toBe(13) @@ -252,13 +252,13 @@ describe('Value Node Types', () => { it('OPERATOR type constant', () => { const root = parse('div { font-family: Arial, sans-serif; }') - const comma = root.first_child?.first_child?.next_sibling?.first_child?.values[1] + const comma = root.first_child?.first_child?.next_sibling?.first_child?.value.children[1] expect(comma?.type).toBe(OPERATOR) }) it('PARENTHESIS type constant', () => { const root = parse('div { width: calc((100% - 50px) / 2); }') - const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] const paren = func?.children[0] expect(paren?.type).toBe(PARENTHESIS) }) @@ -302,13 +302,13 @@ describe('Value Node Types', () => { it('OPERATOR type_name', () => { const root = parse('div { font-family: Arial, sans-serif; }') - const comma = root.first_child?.first_child?.next_sibling?.first_child?.values[1] + const comma = root.first_child?.first_child?.next_sibling?.first_child?.value.children[1] expect(comma?.type_name).toBe('Operator') }) it('PARENTHESIS type_name', () => { const root = parse('div { width: calc((100% - 50px) / 2); }') - const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] const paren = func?.children[0] expect(paren?.type_name).toBe('Parentheses') }) @@ -325,20 +325,20 @@ describe('Value Node Types', () => { const root = parse('body { color: red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('red') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('red') + expect(decl?.value.text).toBe('red') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('red') }) it('should parse multiple keywords', () => { const root = parse('body { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(3) - expect(decl?.values[0].type).toBe(IDENTIFIER) - expect(decl?.values[0].text).toBe('Arial') - expect(decl?.values[2].type).toBe(IDENTIFIER) - expect(decl?.values[2].text).toBe('sans-serif') + expect(decl?.value.children).toHaveLength(3) + expect(decl?.value.children[0].type).toBe(IDENTIFIER) + expect(decl?.value.children[0].text).toBe('Arial') + expect(decl?.value.children[2].type).toBe(IDENTIFIER) + expect(decl?.value.children[2].text).toBe('sans-serif') }) }) @@ -347,27 +347,27 @@ describe('Value Node Types', () => { const root = parse('body { opacity: 0.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('0.5') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('0.5') + expect(decl?.value.text).toBe('0.5') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('0.5') }) it('should handle negative numbers', () => { const root = parse('body { margin: -10px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(DIMENSION) - expect(decl?.values[0].text).toBe('-10px') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(DIMENSION) + expect(decl?.value.children[0].text).toBe('-10px') }) it('should handle zero without unit', () => { const root = parse('body { margin: 0; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(NUMBER) - expect(decl?.values[0].text).toBe('0') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(NUMBER) + expect(decl?.value.children[0].text).toBe('0') }) }) @@ -376,55 +376,55 @@ describe('Value Node Types', () => { const root = parse('body { width: 100px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('100px') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('100px') - expect(decl?.values[0].value).toBe(100) - expect(decl?.values[0].unit).toBe('px') + expect(decl?.value.text).toBe('100px') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('100px') + expect(decl?.value.children[0].value).toBe(100) + expect(decl?.value.children[0].unit).toBe('px') }) it('should parse em dimension values', () => { const root = parse('body { font-size: 3em; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('3em') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('3em') - expect(decl?.values[0].value).toBe(3) - expect(decl?.values[0].unit).toBe('em') + expect(decl?.value.text).toBe('3em') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('3em') + expect(decl?.value.children[0].value).toBe(3) + expect(decl?.value.children[0].unit).toBe('em') }) it('should parse percentage values', () => { const root = parse('body { width: 50%; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('50%') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('50%') + expect(decl?.value.text).toBe('50%') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('50%') }) it('should handle zero with unit', () => { const root = parse('body { margin: 0px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(DIMENSION) - expect(decl?.values[0].text).toBe('0px') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(DIMENSION) + expect(decl?.value.children[0].text).toBe('0px') }) it('should parse margin shorthand', () => { const root = parse('body { margin: 10px 20px 30px 40px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(4) - expect(decl?.values[0].type).toBe(DIMENSION) - expect(decl?.values[0].text).toBe('10px') - expect(decl?.values[1].type).toBe(DIMENSION) - expect(decl?.values[1].text).toBe('20px') - expect(decl?.values[2].type).toBe(DIMENSION) - expect(decl?.values[2].text).toBe('30px') - expect(decl?.values[3].type).toBe(DIMENSION) - expect(decl?.values[3].text).toBe('40px') + expect(decl?.value.children).toHaveLength(4) + expect(decl?.value.children[0].type).toBe(DIMENSION) + expect(decl?.value.children[0].text).toBe('10px') + expect(decl?.value.children[1].type).toBe(DIMENSION) + expect(decl?.value.children[1].text).toBe('20px') + expect(decl?.value.children[2].type).toBe(DIMENSION) + expect(decl?.value.children[2].text).toBe('30px') + expect(decl?.value.children[3].type).toBe(DIMENSION) + expect(decl?.value.children[3].text).toBe('40px') }) }) @@ -433,9 +433,9 @@ describe('Value Node Types', () => { const root = parse('body { content: "hello"; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('"hello"') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('"hello"') + expect(decl?.value.text).toBe('"hello"') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('"hello"') }) }) @@ -444,9 +444,9 @@ describe('Value Node Types', () => { const root = parse('body { color: #ff0000; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('#ff0000') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].text).toBe('#ff0000') + expect(decl?.value.text).toBe('#ff0000') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].text).toBe('#ff0000') }) }) @@ -455,16 +455,16 @@ describe('Value Node Types', () => { const root = parse('body { color: rgb(255, 0, 0); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(FUNCTION) - expect(decl?.values[0].name).toBe('rgb') - expect(decl?.values[0].text).toBe('rgb(255, 0, 0)') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(FUNCTION) + expect(decl?.value.children[0].name).toBe('rgb') + expect(decl?.value.children[0].text).toBe('rgb(255, 0, 0)') }) it('should parse function arguments', () => { const root = parse('body { color: rgb(255, 0, 0); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.children).toHaveLength(5) expect(func?.children[0].type).toBe(NUMBER) @@ -483,34 +483,34 @@ describe('Value Node Types', () => { const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(FUNCTION) - expect(decl?.values[0].name).toBe('calc') - expect(decl?.values[0].children).toHaveLength(3) - expect(decl?.values[0].children[0].type).toBe(DIMENSION) - expect(decl?.values[0].children[0].text).toBe('100%') - expect(decl?.values[0].children[1].type).toBe(OPERATOR) - expect(decl?.values[0].children[1].text).toBe('-') - expect(decl?.values[0].children[2].type).toBe(DIMENSION) - expect(decl?.values[0].children[2].text).toBe('20px') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(FUNCTION) + expect(decl?.value.children[0].name).toBe('calc') + expect(decl?.value.children[0].children).toHaveLength(3) + expect(decl?.value.children[0].children[0].type).toBe(DIMENSION) + expect(decl?.value.children[0].children[0].text).toBe('100%') + expect(decl?.value.children[0].children[1].type).toBe(OPERATOR) + expect(decl?.value.children[0].children[1].text).toBe('-') + expect(decl?.value.children[0].children[2].type).toBe(DIMENSION) + expect(decl?.value.children[0].children[2].text).toBe('20px') }) it('should parse var() function', () => { const root = parse('body { color: var(--primary-color); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(FUNCTION) - expect(decl?.values[0].name).toBe('var') - expect(decl?.values[0].children).toHaveLength(1) - expect(decl?.values[0].children[0].type).toBe(IDENTIFIER) - expect(decl?.values[0].children[0].text).toBe('--primary-color') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(FUNCTION) + expect(decl?.value.children[0].name).toBe('var') + expect(decl?.value.children[0].children).toHaveLength(1) + expect(decl?.value.children[0].children[0].type).toBe(IDENTIFIER) + expect(decl?.value.children[0].children[0].text).toBe('--primary-color') }) it('should provide node.value for calc()', () => { const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('calc') @@ -522,7 +522,7 @@ describe('Value Node Types', () => { it('should provide node.value for var() function', () => { const root = parse('body { color: var(--primary-color); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('var') @@ -534,7 +534,7 @@ describe('Value Node Types', () => { it('should provide node.value for var() function with fallback', () => { const root = parse('body { color: var(--primary-color, 1); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('var') @@ -547,24 +547,24 @@ describe('Value Node Types', () => { const root = parse('body { transform: translateX(10px) rotate(45deg); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(2) - expect(decl?.values[0].type).toBe(FUNCTION) - expect(decl?.values[0].name).toBe('translateX') - expect(decl?.values[1].type).toBe(FUNCTION) - expect(decl?.values[1].name).toBe('rotate') + expect(decl?.value.children).toHaveLength(2) + expect(decl?.value.children[0].type).toBe(FUNCTION) + expect(decl?.value.children[0].name).toBe('translateX') + expect(decl?.value.children[1].type).toBe(FUNCTION) + expect(decl?.value.children[1].name).toBe('rotate') }) it('should parse filter value', () => { const root = parse('body { filter: blur(5px) brightness(1.2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(2) - expect(decl?.values[0].type).toBe(FUNCTION) - expect(decl?.values[0].name).toBe('blur') - expect(decl?.values[0].children[0].text).toBe('5px') - expect(decl?.values[1].type).toBe(FUNCTION) - expect(decl?.values[1].name).toBe('brightness') - expect(decl?.values[1].children[0].text).toBe('1.2') + expect(decl?.value.children).toHaveLength(2) + expect(decl?.value.children[0].type).toBe(FUNCTION) + expect(decl?.value.children[0].name).toBe('blur') + expect(decl?.value.children[0].children[0].text).toBe('5px') + expect(decl?.value.children[1].type).toBe(FUNCTION) + expect(decl?.value.children[1].name).toBe('brightness') + expect(decl?.value.children[1].children[0].text).toBe('1.2') }) }) @@ -573,14 +573,14 @@ describe('Value Node Types', () => { const root = parse('body { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values[1].type).toBe(OPERATOR) - expect(decl?.values[1].text).toBe(',') + expect(decl?.value.children[1].type).toBe(OPERATOR) + expect(decl?.value.children[1].text).toBe(',') }) it('should parse calc operators', () => { const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.children[1].type).toBe(OPERATOR) expect(func?.children[1].text).toBe('-') @@ -589,7 +589,7 @@ describe('Value Node Types', () => { it('should parse all calc operators', () => { const root = parse('body { width: calc(1px + 2px * 3px / 4px - 5px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] const operators = func?.children.filter((n) => n.type === OPERATOR) expect(operators).toHaveLength(4) @@ -604,7 +604,7 @@ describe('Value Node Types', () => { it('should parse parenthesized expressions in calc()', () => { const root = parse('body { width: calc((100% - 50px) / 2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('calc') @@ -636,7 +636,7 @@ describe('Value Node Types', () => { it('should parse complex nested parentheses', () => { const root = parse('body { width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('calc') @@ -676,20 +676,20 @@ describe('Value Node Types', () => { const root = parse('body { background: url("image.png"); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(URL) - expect(decl?.values[0].name).toBe('url') - expect(decl?.values[0].children).toHaveLength(1) - expect(decl?.values[0].children[0].type).toBe(STRING) - expect(decl?.values[0].children[0].text).toBe('"image.png"') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(URL) + expect(decl?.value.children[0].name).toBe('url') + expect(decl?.value.children[0].children).toHaveLength(1) + expect(decl?.value.children[0].children[0].type).toBe(STRING) + expect(decl?.value.children[0].children[0].text).toBe('"image.png"') // URL node with quoted string returns the string value with quotes - expect(decl?.values[0].value).toBe('"image.png"') + expect(decl?.value.children[0].value).toBe('"image.png"') }) it('should parse url() function with unquoted URL containing dots', () => { const root = parse('body { cursor: url(mycursor.cur); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(URL) expect(func?.name).toBe('url') @@ -703,7 +703,7 @@ describe('Value Node Types', () => { it('should parse src() function with unquoted URL', () => { const root = parse('body { content: src(myfont.woff2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('src') @@ -716,20 +716,20 @@ describe('Value Node Types', () => { const root = parse("body { background: url('image.png'); }") const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(URL) - expect(decl?.values[0].name).toBe('url') - expect(decl?.values[0].children).toHaveLength(1) - expect(decl?.values[0].children[0].type).toBe(STRING) - expect(decl?.values[0].children[0].text).toBe("'image.png'") + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(URL) + expect(decl?.value.children[0].name).toBe('url') + expect(decl?.value.children[0].children).toHaveLength(1) + expect(decl?.value.children[0].children[0].type).toBe(STRING) + expect(decl?.value.children[0].children[0].text).toBe("'image.png'") // URL node with single-quoted string returns the string value with quotes - expect(decl?.values[0].value).toBe("'image.png'") + expect(decl?.value.children[0].value).toBe("'image.png'") }) it('should parse url() with base64 data URL', () => { const root = parse('body { background: url(data:image/png;base64,iVBORw0KGg); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(URL) expect(func?.name).toBe('url') @@ -740,7 +740,7 @@ describe('Value Node Types', () => { it('should parse url() with inline SVG', () => { const root = parse('body { background: url(data:image/svg+xml,); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.values[0] + const func = decl?.value.children[0] expect(func?.type).toBe(URL) expect(func?.name).toBe('url') @@ -751,12 +751,12 @@ describe('Value Node Types', () => { it('should parse complex background value with url()', () => { const root = parse('body { background: url("bg.png") no-repeat center center / cover; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - + expect(decl?.value.children.length).toBeGreaterThan(1) expect(decl?.values.length).toBeGreaterThan(1) - expect(decl?.values[0].type).toBe(URL) - expect(decl?.values[0].name).toBe('url') - expect(decl?.values[1].type).toBe(IDENTIFIER) - expect(decl?.values[1].text).toBe('no-repeat') + expect(decl?.value.children[0].type).toBe(URL) + expect(decl?.value.children[0].name).toBe('url') + expect(decl?.value.children[1].type).toBe(IDENTIFIER) + expect(decl?.value.children[1].text).toBe('no-repeat') }) }) @@ -765,31 +765,31 @@ describe('Value Node Types', () => { const root = parse('body { border: 1px solid red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.values).toHaveLength(3) - expect(decl?.values[0].type).toBe(DIMENSION) - expect(decl?.values[0].text).toBe('1px') - expect(decl?.values[1].type).toBe(IDENTIFIER) - expect(decl?.values[1].text).toBe('solid') - expect(decl?.values[2].type).toBe(IDENTIFIER) - expect(decl?.values[2].text).toBe('red') + expect(decl?.value.children).toHaveLength(3) + expect(decl?.value.children[0].type).toBe(DIMENSION) + expect(decl?.value.children[0].text).toBe('1px') + expect(decl?.value.children[1].type).toBe(IDENTIFIER) + expect(decl?.value.children[1].text).toBe('solid') + expect(decl?.value.children[2].type).toBe(IDENTIFIER) + expect(decl?.value.children[2].text).toBe('red') }) it('should handle empty value', () => { const root = parse('body { color: ; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBeNull() - expect(decl?.values).toHaveLength(0) + expect(decl?.value.type).toBe(VALUE) + expect(decl?.value.children).toHaveLength(0) }) it('should handle value with !important', () => { const root = parse('body { color: red !important; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value).toBe('red') - expect(decl?.values).toHaveLength(1) - expect(decl?.values[0].type).toBe(IDENTIFIER) - expect(decl?.values[0].text).toBe('red') + expect(decl?.value.text).toBe('red') + expect(decl?.value.children).toHaveLength(1) + expect(decl?.value.children[0].type).toBe(IDENTIFIER) + expect(decl?.value.children[0].text).toBe('red') expect(decl?.is_important).toBe(true) }) }) @@ -799,7 +799,7 @@ describe('Value Node Types', () => { it('should return number for NUMBER nodes', () => { const root = parse('div { opacity: 0.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const numberNode = decl?.values[0] + const numberNode = decl?.value.children[0] expect(numberNode?.type).toBe(NUMBER) expect(numberNode?.value_as_number).toBe(0.5) @@ -808,7 +808,7 @@ describe('Value Node Types', () => { it('should return number for DIMENSION nodes', () => { const root = parse('div { width: 100px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const dimNode = decl?.values[0] + const dimNode = decl?.value.children[0] expect(dimNode?.type).toBe(DIMENSION) expect(dimNode?.value_as_number).toBe(100) @@ -817,7 +817,7 @@ describe('Value Node Types', () => { it('should handle negative numbers', () => { const root = parse('div { margin: -10px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const dimNode = decl?.values[0] + const dimNode = decl?.value.children[0] expect(dimNode?.type).toBe(DIMENSION) expect(dimNode?.value_as_number).toBe(-10) @@ -826,7 +826,7 @@ describe('Value Node Types', () => { it('should handle zero', () => { const root = parse('div { margin: 0; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const numberNode = decl?.values[0] + const numberNode = decl?.value.children[0] expect(numberNode?.type).toBe(NUMBER) expect(numberNode?.value_as_number).toBe(0) @@ -835,7 +835,7 @@ describe('Value Node Types', () => { it('should handle decimal numbers', () => { const root = parse('div { line-height: 1.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const numberNode = decl?.values[0] + const numberNode = decl?.value.children[0] expect(numberNode?.type).toBe(NUMBER) expect(numberNode?.value_as_number).toBe(1.5) @@ -844,7 +844,7 @@ describe('Value Node Types', () => { it('should handle percentage dimensions', () => { const root = parse('div { width: 50%; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const dimNode = decl?.values[0] + const dimNode = decl?.value.children[0] expect(dimNode?.type).toBe(DIMENSION) expect(dimNode?.value_as_number).toBe(50) @@ -854,7 +854,7 @@ describe('Value Node Types', () => { it('should return null for IDENTIFIER nodes', () => { const root = parse('div { color: red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const identNode = decl?.values[0] + const identNode = decl?.value.children[0] expect(identNode?.type).toBe(IDENTIFIER) expect(identNode?.value_as_number).toBeNull() @@ -863,7 +863,7 @@ describe('Value Node Types', () => { it('should return null for STRING nodes', () => { const root = parse('div { content: "hello"; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const stringNode = decl?.values[0] + const stringNode = decl?.value.children[0] expect(stringNode?.type).toBe(STRING) expect(stringNode?.value_as_number).toBeNull() @@ -872,7 +872,7 @@ describe('Value Node Types', () => { it('should return null for FUNCTION nodes', () => { const root = parse('div { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const funcNode = decl?.values[0] + const funcNode = decl?.value.children[0] expect(funcNode?.type).toBe(FUNCTION) expect(funcNode?.value_as_number).toBeNull() @@ -884,7 +884,7 @@ describe('Value Node Types', () => { const root = parse(css) const rule = root.first_child const decl = rule?.first_child?.next_sibling?.first_child - return decl?.values[0] + return decl?.value.children[0] } it('should parse URL() with uppercase', () => { diff --git a/src/parse-value.ts b/src/parse-value.ts index 86a0460..261cb6f 100644 --- a/src/parse-value.ts +++ b/src/parse-value.ts @@ -1,6 +1,6 @@ // Value Parser - Parses CSS declaration values into structured AST nodes import { Lexer } from './tokenize' -import { CSSDataArena, IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL } from './arena' +import { CSSDataArena, IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL, VALUE } from './arena' import { TOKEN_IDENT, TOKEN_NUMBER, @@ -33,9 +33,9 @@ export class ValueParser { this.value_end = 0 } - // Parse a declaration value range into value nodes (standalone use) - // Returns array of value node indices - parse_value(start: number, end: number, start_line: number, start_column: number): number[] { + // Parse a declaration value range into a VALUE wrapper node + // Returns single VALUE node index + parse_value(start: number, end: number, start_line: number, start_column: number): number { this.value_end = end // Position lexer at value start with provided line/column @@ -43,7 +43,28 @@ export class ValueParser { this.lexer.line = start_line this.lexer.column = start_column - return this.parse_value_tokens() + // Parse individual value tokens + let value_nodes = this.parse_value_tokens() + + // Wrap in VALUE node + if (value_nodes.length === 0) { + // Empty value - create VALUE node with no children + let value_node = this.arena.create_node(VALUE, start, 0, start_line, start_column) + return value_node + } + + // Create VALUE wrapper node spanning all value tokens + let first_node_start = this.arena.get_start_offset(value_nodes[0]) + let last_node_index = value_nodes[value_nodes.length - 1] + let last_node_end = + this.arena.get_start_offset(last_node_index) + this.arena.get_length(last_node_index) + + let value_node = this.arena.create_node(VALUE, first_node_start, last_node_end - first_node_start, start_line, start_column) + + // Link value tokens as children + this.arena.append_children(value_node, value_nodes) + + return value_node } // Core token parsing logic @@ -337,11 +358,11 @@ export class ValueParser { } /** - * Parse a CSS declaration value string and return an array of value AST nodes + * Parse a CSS declaration value string and return a VALUE node * @param value_string - The CSS value to parse (e.g., "1px solid red") - * @returns An array of CSSNode objects representing the parsed value + * @returns A CSSNode VALUE wrapper containing the parsed value tokens as children */ -export function parse_value(value_string: string): CSSNode[] { +export function parse_value(value_string: string): CSSNode { // Create an arena for the value nodes const arena = new CSSDataArena(CSSDataArena.capacity_for_source(value_string.length)) @@ -349,8 +370,9 @@ export function parse_value(value_string: string): CSSNode[] { const value_parser = new ValueParser(arena, value_string) // Parse the entire source as a value (starting at line 1, column 1) - const node_indices = value_parser.parse_value(0, value_string.length, 1, 1) + // Returns single VALUE node index now + const value_node_index = value_parser.parse_value(0, value_string.length, 1, 1) - // Wrap each node index in a CSSNode - return node_indices.map((index) => new CSSNode(arena, value_string, index)) + // Wrap the VALUE node in a CSSNode + return new CSSNode(arena, value_string, value_node_index) } diff --git a/src/walk.test.ts b/src/walk.test.ts index 4cec6d3..ff9822c 100644 --- a/src/walk.test.ts +++ b/src/walk.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { parse } from './parse' -import { STYLESHEET, STYLE_RULE, SELECTOR_LIST, DECLARATION, AT_RULE, BLOCK, IDENTIFIER, NUMBER, DIMENSION } from './constants' +import { STYLESHEET, STYLE_RULE, SELECTOR_LIST, DECLARATION, AT_RULE, BLOCK, IDENTIFIER, NUMBER, DIMENSION, VALUE } from './constants' import { walk, traverse, SKIP, BREAK } from './walk' describe('walk', () => { @@ -29,6 +29,7 @@ describe('walk', () => { SELECTOR_LIST, BLOCK, DECLARATION, + VALUE, IDENTIFIER, // red ]) }) @@ -47,13 +48,16 @@ describe('walk', () => { SELECTOR_LIST, // body selector BLOCK, // body block DECLARATION, // color: red + VALUE, IDENTIFIER, // red DECLARATION, // margin: 0 + VALUE, NUMBER, // 0 STYLE_RULE, // div rule SELECTOR_LIST, // div selector BLOCK, // div block DECLARATION, // padding: 1rem + VALUE, DIMENSION, // 1rem ]) }) @@ -171,8 +175,8 @@ describe('walk', () => { depths.push(depth) }) - // STYLESHEET (0), STYLE_RULE (1), SELECTOR_LIST (2), BLOCK (2), DECLARATION (3), IDENTIFIER (4) - expect(depths).toEqual([0, 1, 2, 2, 3, 4]) + // STYLESHEET (0), STYLE_RULE (1), SELECTOR_LIST (2), BLOCK (2), DECLARATION (3), VALUE (4), IDENTIFIER (5) + expect(depths).toEqual([0, 1, 2, 2, 3, 4, 5]) }) it('should track depth in nested structures', () => { @@ -303,7 +307,7 @@ describe('walk with SKIP and BREAK', () => { }) // All nodes should be visited (SKIP on leaf has no effect) - expect(visited).toEqual([STYLESHEET, STYLE_RULE, SELECTOR_LIST, BLOCK, DECLARATION, IDENTIFIER]) + expect(visited).toEqual([STYLESHEET, STYLE_RULE, SELECTOR_LIST, BLOCK, DECLARATION, VALUE, IDENTIFIER]) }) it('should stop traversal when BREAK is returned', () => { @@ -384,7 +388,7 @@ describe('walk with SKIP and BREAK', () => { // No return value - should continue normally }) - expect(visited).toEqual([STYLESHEET, STYLE_RULE, SELECTOR_LIST, BLOCK, DECLARATION, IDENTIFIER]) + expect(visited).toEqual([STYLESHEET, STYLE_RULE, SELECTOR_LIST, BLOCK, DECLARATION, VALUE, IDENTIFIER]) }) it('should find first declaration with specific property using BREAK', () => { From 55d654251d6727d54e0555dee31cd564f2d87e7e Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Fri, 2 Jan 2026 17:39:37 +0100 Subject: [PATCH 2/3] fix remaining tests --- src/api.test.ts | 59 +++++++++++++++++++++------------------ src/css-node.ts | 7 +++-- src/parse-options.test.ts | 8 +++--- src/parse-value.test.ts | 1 - src/parse.test.ts | 30 ++++++++++---------- 5 files changed, 55 insertions(+), 50 deletions(-) diff --git a/src/api.test.ts b/src/api.test.ts index 05874bf..0a9a358 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -162,7 +162,7 @@ describe('CSSNode', () => { test('should work for other node types that use value field', () => { const source = 'body { color: red; }' - const root = parse(source) + const root = parse(source, { parse_values: false }) const rule = root.first_child! const selector = rule.first_child! const block = selector.next_sibling! @@ -509,7 +509,7 @@ describe('CSSNode', () => { const rule = root.first_child! const block = rule.block! const decl = block.first_child! - const value = decl.first_child! + const value = decl.first_child!.first_child! expect(value.type_name).toBe('Identifier') }) @@ -520,7 +520,7 @@ describe('CSSNode', () => { const rule = root.first_child! const block = rule.block! const decl = block.first_child! - const value = decl.first_child! + const value = decl.first_child!.first_child! expect(value.type_name).toBe('Number') }) @@ -531,7 +531,7 @@ describe('CSSNode', () => { const rule = root.first_child! const block = rule.block! const decl = block.first_child! - const value = decl.first_child! + const value = decl.first_child!.first_child! expect(value.type_name).toBe('Dimension') }) @@ -542,7 +542,7 @@ describe('CSSNode', () => { const rule = root.first_child! const block = rule.block! const decl = block.first_child! - const value = decl.first_child! + const value = decl.first_child!.first_child! expect(value.type_name).toBe('String') }) @@ -553,7 +553,7 @@ describe('CSSNode', () => { const rule = root.first_child! const block = rule.block! const decl = block.first_child! - const value = decl.first_child! + const value = decl.first_child!.first_child! expect(value.type_name).toBe('Hash') }) @@ -564,7 +564,7 @@ describe('CSSNode', () => { const rule = root.first_child! const block = rule.block! const decl = block.first_child! - const value = decl.first_child! + const value = decl.first_child!.first_child! expect(value.type_name).toBe('Function') }) @@ -1028,12 +1028,13 @@ describe('CSSNode', () => { const deep = decl.clone() - expect(deep.children.length).toBe(2) - expect(deep.children[0].type).toBe(DIMENSION) - expect(deep.children[0].value).toBe(10) - expect(deep.children[0].unit).toBe('px') - expect(deep.children[1].value).toBe(20) - expect(deep.children[1].unit).toBe('px') + expect(deep.children.length).toBe(1) // VALUE node + expect(deep.children[0].children.length).toBe(2) + expect(deep.children[0].children[0].type).toBe(DIMENSION) + expect(deep.children[0].children[0].value).toBe(10) + expect(deep.children[0].children[0].unit).toBe('px') + expect(deep.children[0].children[1].value).toBe(20) + expect(deep.children[0].children[1].unit).toBe('px') }) test('collects multiple children correctly', () => { @@ -1042,11 +1043,12 @@ describe('CSSNode', () => { const clone = decl.clone() - expect(clone.children.length).toBe(4) - expect(clone.children[0].value).toBe(10) - expect(clone.children[1].value).toBe(20) - expect(clone.children[2].value).toBe(30) - expect(clone.children[3].value).toBe(40) + expect(clone.children.length).toBe(1) // VALUE node + expect(clone.children[0].children.length).toBe(4) + expect(clone.children[0].children[0].value).toBe(10) + expect(clone.children[0].children[1].value).toBe(20) + expect(clone.children[0].children[2].value).toBe(30) + expect(clone.children[0].children[3].value).toBe(40) }) test('handles nested children', () => { @@ -1055,11 +1057,12 @@ describe('CSSNode', () => { const clone = decl.clone() - expect(clone.children.length).toBe(1) - expect(clone.children[0].type).toBe(FUNCTION) - expect(clone.children[0].name).toBe('calc') + expect(clone.children.length).toBe(1) // VALUE node + expect(clone.children[0].children.length).toBe(1) + expect(clone.children[0].children[0].type).toBe(FUNCTION) + expect(clone.children[0].children[0].name).toBe('calc') // Function should have nested children - expect(clone.children[0].children.length).toBeGreaterThan(0) + expect(clone.children[0].children[0].children.length).toBeGreaterThan(0) }) }) @@ -1093,7 +1096,7 @@ describe('CSSNode', () => { test('extracts dimension value with unit', () => { const ast = parse('div { width: 100px; }') const decl = ast.first_child!.block!.first_child! - const dimension = decl.first_child! + const dimension = decl.first_child!.first_child! const clone = dimension.clone({ deep: false }) @@ -1106,7 +1109,7 @@ describe('CSSNode', () => { test('extracts number value', () => { const ast = parse('div { opacity: 0.5; }') const decl = ast.first_child!.block!.first_child! - const number = decl.first_child! + const number = decl.first_child!.first_child! const clone = number.clone({ deep: false }) @@ -1213,10 +1216,12 @@ describe('CSSNode', () => { const clone = decl.clone({ locations: true }) - expect(clone.children[0].line).toBeDefined() + expect(clone.children[0].line).toBeDefined() // VALUE node expect(clone.children[0].column).toBeDefined() - expect(clone.children[1].line).toBeDefined() - expect(clone.children[1].column).toBeDefined() + expect(clone.children[0].children[0].line).toBeDefined() // First dimension + expect(clone.children[0].children[0].column).toBeDefined() + expect(clone.children[0].children[1].line).toBeDefined() // Second dimension + expect(clone.children[0].children[1].column).toBeDefined() }) }) }) diff --git a/src/css-node.ts b/src/css-node.ts index ab417ce..f3858aa 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -246,9 +246,10 @@ export class CSSNode { get value(): CSSNode | string | number | null { let { type, text } = this - // For DECLARATION nodes, return the VALUE node - if (type === DECLARATION) { - return this.first_child // VALUE node + // For DECLARATION nodes with parsed values, return the VALUE node + // For DECLARATION nodes without parsed values, fall through to get raw text + if (type === DECLARATION && this.first_child) { + return this.first_child // VALUE node (when parse_values=true) } if (type === DIMENSION) { diff --git a/src/parse-options.test.ts b/src/parse-options.test.ts index c3b9ac9..f846626 100644 --- a/src/parse-options.test.ts +++ b/src/parse-options.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { parse } from './parse' -import { SELECTOR_LIST, DECLARATION, IDENTIFIER } from './arena' +import { SELECTOR_LIST, DECLARATION, IDENTIFIER, VALUE } from './arena' describe('Parser Options', () => { const css = 'body { color: red; }' @@ -22,7 +22,7 @@ describe('Parser Options', () => { expect(declaration).not.toBeNull() expect(declaration?.type).toBe(DECLARATION) expect(declaration?.has_children).toBe(true) - expect(declaration?.first_child?.type).toBe(IDENTIFIER) + expect(declaration?.first_child?.type).toBe(VALUE) }) it('should parse values and selectors with explicit options', () => { @@ -38,7 +38,7 @@ describe('Parser Options', () => { const block = selector?.next_sibling const declaration = block?.first_child expect(declaration?.has_children).toBe(true) - expect(declaration?.first_child?.type).toBe(IDENTIFIER) + expect(declaration?.first_child?.type).toBe(VALUE) }) }) @@ -104,7 +104,7 @@ describe('Parser Options', () => { const block = selector?.next_sibling const declaration = block?.first_child expect(declaration?.has_children).toBe(true) - expect(declaration?.first_child?.type).toBe(IDENTIFIER) + expect(declaration?.first_child?.type).toBe(VALUE) }) it('should handle complex selectors without parsing', () => { diff --git a/src/parse-value.test.ts b/src/parse-value.test.ts index cf277f5..1dc2ac5 100644 --- a/src/parse-value.test.ts +++ b/src/parse-value.test.ts @@ -752,7 +752,6 @@ describe('Value Node Types', () => { const root = parse('body { background: url("bg.png") no-repeat center center / cover; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value.children.length).toBeGreaterThan(1) - expect(decl?.values.length).toBeGreaterThan(1) expect(decl?.value.children[0].type).toBe(URL) expect(decl?.value.children[0].name).toBe('url') expect(decl?.value.children[1].type).toBe(IDENTIFIER) diff --git a/src/parse.test.ts b/src/parse.test.ts index 73d7a0f..fd69c04 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -1245,7 +1245,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value).toBe('blue') + expect(decl.value.text).toBe('blue') }) test('extract value with spaces', () => { @@ -1257,7 +1257,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('padding') - expect(decl.value).toBe('1rem 2rem 3rem 4rem') + expect(decl.value.text).toBe('1rem 2rem 3rem 4rem') }) test('extract function value', () => { @@ -1269,7 +1269,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('background') - expect(decl.value).toBe('linear-gradient(to bottom, red, blue)') + expect(decl.value.text).toBe('linear-gradient(to bottom, red, blue)') }) test('extract calc value', () => { @@ -1281,7 +1281,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('width') - expect(decl.value).toBe('calc(100% - 2rem)') + expect(decl.value.text).toBe('calc(100% - 2rem)') }) test('exclude !important from value', () => { @@ -1293,7 +1293,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value).toBe('blue') + expect(decl.value.text).toBe('blue') expect(decl.is_important).toBe(true) }) @@ -1306,7 +1306,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value).toBe('blue') + expect(decl.value.text).toBe('blue') }) test('CSS custom property value', () => { @@ -1318,7 +1318,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('--brand-color') - expect(decl.value).toBe('rgb(0% 10% 50% / 0.5)') + expect(decl.value.text).toBe('rgb(0% 10% 50% / 0.5)') }) test('var() reference value', () => { @@ -1330,7 +1330,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value).toBe('var(--primary-color)') + expect(decl.value.text).toBe('var(--primary-color)') }) test('nested function value', () => { @@ -1342,7 +1342,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('transform') - expect(decl.value).toBe('translate(calc(50% - 1rem), 0)') + expect(decl.value.text).toBe('translate(calc(50% - 1rem), 0)') }) test('value without semicolon', () => { @@ -1354,7 +1354,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value).toBe('blue') + expect(decl.value.text).toBe('blue') }) test('empty value', () => { @@ -1366,7 +1366,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value).toBe(null) + expect(decl.value.text).toBe('') }) test('URL value', () => { @@ -1378,7 +1378,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('background') - expect(decl.value).toBe('url("image.png")') + expect(decl.value.text).toBe('url("image.png")') }) }) @@ -2505,7 +2505,7 @@ describe('Core Nodes', () => { const declaration = nestingRule.block!.first_child! expect(declaration.type).toBe(DECLARATION) expect(declaration.property).toBe('--is') - expect(declaration.value).toBe('this') + expect(declaration.value.text).toBe('this') }) }) }) @@ -2541,7 +2541,7 @@ describe('Core Nodes', () => { expect(declaration.text).toContain(longSvg.substring(longSvg.length - 100)) // Verify the value is parsed into nodes - const urlNode = declaration.first_child! + const urlNode = declaration.first_child!.first_child! expect(urlNode.type).toBe(URL) expect(urlNode.name).toBe('url') @@ -2560,7 +2560,7 @@ describe('Core Nodes', () => { expect(secondDecl).toBeTruthy() expect(secondDecl.type).toBe(DECLARATION) expect(secondDecl.property).toBe('color') - expect(secondDecl.value).toBe('red') + expect(secondDecl.value.text).toBe('red') // Calculate expected column: '.test { ' + declaration.text + ' ' + 1 (columns are 1-indexed) const expectedColumn = '.test { '.length + declText.length + ' '.length + 1 From 3c2e180ca53653549d3cdbab6dfe41ac58e5987b Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Fri, 2 Jan 2026 17:45:22 +0100 Subject: [PATCH 3/3] fixes --- src/parse-atrule-prelude.test.ts | 1 - src/parse-declaration.test.ts | 100 +++++------ src/parse-options.test.ts | 2 +- src/parse-value.test.ts | 290 +++++++++++++++---------------- src/parse.test.ts | 28 +-- 5 files changed, 210 insertions(+), 211 deletions(-) diff --git a/src/parse-atrule-prelude.test.ts b/src/parse-atrule-prelude.test.ts index 036ca20..0a7a1fd 100644 --- a/src/parse-atrule-prelude.test.ts +++ b/src/parse-atrule-prelude.test.ts @@ -13,7 +13,6 @@ import { IDENTIFIER, PRELUDE_OPERATOR, URL, - FUNCTION, DIMENSION, FEATURE_RANGE, } from './arena' diff --git a/src/parse-declaration.test.ts b/src/parse-declaration.test.ts index d60866a..3f34a34 100644 --- a/src/parse-declaration.test.ts +++ b/src/parse-declaration.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest' import { parse_declaration } from './parse-declaration' -import { DECLARATION, IDENTIFIER, DIMENSION, NUMBER, FUNCTION, VALUE } from './arena' +import { DECLARATION, IDENTIFIER, DIMENSION, NUMBER, FUNCTION } from './arena' describe('parse_declaration', () => { describe('Location Tracking', () => { @@ -41,7 +41,7 @@ describe('parse_declaration', () => { test('value nodes have correct line/column', () => { const node = parse_declaration('color: red blue') - const [value1, value2] = node.value.children + const [value1, value2] = node.first_child!.children expect(value1.line).toBe(1) expect(value1.column).toBe(8) // Position of 'red' expect(value2.line).toBe(1) @@ -50,7 +50,7 @@ describe('parse_declaration', () => { test('value nodes on multi-line have correct positions', () => { const node = parse_declaration('margin:\n 10px 20px') - const [value1, value2] = node.value.children + const [value1, value2] = node.first_child!.children expect(value1.line).toBe(2) expect(value1.column).toBe(3) // Position of '10px' expect(value2.line).toBe(2) @@ -63,7 +63,7 @@ describe('parse_declaration', () => { const node = parse_declaration('color: red') expect(node.type).toBe(DECLARATION) expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') expect(node.is_important).toBe(false) }) @@ -71,41 +71,41 @@ describe('parse_declaration', () => { const node = parse_declaration('color: red;') expect(node.type).toBe(DECLARATION) expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') }) test('declaration without semicolon', () => { const node = parse_declaration('color: red') expect(node.type).toBe(DECLARATION) expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') }) test('declaration with whitespace variations', () => { const node = parse_declaration('color : red') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') }) test('declaration with leading and trailing whitespace', () => { const node = parse_declaration(' color: red ') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') }) test('empty value', () => { const node = parse_declaration('color:') expect(node.name).toBe('color') // Empty values return null (consistent with main parser) - expect(node.value.text).toBe("") - expect(node.value.children).toHaveLength(0) + expect(node.first_child!.text).toBe('') + expect(node.first_child!.children).toHaveLength(0) }) test('empty value with semicolon', () => { const node = parse_declaration('color:;') expect(node.name).toBe('color') // Empty values return null (consistent with main parser) - expect(node.value.text).toBe("") + expect(node.first_child!.text).toBe('') }) }) @@ -113,28 +113,28 @@ describe('parse_declaration', () => { test('declaration with !important', () => { const node = parse_declaration('color: red !important') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') expect(node.is_important).toBe(true) }) test('declaration with !important and semicolon', () => { const node = parse_declaration('color: red !important;') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') expect(node.is_important).toBe(true) }) test('historic !ie variant', () => { const node = parse_declaration('color: red !ie') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') expect(node.is_important).toBe(true) }) test('any identifier after ! is treated as important', () => { const node = parse_declaration('color: red !foo') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') expect(node.is_important).toBe(true) }) @@ -193,7 +193,7 @@ describe('parse_declaration', () => { test('value\\9', () => { const node = parse_declaration('property: value\\9') - expect(node.value.text).toBe('value\\9') + expect(node.first_child!.text).toBe('value\\9') expect(node.is_browserhack).toBe(false) }) @@ -216,63 +216,63 @@ describe('parse_declaration', () => { describe('Value Parsing', () => { test('identifier value', () => { const node = parse_declaration('display: flex') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(IDENTIFIER) - expect(node.value.children[0].text).toBe('flex') + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(IDENTIFIER) + expect(node.first_child!.children[0].text).toBe('flex') }) test('number value', () => { const node = parse_declaration('opacity: 0.5') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(NUMBER) - expect(node.value.children[0].value).toBe(0.5) + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(NUMBER) + expect(node.first_child!.children[0].value).toBe(0.5) }) test('dimension value', () => { const node = parse_declaration('width: 100px') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(DIMENSION) - expect(node.value.children[0].value).toBe(100) - expect(node.value.children[0].unit).toBe('px') + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(DIMENSION) + expect(node.first_child!.children[0].value).toBe(100) + expect(node.first_child!.children[0].unit).toBe('px') }) test('multiple values', () => { const node = parse_declaration('margin: 10px 20px 30px 40px') - expect(node.value.children).toHaveLength(4) - expect(node.value.children[0].type).toBe(DIMENSION) - expect(node.value.children[0].text).toBe('10px') - expect(node.value.children[1].text).toBe('20px') - expect(node.value.children[2].text).toBe('30px') - expect(node.value.children[3].text).toBe('40px') + expect(node.first_child!.children).toHaveLength(4) + expect(node.first_child!.children[0].type).toBe(DIMENSION) + expect(node.first_child!.children[0].text).toBe('10px') + expect(node.first_child!.children[1].text).toBe('20px') + expect(node.first_child!.children[2].text).toBe('30px') + expect(node.first_child!.children[3].text).toBe('40px') }) test('function value', () => { const node = parse_declaration('transform: rotate(45deg)') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(FUNCTION) - expect(node.value.children[0].name).toBe('rotate') + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(FUNCTION) + expect(node.first_child!.children[0].name).toBe('rotate') }) test('nested functions', () => { const node = parse_declaration('width: calc(100% - 20px)') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(FUNCTION) - expect(node.value.children[0].name).toBe('calc') - expect(node.value.children[0].children.length).toBeGreaterThan(0) + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(FUNCTION) + expect(node.first_child!.children[0].name).toBe('calc') + expect(node.first_child!.children[0].children.length).toBeGreaterThan(0) }) test('complex value with multiple functions', () => { const node = parse_declaration('background: linear-gradient(to bottom, red, blue)') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(FUNCTION) - expect(node.value.children[0].name).toBe('linear-gradient') + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(FUNCTION) + expect(node.first_child!.children[0].name).toBe('linear-gradient') }) test('CSS variable', () => { const node = parse_declaration('color: var(--primary-color)') - expect(node.value.children).toHaveLength(1) - expect(node.value.children[0].type).toBe(FUNCTION) - expect(node.value.children[0].name).toBe('var') + expect(node.first_child!.children).toHaveLength(1) + expect(node.first_child!.children[0].type).toBe(FUNCTION) + expect(node.first_child!.children[0].name).toBe('var') }) }) @@ -302,7 +302,7 @@ describe('parse_declaration', () => { test('property with colon but value with invalid token', () => { const node = parse_declaration('color: red') expect(node.name).toBe('color') - expect(node.value.text).toBe('red') + expect(node.first_child!.text).toBe('red') }) }) @@ -319,7 +319,7 @@ describe('parse_declaration', () => { test('node.value returns raw value string', () => { const node = parse_declaration('margin: 10px 20px') - expect(node.value.text).toBe('10px 20px') + expect(node.first_child!.text).toBe('10px 20px') }) test('node.is_important returns boolean', () => { @@ -340,9 +340,9 @@ describe('parse_declaration', () => { test('node.children returns value nodes', () => { const node = parse_declaration('margin: 10px 20px') - expect(node.value.children).toHaveLength(2) - expect(node.value.children[0].type).toBe(DIMENSION) - expect(node.value.children[1].type).toBe(DIMENSION) + expect(node.first_child!.children).toHaveLength(2) + expect(node.first_child!.children[0].type).toBe(DIMENSION) + expect(node.first_child!.children[1].type).toBe(DIMENSION) }) test('node.text returns full declaration text', () => { diff --git a/src/parse-options.test.ts b/src/parse-options.test.ts index f846626..803cd13 100644 --- a/src/parse-options.test.ts +++ b/src/parse-options.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { parse } from './parse' -import { SELECTOR_LIST, DECLARATION, IDENTIFIER, VALUE } from './arena' +import { SELECTOR_LIST, DECLARATION, VALUE } from './arena' describe('Parser Options', () => { const css = 'body { color: red; }' diff --git a/src/parse-value.test.ts b/src/parse-value.test.ts index 1dc2ac5..fc59cb5 100644 --- a/src/parse-value.test.ts +++ b/src/parse-value.test.ts @@ -8,7 +8,7 @@ describe('Value Node Types', () => { const root = parse(css) const rule = root.first_child const decl = rule?.first_child?.next_sibling?.first_child // selector → block → declaration - return decl?.value.children[0] + return decl?.first_child!.children[0] } describe('Locations', () => { @@ -154,7 +154,7 @@ describe('Value Node Types', () => { it('should have correct offset and length', () => { const root = parse('div { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const comma = decl?.value.children[1] + const comma = decl?.first_child!.children[1] expect(comma?.start).toBe(24) expect(comma?.length).toBe(1) expect(comma?.end).toBe(25) @@ -165,7 +165,7 @@ describe('Value Node Types', () => { it('should have correct line and column on line 2', () => { const root = parse('div {\n font-family: Arial, sans-serif;\n}') const decl = root.first_child?.first_child?.next_sibling?.first_child - const comma = decl?.value.children[1] + const comma = decl?.first_child!.children[1] expect(comma?.start).toBe(26) expect(comma?.length).toBe(1) expect(comma?.end).toBe(27) @@ -177,7 +177,7 @@ describe('Value Node Types', () => { describe('PARENTHESIS', () => { it('should have correct offset and length', () => { const root = parse('div { width: calc((100% - 50px) / 2); }') - const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.first_child!.children[0] const paren = func?.children[0] expect(paren?.start).toBe(18) expect(paren?.length).toBe(13) @@ -188,7 +188,7 @@ describe('Value Node Types', () => { it('should have correct line and column on line 2', () => { const root = parse('div {\n width: calc((100% - 50px) / 2);\n}') - const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.first_child!.children[0] const paren = func?.children[0] expect(paren?.start).toBe(20) expect(paren?.length).toBe(13) @@ -252,13 +252,13 @@ describe('Value Node Types', () => { it('OPERATOR type constant', () => { const root = parse('div { font-family: Arial, sans-serif; }') - const comma = root.first_child?.first_child?.next_sibling?.first_child?.value.children[1] + const comma = root.first_child?.first_child?.next_sibling?.first_child?.first_child!.children[1] expect(comma?.type).toBe(OPERATOR) }) it('PARENTHESIS type constant', () => { const root = parse('div { width: calc((100% - 50px) / 2); }') - const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.first_child!.children[0] const paren = func?.children[0] expect(paren?.type).toBe(PARENTHESIS) }) @@ -302,13 +302,13 @@ describe('Value Node Types', () => { it('OPERATOR type_name', () => { const root = parse('div { font-family: Arial, sans-serif; }') - const comma = root.first_child?.first_child?.next_sibling?.first_child?.value.children[1] + const comma = root.first_child?.first_child?.next_sibling?.first_child?.first_child!.children[1] expect(comma?.type_name).toBe('Operator') }) it('PARENTHESIS type_name', () => { const root = parse('div { width: calc((100% - 50px) / 2); }') - const func = root.first_child?.first_child?.next_sibling?.first_child?.value.children[0] + const func = root.first_child?.first_child?.next_sibling?.first_child?.first_child!.children[0] const paren = func?.children[0] expect(paren?.type_name).toBe('Parentheses') }) @@ -325,20 +325,20 @@ describe('Value Node Types', () => { const root = parse('body { color: red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('red') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('red') + expect(decl?.first_child!.text).toBe('red') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('red') }) it('should parse multiple keywords', () => { const root = parse('body { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(3) - expect(decl?.value.children[0].type).toBe(IDENTIFIER) - expect(decl?.value.children[0].text).toBe('Arial') - expect(decl?.value.children[2].type).toBe(IDENTIFIER) - expect(decl?.value.children[2].text).toBe('sans-serif') + expect(decl?.first_child!.children).toHaveLength(3) + expect(decl?.first_child!.children[0].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[0].text).toBe('Arial') + expect(decl?.first_child!.children[2].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[2].text).toBe('sans-serif') }) }) @@ -347,27 +347,27 @@ describe('Value Node Types', () => { const root = parse('body { opacity: 0.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('0.5') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('0.5') + expect(decl?.first_child!.text).toBe('0.5') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('0.5') }) it('should handle negative numbers', () => { const root = parse('body { margin: -10px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(DIMENSION) - expect(decl?.value.children[0].text).toBe('-10px') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(DIMENSION) + expect(decl?.first_child!.children[0].text).toBe('-10px') }) it('should handle zero without unit', () => { const root = parse('body { margin: 0; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(NUMBER) - expect(decl?.value.children[0].text).toBe('0') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(NUMBER) + expect(decl?.first_child!.children[0].text).toBe('0') }) }) @@ -376,55 +376,55 @@ describe('Value Node Types', () => { const root = parse('body { width: 100px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('100px') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('100px') - expect(decl?.value.children[0].value).toBe(100) - expect(decl?.value.children[0].unit).toBe('px') + expect(decl?.first_child!.text).toBe('100px') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('100px') + expect(decl?.first_child!.children[0].value).toBe(100) + expect(decl?.first_child!.children[0].unit).toBe('px') }) it('should parse em dimension values', () => { const root = parse('body { font-size: 3em; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('3em') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('3em') - expect(decl?.value.children[0].value).toBe(3) - expect(decl?.value.children[0].unit).toBe('em') + expect(decl?.first_child!.text).toBe('3em') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('3em') + expect(decl?.first_child!.children[0].value).toBe(3) + expect(decl?.first_child!.children[0].unit).toBe('em') }) it('should parse percentage values', () => { const root = parse('body { width: 50%; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('50%') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('50%') + expect(decl?.first_child!.text).toBe('50%') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('50%') }) it('should handle zero with unit', () => { const root = parse('body { margin: 0px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(DIMENSION) - expect(decl?.value.children[0].text).toBe('0px') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(DIMENSION) + expect(decl?.first_child!.children[0].text).toBe('0px') }) it('should parse margin shorthand', () => { const root = parse('body { margin: 10px 20px 30px 40px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(4) - expect(decl?.value.children[0].type).toBe(DIMENSION) - expect(decl?.value.children[0].text).toBe('10px') - expect(decl?.value.children[1].type).toBe(DIMENSION) - expect(decl?.value.children[1].text).toBe('20px') - expect(decl?.value.children[2].type).toBe(DIMENSION) - expect(decl?.value.children[2].text).toBe('30px') - expect(decl?.value.children[3].type).toBe(DIMENSION) - expect(decl?.value.children[3].text).toBe('40px') + expect(decl?.first_child!.children).toHaveLength(4) + expect(decl?.first_child!.children[0].type).toBe(DIMENSION) + expect(decl?.first_child!.children[0].text).toBe('10px') + expect(decl?.first_child!.children[1].type).toBe(DIMENSION) + expect(decl?.first_child!.children[1].text).toBe('20px') + expect(decl?.first_child!.children[2].type).toBe(DIMENSION) + expect(decl?.first_child!.children[2].text).toBe('30px') + expect(decl?.first_child!.children[3].type).toBe(DIMENSION) + expect(decl?.first_child!.children[3].text).toBe('40px') }) }) @@ -433,9 +433,9 @@ describe('Value Node Types', () => { const root = parse('body { content: "hello"; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('"hello"') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('"hello"') + expect(decl?.first_child!.text).toBe('"hello"') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('"hello"') }) }) @@ -444,9 +444,9 @@ describe('Value Node Types', () => { const root = parse('body { color: #ff0000; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('#ff0000') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].text).toBe('#ff0000') + expect(decl?.first_child!.text).toBe('#ff0000') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].text).toBe('#ff0000') }) }) @@ -455,16 +455,16 @@ describe('Value Node Types', () => { const root = parse('body { color: rgb(255, 0, 0); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(FUNCTION) - expect(decl?.value.children[0].name).toBe('rgb') - expect(decl?.value.children[0].text).toBe('rgb(255, 0, 0)') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(FUNCTION) + expect(decl?.first_child!.children[0].name).toBe('rgb') + expect(decl?.first_child!.children[0].text).toBe('rgb(255, 0, 0)') }) it('should parse function arguments', () => { const root = parse('body { color: rgb(255, 0, 0); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.children).toHaveLength(5) expect(func?.children[0].type).toBe(NUMBER) @@ -483,34 +483,34 @@ describe('Value Node Types', () => { const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(FUNCTION) - expect(decl?.value.children[0].name).toBe('calc') - expect(decl?.value.children[0].children).toHaveLength(3) - expect(decl?.value.children[0].children[0].type).toBe(DIMENSION) - expect(decl?.value.children[0].children[0].text).toBe('100%') - expect(decl?.value.children[0].children[1].type).toBe(OPERATOR) - expect(decl?.value.children[0].children[1].text).toBe('-') - expect(decl?.value.children[0].children[2].type).toBe(DIMENSION) - expect(decl?.value.children[0].children[2].text).toBe('20px') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(FUNCTION) + expect(decl?.first_child!.children[0].name).toBe('calc') + expect(decl?.first_child!.children[0].children).toHaveLength(3) + expect(decl?.first_child!.children[0].children[0].type).toBe(DIMENSION) + expect(decl?.first_child!.children[0].children[0].text).toBe('100%') + expect(decl?.first_child!.children[0].children[1].type).toBe(OPERATOR) + expect(decl?.first_child!.children[0].children[1].text).toBe('-') + expect(decl?.first_child!.children[0].children[2].type).toBe(DIMENSION) + expect(decl?.first_child!.children[0].children[2].text).toBe('20px') }) it('should parse var() function', () => { const root = parse('body { color: var(--primary-color); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(FUNCTION) - expect(decl?.value.children[0].name).toBe('var') - expect(decl?.value.children[0].children).toHaveLength(1) - expect(decl?.value.children[0].children[0].type).toBe(IDENTIFIER) - expect(decl?.value.children[0].children[0].text).toBe('--primary-color') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(FUNCTION) + expect(decl?.first_child!.children[0].name).toBe('var') + expect(decl?.first_child!.children[0].children).toHaveLength(1) + expect(decl?.first_child!.children[0].children[0].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[0].children[0].text).toBe('--primary-color') }) it('should provide node.value for calc()', () => { const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('calc') @@ -522,7 +522,7 @@ describe('Value Node Types', () => { it('should provide node.value for var() function', () => { const root = parse('body { color: var(--primary-color); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('var') @@ -534,7 +534,7 @@ describe('Value Node Types', () => { it('should provide node.value for var() function with fallback', () => { const root = parse('body { color: var(--primary-color, 1); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('var') @@ -547,24 +547,24 @@ describe('Value Node Types', () => { const root = parse('body { transform: translateX(10px) rotate(45deg); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(2) - expect(decl?.value.children[0].type).toBe(FUNCTION) - expect(decl?.value.children[0].name).toBe('translateX') - expect(decl?.value.children[1].type).toBe(FUNCTION) - expect(decl?.value.children[1].name).toBe('rotate') + expect(decl?.first_child!.children).toHaveLength(2) + expect(decl?.first_child!.children[0].type).toBe(FUNCTION) + expect(decl?.first_child!.children[0].name).toBe('translateX') + expect(decl?.first_child!.children[1].type).toBe(FUNCTION) + expect(decl?.first_child!.children[1].name).toBe('rotate') }) it('should parse filter value', () => { const root = parse('body { filter: blur(5px) brightness(1.2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(2) - expect(decl?.value.children[0].type).toBe(FUNCTION) - expect(decl?.value.children[0].name).toBe('blur') - expect(decl?.value.children[0].children[0].text).toBe('5px') - expect(decl?.value.children[1].type).toBe(FUNCTION) - expect(decl?.value.children[1].name).toBe('brightness') - expect(decl?.value.children[1].children[0].text).toBe('1.2') + expect(decl?.first_child!.children).toHaveLength(2) + expect(decl?.first_child!.children[0].type).toBe(FUNCTION) + expect(decl?.first_child!.children[0].name).toBe('blur') + expect(decl?.first_child!.children[0].children[0].text).toBe('5px') + expect(decl?.first_child!.children[1].type).toBe(FUNCTION) + expect(decl?.first_child!.children[1].name).toBe('brightness') + expect(decl?.first_child!.children[1].children[0].text).toBe('1.2') }) }) @@ -573,14 +573,14 @@ describe('Value Node Types', () => { const root = parse('body { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children[1].type).toBe(OPERATOR) - expect(decl?.value.children[1].text).toBe(',') + expect(decl?.first_child!.children[1].type).toBe(OPERATOR) + expect(decl?.first_child!.children[1].text).toBe(',') }) it('should parse calc operators', () => { const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.children[1].type).toBe(OPERATOR) expect(func?.children[1].text).toBe('-') @@ -589,7 +589,7 @@ describe('Value Node Types', () => { it('should parse all calc operators', () => { const root = parse('body { width: calc(1px + 2px * 3px / 4px - 5px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] const operators = func?.children.filter((n) => n.type === OPERATOR) expect(operators).toHaveLength(4) @@ -604,7 +604,7 @@ describe('Value Node Types', () => { it('should parse parenthesized expressions in calc()', () => { const root = parse('body { width: calc((100% - 50px) / 2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('calc') @@ -636,7 +636,7 @@ describe('Value Node Types', () => { it('should parse complex nested parentheses', () => { const root = parse('body { width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('calc') @@ -676,20 +676,20 @@ describe('Value Node Types', () => { const root = parse('body { background: url("image.png"); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(URL) - expect(decl?.value.children[0].name).toBe('url') - expect(decl?.value.children[0].children).toHaveLength(1) - expect(decl?.value.children[0].children[0].type).toBe(STRING) - expect(decl?.value.children[0].children[0].text).toBe('"image.png"') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(URL) + expect(decl?.first_child!.children[0].name).toBe('url') + expect(decl?.first_child!.children[0].children).toHaveLength(1) + expect(decl?.first_child!.children[0].children[0].type).toBe(STRING) + expect(decl?.first_child!.children[0].children[0].text).toBe('"image.png"') // URL node with quoted string returns the string value with quotes - expect(decl?.value.children[0].value).toBe('"image.png"') + expect(decl?.first_child!.children[0].value).toBe('"image.png"') }) it('should parse url() function with unquoted URL containing dots', () => { const root = parse('body { cursor: url(mycursor.cur); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(URL) expect(func?.name).toBe('url') @@ -703,7 +703,7 @@ describe('Value Node Types', () => { it('should parse src() function with unquoted URL', () => { const root = parse('body { content: src(myfont.woff2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(FUNCTION) expect(func?.name).toBe('src') @@ -716,20 +716,20 @@ describe('Value Node Types', () => { const root = parse("body { background: url('image.png'); }") const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(URL) - expect(decl?.value.children[0].name).toBe('url') - expect(decl?.value.children[0].children).toHaveLength(1) - expect(decl?.value.children[0].children[0].type).toBe(STRING) - expect(decl?.value.children[0].children[0].text).toBe("'image.png'") + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(URL) + expect(decl?.first_child!.children[0].name).toBe('url') + expect(decl?.first_child!.children[0].children).toHaveLength(1) + expect(decl?.first_child!.children[0].children[0].type).toBe(STRING) + expect(decl?.first_child!.children[0].children[0].text).toBe("'image.png'") // URL node with single-quoted string returns the string value with quotes - expect(decl?.value.children[0].value).toBe("'image.png'") + expect(decl?.first_child!.children[0].value).toBe("'image.png'") }) it('should parse url() with base64 data URL', () => { const root = parse('body { background: url(data:image/png;base64,iVBORw0KGg); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(URL) expect(func?.name).toBe('url') @@ -740,7 +740,7 @@ describe('Value Node Types', () => { it('should parse url() with inline SVG', () => { const root = parse('body { background: url(data:image/svg+xml,); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const func = decl?.value.children[0] + const func = decl?.first_child!.children[0] expect(func?.type).toBe(URL) expect(func?.name).toBe('url') @@ -751,11 +751,11 @@ describe('Value Node Types', () => { it('should parse complex background value with url()', () => { const root = parse('body { background: url("bg.png") no-repeat center center / cover; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children.length).toBeGreaterThan(1) - expect(decl?.value.children[0].type).toBe(URL) - expect(decl?.value.children[0].name).toBe('url') - expect(decl?.value.children[1].type).toBe(IDENTIFIER) - expect(decl?.value.children[1].text).toBe('no-repeat') + expect(decl?.first_child!.children.length).toBeGreaterThan(1) + expect(decl?.first_child!.children[0].type).toBe(URL) + expect(decl?.first_child!.children[0].name).toBe('url') + expect(decl?.first_child!.children[1].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[1].text).toBe('no-repeat') }) }) @@ -764,31 +764,31 @@ describe('Value Node Types', () => { const root = parse('body { border: 1px solid red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.children).toHaveLength(3) - expect(decl?.value.children[0].type).toBe(DIMENSION) - expect(decl?.value.children[0].text).toBe('1px') - expect(decl?.value.children[1].type).toBe(IDENTIFIER) - expect(decl?.value.children[1].text).toBe('solid') - expect(decl?.value.children[2].type).toBe(IDENTIFIER) - expect(decl?.value.children[2].text).toBe('red') + expect(decl?.first_child!.children).toHaveLength(3) + expect(decl?.first_child!.children[0].type).toBe(DIMENSION) + expect(decl?.first_child!.children[0].text).toBe('1px') + expect(decl?.first_child!.children[1].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[1].text).toBe('solid') + expect(decl?.first_child!.children[2].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[2].text).toBe('red') }) it('should handle empty value', () => { const root = parse('body { color: ; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.type).toBe(VALUE) - expect(decl?.value.children).toHaveLength(0) + expect(decl?.first_child!.type).toBe(VALUE) + expect(decl?.first_child!.children).toHaveLength(0) }) it('should handle value with !important', () => { const root = parse('body { color: red !important; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - expect(decl?.value.text).toBe('red') - expect(decl?.value.children).toHaveLength(1) - expect(decl?.value.children[0].type).toBe(IDENTIFIER) - expect(decl?.value.children[0].text).toBe('red') + expect(decl?.first_child!.text).toBe('red') + expect(decl?.first_child!.children).toHaveLength(1) + expect(decl?.first_child!.children[0].type).toBe(IDENTIFIER) + expect(decl?.first_child!.children[0].text).toBe('red') expect(decl?.is_important).toBe(true) }) }) @@ -798,7 +798,7 @@ describe('Value Node Types', () => { it('should return number for NUMBER nodes', () => { const root = parse('div { opacity: 0.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const numberNode = decl?.value.children[0] + const numberNode = decl?.first_child!.children[0] expect(numberNode?.type).toBe(NUMBER) expect(numberNode?.value_as_number).toBe(0.5) @@ -807,7 +807,7 @@ describe('Value Node Types', () => { it('should return number for DIMENSION nodes', () => { const root = parse('div { width: 100px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const dimNode = decl?.value.children[0] + const dimNode = decl?.first_child!.children[0] expect(dimNode?.type).toBe(DIMENSION) expect(dimNode?.value_as_number).toBe(100) @@ -816,7 +816,7 @@ describe('Value Node Types', () => { it('should handle negative numbers', () => { const root = parse('div { margin: -10px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const dimNode = decl?.value.children[0] + const dimNode = decl?.first_child!.children[0] expect(dimNode?.type).toBe(DIMENSION) expect(dimNode?.value_as_number).toBe(-10) @@ -825,7 +825,7 @@ describe('Value Node Types', () => { it('should handle zero', () => { const root = parse('div { margin: 0; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const numberNode = decl?.value.children[0] + const numberNode = decl?.first_child!.children[0] expect(numberNode?.type).toBe(NUMBER) expect(numberNode?.value_as_number).toBe(0) @@ -834,7 +834,7 @@ describe('Value Node Types', () => { it('should handle decimal numbers', () => { const root = parse('div { line-height: 1.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const numberNode = decl?.value.children[0] + const numberNode = decl?.first_child!.children[0] expect(numberNode?.type).toBe(NUMBER) expect(numberNode?.value_as_number).toBe(1.5) @@ -843,7 +843,7 @@ describe('Value Node Types', () => { it('should handle percentage dimensions', () => { const root = parse('div { width: 50%; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const dimNode = decl?.value.children[0] + const dimNode = decl?.first_child!.children[0] expect(dimNode?.type).toBe(DIMENSION) expect(dimNode?.value_as_number).toBe(50) @@ -853,7 +853,7 @@ describe('Value Node Types', () => { it('should return null for IDENTIFIER nodes', () => { const root = parse('div { color: red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const identNode = decl?.value.children[0] + const identNode = decl?.first_child!.children[0] expect(identNode?.type).toBe(IDENTIFIER) expect(identNode?.value_as_number).toBeNull() @@ -862,7 +862,7 @@ describe('Value Node Types', () => { it('should return null for STRING nodes', () => { const root = parse('div { content: "hello"; }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const stringNode = decl?.value.children[0] + const stringNode = decl?.first_child!.children[0] expect(stringNode?.type).toBe(STRING) expect(stringNode?.value_as_number).toBeNull() @@ -871,7 +871,7 @@ describe('Value Node Types', () => { it('should return null for FUNCTION nodes', () => { const root = parse('div { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child - const funcNode = decl?.value.children[0] + const funcNode = decl?.first_child!.children[0] expect(funcNode?.type).toBe(FUNCTION) expect(funcNode?.value_as_number).toBeNull() @@ -883,7 +883,7 @@ describe('Value Node Types', () => { const root = parse(css) const rule = root.first_child const decl = rule?.first_child?.next_sibling?.first_child - return decl?.value.children[0] + return decl?.first_child!.children[0] } it('should parse URL() with uppercase', () => { diff --git a/src/parse.test.ts b/src/parse.test.ts index fd69c04..3f9b289 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -1245,7 +1245,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value.text).toBe('blue') + expect(decl.first_child!.text).toBe('blue') }) test('extract value with spaces', () => { @@ -1257,7 +1257,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('padding') - expect(decl.value.text).toBe('1rem 2rem 3rem 4rem') + expect(decl.first_child!.text).toBe('1rem 2rem 3rem 4rem') }) test('extract function value', () => { @@ -1269,7 +1269,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('background') - expect(decl.value.text).toBe('linear-gradient(to bottom, red, blue)') + expect(decl.first_child!.text).toBe('linear-gradient(to bottom, red, blue)') }) test('extract calc value', () => { @@ -1281,7 +1281,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('width') - expect(decl.value.text).toBe('calc(100% - 2rem)') + expect(decl.first_child!.text).toBe('calc(100% - 2rem)') }) test('exclude !important from value', () => { @@ -1293,7 +1293,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value.text).toBe('blue') + expect(decl.first_child!.text).toBe('blue') expect(decl.is_important).toBe(true) }) @@ -1306,7 +1306,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value.text).toBe('blue') + expect(decl.first_child!.text).toBe('blue') }) test('CSS custom property value', () => { @@ -1318,7 +1318,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('--brand-color') - expect(decl.value.text).toBe('rgb(0% 10% 50% / 0.5)') + expect(decl.first_child!.text).toBe('rgb(0% 10% 50% / 0.5)') }) test('var() reference value', () => { @@ -1330,7 +1330,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value.text).toBe('var(--primary-color)') + expect(decl.first_child!.text).toBe('var(--primary-color)') }) test('nested function value', () => { @@ -1342,7 +1342,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('transform') - expect(decl.value.text).toBe('translate(calc(50% - 1rem), 0)') + expect(decl.first_child!.text).toBe('translate(calc(50% - 1rem), 0)') }) test('value without semicolon', () => { @@ -1354,7 +1354,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value.text).toBe('blue') + expect(decl.first_child!.text).toBe('blue') }) test('empty value', () => { @@ -1366,7 +1366,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('color') - expect(decl.value.text).toBe('') + expect(decl.first_child!.text).toBe('') }) test('URL value', () => { @@ -1378,7 +1378,7 @@ describe('Core Nodes', () => { let decl = block.first_child! expect(decl.name).toBe('background') - expect(decl.value.text).toBe('url("image.png")') + expect(decl.first_child!.text).toBe('url("image.png")') }) }) @@ -2505,7 +2505,7 @@ describe('Core Nodes', () => { const declaration = nestingRule.block!.first_child! expect(declaration.type).toBe(DECLARATION) expect(declaration.property).toBe('--is') - expect(declaration.value.text).toBe('this') + expect(declaration.first_child!.text).toBe('this') }) }) }) @@ -2560,7 +2560,7 @@ describe('Core Nodes', () => { expect(secondDecl).toBeTruthy() expect(secondDecl.type).toBe(DECLARATION) expect(secondDecl.property).toBe('color') - expect(secondDecl.value.text).toBe('red') + expect(secondDecl.first_child!.text).toBe('red') // Calculate expected column: '.test { ' + declaration.text + ' ' + 1 (columns are 1-indexed) const expectedColumn = '.test { '.length + declText.length + ' '.length + 1