Skip to content

Commit 59865c1

Browse files
authored
fix: parsing of namespace selectors (#24)
1 parent e7ba334 commit 59865c1

File tree

2 files changed

+318
-5
lines changed

2 files changed

+318
-5
lines changed

src/parse-selector.test.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,4 +1739,241 @@ describe('parse_selector()', () => {
17391739
expect(result.has_children).toBe(true)
17401740
expect(result.children.length).toBeGreaterThan(0)
17411741
})
1742+
1743+
describe('Namespace selectors', () => {
1744+
test('should parse ns|* (namespace with universal selector)', () => {
1745+
const result = parse_selector('ns|*')
1746+
1747+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1748+
expect(result.text).toBe('ns|*')
1749+
1750+
const selector = result.first_child
1751+
expect(selector?.type).toBe(NODE_SELECTOR)
1752+
expect(selector?.text).toBe('ns|*')
1753+
1754+
const universal = selector?.first_child
1755+
expect(universal?.type).toBe(NODE_SELECTOR_UNIVERSAL)
1756+
expect(universal?.text).toBe('ns|*')
1757+
expect(universal?.name).toBe('ns')
1758+
})
1759+
1760+
test('should parse ns|div (namespace with type selector)', () => {
1761+
const result = parse_selector('ns|div')
1762+
1763+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1764+
expect(result.text).toBe('ns|div')
1765+
1766+
const selector = result.first_child
1767+
expect(selector?.type).toBe(NODE_SELECTOR)
1768+
1769+
const typeSelector = selector?.first_child
1770+
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
1771+
expect(typeSelector?.text).toBe('ns|div')
1772+
expect(typeSelector?.name).toBe('ns')
1773+
})
1774+
1775+
test('should parse *|* (any namespace with universal selector)', () => {
1776+
const result = parse_selector('*|*')
1777+
1778+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1779+
expect(result.text).toBe('*|*')
1780+
1781+
const selector = result.first_child
1782+
const universal = selector?.first_child
1783+
expect(universal?.type).toBe(NODE_SELECTOR_UNIVERSAL)
1784+
expect(universal?.text).toBe('*|*')
1785+
expect(universal?.name).toBe('*')
1786+
})
1787+
1788+
test('should parse *|div (any namespace with type selector)', () => {
1789+
const result = parse_selector('*|div')
1790+
1791+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1792+
expect(result.text).toBe('*|div')
1793+
1794+
const selector = result.first_child
1795+
const typeSelector = selector?.first_child
1796+
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
1797+
expect(typeSelector?.text).toBe('*|div')
1798+
expect(typeSelector?.name).toBe('*')
1799+
})
1800+
1801+
test('should parse |* (empty namespace with universal selector)', () => {
1802+
const result = parse_selector('|*')
1803+
1804+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1805+
expect(result.text).toBe('|*')
1806+
1807+
const selector = result.first_child
1808+
const universal = selector?.first_child
1809+
expect(universal?.type).toBe(NODE_SELECTOR_UNIVERSAL)
1810+
expect(universal?.text).toBe('|*')
1811+
// Empty namespace should result in empty name
1812+
expect(universal?.name).toBe('|')
1813+
})
1814+
1815+
test('should parse |div (empty namespace with type selector)', () => {
1816+
const result = parse_selector('|div')
1817+
1818+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1819+
expect(result.text).toBe('|div')
1820+
1821+
const selector = result.first_child
1822+
const typeSelector = selector?.first_child
1823+
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
1824+
expect(typeSelector?.text).toBe('|div')
1825+
// Empty namespace should result in empty name
1826+
expect(typeSelector?.name).toBe('|')
1827+
})
1828+
1829+
test('should parse namespace selector with class', () => {
1830+
const result = parse_selector('ns|div.class')
1831+
1832+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1833+
expect(result.text).toBe('ns|div.class')
1834+
1835+
const selector = result.first_child
1836+
const children = selector?.children || []
1837+
expect(children.length).toBe(2)
1838+
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
1839+
expect(children[0].text).toBe('ns|div')
1840+
expect(children[0].name).toBe('ns')
1841+
expect(children[1].type).toBe(NODE_SELECTOR_CLASS)
1842+
})
1843+
1844+
test('should parse namespace selector with ID', () => {
1845+
const result = parse_selector('ns|*#id')
1846+
1847+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1848+
expect(result.text).toBe('ns|*#id')
1849+
1850+
const selector = result.first_child
1851+
const children = selector?.children || []
1852+
expect(children.length).toBe(2)
1853+
expect(children[0].type).toBe(NODE_SELECTOR_UNIVERSAL)
1854+
expect(children[0].text).toBe('ns|*')
1855+
expect(children[1].type).toBe(NODE_SELECTOR_ID)
1856+
})
1857+
1858+
test('should parse namespace selector in complex selector', () => {
1859+
const result = parse_selector('ns|div > *|span')
1860+
1861+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1862+
expect(result.text).toBe('ns|div > *|span')
1863+
1864+
const selector = result.first_child
1865+
const children = selector?.children || []
1866+
expect(children.length).toBe(3) // div, >, span
1867+
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
1868+
expect(children[0].text).toBe('ns|div')
1869+
expect(children[1].type).toBe(NODE_SELECTOR_COMBINATOR)
1870+
expect(children[2].type).toBe(NODE_SELECTOR_TYPE)
1871+
expect(children[2].text).toBe('*|span')
1872+
})
1873+
1874+
test('should parse namespace selector in selector list', () => {
1875+
const result = parse_selector('ns|div, |span, *|p')
1876+
1877+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1878+
expect(result.text).toBe('ns|div, |span, *|p')
1879+
1880+
const selectors = result.children
1881+
expect(selectors.length).toBe(3)
1882+
1883+
const firstType = selectors[0].first_child
1884+
expect(firstType?.type).toBe(NODE_SELECTOR_TYPE)
1885+
expect(firstType?.text).toBe('ns|div')
1886+
expect(firstType?.name).toBe('ns')
1887+
1888+
const secondType = selectors[1].first_child
1889+
expect(secondType?.type).toBe(NODE_SELECTOR_TYPE)
1890+
expect(secondType?.text).toBe('|span')
1891+
expect(secondType?.name).toBe('|')
1892+
1893+
const thirdType = selectors[2].first_child
1894+
expect(thirdType?.type).toBe(NODE_SELECTOR_TYPE)
1895+
expect(thirdType?.text).toBe('*|p')
1896+
expect(thirdType?.name).toBe('*')
1897+
})
1898+
1899+
test('should parse namespace selector with attribute', () => {
1900+
const result = parse_selector('ns|div[attr="value"]')
1901+
1902+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1903+
expect(result.text).toBe('ns|div[attr="value"]')
1904+
1905+
const selector = result.first_child
1906+
const children = selector?.children || []
1907+
expect(children.length).toBe(2)
1908+
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
1909+
expect(children[0].name).toBe('ns')
1910+
expect(children[1].type).toBe(NODE_SELECTOR_ATTRIBUTE)
1911+
})
1912+
1913+
test('should parse namespace selector with pseudo-class', () => {
1914+
const result = parse_selector('ns|a:hover')
1915+
1916+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1917+
expect(result.text).toBe('ns|a:hover')
1918+
1919+
const selector = result.first_child
1920+
const children = selector?.children || []
1921+
expect(children.length).toBe(2)
1922+
expect(children[0].type).toBe(NODE_SELECTOR_TYPE)
1923+
expect(children[0].name).toBe('ns')
1924+
expect(children[1].type).toBe(NODE_SELECTOR_PSEUDO_CLASS)
1925+
})
1926+
1927+
test('should parse namespace with various identifiers', () => {
1928+
const result = parse_selector('svg|rect')
1929+
1930+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1931+
expect(result.text).toBe('svg|rect')
1932+
1933+
const selector = result.first_child
1934+
const typeSelector = selector?.first_child
1935+
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
1936+
expect(typeSelector?.text).toBe('svg|rect')
1937+
expect(typeSelector?.name).toBe('svg')
1938+
})
1939+
1940+
test('should parse long namespace identifier', () => {
1941+
const result = parse_selector('myNamespace|element')
1942+
1943+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1944+
expect(result.text).toBe('myNamespace|element')
1945+
1946+
const selector = result.first_child
1947+
const typeSelector = selector?.first_child
1948+
expect(typeSelector?.type).toBe(NODE_SELECTOR_TYPE)
1949+
expect(typeSelector?.name).toBe('myNamespace')
1950+
})
1951+
1952+
test('should handle namespace in nested pseudo-class', () => {
1953+
const result = parse_selector(':is(ns|div, *|span)')
1954+
1955+
expect(result.type).toBe(NODE_SELECTOR_LIST)
1956+
expect(result.text).toBe(':is(ns|div, *|span)')
1957+
1958+
const selector = result.first_child
1959+
const pseudo = selector?.first_child
1960+
expect(pseudo?.type).toBe(NODE_SELECTOR_PSEUDO_CLASS)
1961+
expect(pseudo?.name).toBe('is')
1962+
1963+
// The content should contain namespace selectors
1964+
const nestedList = pseudo?.first_child
1965+
expect(nestedList?.type).toBe(NODE_SELECTOR_LIST)
1966+
1967+
const nestedSelectors = nestedList?.children || []
1968+
expect(nestedSelectors.length).toBe(2)
1969+
1970+
const firstNestedType = nestedSelectors[0].first_child
1971+
expect(firstNestedType?.type).toBe(NODE_SELECTOR_TYPE)
1972+
expect(firstNestedType?.text).toBe('ns|div')
1973+
1974+
const secondNestedType = nestedSelectors[1].first_child
1975+
expect(secondNestedType?.type).toBe(NODE_SELECTOR_TYPE)
1976+
expect(secondNestedType?.text).toBe('*|span')
1977+
})
1978+
})
17421979
})

src/parse-selector.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,25 +296,29 @@ export class SelectorParser {
296296

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

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

306307
case TOKEN_DELIM:
307-
// Could be: . (class), * (universal), & (nesting)
308+
// Could be: . (class), * (universal), & (nesting), | (namespace)
308309
let ch = this.source.charCodeAt(start)
309310
if (ch === CHAR_PERIOD) {
310311
// . - class selector
311312
return this.parse_class_selector(start)
312313
} else if (ch === CHAR_ASTERISK) {
313-
// * - universal selector
314-
return this.create_node(NODE_SELECTOR_UNIVERSAL, start, end)
314+
// * - could be universal selector or namespace prefix (*|)
315+
return this.parse_universal_or_namespace_selector(start, end)
315316
} else if (ch === CHAR_AMPERSAND) {
316317
// & - nesting selector
317318
return this.create_node(NODE_SELECTOR_NESTING, start, end)
319+
} else if (ch === CHAR_PIPE) {
320+
// | - empty namespace prefix (|E or |*)
321+
return this.parse_empty_namespace_selector(start)
318322
}
319323
// Other delimiters signal end of selector
320324
return null
@@ -341,6 +345,78 @@ export class SelectorParser {
341345
}
342346
}
343347

348+
// Parse the local part after | in a namespace selector (E or *)
349+
// Returns the node type (TYPE or UNIVERSAL) or null if invalid
350+
private parse_namespace_local_part(
351+
selector_start: number,
352+
namespace_start: number,
353+
namespace_length: number,
354+
): number | null {
355+
const saved = this.lexer.save_position()
356+
this.lexer.next_token_fast(false)
357+
358+
let node_type: number
359+
if (this.lexer.token_type === TOKEN_IDENT) {
360+
// ns|type
361+
node_type = NODE_SELECTOR_TYPE
362+
} else if (
363+
this.lexer.token_type === TOKEN_DELIM &&
364+
this.source.charCodeAt(this.lexer.token_start) === CHAR_ASTERISK
365+
) {
366+
// ns|*
367+
node_type = NODE_SELECTOR_UNIVERSAL
368+
} else {
369+
// Invalid - restore position
370+
this.lexer.restore_position(saved)
371+
return null
372+
}
373+
374+
let node = this.create_node(node_type, selector_start, this.lexer.token_end)
375+
// Store namespace in content fields
376+
this.arena.set_content_start(node, namespace_start)
377+
this.arena.set_content_length(node, namespace_length)
378+
return node
379+
}
380+
381+
// Parse type selector or namespace selector (ns|E or ns|*)
382+
// Called when we've seen an IDENT token
383+
private parse_type_or_namespace_selector(start: number, end: number): number | null {
384+
// Check if followed by | (namespace separator)
385+
if (this.lexer.pos < this.selector_end && this.source.charCodeAt(this.lexer.pos) === CHAR_PIPE) {
386+
this.lexer.pos++ // skip |
387+
let node = this.parse_namespace_local_part(start, start, end - start)
388+
if (node !== null) return node
389+
// Invalid - restore and treat as regular type selector
390+
this.lexer.pos = end
391+
}
392+
393+
// Regular type selector (no namespace)
394+
return this.create_node(NODE_SELECTOR_TYPE, start, end)
395+
}
396+
397+
// Parse universal selector or namespace selector (*|E or *|*)
398+
// Called when we've seen a * DELIM token
399+
private parse_universal_or_namespace_selector(start: number, end: number): number | null {
400+
// Check if followed by | (any-namespace prefix)
401+
if (this.lexer.pos < this.selector_end && this.source.charCodeAt(this.lexer.pos) === CHAR_PIPE) {
402+
this.lexer.pos++ // skip |
403+
let node = this.parse_namespace_local_part(start, start, end - start)
404+
if (node !== null) return node
405+
// Invalid - restore and treat as regular universal selector
406+
this.lexer.pos = end
407+
}
408+
409+
// Regular universal selector (no namespace)
410+
return this.create_node(NODE_SELECTOR_UNIVERSAL, start, end)
411+
}
412+
413+
// Parse empty namespace selector (|E or |*)
414+
// Called when we've seen a | DELIM token at the start
415+
private parse_empty_namespace_selector(start: number): number | null {
416+
// The | character is the namespace indicator (length = 1)
417+
return this.parse_namespace_local_part(start, start, 1)
418+
}
419+
344420
// Parse combinator (>, +, ~, or descendant space)
345421
private try_parse_combinator(): number | null {
346422
let whitespace_start = this.lexer.pos

0 commit comments

Comments
 (0)