diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c4530b..ea6cfdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,7 @@ jobs: uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + slug: projectwallace/css-parser check-ts: name: Check types diff --git a/src/arena.test.ts b/src/arena.test.ts index 23ef8f7..a2b4f03 100644 --- a/src/arena.test.ts +++ b/src/arena.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest' -import { CSSDataArena, STYLESHEET, DECLARATION, FLAG_IMPORTANT, FLAG_HAS_ERROR } from './arena' +import { CSSDataArena, STYLESHEET, STYLE_RULE, DECLARATION, FLAG_IMPORTANT, FLAG_HAS_ERROR } from './arena' describe('CSSDataArena', () => { describe('initialization', () => { @@ -20,11 +20,11 @@ describe('CSSDataArena', () => { test('should create nodes and increment count', () => { const arena = new CSSDataArena(10) - const node1 = arena.create_node() + const node1 = arena.create_node(STYLESHEET, 0, 0, 1, 1) expect(node1).toBe(1) // First node index is 1 (0 is reserved for "no node") expect(arena.get_count()).toBe(2) - const node2 = arena.create_node() + const node2 = arena.create_node(STYLESHEET, 0, 0, 1, 1) expect(node2).toBe(2) expect(arena.get_count()).toBe(3) }) @@ -32,12 +32,12 @@ describe('CSSDataArena', () => { test('should automatically grow when capacity is exceeded', () => { const arena = new CSSDataArena(3) - arena.create_node() // 1 - arena.create_node() // 2 + arena.create_node(STYLESHEET, 0, 0, 1, 1) // 1 + arena.create_node(STYLESHEET, 0, 0, 1, 1) // 2 expect(arena.get_capacity()).toBe(3) // This should trigger growth (count is now 3, capacity is 3) - const node3 = arena.create_node() // 3 + const node3 = arena.create_node(STYLESHEET, 0, 0, 1, 1) // 3 expect(node3).toBe(3) expect(arena.get_count()).toBe(4) // Capacity should be ceil(3 * 1.3) = 4 @@ -47,17 +47,11 @@ describe('CSSDataArena', () => { test('should preserve existing data when growing', () => { const arena = new CSSDataArena(2) - const node1 = arena.create_node() - const node2 = arena.create_node() - - // Set data on existing nodes - arena.set_type(node1, STYLESHEET) - arena.set_start_offset(node1, 100) - arena.set_type(node2, DECLARATION) - arena.set_start_offset(node2, 200) + const node1 = arena.create_node(STYLESHEET, 100, 0, 1, 1) + const node2 = arena.create_node(DECLARATION, 200, 0, 1, 1) // Trigger growth - const node3 = arena.create_node() + const node3 = arena.create_node(STYLESHEET, 0, 0, 1, 1) // Verify old data is preserved expect(arena.get_type(node1)).toBe(STYLESHEET) @@ -74,9 +68,9 @@ describe('CSSDataArena', () => { describe('node reading and writing', () => { test('should read default values for uninitialized nodes', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(STYLESHEET, 0, 0, 1, 1) - expect(arena.get_type(node)).toBe(0) + expect(arena.get_type(node)).toBe(STYLESHEET) expect(arena.get_flags(node)).toBe(0) expect(arena.get_start_offset(node)).toBe(0) expect(arena.get_length(node)).toBe(0) @@ -84,7 +78,7 @@ describe('CSSDataArena', () => { test('should write and read node type', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(DECLARATION, 0, 0, 1, 1) arena.set_type(node, STYLESHEET) expect(arena.get_type(node)).toBe(STYLESHEET) @@ -92,7 +86,7 @@ describe('CSSDataArena', () => { test('should write and read node flags', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(DECLARATION, 0, 0, 1, 1) arena.set_flags(node, FLAG_IMPORTANT) expect(arena.get_flags(node)).toBe(FLAG_IMPORTANT) @@ -100,15 +94,11 @@ describe('CSSDataArena', () => { test('should write and read all node properties', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(DECLARATION, 100, 50, 5, 1) - arena.set_type(node, DECLARATION) arena.set_flags(node, FLAG_IMPORTANT) - arena.set_start_offset(node, 100) - arena.set_length(node, 50) - arena.set_content_start(node, 110) + arena.set_content_start_delta(node, 10) arena.set_content_length(node, 30) - arena.set_start_line(node, 5) expect(arena.get_type(node)).toBe(DECLARATION) expect(arena.get_flags(node)).toBe(FLAG_IMPORTANT) @@ -121,14 +111,8 @@ describe('CSSDataArena', () => { test('should handle multiple nodes independently', () => { const arena = new CSSDataArena(10) - const node1 = arena.create_node() - const node2 = arena.create_node() - - arena.set_type(node1, STYLESHEET) - arena.set_start_offset(node1, 0) - - arena.set_type(node2, DECLARATION) - arena.set_start_offset(node2, 100) + const node1 = arena.create_node(STYLESHEET, 0, 0, 1, 1) + const node2 = arena.create_node(DECLARATION, 100, 0, 1, 1) expect(arena.get_type(node1)).toBe(STYLESHEET) expect(arena.get_start_offset(node1)).toBe(0) @@ -140,10 +124,10 @@ describe('CSSDataArena', () => { describe('tree linking', () => { test('should append first child to parent', () => { const arena = new CSSDataArena(10) - const parent = arena.create_node() - const child = arena.create_node() + const parent = arena.create_node(STYLESHEET, 0, 0, 1, 1) + const child = arena.create_node(STYLE_RULE, 0, 0, 1, 1) - arena.append_child(parent, child) + arena.append_children(parent, [child]) expect(arena.get_first_child(parent)).toBe(child) expect(arena.has_children(parent)).toBe(true) @@ -152,14 +136,12 @@ describe('CSSDataArena', () => { test('should append multiple children as siblings', () => { const arena = new CSSDataArena(10) - const parent = arena.create_node() - const child1 = arena.create_node() - const child2 = arena.create_node() - const child3 = arena.create_node() + const parent = arena.create_node(STYLESHEET, 0, 0, 1, 1) + const child1 = arena.create_node(STYLE_RULE, 0, 0, 1, 1) + const child2 = arena.create_node(STYLE_RULE, 0, 0, 1, 1) + const child3 = arena.create_node(STYLE_RULE, 0, 0, 1, 1) - arena.append_child(parent, child1) - arena.append_child(parent, child2) - arena.append_child(parent, child3) + arena.append_children(parent, [child1, child2, child3]) expect(arena.get_first_child(parent)).toBe(child1) expect(arena.get_next_sibling(child1)).toBe(child2) @@ -169,18 +151,16 @@ describe('CSSDataArena', () => { test('should build complex tree structure', () => { const arena = new CSSDataArena(10) - const root = arena.create_node() - const rule1 = arena.create_node() - const rule2 = arena.create_node() - const decl1 = arena.create_node() - const decl2 = arena.create_node() + const root = arena.create_node(STYLESHEET, 0, 0, 1, 1) + const rule1 = arena.create_node(STYLE_RULE, 0, 0, 1, 1) + const rule2 = arena.create_node(STYLE_RULE, 0, 0, 1, 1) + const decl1 = arena.create_node(DECLARATION, 0, 0, 1, 1) + const decl2 = arena.create_node(DECLARATION, 0, 0, 1, 1) // Build tree: root -> [rule1, rule2] // rule1 -> [decl1, decl2] - arena.append_child(root, rule1) - arena.append_child(root, rule2) - arena.append_child(rule1, decl1) - arena.append_child(rule1, decl2) + arena.append_children(root, [rule1, rule2]) + arena.append_children(rule1, [decl1, decl2]) // Verify root level expect(arena.get_first_child(root)).toBe(rule1) @@ -198,7 +178,7 @@ describe('CSSDataArena', () => { test('should handle nodes with no children or siblings', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(STYLESHEET, 0, 0, 1, 1) expect(arena.has_children(node)).toBe(false) expect(arena.has_next_sibling(node)).toBe(false) @@ -210,7 +190,7 @@ describe('CSSDataArena', () => { describe('flag management', () => { test('should set and check individual flags', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(STYLESHEET, 0, 0, 1, 1) expect(arena.has_flag(node, FLAG_IMPORTANT)).toBe(false) @@ -220,7 +200,7 @@ describe('CSSDataArena', () => { test('should set multiple flags independently', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(STYLESHEET, 0, 0, 1, 1) arena.set_flag(node, FLAG_IMPORTANT) @@ -230,7 +210,7 @@ describe('CSSDataArena', () => { test('should clear individual flags without affecting others', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(STYLESHEET, 0, 0, 1, 1) arena.set_flag(node, FLAG_IMPORTANT) arena.set_flag(node, FLAG_HAS_ERROR) @@ -241,7 +221,7 @@ describe('CSSDataArena', () => { test('should handle all flag combinations', () => { const arena = new CSSDataArena(10) - const node = arena.create_node() + const node = arena.create_node(STYLESHEET, 0, 0, 1, 1) // Set all flags at once using setFlags const allFlags = FLAG_IMPORTANT | FLAG_HAS_ERROR diff --git a/src/arena.ts b/src/arena.ts index 27a631f..b1695cc 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -238,10 +238,8 @@ export class CSSDataArena { this.view.setUint16(this.node_offset(node_index) + 8, length, true) } - // Write content start offset (stored as delta from startOffset) - set_content_start(node_index: number, offset: number): void { - const startOffset = this.get_start_offset(node_index) - const delta = offset - startOffset + // Write content start delta (offset from startOffset) + set_content_start_delta(node_index: number, delta: number): void { this.view.setUint16(this.node_offset(node_index) + 12, delta, true) } @@ -285,10 +283,8 @@ export class CSSDataArena { this.view.setUint16(this.node_offset(node_index) + 36, column, true) } - // Write value start offset (stored as delta from startOffset, declaration value / at-rule prelude) - set_value_start(node_index: number, offset: number): void { - const startOffset = this.get_start_offset(node_index) - const delta = offset - startOffset + // Write value start delta (offset from startOffset, declaration value / at-rule prelude) + set_value_start_delta(node_index: number, delta: number): void { this.view.setUint16(this.node_offset(node_index) + 16, delta, true) } @@ -313,35 +309,45 @@ export class CSSDataArena { this.capacity = new_capacity } - // Allocate a new node and return its index - // The node is zero-initialized by default (ArrayBuffer guarantees this) + // Allocate and initialize a new node with core properties // Automatically grows the arena if capacity is exceeded - create_node(): number { + create_node( + type: number, + start_offset: number, + length: number, + start_line: number, + start_column: number + ): number { if (this.count >= this.capacity) { this.grow() } - let node_index = this.count + const node_index = this.count this.count++ + + const offset = node_index * BYTES_PER_NODE + this.view.setUint8(offset, type) // +0: type + this.view.setUint32(offset + 4, start_offset, true) // +4: startOffset + this.view.setUint16(offset + 8, length, true) // +8: length + this.view.setUint32(offset + 32, start_line, true) // +32: startLine + this.view.setUint16(offset + 36, start_column, true) // +36: startColumn + return node_index } // --- Tree Building Helpers --- - // Add a child node to a parent node - // This appends to the end of the child list using the sibling chain - // O(1) operation using lastChild pointer - append_child(parentIndex: number, childIndex: number): void { - let last_child = this.get_last_child(parentIndex) - - if (last_child === 0) { - // No children yet, make this the first and last child - this.set_first_child(parentIndex, childIndex) - this.set_last_child(parentIndex, childIndex) - } else { - // Append to the current last child's sibling chain - this.set_next_sibling(last_child, childIndex) - // Update parent's last child pointer - this.set_last_child(parentIndex, childIndex) + // Link multiple child nodes to a parent + // Children are linked as siblings in the order provided + append_children(parent_index: number, children: number[]): void { + if (children.length === 0) return + + const offset = this.node_offset(parent_index) + this.view.setUint32(offset + 20, children[0], true) // firstChild + this.view.setUint32(offset + 24, children[children.length - 1], true) // lastChild + + // Chain siblings + for (let i = 0; i < children.length - 1; i++) { + this.set_next_sibling(children[i], children[i + 1]) } } diff --git a/src/parse-anplusb.ts b/src/parse-anplusb.ts index 5e03298..ac3db97 100644 --- a/src/parse-anplusb.ts +++ b/src/parse-anplusb.ts @@ -265,21 +265,23 @@ export class ANplusBParser { } private create_anplusb_node(start: number, a_start: number, a_end: number, b_start: number, b_end: number): number { - const node = this.arena.create_node() - this.arena.set_type(node, NTH_SELECTOR) - this.arena.set_start_offset(node, start) - this.arena.set_length(node, this.lexer.pos - start) - this.arena.set_start_line(node, this.lexer.line) + const node = this.arena.create_node( + NTH_SELECTOR, + start, + this.lexer.pos - start, + this.lexer.line, + 1 + ) // Store 'a' coefficient in content fields if it exists (length > 0) if (a_end > a_start) { - this.arena.set_content_start(node, a_start) + this.arena.set_content_start_delta(node, a_start - start) this.arena.set_content_length(node, a_end - a_start) } // Store 'b' coefficient in value fields if it exists (length > 0) if (b_end > b_start) { - this.arena.set_value_start(node, b_start) + this.arena.set_value_start_delta(node, b_start - start) this.arena.set_value_length(node, b_end - b_start) } diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index f98c089..26441a0 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -99,13 +99,13 @@ export class AtRulePreludeParser { } private create_node(type: number, start: number, end: number): number { - let node = this.arena.create_node() - this.arena.set_type(node, type) - this.arena.set_start_offset(node, start) - this.arena.set_length(node, end - start) - this.arena.set_start_line(node, this.lexer.token_line) - this.arena.set_start_column(node, this.lexer.token_column) - return node + return this.arena.create_node( + type, + start, + end - start, + this.lexer.token_line, + this.lexer.token_column + ) } private is_and_or_not(str: string): boolean { @@ -182,9 +182,7 @@ export class AtRulePreludeParser { let query_node = this.create_node(MEDIA_QUERY, query_start, this.lexer.pos) // Append components as children - for (let component of components) { - this.arena.append_child(query_node, component) - } + this.arena.append_children(query_node, components) return query_node } @@ -218,7 +216,7 @@ export class AtRulePreludeParser { // Store feature content (without parentheses) in value fields, trimmed let trimmed = trim_boundaries(this.source, content_start, content_end) if (trimmed) { - this.arena.set_value_start(feature, trimmed[0]) + this.arena.set_value_start_delta(feature, trimmed[0] - feature_start) this.arena.set_value_length(feature, trimmed[1] - trimmed[0]) } @@ -269,9 +267,7 @@ export class AtRulePreludeParser { let query_node = this.create_node(CONTAINER_QUERY, query_start, this.lexer.pos) // Append components as children - for (let component of components) { - this.arena.append_child(query_node, component) - } + this.arena.append_children(query_node, components) nodes.push(query_node) return nodes @@ -316,7 +312,7 @@ export class AtRulePreludeParser { // Store query content in value fields, trimmed let trimmed = trim_boundaries(this.source, content_start, content_end) if (trimmed) { - this.arena.set_value_start(query, trimmed[0]) + this.arena.set_value_start_delta(query, trimmed[0] - feature_start) this.arena.set_value_length(query, trimmed[1] - trimmed[0]) } @@ -509,7 +505,7 @@ export class AtRulePreludeParser { if (content_length > 0) { let trimmed = trim_boundaries(this.source, content_start, content_start + content_length) if (trimmed) { - this.arena.set_content_start(layer_node, trimmed[0]) + this.arena.set_content_start_delta(layer_node, trimmed[0] - layer_start) this.arena.set_content_length(layer_node, trimmed[1] - trimmed[0]) } } diff --git a/src/parse-selector.ts b/src/parse-selector.ts index 3091767..0276a43 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -147,21 +147,16 @@ export class SelectorParser { // Always wrap in selector list node, even for single selectors if (selectors.length >= 1) { - let list_node = this.arena.create_node() - this.arena.set_type(list_node, SELECTOR_LIST) - this.arena.set_start_offset(list_node, list_start) - this.arena.set_length(list_node, this.lexer.pos - list_start) - this.arena.set_start_line(list_node, list_line) - this.arena.set_start_column(list_node, list_column) + let list_node = this.arena.create_node( + SELECTOR_LIST, + list_start, + this.lexer.pos - list_start, + list_line, + list_column + ) // Link selector wrapper nodes as children - this.arena.set_first_child(list_node, selectors[0]) - this.arena.set_last_child(list_node, selectors[selectors.length - 1]) - - // Chain selector wrappers as siblings (simple since they're already wrapped) - for (let i = 0; i < selectors.length - 1; i++) { - this.arena.set_next_sibling(selectors[i], selectors[i + 1]) - } + this.arena.append_children(list_node, selectors) return list_node } @@ -370,7 +365,7 @@ export class SelectorParser { let node = this.create_node(node_type, selector_start, this.lexer.token_end) // Store namespace in content fields - this.arena.set_content_start(node, namespace_start) + this.arena.set_content_start_delta(node, namespace_start - selector_start) this.arena.set_content_length(node, namespace_length) return node } @@ -548,7 +543,7 @@ export class SelectorParser { // Store attribute name in content fields if (name_end > name_start) { - this.arena.set_content_start(node, name_start) + this.arena.set_content_start_delta(node, name_start - this.arena.get_start_offset(node)) this.arena.set_content_length(node, name_end - name_start) } @@ -644,7 +639,7 @@ export class SelectorParser { // Store value in value fields if (value_end > value_start) { - this.arena.set_value_start(node, value_start) + this.arena.set_value_start_delta(node, value_start - this.arena.get_start_offset(node)) this.arena.set_value_length(node, value_end - value_start) } @@ -687,7 +682,7 @@ export class SelectorParser { let node = this.create_node(is_pseudo_element ? PSEUDO_ELEMENT_SELECTOR : PSEUDO_CLASS_SELECTOR, start, this.lexer.token_end) // Content is the pseudo name (without colons) - this.arena.set_content_start(node, this.lexer.token_start) + this.arena.set_content_start_delta(node, this.lexer.token_start - start) this.arena.set_content_length(node, this.lexer.token_end - this.lexer.token_start) // Check for vendor prefix and set flag if detected if (is_vendor_prefixed(this.source, this.lexer.token_start, this.lexer.token_end)) { @@ -740,7 +735,7 @@ export class SelectorParser { let node = this.create_node(is_pseudo_element ? PSEUDO_ELEMENT_SELECTOR : PSEUDO_CLASS_SELECTOR, start, end) // Content is the function name (without colons and parentheses) - this.arena.set_content_start(node, func_name_start) + this.arena.set_content_start_delta(node, func_name_start - start) this.arena.set_content_length(node, func_name_end - func_name_start) // Set FLAG_HAS_PARENS to indicate this is a function syntax (even if empty) @@ -900,11 +895,13 @@ export class SelectorParser { this.lexer.restore_position(saved) // Create NTH_OF wrapper - let of_node = this.arena.create_node() - this.arena.set_type(of_node, NTH_OF_SELECTOR) - this.arena.set_start_offset(of_node, start) - this.arena.set_length(of_node, end - start) - this.arena.set_start_line(of_node, this.lexer.line) + let of_node = this.arena.create_node( + NTH_OF_SELECTOR, + start, + end - start, + this.lexer.line, + 1 + ) // Link An+B and selector list if (anplusb_node !== null && selector_list !== null) { @@ -941,13 +938,14 @@ export class SelectorParser { } private create_node(type: number, start: number, end: number): number { - let node = this.arena.create_node() - this.arena.set_type(node, type) - this.arena.set_start_offset(node, start) - this.arena.set_length(node, end - start) - this.arena.set_start_line(node, this.lexer.line) - this.arena.set_start_column(node, this.lexer.column) - this.arena.set_content_start(node, start) + let node = this.arena.create_node( + type, + start, + end - start, + this.lexer.line, + this.lexer.column + ) + this.arena.set_content_start_delta(node, 0) this.arena.set_content_length(node, end - start) return node } @@ -975,11 +973,7 @@ export function parse_selector(source: string): CSSNode { if (selector_index === null) { // Return empty selector list node if parsing failed - const empty = arena.create_node() - arena.set_type(empty, SELECTOR_LIST) - arena.set_start_offset(empty, 0) - arena.set_length(empty, 0) - arena.set_start_line(empty, 1) + const empty = arena.create_node(SELECTOR_LIST, 0, 0, 1, 1) return new CSSNode(arena, source, empty) } diff --git a/src/parse-value.ts b/src/parse-value.ts index 508d10b..998a714 100644 --- a/src/parse-value.ts +++ b/src/parse-value.ts @@ -121,13 +121,15 @@ export class ValueParser { } private create_node(node_type: number, start: number, end: number): number { - let node = this.arena.create_node() - this.arena.set_type(node, node_type) - this.arena.set_start_offset(node, start) - let length = end - start - this.arena.set_length(node, length) - // Skip set_content_start since it would compute delta = start - start = 0 (already zero-initialized) - this.arena.set_content_length(node, length) + let node = this.arena.create_node( + node_type, + start, + end - start, + this.lexer.line, + this.lexer.column + ) + // Skip set_content_start_delta since delta = start - start = 0 (already zero-initialized) + this.arena.set_content_length(node, end - start) return node } @@ -153,11 +155,15 @@ export class ValueParser { // Get function name to check for special handling let func_name = this.source.substring(start, name_end).toLowerCase() - // Create URL or function node based on function name - let node = this.arena.create_node() - this.arena.set_type(node, func_name === 'url' ? URL : FUNCTION) - this.arena.set_start_offset(node, start) - this.arena.set_content_start(node, start) + // Create URL or function node based on function name (length will be set later) + let node = this.arena.create_node( + func_name === 'url' ? URL : FUNCTION, + start, + 0, // length unknown yet + this.lexer.line, + this.lexer.column + ) + this.arena.set_content_start_delta(node, 0) this.arena.set_content_length(node, name_end - start) // Special handling for url() and src() functions with unquoted content: @@ -218,7 +224,7 @@ export class ValueParser { this.arena.set_length(node, func_end - start) // Set value to the content between parentheses (accessible via node.value) - this.arena.set_value_start(node, content_start) + this.arena.set_value_start_delta(node, content_start - start) this.arena.set_value_length(node, content_end - content_start) return node @@ -265,28 +271,24 @@ export class ValueParser { this.arena.set_length(node, func_end - start) // Set value to the content between parentheses (accessible via node.value) - this.arena.set_value_start(node, content_start) + this.arena.set_value_start_delta(node, content_start - start) this.arena.set_value_length(node, content_end - content_start) // Link arguments as children - if (args.length > 0) { - this.arena.set_first_child(node, args[0]) - this.arena.set_last_child(node, args[args.length - 1]) - - // Chain arguments as siblings - for (let i = 0; i < args.length - 1; i++) { - this.arena.set_next_sibling(args[i], args[i + 1]) - } - } + this.arena.append_children(node, args) return node } private parse_parenthesis_node(start: number, end: number): number { - // Create parenthesis node - let node = this.arena.create_node() - this.arena.set_type(node, PARENTHESIS) - this.arena.set_start_offset(node, start) + // Create parenthesis node (length will be set later) + let node = this.arena.create_node( + PARENTHESIS, + start, + 0, // length unknown yet + this.lexer.line, + this.lexer.column + ) // Parse parenthesized content (everything until matching ')') let children: number[] = [] @@ -327,15 +329,7 @@ export class ValueParser { this.arena.set_length(node, paren_end - start) // Link children as siblings - if (children.length > 0) { - this.arena.set_first_child(node, children[0]) - this.arena.set_last_child(node, children[children.length - 1]) - - // Chain children as siblings - for (let i = 0; i < children.length - 1; i++) { - this.arena.set_next_sibling(children[i], children[i + 1]) - } - } + this.arena.append_children(node, children) return node } diff --git a/src/parse.ts b/src/parse.ts index f85fccd..35c38b1 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -105,24 +105,29 @@ export class Parser { this.next_token() // Create the root stylesheet node - let stylesheet = this.arena.create_node() - this.arena.set_type(stylesheet, STYLESHEET) - this.arena.set_start_offset(stylesheet, 0) - this.arena.set_length(stylesheet, this.source.length) - this.arena.set_start_line(stylesheet, 1) - this.arena.set_start_column(stylesheet, 1) + let stylesheet = this.arena.create_node( + STYLESHEET, + 0, + this.source.length, + 1, + 1 + ) // Parse all rules at the top level + let rules: number[] = [] while (!this.is_eof()) { let rule = this.parse_rule() if (rule !== null) { - this.arena.append_child(stylesheet, rule) + rules.push(rule) } else { // Skip unknown tokens this.next_token() } } + // Link all rules as children + this.arena.append_children(stylesheet, rules) + // Return wrapped node return new CSSNode(this.arena, this.source, stylesheet) } @@ -150,17 +155,17 @@ export class Parser { let rule_line = this.lexer.token_line let rule_column = this.lexer.token_column - // Create the style rule node - let style_rule = this.arena.create_node() - this.arena.set_type(style_rule, STYLE_RULE) - this.arena.set_start_line(style_rule, rule_line) - this.arena.set_start_column(style_rule, rule_column) + // Create the style rule node (length will be set later) + let style_rule = this.arena.create_node( + STYLE_RULE, + rule_start, + 0, // length unknown yet + rule_line, + rule_column + ) // Parse selector (everything until '{') let selector = this.parse_selector() - if (selector !== null) { - this.arena.append_child(style_rule, selector) - } // Expect '{' if (this.peek_type() !== TOKEN_LEFT_BRACE) { @@ -172,16 +177,19 @@ export class Parser { this.next_token() // consume '{' this.arena.set_flag(style_rule, FLAG_HAS_BLOCK) // Style rules always have blocks - // Create block node + // Create block node (length will be set later) let block_line = this.lexer.token_line let block_column = this.lexer.token_column - let block_node = this.arena.create_node() - this.arena.set_type(block_node, BLOCK) - this.arena.set_start_offset(block_node, block_start) - this.arena.set_start_line(block_node, block_line) - this.arena.set_start_column(block_node, block_column) + let block_node = this.arena.create_node( + BLOCK, + block_start, + 0, // length unknown yet + block_line, + block_column + ) // Parse declarations block (and nested rules for CSS Nesting) + let block_children: number[] = [] while (!this.is_eof()) { let token_type = this.peek_type() if (token_type === TOKEN_RIGHT_BRACE) break @@ -190,7 +198,7 @@ export class Parser { if (token_type === TOKEN_AT_KEYWORD) { let nested_at_rule = this.parse_atrule() if (nested_at_rule !== null) { - this.arena.append_child(block_node, nested_at_rule) + block_children.push(nested_at_rule) } else { this.next_token() } @@ -201,14 +209,14 @@ export class Parser { let declaration = this.parse_declaration() if (declaration !== null) { this.arena.set_flag(style_rule, FLAG_HAS_DECLARATIONS) - this.arena.append_child(block_node, declaration) + block_children.push(declaration) continue } // If not a declaration, try parsing as nested style rule let nested_rule = this.parse_style_rule() if (nested_rule !== null) { - this.arena.append_child(block_node, nested_rule) + block_children.push(nested_rule) } else { // Skip unknown tokens this.next_token() @@ -224,13 +232,18 @@ export class Parser { this.next_token() // consume '}' } - // Set block length and append to style rule + // Set block length and link its children this.arena.set_length(block_node, block_end - block_start) - this.arena.append_child(style_rule, block_node) + this.arena.append_children(block_node, block_children) - // Set the rule's offsets - this.arena.set_start_offset(style_rule, rule_start) + // Set the rule's length and link children (selector + block) this.arena.set_length(style_rule, rule_end - rule_start) + let style_rule_children: number[] = [] + if (selector !== null) { + style_rule_children.push(selector) + } + style_rule_children.push(block_node) + this.arena.append_children(style_rule, style_rule_children) return style_rule } @@ -259,12 +272,13 @@ export class Parser { } // Otherwise create a simple selector list node with just text offsets - let selector = this.arena.create_node() - this.arena.set_type(selector, SELECTOR_LIST) - this.arena.set_start_line(selector, selector_line) - this.arena.set_start_column(selector, selector_column) - this.arena.set_start_offset(selector, selector_start) - this.arena.set_length(selector, last_end - selector_start) + let selector = this.arena.create_node( + SELECTOR_LIST, + selector_start, + last_end - selector_start, + selector_line, + selector_column + ) return selector } @@ -294,15 +308,17 @@ export class Parser { } this.next_token() // consume ':' - // Create declaration node - let declaration = this.arena.create_node() - this.arena.set_type(declaration, DECLARATION) - this.arena.set_start_line(declaration, decl_line) - this.arena.set_start_column(declaration, decl_column) - this.arena.set_start_offset(declaration, prop_start) - - // Store property name position - this.arena.set_content_start(declaration, prop_start) + // Create declaration node (length will be set later) + let declaration = this.arena.create_node( + DECLARATION, + prop_start, + 0, // length unknown yet + decl_line, + decl_column + ) + + // Store property name position (delta = 0 since content starts at same offset as node) + this.arena.set_content_start_delta(declaration, 0) this.arena.set_content_length(declaration, prop_end - prop_start) // Check for vendor prefix and set flag if detected @@ -352,7 +368,7 @@ export class Parser { let trimmed = trim_boundaries(this.source, value_start, value_end) if (trimmed) { // Store raw value string offsets (for fast string access) - this.arena.set_value_start(declaration, trimmed[0]) + this.arena.set_value_start_delta(declaration, trimmed[0] - prop_start) this.arena.set_value_length(declaration, trimmed[1] - trimmed[0]) // Parse value into structured nodes (only if enabled) @@ -360,15 +376,7 @@ export class Parser { let valueNodes = this.value_parser.parse_value(trimmed[0], trimmed[1]) // Link value nodes as children of the declaration - if (valueNodes.length > 0) { - this.arena.set_first_child(declaration, valueNodes[0]) - this.arena.set_last_child(declaration, valueNodes[valueNodes.length - 1]) - - // Chain value nodes as siblings - for (let i = 0; i < valueNodes.length - 1; i++) { - this.arena.set_next_sibling(valueNodes[i], valueNodes[i + 1]) - } - } + this.arena.append_children(declaration, valueNodes) } } @@ -406,15 +414,17 @@ export class Parser { this.next_token() // consume @keyword - // Create at-rule node - let at_rule = this.arena.create_node() - this.arena.set_type(at_rule, AT_RULE) - this.arena.set_start_line(at_rule, at_rule_line) - this.arena.set_start_column(at_rule, at_rule_column) - this.arena.set_start_offset(at_rule, at_rule_start) + // Create at-rule node (length will be set later) + let at_rule = this.arena.create_node( + AT_RULE, + at_rule_start, + 0, // length unknown yet + at_rule_line, + at_rule_column + ) // Store at-rule name in contentStart/contentLength - this.arena.set_content_start(at_rule, name_start) + this.arena.set_content_start_delta(at_rule, name_start - at_rule_start) this.arena.set_content_length(at_rule, name_length) // Track prelude start and end @@ -431,16 +441,14 @@ export class Parser { // Store prelude position (trimmed) let trimmed = trim_boundaries(this.source, prelude_start, prelude_end) + let prelude_nodes: number[] = [] if (trimmed) { - this.arena.set_value_start(at_rule, trimmed[0]) + this.arena.set_value_start_delta(at_rule, trimmed[0] - at_rule_start) this.arena.set_value_length(at_rule, trimmed[1] - trimmed[0]) // Parse prelude if enabled if (this.prelude_parser) { - let prelude_nodes = this.prelude_parser.parse_prelude(at_rule_name, trimmed[0], trimmed[1], at_rule_line, at_rule_column) - for (let prelude_node of prelude_nodes) { - this.arena.append_child(at_rule, prelude_node) - } + prelude_nodes = this.prelude_parser.parse_prelude(at_rule_name, trimmed[0], trimmed[1], at_rule_line, at_rule_column) } } @@ -453,18 +461,21 @@ export class Parser { this.next_token() // consume '{' this.arena.set_flag(at_rule, FLAG_HAS_BLOCK) // At-rule has a block - // Create block node + // Create block node (length will be set later) let block_line = this.lexer.token_line let block_column = this.lexer.token_column - let block_node = this.arena.create_node() - this.arena.set_type(block_node, BLOCK) - this.arena.set_start_offset(block_node, block_start) - this.arena.set_start_line(block_node, block_line) - this.arena.set_start_column(block_node, block_column) + let block_node = this.arena.create_node( + BLOCK, + block_start, + 0, // length unknown yet + block_line, + block_column + ) // Determine what to parse inside the block based on the at-rule name let has_declarations = this.atrule_has_declarations(at_rule_name) let is_conditional = this.atrule_is_conditional(at_rule_name) + let block_children: number[] = [] if (has_declarations) { // Parse declarations only (like @font-face, @page) @@ -474,7 +485,7 @@ export class Parser { let declaration = this.parse_declaration() if (declaration !== null) { - this.arena.append_child(block_node, declaration) + block_children.push(declaration) } else { this.next_token() } @@ -489,7 +500,7 @@ export class Parser { if (token_type === TOKEN_AT_KEYWORD) { let nested_at_rule = this.parse_atrule() if (nested_at_rule !== null) { - this.arena.append_child(block_node, nested_at_rule) + block_children.push(nested_at_rule) } else { this.next_token() } @@ -499,14 +510,14 @@ export class Parser { // Try to parse as declaration first let declaration = this.parse_declaration() if (declaration !== null) { - this.arena.append_child(block_node, declaration) + block_children.push(declaration) continue } // If not a declaration, try parsing as nested style rule let nested_rule = this.parse_style_rule() if (nested_rule !== null) { - this.arena.append_child(block_node, nested_rule) + block_children.push(nested_rule) } else { // Skip unknown tokens this.next_token() @@ -520,7 +531,7 @@ export class Parser { let rule = this.parse_rule() if (rule !== null) { - this.arena.append_child(block_node, rule) + block_children.push(rule) } else { this.next_token() } @@ -540,15 +551,20 @@ export class Parser { this.arena.set_length(block_node, last_end - block_start) } - this.arena.append_child(at_rule, block_node) + // Link block children + this.arena.append_children(block_node, block_children) + + // Add block to at-rule children + prelude_nodes.push(block_node) } else if (this.peek_type() === TOKEN_SEMICOLON) { // Statement at-rule (like @import, @namespace) last_end = this.lexer.token_end this.next_token() // consume ';' } - // Set at-rule length + // Set at-rule length and link children (prelude nodes + optional block) this.arena.set_length(at_rule, last_end - at_rule_start) + this.arena.append_children(at_rule, prelude_nodes) return at_rule }