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
160 changes: 160 additions & 0 deletions src/parse-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,166 @@ describe('SelectorParser', () => {
})
})

describe('Relaxed nesting (CSS Nesting Module Level 1)', () => {
it('should parse selector starting with child combinator', () => {
const { arena, rootNode, source } = parseSelectorInternal('> a')

expect(rootNode).not.toBeNull()
if (!rootNode) return

// Should have one selector
const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(1)

// The selector should have 2 children: combinator (>) and type selector (a)
const selectorWrapper = selectorWrappers[0]
const children = getChildren(arena, source, selectorWrapper)
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('a')
})

it('should parse selector starting with next-sibling combinator', () => {
const { arena, rootNode, source } = parseSelectorInternal('+ div')

expect(rootNode).not.toBeNull()
if (!rootNode) return

const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(1)

const selectorWrapper = selectorWrappers[0]
const children = getChildren(arena, source, selectorWrapper)
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('+')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('div')
})

it('should parse selector starting with subsequent-sibling combinator', () => {
const { arena, rootNode, source } = parseSelectorInternal('~ span')

expect(rootNode).not.toBeNull()
if (!rootNode) return

const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(1)

const selectorWrapper = selectorWrappers[0]
const children = getChildren(arena, source, selectorWrapper)
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('~')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('span')
})

it('should parse complex selector after leading combinator', () => {
const { arena, rootNode, source } = parseSelectorInternal('> a.link#nav[href]:hover')

expect(rootNode).not.toBeNull()
if (!rootNode) return

const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(1)

const selectorWrapper = selectorWrappers[0]
const children = getChildren(arena, source, selectorWrapper)

// Should have: combinator (>), type (a), class (.link), id (#nav), attribute ([href]), pseudo-class (:hover)
expect(children.length).toBeGreaterThanOrEqual(6)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('a')
expect(arena.get_type(children[2])).toBe(NODE_SELECTOR_CLASS)
expect(getNodeText(arena, source, children[2])).toBe('.link')
expect(arena.get_type(children[3])).toBe(NODE_SELECTOR_ID)
expect(getNodeText(arena, source, children[3])).toBe('#nav')
expect(arena.get_type(children[4])).toBe(NODE_SELECTOR_ATTRIBUTE)
expect(arena.get_type(children[5])).toBe(NODE_SELECTOR_PSEUDO_CLASS)
})

it('should parse multiple selectors with leading combinators', () => {
const { arena, rootNode, source } = parseSelectorInternal('> a, ~ span, + div')

expect(rootNode).not.toBeNull()
if (!rootNode) return

// Should have three selectors
const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(3)

// First selector: > a
let children = getChildren(arena, source, selectorWrappers[0])
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('a')

// Second selector: ~ span
children = getChildren(arena, source, selectorWrappers[1])
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('~')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('span')

// Third selector: + div
children = getChildren(arena, source, selectorWrappers[2])
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('+')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('div')
})

it('should parse leading combinator with whitespace', () => {
const { arena, rootNode, source } = parseSelectorInternal('> a')

expect(rootNode).not.toBeNull()
if (!rootNode) return

const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(1)

const selectorWrapper = selectorWrappers[0]
const children = getChildren(arena, source, selectorWrapper)
expect(children).toHaveLength(2)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('a')
})

it('should parse selector with both leading and middle combinators', () => {
const { arena, rootNode, source } = parseSelectorInternal('> div span')

expect(rootNode).not.toBeNull()
if (!rootNode) return

const selectorWrappers = getChildren(arena, source, rootNode)
expect(selectorWrappers).toHaveLength(1)

const selectorWrapper = selectorWrappers[0]
const children = getChildren(arena, source, selectorWrapper)

// Should have: combinator (>), type (div), combinator (descendant), type (span)
expect(children).toHaveLength(4)
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[1])).toBe('div')
expect(arena.get_type(children[2])).toBe(NODE_SELECTOR_COMBINATOR)
expect(arena.get_type(children[3])).toBe(NODE_SELECTOR_TYPE)
expect(getNodeText(arena, source, children[3])).toBe('span')
})
})

describe('Edge cases', () => {
it('should parse selector with multiple spaces', () => {
const { arena, rootNode, source } = parseSelectorInternal('div p')
Expand Down
7 changes: 4 additions & 3 deletions src/parse-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class SelectorParser {

// Parse a selector range into selector nodes
// Always returns a NODE_SELECTOR_LIST with selector components as children
parse_selector(start: number, end: number, line: number = 1, column: number = 1, allow_relative: boolean = false): number | null {
parse_selector(start: number, end: number, line: number = 1, column: number = 1, allow_relative: boolean = true): number | null {
this.selector_end = end

// Position lexer at selector start
Expand All @@ -95,7 +95,7 @@ export class SelectorParser {
}

// Parse comma-separated selectors
private parse_selector_list(allow_relative: boolean = false): number | null {
private parse_selector_list(allow_relative: boolean = true): number | null {
let selectors: number[] = []
let list_start = this.lexer.pos
let list_line = this.lexer.line
Expand Down Expand Up @@ -170,7 +170,8 @@ export class SelectorParser {

// Parse a complex selector (with combinators)
// e.g., "div.class > p + span"
private parse_complex_selector(allow_relative: boolean = false): number | null {
// Also supports CSS Nesting relaxed syntax: "> a", "~ span", etc.
private parse_complex_selector(allow_relative: boolean = true): number | null {
let components: number[] = []

// Skip leading whitespace
Expand Down
122 changes: 122 additions & 0 deletions src/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,128 @@ describe('Parser', () => {

expect(body.type).toBe(NODE_STYLE_RULE)
})

describe('Relaxed nesting (CSS Nesting Module Level 1)', () => {
test('should parse nested rule with leading child combinator', () => {
let source = '.parent { > a { color: red; } }'
let parser = new Parser(source)
let root = parser.parse()

let parent = root.first_child!
expect(parent.type).toBe(NODE_STYLE_RULE)

let [_selector, block] = parent.children
let nested_rule = block.first_child!
expect(nested_rule.type).toBe(NODE_STYLE_RULE)

let nested_selector = nested_rule.first_child!
expect(nested_selector.text).toBe('> a')
// Verify selector has children (was parsed, not left empty)
expect(nested_selector.has_children).toBe(true)
})

test('should parse nested rule with leading next-sibling combinator', () => {
let source = '.parent { + span { color: blue; } }'
let parser = new Parser(source)
let root = parser.parse()

let parent = root.first_child!
let [_selector, block] = parent.children
let nested_rule = block.first_child!
expect(nested_rule.type).toBe(NODE_STYLE_RULE)

let nested_selector = nested_rule.first_child!
expect(nested_selector.text).toBe('+ span')
expect(nested_selector.has_children).toBe(true)
})

test('should parse nested rule with leading subsequent-sibling combinator', () => {
let source = '.parent { ~ div { color: green; } }'
let parser = new Parser(source)
let root = parser.parse()

let parent = root.first_child!
let [_selector, block] = parent.children
let nested_rule = block.first_child!
expect(nested_rule.type).toBe(NODE_STYLE_RULE)

let nested_selector = nested_rule.first_child!
expect(nested_selector.text).toBe('~ div')
expect(nested_selector.has_children).toBe(true)
})

test('should parse multiple nested rules with different leading combinators', () => {
let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }'
let parser = new Parser(source)
let root = parser.parse()

let parent = root.first_child!
let [_selector, block] = parent.children
let [rule1, rule2, rule3] = block.children

expect(rule1.type).toBe(NODE_STYLE_RULE)
expect(rule1.first_child!.text).toBe('> a')
expect(rule1.first_child!.has_children).toBe(true)

expect(rule2.type).toBe(NODE_STYLE_RULE)
expect(rule2.first_child!.text).toBe('~ span')
expect(rule2.first_child!.has_children).toBe(true)

expect(rule3.type).toBe(NODE_STYLE_RULE)
expect(rule3.first_child!.text).toBe('+ div')
expect(rule3.first_child!.has_children).toBe(true)
})

test('should parse complex selector after leading combinator', () => {
let source = '.parent { > a.link#nav[href]:hover { color: red; } }'
let parser = new Parser(source)
let root = parser.parse()

let parent = root.first_child!
let [_selector, block] = parent.children
let nested_rule = block.first_child!

let nested_selector = nested_rule.first_child!
expect(nested_selector.text).toBe('> a.link#nav[href]:hover')
expect(nested_selector.has_children).toBe(true)
})

test('should parse deeply nested rules with leading combinators', () => {
let source = '.a { > .b { > .c { color: red; } } }'
let parser = new Parser(source)
let root = parser.parse()

let a = root.first_child!
let [_selector_a, block_a] = a.children
let b = block_a.first_child!
expect(b.type).toBe(NODE_STYLE_RULE)
expect(b.first_child!.text).toBe('> .b')
expect(b.first_child!.has_children).toBe(true)

let [_selector_b, block_b] = b.children
let c = block_b.first_child!
expect(c.type).toBe(NODE_STYLE_RULE)
expect(c.first_child!.text).toBe('> .c')
expect(c.first_child!.has_children).toBe(true)
})

test('should parse mixed nested rules with and without leading combinators', () => {
let source = '.parent { .normal { } > .combinator { } }'
let parser = new Parser(source)
let root = parser.parse()

let parent = root.first_child!
let [_selector, block] = parent.children
let [normal, combinator] = block.children

expect(normal.type).toBe(NODE_STYLE_RULE)
expect(normal.first_child!.text).toBe('.normal')

expect(combinator.type).toBe(NODE_STYLE_RULE)
expect(combinator.first_child!.text).toBe('> .combinator')
expect(combinator.first_child!.has_children).toBe(true)
})
})
})

describe('@keyframes parsing', () => {
Expand Down
Loading