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
1 change: 1 addition & 0 deletions src/arena.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
23 changes: 21 additions & 2 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
FLAG_HAS_DECLARATIONS,
FLAG_HAS_PARENS,
FLAG_BROWSERHACK,
FLAG_HAS_NAMESPACE,
} from './arena'

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

const enumerable_properties = [
'name',
'namespace',
'property',
'value',
'unit',
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeSelector>
}

Expand Down Expand Up @@ -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<UniversalSelector>
}

Expand Down
51 changes: 33 additions & 18 deletions src/parse-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)', () => {
Expand All @@ -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)', () => {
Expand All @@ -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)', () => {
Expand All @@ -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)', () => {
Expand All @@ -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)', () => {
Expand All @@ -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', () => {
Expand All @@ -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)
})

Expand Down Expand Up @@ -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', () => {
Expand All @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
25 changes: 17 additions & 8 deletions src/parse-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
LANG_SELECTOR,
DIMENSION,
FLAG_HAS_PARENS,
FLAG_HAS_NAMESPACE,
} from './arena'
import {
TOKEN_IDENT,
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down