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
237 changes: 237 additions & 0 deletions src/parse-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1739,4 +1739,241 @@ describe('parse_selector()', () => {
expect(result.has_children).toBe(true)
expect(result.children.length).toBeGreaterThan(0)
})

describe('Namespace selectors', () => {
test('should parse ns|* (namespace with universal selector)', () => {
const result = parse_selector('ns|*')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|*')

const selector = result.first_child
expect(selector?.type).toBe(NODE_SELECTOR)
expect(selector?.text).toBe('ns|*')

const universal = selector?.first_child
expect(universal?.type).toBe(NODE_SELECTOR_UNIVERSAL)
expect(universal?.text).toBe('ns|*')
expect(universal?.name).toBe('ns')
})

test('should parse ns|div (namespace with type selector)', () => {
const result = parse_selector('ns|div')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|div')

const selector = result.first_child
expect(selector?.type).toBe(NODE_SELECTOR)

const typeSelector = selector?.first_child
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
expect(typeSelector?.text).toBe('ns|div')
expect(typeSelector?.name).toBe('ns')
})

test('should parse *|* (any namespace with universal selector)', () => {
const result = parse_selector('*|*')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('*|*')

const selector = result.first_child
const universal = selector?.first_child
expect(universal?.type).toBe(NODE_SELECTOR_UNIVERSAL)
expect(universal?.text).toBe('*|*')
expect(universal?.name).toBe('*')
})

test('should parse *|div (any namespace with type selector)', () => {
const result = parse_selector('*|div')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('*|div')

const selector = result.first_child
const typeSelector = selector?.first_child
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
expect(typeSelector?.text).toBe('*|div')
expect(typeSelector?.name).toBe('*')
})

test('should parse |* (empty namespace with universal selector)', () => {
const result = parse_selector('|*')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('|*')

const selector = result.first_child
const universal = selector?.first_child
expect(universal?.type).toBe(NODE_SELECTOR_UNIVERSAL)
expect(universal?.text).toBe('|*')
// Empty namespace should result in empty name
expect(universal?.name).toBe('|')
})

test('should parse |div (empty namespace with type selector)', () => {
const result = parse_selector('|div')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('|div')

const selector = result.first_child
const typeSelector = selector?.first_child
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
expect(typeSelector?.text).toBe('|div')
// Empty namespace should result in empty name
expect(typeSelector?.name).toBe('|')
})

test('should parse namespace selector with class', () => {
const result = parse_selector('ns|div.class')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|div.class')

const selector = result.first_child
const children = selector?.children || []
expect(children.length).toBe(2)
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
expect(children[0].text).toBe('ns|div')
expect(children[0].name).toBe('ns')
expect(children[1].type).toBe(NODE_SELECTOR_CLASS)
})

test('should parse namespace selector with ID', () => {
const result = parse_selector('ns|*#id')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|*#id')

const selector = result.first_child
const children = selector?.children || []
expect(children.length).toBe(2)
expect(children[0].type).toBe(NODE_SELECTOR_UNIVERSAL)
expect(children[0].text).toBe('ns|*')
expect(children[1].type).toBe(NODE_SELECTOR_ID)
})

test('should parse namespace selector in complex selector', () => {
const result = parse_selector('ns|div > *|span')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|div > *|span')

const selector = result.first_child
const children = selector?.children || []
expect(children.length).toBe(3) // div, >, span
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
expect(children[0].text).toBe('ns|div')
expect(children[1].type).toBe(NODE_SELECTOR_COMBINATOR)
expect(children[2].type).toBe(NODE_SELECTOR_TYPE)
expect(children[2].text).toBe('*|span')
})

test('should parse namespace selector in selector list', () => {
const result = parse_selector('ns|div, |span, *|p')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|div, |span, *|p')

const selectors = result.children
expect(selectors.length).toBe(3)

const firstType = selectors[0].first_child
expect(firstType?.type).toBe(NODE_SELECTOR_TYPE)
expect(firstType?.text).toBe('ns|div')
expect(firstType?.name).toBe('ns')

const secondType = selectors[1].first_child
expect(secondType?.type).toBe(NODE_SELECTOR_TYPE)
expect(secondType?.text).toBe('|span')
expect(secondType?.name).toBe('|')

const thirdType = selectors[2].first_child
expect(thirdType?.type).toBe(NODE_SELECTOR_TYPE)
expect(thirdType?.text).toBe('*|p')
expect(thirdType?.name).toBe('*')
})

test('should parse namespace selector with attribute', () => {
const result = parse_selector('ns|div[attr="value"]')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|div[attr="value"]')

const selector = result.first_child
const children = selector?.children || []
expect(children.length).toBe(2)
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
expect(children[0].name).toBe('ns')
expect(children[1].type).toBe(NODE_SELECTOR_ATTRIBUTE)
})

test('should parse namespace selector with pseudo-class', () => {
const result = parse_selector('ns|a:hover')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('ns|a:hover')

const selector = result.first_child
const children = selector?.children || []
expect(children.length).toBe(2)
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
expect(children[0].name).toBe('ns')
expect(children[1].type).toBe(NODE_SELECTOR_PSEUDO_CLASS)
})

test('should parse namespace with various identifiers', () => {
const result = parse_selector('svg|rect')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('svg|rect')

const selector = result.first_child
const typeSelector = selector?.first_child
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
expect(typeSelector?.text).toBe('svg|rect')
expect(typeSelector?.name).toBe('svg')
})

test('should parse long namespace identifier', () => {
const result = parse_selector('myNamespace|element')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe('myNamespace|element')

const selector = result.first_child
const typeSelector = selector?.first_child
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
expect(typeSelector?.name).toBe('myNamespace')
})

test('should handle namespace in nested pseudo-class', () => {
const result = parse_selector(':is(ns|div, *|span)')

expect(result.type).toBe(NODE_SELECTOR_LIST)
expect(result.text).toBe(':is(ns|div, *|span)')

const selector = result.first_child
const pseudo = selector?.first_child
expect(pseudo?.type).toBe(NODE_SELECTOR_PSEUDO_CLASS)
expect(pseudo?.name).toBe('is')

// The content should contain namespace selectors
const nestedList = pseudo?.first_child
expect(nestedList?.type).toBe(NODE_SELECTOR_LIST)

const nestedSelectors = nestedList?.children || []
expect(nestedSelectors.length).toBe(2)

const firstNestedType = nestedSelectors[0].first_child
expect(firstNestedType?.type).toBe(NODE_SELECTOR_TYPE)
expect(firstNestedType?.text).toBe('ns|div')

const secondNestedType = nestedSelectors[1].first_child
expect(secondNestedType?.type).toBe(NODE_SELECTOR_TYPE)
expect(secondNestedType?.text).toBe('*|span')
})
})
})
86 changes: 81 additions & 5 deletions src/parse-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,25 +296,29 @@ export class SelectorParser {

switch (token_type) {
case TOKEN_IDENT:
// Type selector: div, span, p
return this.create_node(NODE_SELECTOR_TYPE, start, end)
// Could be a type selector or namespace prefix
// Check if followed by | (namespace separator)
return this.parse_type_or_namespace_selector(start, end)

case TOKEN_HASH:
// ID selector: #id
return this.create_node(NODE_SELECTOR_ID, start, end)

case TOKEN_DELIM:
// Could be: . (class), * (universal), & (nesting)
// Could be: . (class), * (universal), & (nesting), | (namespace)
let ch = this.source.charCodeAt(start)
if (ch === CHAR_PERIOD) {
// . - class selector
return this.parse_class_selector(start)
} else if (ch === CHAR_ASTERISK) {
// * - universal selector
return this.create_node(NODE_SELECTOR_UNIVERSAL, start, end)
// * - could be universal selector or namespace prefix (*|)
return this.parse_universal_or_namespace_selector(start, end)
} else if (ch === CHAR_AMPERSAND) {
// & - nesting selector
return this.create_node(NODE_SELECTOR_NESTING, start, end)
} else if (ch === CHAR_PIPE) {
// | - empty namespace prefix (|E or |*)
return this.parse_empty_namespace_selector(start)
}
// Other delimiters signal end of selector
return null
Expand All @@ -341,6 +345,78 @@ export class SelectorParser {
}
}

// Parse the local part after | in a namespace selector (E or *)
// Returns the node type (TYPE or UNIVERSAL) or null if invalid
private parse_namespace_local_part(
selector_start: number,
namespace_start: number,
namespace_length: number,
): number | null {
const saved = this.lexer.save_position()
this.lexer.next_token_fast(false)

let node_type: number
if (this.lexer.token_type === TOKEN_IDENT) {
// ns|type
node_type = NODE_SELECTOR_TYPE
} else if (
this.lexer.token_type === TOKEN_DELIM &&
this.source.charCodeAt(this.lexer.token_start) === CHAR_ASTERISK
) {
// ns|*
node_type = NODE_SELECTOR_UNIVERSAL
} else {
// Invalid - restore position
this.lexer.restore_position(saved)
return null
}

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_length(node, namespace_length)
return node
}

// Parse type selector or namespace selector (ns|E or ns|*)
// Called when we've seen an IDENT token
private parse_type_or_namespace_selector(start: number, end: number): number | null {
// Check if followed by | (namespace separator)
if (this.lexer.pos < this.selector_end && this.source.charCodeAt(this.lexer.pos) === CHAR_PIPE) {
this.lexer.pos++ // skip |
let node = this.parse_namespace_local_part(start, start, end - start)
if (node !== null) return node
// Invalid - restore and treat as regular type selector
this.lexer.pos = end
}

// Regular type selector (no namespace)
return this.create_node(NODE_SELECTOR_TYPE, start, end)
}

// Parse universal selector or namespace selector (*|E or *|*)
// Called when we've seen a * DELIM token
private parse_universal_or_namespace_selector(start: number, end: number): number | null {
// Check if followed by | (any-namespace prefix)
if (this.lexer.pos < this.selector_end && this.source.charCodeAt(this.lexer.pos) === CHAR_PIPE) {
this.lexer.pos++ // skip |
let node = this.parse_namespace_local_part(start, start, end - start)
if (node !== null) return node
// Invalid - restore and treat as regular universal selector
this.lexer.pos = end
}

// Regular universal selector (no namespace)
return this.create_node(NODE_SELECTOR_UNIVERSAL, start, end)
}

// Parse empty namespace selector (|E or |*)
// Called when we've seen a | DELIM token at the start
private parse_empty_namespace_selector(start: number): number | null {
// The | character is the namespace indicator (length = 1)
return this.parse_namespace_local_part(start, start, 1)
}

// Parse combinator (>, +, ~, or descendant space)
private try_parse_combinator(): number | null {
let whitespace_start = this.lexer.pos
Expand Down
Loading