diff --git a/src/parse-selector.test.ts b/src/parse-selector.test.ts index 39d15e3..b55fe83 100644 --- a/src/parse-selector.test.ts +++ b/src/parse-selector.test.ts @@ -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') + }) + }) }) diff --git a/src/parse-selector.ts b/src/parse-selector.ts index f6d88fe..26a0eff 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -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 @@ -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