Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: projectwallace/css-parser

check-ts:
name: Check types
Expand Down
94 changes: 37 additions & 57 deletions src/arena.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -20,24 +20,24 @@ 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)
})

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
Expand All @@ -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)
Expand All @@ -74,41 +68,37 @@ 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)
})

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)
})

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)
})

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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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
Expand Down
60 changes: 33 additions & 27 deletions src/arena.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand All @@ -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])
}
}

Expand Down
16 changes: 9 additions & 7 deletions src/parse-anplusb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading
Loading