Skip to content

Commit ba3c31e

Browse files
authored
fix: add namespace getter for type selectors (#228)
feat: add namespace property to TypeSelector and UniversalSelector Fixes the selector namespace API: - `TypeSelector.name` now always returns the local element name (e.g. `'div'` for both `div` and `ns|div`). Previously it returned the namespace prefix for namespaced selectors, making it impossible to get the element name without parsing `.text` manually. - `UniversalSelector.name` now always returns `null`. Previously it returned `'*'` for both plain `*` and `*|*`, making the two indistinguishable. - New `namespace` property on both types returns the namespace prefix: `null` (no qualifier), `''` (empty, `|div`), `'ns'` (named), or `'*'` (any-namespace wildcard). Implementation: adds `FLAG_HAS_NAMESPACE` (bit 4 in the flags byte). When set, content fields hold the local name and value fields hold the namespace prefix. The flag distinguishes `|div` (empty namespace, flag set, value length 0) from plain `div` (no qualifier, flag not set).
1 parent 95e119a commit ba3c31e

5 files changed

Lines changed: 79 additions & 31 deletions

File tree

src/arena.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const FLAG_HAS_ERROR = 1 << 1 // Syntax error
9999
export const FLAG_LENGTH_OVERFLOW = 1 << 2 // Node > 65k chars
100100
export const FLAG_HAS_BLOCK = 1 << 3 // Has { } block (for style rules and at-rules)
101101
// export const FLAG_VENDOR_PREFIXED = 1 << 4 // Has vendor prefix (-webkit-, -moz-, -ms-, -o-)
102+
export const FLAG_HAS_NAMESPACE = 1 << 4 // Has namespace qualifier (for type/universal selectors)
102103
export const FLAG_HAS_DECLARATIONS = 1 << 5 // Has declarations (for style rules)
103104
export const FLAG_HAS_PARENS = 1 << 6 // Has parentheses syntax (for pseudo-class/pseudo-element functions)
104105
export const FLAG_BROWSERHACK = 1 << 7 // Has browser hack prefix (*property, _property, etc.)

src/css-node.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
FLAG_HAS_DECLARATIONS,
5151
FLAG_HAS_PARENS,
5252
FLAG_BROWSERHACK,
53+
FLAG_HAS_NAMESPACE,
5354
} from './arena'
5455

5556
import {
@@ -251,6 +252,7 @@ const nodes_with_children = new Set<number>([
251252

252253
const enumerable_properties = [
253254
'name',
255+
'namespace',
254256
'property',
255257
'value',
256258
'unit',
@@ -311,12 +313,29 @@ export class CSSNode {
311313
/** Get the "content" text (at-rule name for at-rules, layer name for import layers) */
312314
get name(): string | null | undefined {
313315
if (!nodes_with_name.has(this.type)) return
314-
let content = this.get_content()
315316
let { type } = this
316-
if ((type === UNIVERSAL_SELECTOR || type === LANG_SELECTOR) && content === '') return null
317+
if (type === UNIVERSAL_SELECTOR) return null
318+
let content = this.get_content()
319+
if (type === LANG_SELECTOR && content === '') return null
317320
return content
318321
}
319322

323+
/**
324+
* Namespace prefix for type and universal selectors.
325+
* - `null` — no namespace qualifier (plain `div` or `*`)
326+
* - `''` — empty namespace (`|div` or `|*`)
327+
* - `'ns'` — named namespace (`ns|div` or `ns|*`)
328+
* - `'*'` — any namespace (`*|div` or `*|*`)
329+
*/
330+
get namespace(): string | null | undefined {
331+
let { type } = this
332+
if (type !== TYPE_SELECTOR && type !== UNIVERSAL_SELECTOR) return undefined
333+
if (!this.arena.has_flag(this.index, FLAG_HAS_NAMESPACE)) return null
334+
let start = this.arena.get_value_start(this.index)
335+
let length = this.arena.get_value_length(this.index)
336+
return this.source.substring(start, start + length)
337+
}
338+
320339
/**
321340
* Alias for name (for declarations: "color" in "color: blue")
322341
* More semantic than `name` for declaration nodes

src/node-types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,10 @@ export type Value = CSSNode &
343343

344344
export type TypeSelector = CSSNode & {
345345
readonly type: typeof TYPE_SELECTOR
346-
/** Element type, e.g. "div", "span" */
346+
/** Local element name, e.g. "div" in both "div" and "ns|div" */
347347
readonly name: string
348+
/** Namespace prefix: null if no qualifier, '' for |div, 'ns' for ns|div, '*' for *|div */
349+
readonly namespace: string | null
348350
clone(options?: CloneOptions): ToPlain<TypeSelector>
349351
}
350352

@@ -400,8 +402,10 @@ export type Combinator = CSSNode & {
400402

401403
export type UniversalSelector = CSSNode & {
402404
readonly type: typeof UNIVERSAL_SELECTOR
403-
/** Namespace qualifier (e.g. 'ns' in 'ns|*'), null if no namespace */
404-
readonly name: string | null
405+
/** Always null — universal selector has no element name */
406+
readonly name: null
407+
/** Namespace prefix: null if no qualifier, '' for |*, 'ns' for ns|*, '*' for *|* */
408+
readonly namespace: string | null
405409
clone(options?: CloneOptions): ToPlain<UniversalSelector>
406410
}
407411

src/parse-selector.test.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ describe('Selector Nodes', () => {
472472
const selector = node.first_child! as Selector
473473
const universalSelector = selector.first_child! as UniversalSelector
474474
expect(universalSelector.type_name).toBe('UniversalSelector')
475-
expect(universalSelector.name).toBe('*')
475+
expect(universalSelector.name).toBeNull()
476+
expect(universalSelector.namespace).toBeNull()
476477
})
477478

478479
test('NESTING_SELECTOR type_name', () => {
@@ -523,9 +524,11 @@ describe('Selector Nodes', () => {
523524
const firstSelector = node.first_child as Selector | null
524525
expect(firstSelector?.type).toBe(SELECTOR)
525526

526-
const typeNode = firstSelector?.first_child
527+
const typeNode = firstSelector?.first_child as TypeSelector | null
527528
expect(typeNode?.type).toBe(TYPE_SELECTOR)
528529
expect(typeNode?.text).toBe('div')
530+
expect(typeNode?.name).toBe('div')
531+
expect(typeNode?.namespace).toBeNull()
529532
})
530533

531534
test('should parse class selector', () => {
@@ -2291,7 +2294,8 @@ describe('Selector Nodes', () => {
22912294
const universal = selector?.first_child as UniversalSelector | null | undefined
22922295
expect(universal?.type).toBe(UNIVERSAL_SELECTOR)
22932296
expect(universal?.text).toBe('ns|*')
2294-
expect(universal?.name).toBe('ns')
2297+
expect(universal?.name).toBeNull()
2298+
expect(universal?.namespace).toBe('ns')
22952299
})
22962300

22972301
test('should parse ns|div (namespace with type selector)', () => {
@@ -2306,7 +2310,8 @@ describe('Selector Nodes', () => {
23062310
const typeSelector = selector?.first_child as TypeSelector | null | undefined
23072311
expect(typeSelector?.type).toBe(TYPE_SELECTOR)
23082312
expect(typeSelector?.text).toBe('ns|div')
2309-
expect(typeSelector?.name).toBe('ns')
2313+
expect(typeSelector?.name).toBe('div')
2314+
expect(typeSelector?.namespace).toBe('ns')
23102315
})
23112316

23122317
test('should parse *|* (any namespace with universal selector)', () => {
@@ -2319,7 +2324,8 @@ describe('Selector Nodes', () => {
23192324
const universal = selector?.first_child as UniversalSelector | null | undefined
23202325
expect(universal?.type).toBe(UNIVERSAL_SELECTOR)
23212326
expect(universal?.text).toBe('*|*')
2322-
expect(universal?.name).toBe('*')
2327+
expect(universal?.name).toBeNull()
2328+
expect(universal?.namespace).toBe('*')
23232329
})
23242330

23252331
test('should parse *|div (any namespace with type selector)', () => {
@@ -2332,7 +2338,8 @@ describe('Selector Nodes', () => {
23322338
const typeSelector = selector?.first_child as TypeSelector | null | undefined
23332339
expect(typeSelector?.type).toBe(TYPE_SELECTOR)
23342340
expect(typeSelector?.text).toBe('*|div')
2335-
expect(typeSelector?.name).toBe('*')
2341+
expect(typeSelector?.name).toBe('div')
2342+
expect(typeSelector?.namespace).toBe('*')
23362343
})
23372344

23382345
test('should parse |* (empty namespace with universal selector)', () => {
@@ -2345,8 +2352,8 @@ describe('Selector Nodes', () => {
23452352
const universal = selector?.first_child as UniversalSelector | null | undefined
23462353
expect(universal?.type).toBe(UNIVERSAL_SELECTOR)
23472354
expect(universal?.text).toBe('|*')
2348-
// Empty namespace should result in empty name
2349-
expect(universal?.name).toBe('|')
2355+
expect(universal?.name).toBeNull()
2356+
expect(universal?.namespace).toBe('')
23502357
})
23512358

23522359
test('should parse |div (empty namespace with type selector)', () => {
@@ -2359,8 +2366,8 @@ describe('Selector Nodes', () => {
23592366
const typeSelector = selector?.first_child as TypeSelector | null | undefined
23602367
expect(typeSelector?.type).toBe(TYPE_SELECTOR)
23612368
expect(typeSelector?.text).toBe('|div')
2362-
// Empty namespace should result in empty name
2363-
expect(typeSelector?.name).toBe('|')
2369+
expect(typeSelector?.name).toBe('div')
2370+
expect(typeSelector?.namespace).toBe('')
23642371
})
23652372

23662373
test('should parse namespace selector with class', () => {
@@ -2374,7 +2381,8 @@ describe('Selector Nodes', () => {
23742381
expect(children.length).toBe(2)
23752382
expect(children[0].type).toBe(TYPE_SELECTOR)
23762383
expect(children[0].text).toBe('ns|div')
2377-
expect((children[0] as TypeSelector).name).toBe('ns')
2384+
expect((children[0] as TypeSelector).name).toBe('div')
2385+
expect((children[0] as TypeSelector).namespace).toBe('ns')
23782386
expect(children[1].type).toBe(CLASS_SELECTOR)
23792387
})
23802388

@@ -2420,17 +2428,20 @@ describe('Selector Nodes', () => {
24202428
const firstType = selectors[0].first_child as TypeSelector | null
24212429
expect(firstType?.type).toBe(TYPE_SELECTOR)
24222430
expect(firstType?.text).toBe('ns|div')
2423-
expect(firstType?.name).toBe('ns')
2431+
expect(firstType?.name).toBe('div')
2432+
expect(firstType?.namespace).toBe('ns')
24242433

24252434
const secondType = selectors[1].first_child as TypeSelector | null
24262435
expect(secondType?.type).toBe(TYPE_SELECTOR)
24272436
expect(secondType?.text).toBe('|span')
2428-
expect(secondType?.name).toBe('|')
2437+
expect(secondType?.name).toBe('span')
2438+
expect(secondType?.namespace).toBe('')
24292439

24302440
const thirdType = selectors[2].first_child as TypeSelector | null
24312441
expect(thirdType?.type).toBe(TYPE_SELECTOR)
24322442
expect(thirdType?.text).toBe('*|p')
2433-
expect(thirdType?.name).toBe('*')
2443+
expect(thirdType?.name).toBe('p')
2444+
expect(thirdType?.namespace).toBe('*')
24342445
})
24352446

24362447
test('should parse namespace selector with attribute', () => {
@@ -2443,7 +2454,8 @@ describe('Selector Nodes', () => {
24432454
const children = (selector as Selector | null)?.children || []
24442455
expect(children.length).toBe(2)
24452456
expect(children[0].type).toBe(TYPE_SELECTOR)
2446-
expect((children[0] as TypeSelector).name).toBe('ns')
2457+
expect((children[0] as TypeSelector).name).toBe('div')
2458+
expect((children[0] as TypeSelector).namespace).toBe('ns')
24472459
expect(children[1].type).toBe(ATTRIBUTE_SELECTOR)
24482460
})
24492461

@@ -2457,7 +2469,8 @@ describe('Selector Nodes', () => {
24572469
const children = (selector as Selector | null)?.children || []
24582470
expect(children.length).toBe(2)
24592471
expect(children[0].type).toBe(TYPE_SELECTOR)
2460-
expect((children[0] as TypeSelector).name).toBe('ns')
2472+
expect((children[0] as TypeSelector).name).toBe('a')
2473+
expect((children[0] as TypeSelector).namespace).toBe('ns')
24612474
expect(children[1].type).toBe(PSEUDO_CLASS_SELECTOR)
24622475
})
24632476

@@ -2471,7 +2484,8 @@ describe('Selector Nodes', () => {
24712484
const typeSelector = selector?.first_child as TypeSelector | null | undefined
24722485
expect(typeSelector?.type).toBe(TYPE_SELECTOR)
24732486
expect(typeSelector?.text).toBe('svg|rect')
2474-
expect(typeSelector?.name).toBe('svg')
2487+
expect(typeSelector?.name).toBe('rect')
2488+
expect(typeSelector?.namespace).toBe('svg')
24752489
})
24762490

24772491
test('should parse long namespace identifier', () => {
@@ -2483,7 +2497,8 @@ describe('Selector Nodes', () => {
24832497
const selector = result.first_child
24842498
const typeSelector = selector?.first_child as TypeSelector | null | undefined
24852499
expect(typeSelector?.type).toBe(TYPE_SELECTOR)
2486-
expect(typeSelector?.name).toBe('myNamespace')
2500+
expect(typeSelector?.name).toBe('element')
2501+
expect(typeSelector?.namespace).toBe('myNamespace')
24872502
})
24882503

24892504
test('should handle namespace in nested pseudo-class', () => {

src/parse-selector.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
LANG_SELECTOR,
1818
DIMENSION,
1919
FLAG_HAS_PARENS,
20+
FLAG_HAS_NAMESPACE,
2021
} from './arena'
2122
import {
2223
TOKEN_IDENT,
@@ -367,11 +368,12 @@ export class SelectorParser {
367368
}
368369

369370
// Parse the local part after | in a namespace selector (E or *)
370-
// Returns the node type (TYPE or UNIVERSAL) or null if invalid
371+
// namespace_prefix_length: length of the namespace text before |, 0 for empty namespace (|E)
372+
// Returns the node index or null if invalid
371373
private parse_namespace_local_part(
372374
selector_start: number,
373375
namespace_start: number,
374-
namespace_length: number,
376+
namespace_prefix_length: number,
375377
): number | null {
376378
const saved = this.lexer.save_position()
377379
this.lexer.next_token_fast(false)
@@ -392,10 +394,17 @@ export class SelectorParser {
392394
return null
393395
}
394396

395-
let node = this.create_node(node_type, selector_start, this.lexer.token_end)
396-
// Store namespace in content fields
397-
this.arena.set_content_start_delta(node, namespace_start - selector_start)
398-
this.arena.set_content_length(node, namespace_length)
397+
let local_start = this.lexer.token_start
398+
let local_end = this.lexer.token_end
399+
let node = this.create_node(node_type, selector_start, local_end)
400+
// FLAG: has namespace qualifier (even if empty like |E)
401+
this.arena.set_flag(node, FLAG_HAS_NAMESPACE)
402+
// Content = local element name (after |)
403+
this.arena.set_content_start_delta(node, local_start - selector_start)
404+
this.arena.set_content_length(node, local_end - local_start)
405+
// Value = namespace prefix (before |, empty string if |E form)
406+
this.arena.set_value_start_delta(node, namespace_start - selector_start)
407+
this.arena.set_value_length(node, namespace_prefix_length)
399408
return node
400409
}
401410

@@ -462,8 +471,8 @@ export class SelectorParser {
462471
// Parse empty namespace selector (|E or |*)
463472
// Called when we've seen a | DELIM token at the start
464473
private parse_empty_namespace_selector(start: number): number | null {
465-
// The | character is the namespace indicator (length = 1)
466-
return this.parse_namespace_local_part(start, start, 1)
474+
// Namespace prefix has 0 length (empty namespace); | is just the separator
475+
return this.parse_namespace_local_part(start, start, 0)
467476
}
468477

469478
// Parse combinator (>, +, ~, or descendant space)

0 commit comments

Comments
 (0)