diff --git a/src/arena.ts b/src/arena.ts index 447e095..98f67e6 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -99,6 +99,7 @@ export const FLAG_HAS_ERROR = 1 << 1 // Syntax error export const FLAG_LENGTH_OVERFLOW = 1 << 2 // Node > 65k chars export const FLAG_HAS_BLOCK = 1 << 3 // Has { } block (for style rules and at-rules) // export const FLAG_VENDOR_PREFIXED = 1 << 4 // Has vendor prefix (-webkit-, -moz-, -ms-, -o-) +export const FLAG_HAS_NAMESPACE = 1 << 4 // Has namespace qualifier (for type/universal selectors) export const FLAG_HAS_DECLARATIONS = 1 << 5 // Has declarations (for style rules) export const FLAG_HAS_PARENS = 1 << 6 // Has parentheses syntax (for pseudo-class/pseudo-element functions) export const FLAG_BROWSERHACK = 1 << 7 // Has browser hack prefix (*property, _property, etc.) diff --git a/src/css-node.ts b/src/css-node.ts index ebdd4a9..236da83 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -50,6 +50,7 @@ import { FLAG_HAS_DECLARATIONS, FLAG_HAS_PARENS, FLAG_BROWSERHACK, + FLAG_HAS_NAMESPACE, } from './arena' import { @@ -251,6 +252,7 @@ const nodes_with_children = new Set([ const enumerable_properties = [ 'name', + 'namespace', 'property', 'value', 'unit', @@ -311,12 +313,29 @@ export class CSSNode { /** Get the "content" text (at-rule name for at-rules, layer name for import layers) */ get name(): string | null | undefined { if (!nodes_with_name.has(this.type)) return - let content = this.get_content() let { type } = this - if ((type === UNIVERSAL_SELECTOR || type === LANG_SELECTOR) && content === '') return null + if (type === UNIVERSAL_SELECTOR) return null + let content = this.get_content() + if (type === LANG_SELECTOR && content === '') return null return content } + /** + * Namespace prefix for type and universal selectors. + * - `null` — no namespace qualifier (plain `div` or `*`) + * - `''` — empty namespace (`|div` or `|*`) + * - `'ns'` — named namespace (`ns|div` or `ns|*`) + * - `'*'` — any namespace (`*|div` or `*|*`) + */ + get namespace(): string | null | undefined { + let { type } = this + if (type !== TYPE_SELECTOR && type !== UNIVERSAL_SELECTOR) return undefined + if (!this.arena.has_flag(this.index, FLAG_HAS_NAMESPACE)) return null + let start = this.arena.get_value_start(this.index) + let length = this.arena.get_value_length(this.index) + return this.source.substring(start, start + length) + } + /** * Alias for name (for declarations: "color" in "color: blue") * More semantic than `name` for declaration nodes diff --git a/src/node-types.ts b/src/node-types.ts index 714e89e..0d5c097 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -343,8 +343,10 @@ export type Value = CSSNode & export type TypeSelector = CSSNode & { readonly type: typeof TYPE_SELECTOR - /** Element type, e.g. "div", "span" */ + /** Local element name, e.g. "div" in both "div" and "ns|div" */ readonly name: string + /** Namespace prefix: null if no qualifier, '' for |div, 'ns' for ns|div, '*' for *|div */ + readonly namespace: string | null clone(options?: CloneOptions): ToPlain } @@ -400,8 +402,10 @@ export type Combinator = CSSNode & { export type UniversalSelector = CSSNode & { readonly type: typeof UNIVERSAL_SELECTOR - /** Namespace qualifier (e.g. 'ns' in 'ns|*'), null if no namespace */ - readonly name: string | null + /** Always null — universal selector has no element name */ + readonly name: null + /** Namespace prefix: null if no qualifier, '' for |*, 'ns' for ns|*, '*' for *|* */ + readonly namespace: string | null clone(options?: CloneOptions): ToPlain } diff --git a/src/parse-selector.test.ts b/src/parse-selector.test.ts index 669074b..7a3481a 100644 --- a/src/parse-selector.test.ts +++ b/src/parse-selector.test.ts @@ -472,7 +472,8 @@ describe('Selector Nodes', () => { const selector = node.first_child! as Selector const universalSelector = selector.first_child! as UniversalSelector expect(universalSelector.type_name).toBe('UniversalSelector') - expect(universalSelector.name).toBe('*') + expect(universalSelector.name).toBeNull() + expect(universalSelector.namespace).toBeNull() }) test('NESTING_SELECTOR type_name', () => { @@ -523,9 +524,11 @@ describe('Selector Nodes', () => { const firstSelector = node.first_child as Selector | null expect(firstSelector?.type).toBe(SELECTOR) - const typeNode = firstSelector?.first_child + const typeNode = firstSelector?.first_child as TypeSelector | null expect(typeNode?.type).toBe(TYPE_SELECTOR) expect(typeNode?.text).toBe('div') + expect(typeNode?.name).toBe('div') + expect(typeNode?.namespace).toBeNull() }) test('should parse class selector', () => { @@ -2291,7 +2294,8 @@ describe('Selector Nodes', () => { const universal = selector?.first_child as UniversalSelector | null | undefined expect(universal?.type).toBe(UNIVERSAL_SELECTOR) expect(universal?.text).toBe('ns|*') - expect(universal?.name).toBe('ns') + expect(universal?.name).toBeNull() + expect(universal?.namespace).toBe('ns') }) test('should parse ns|div (namespace with type selector)', () => { @@ -2306,7 +2310,8 @@ describe('Selector Nodes', () => { const typeSelector = selector?.first_child as TypeSelector | null | undefined expect(typeSelector?.type).toBe(TYPE_SELECTOR) expect(typeSelector?.text).toBe('ns|div') - expect(typeSelector?.name).toBe('ns') + expect(typeSelector?.name).toBe('div') + expect(typeSelector?.namespace).toBe('ns') }) test('should parse *|* (any namespace with universal selector)', () => { @@ -2319,7 +2324,8 @@ describe('Selector Nodes', () => { const universal = selector?.first_child as UniversalSelector | null | undefined expect(universal?.type).toBe(UNIVERSAL_SELECTOR) expect(universal?.text).toBe('*|*') - expect(universal?.name).toBe('*') + expect(universal?.name).toBeNull() + expect(universal?.namespace).toBe('*') }) test('should parse *|div (any namespace with type selector)', () => { @@ -2332,7 +2338,8 @@ describe('Selector Nodes', () => { const typeSelector = selector?.first_child as TypeSelector | null | undefined expect(typeSelector?.type).toBe(TYPE_SELECTOR) expect(typeSelector?.text).toBe('*|div') - expect(typeSelector?.name).toBe('*') + expect(typeSelector?.name).toBe('div') + expect(typeSelector?.namespace).toBe('*') }) test('should parse |* (empty namespace with universal selector)', () => { @@ -2345,8 +2352,8 @@ describe('Selector Nodes', () => { const universal = selector?.first_child as UniversalSelector | null | undefined expect(universal?.type).toBe(UNIVERSAL_SELECTOR) expect(universal?.text).toBe('|*') - // Empty namespace should result in empty name - expect(universal?.name).toBe('|') + expect(universal?.name).toBeNull() + expect(universal?.namespace).toBe('') }) test('should parse |div (empty namespace with type selector)', () => { @@ -2359,8 +2366,8 @@ describe('Selector Nodes', () => { const typeSelector = selector?.first_child as TypeSelector | null | undefined expect(typeSelector?.type).toBe(TYPE_SELECTOR) expect(typeSelector?.text).toBe('|div') - // Empty namespace should result in empty name - expect(typeSelector?.name).toBe('|') + expect(typeSelector?.name).toBe('div') + expect(typeSelector?.namespace).toBe('') }) test('should parse namespace selector with class', () => { @@ -2374,7 +2381,8 @@ describe('Selector Nodes', () => { expect(children.length).toBe(2) expect(children[0].type).toBe(TYPE_SELECTOR) expect(children[0].text).toBe('ns|div') - expect((children[0] as TypeSelector).name).toBe('ns') + expect((children[0] as TypeSelector).name).toBe('div') + expect((children[0] as TypeSelector).namespace).toBe('ns') expect(children[1].type).toBe(CLASS_SELECTOR) }) @@ -2420,17 +2428,20 @@ describe('Selector Nodes', () => { const firstType = selectors[0].first_child as TypeSelector | null expect(firstType?.type).toBe(TYPE_SELECTOR) expect(firstType?.text).toBe('ns|div') - expect(firstType?.name).toBe('ns') + expect(firstType?.name).toBe('div') + expect(firstType?.namespace).toBe('ns') const secondType = selectors[1].first_child as TypeSelector | null expect(secondType?.type).toBe(TYPE_SELECTOR) expect(secondType?.text).toBe('|span') - expect(secondType?.name).toBe('|') + expect(secondType?.name).toBe('span') + expect(secondType?.namespace).toBe('') const thirdType = selectors[2].first_child as TypeSelector | null expect(thirdType?.type).toBe(TYPE_SELECTOR) expect(thirdType?.text).toBe('*|p') - expect(thirdType?.name).toBe('*') + expect(thirdType?.name).toBe('p') + expect(thirdType?.namespace).toBe('*') }) test('should parse namespace selector with attribute', () => { @@ -2443,7 +2454,8 @@ describe('Selector Nodes', () => { const children = (selector as Selector | null)?.children || [] expect(children.length).toBe(2) expect(children[0].type).toBe(TYPE_SELECTOR) - expect((children[0] as TypeSelector).name).toBe('ns') + expect((children[0] as TypeSelector).name).toBe('div') + expect((children[0] as TypeSelector).namespace).toBe('ns') expect(children[1].type).toBe(ATTRIBUTE_SELECTOR) }) @@ -2457,7 +2469,8 @@ describe('Selector Nodes', () => { const children = (selector as Selector | null)?.children || [] expect(children.length).toBe(2) expect(children[0].type).toBe(TYPE_SELECTOR) - expect((children[0] as TypeSelector).name).toBe('ns') + expect((children[0] as TypeSelector).name).toBe('a') + expect((children[0] as TypeSelector).namespace).toBe('ns') expect(children[1].type).toBe(PSEUDO_CLASS_SELECTOR) }) @@ -2471,7 +2484,8 @@ describe('Selector Nodes', () => { const typeSelector = selector?.first_child as TypeSelector | null | undefined expect(typeSelector?.type).toBe(TYPE_SELECTOR) expect(typeSelector?.text).toBe('svg|rect') - expect(typeSelector?.name).toBe('svg') + expect(typeSelector?.name).toBe('rect') + expect(typeSelector?.namespace).toBe('svg') }) test('should parse long namespace identifier', () => { @@ -2483,7 +2497,8 @@ describe('Selector Nodes', () => { const selector = result.first_child const typeSelector = selector?.first_child as TypeSelector | null | undefined expect(typeSelector?.type).toBe(TYPE_SELECTOR) - expect(typeSelector?.name).toBe('myNamespace') + expect(typeSelector?.name).toBe('element') + expect(typeSelector?.namespace).toBe('myNamespace') }) test('should handle namespace in nested pseudo-class', () => { diff --git a/src/parse-selector.ts b/src/parse-selector.ts index 0c1ac43..e0c3928 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -17,6 +17,7 @@ import { LANG_SELECTOR, DIMENSION, FLAG_HAS_PARENS, + FLAG_HAS_NAMESPACE, } from './arena' import { TOKEN_IDENT, @@ -367,11 +368,12 @@ 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 + // namespace_prefix_length: length of the namespace text before |, 0 for empty namespace (|E) + // Returns the node index or null if invalid private parse_namespace_local_part( selector_start: number, namespace_start: number, - namespace_length: number, + namespace_prefix_length: number, ): number | null { const saved = this.lexer.save_position() this.lexer.next_token_fast(false) @@ -392,10 +394,17 @@ export class SelectorParser { 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_delta(node, namespace_start - selector_start) - this.arena.set_content_length(node, namespace_length) + let local_start = this.lexer.token_start + let local_end = this.lexer.token_end + let node = this.create_node(node_type, selector_start, local_end) + // FLAG: has namespace qualifier (even if empty like |E) + this.arena.set_flag(node, FLAG_HAS_NAMESPACE) + // Content = local element name (after |) + this.arena.set_content_start_delta(node, local_start - selector_start) + this.arena.set_content_length(node, local_end - local_start) + // Value = namespace prefix (before |, empty string if |E form) + this.arena.set_value_start_delta(node, namespace_start - selector_start) + this.arena.set_value_length(node, namespace_prefix_length) return node } @@ -462,8 +471,8 @@ export class SelectorParser { // 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) + // Namespace prefix has 0 length (empty namespace); | is just the separator + return this.parse_namespace_local_part(start, start, 0) } // Parse combinator (>, +, ~, or descendant space)