From 2931bf6350d9b622415eb1cbc7e5b61893ca274b Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 12 Apr 2026 21:04:50 +0200 Subject: [PATCH 1/2] fix: only return name/property/etc for relevant node types --- package-lock.json | 10 ----- src/css-node.ts | 104 +++++++++++++++++++--------------------------- src/node-types.ts | 1 - 3 files changed, 43 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9380375..fc4b0b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -917,16 +917,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", diff --git a/src/css-node.ts b/src/css-node.ts index fd4f721..90dabc3 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -210,6 +210,23 @@ export type PlainCSSNode = { end?: number } +const nodes_with_name = new Set([ + AT_RULE, + IDENTIFIER, + FUNCTION, + TYPE_SELECTOR, + CLASS_SELECTOR, + ID_SELECTOR, + ATTRIBUTE_SELECTOR, + PSEUDO_CLASS_SELECTOR, + PSEUDO_ELEMENT_SELECTOR, + COMBINATOR, + UNIVERSAL_SELECTOR, + LANG_SELECTOR, + LAYER_NAME, + FEATURE_RANGE, +]) + const nodes_with_children = new Set([ STYLESHEET, SELECTOR, @@ -227,6 +244,22 @@ const nodes_with_children = new Set([ FEATURE_RANGE, ]) +const enumerable_properties = [ + 'name', + 'property', + 'value', + 'unit', + 'attr_operator', + 'attr_flags', + 'nth_a', + 'nth_b', + 'selector', + 'is_browserhack', + 'is_vendor_prefixed', + 'has_error', + 'is_important', +] as const + export class CSSNode { private arena: CSSDataArena private source: string @@ -272,9 +305,7 @@ export class CSSNode { /** Get the "content" text (at-rule name for at-rules, layer name for import layers) */ get name(): string | undefined { - let { type } = this - if (type === DECLARATION || type === OPERATOR || type === SELECTOR || type === MEDIA_FEATURE) - return + if (!nodes_with_name.has(this.type)) return return this.get_content() } @@ -495,7 +526,9 @@ export class CSSNode { } /** Check if this style rule has declarations */ - get has_declarations(): boolean { + get has_declarations(): boolean | undefined { + let { type } = this + if (type !== AT_RULE && type !== STYLE_RULE) return undefined return this.arena.has_flag(this.index, FLAG_HAS_DECLARATIONS) } @@ -717,71 +750,21 @@ export class CSSNode { */ clone(options: CloneOptions = {}): PlainCSSNode { const { deep = true, locations = false } = options - let { type, name, property, value, unit } = this + let { type } = this - // 1. Create plain object with base properties let plain: any = { - type: type, + type, type_name: this.type_name, text: this.text, } - // 2. Extract type-specific properties (only if meaningful) - if (name) { - plain.name = name - } - if (property) { - plain.property = property - } - - // 3. Handle value types - if (value) { - plain.value = value - - if (unit) { - plain.unit = unit - } - } - - // 4. At-rule preludes are now child nodes (AT_RULE_PRELUDE wrapper) - // They will be cloned as part of children in deep clones - // No special extraction needed - breaking change from string to CSSNode - - // 5. Extract flags - if (type === DECLARATION) { - let { is_important, is_browserhack } = this - if (is_important) { - plain.is_important = true - } - if (is_browserhack) { - plain.is_browserhack = true + for (let key of enumerable_properties) { + let val = this[key] + if (val !== undefined && val !== false) { + plain[key] = val } } - let { is_vendor_prefixed, has_error } = this - - if (is_vendor_prefixed) { - plain.is_vendor_prefixed = true - } - - if (has_error) { - plain.has_error = true - } - - // 6. Extract selector-specific properties - if (type === ATTRIBUTE_SELECTOR) { - plain.attr_operator = this.attr_operator - plain.attr_flags = this.attr_flags - } - if (type === NTH_SELECTOR || type === NTH_OF_SELECTOR) { - plain.nth_a = this.nth_a - plain.nth_b = this.nth_b - } - if (type === NTH_OF_SELECTOR) { - plain.selector = this.selector - } - - // 7. Include location if requested if (locations) { plain.line = this.line plain.column = this.column @@ -790,7 +773,6 @@ export class CSSNode { plain.end = this.end } - // 8. Deep clone children - just push to array! if (deep && nodes_with_children.has(type)) { plain.children = [] plain.child_count = this.child_count diff --git a/src/node-types.ts b/src/node-types.ts index 75c4798..adc1e5e 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -148,7 +148,6 @@ export type StyleSheet = CSSNode & export type Rule = CSSNode & { readonly type: typeof STYLE_RULE - readonly name: string readonly has_declarations: boolean clone(options?: CloneOptions): ToPlain } & From baba8941708595daab6f299fa50018fd21b950af Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 12 Apr 2026 21:17:24 +0200 Subject: [PATCH 2/2] fix: retun `null` when applicable --- src/css-node.ts | 15 +++++++++------ src/parse-selector.test.ts | 31 +++++++++++++++++-------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/css-node.ts b/src/css-node.ts index 90dabc3..638d4ae 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -304,9 +304,12 @@ export class CSSNode { } /** Get the "content" text (at-rule name for at-rules, layer name for import layers) */ - get name(): string | undefined { + get name(): string | null | undefined { if (!nodes_with_name.has(this.type)) return - return this.get_content() + let content = this.get_content() + let { type } = this + if ((type === UNIVERSAL_SELECTOR || type === LANG_SELECTOR) && content === '') return null + return content } /** @@ -681,23 +684,23 @@ export class CSSNode { // --- An+B Expression Helpers (for NODE_SELECTOR_NTH) --- /** Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") */ - get nth_a(): string | undefined { + get nth_a(): string | null | undefined { let { type, arena, index } = this if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined let len = arena.get_content_length(index) - if (len === 0) return undefined + if (len === 0) return null let start = arena.get_content_start(index) return this.source.substring(start, start + len) } /** Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") */ - get nth_b(): string | undefined { + get nth_b(): string | null | undefined { let { type, arena, index, source } = this if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined let len = arena.get_value_length(index) - if (len === 0) return undefined + if (len === 0) return null let start = arena.get_value_start(index) let value = source.substring(start, start + len) diff --git a/src/parse-selector.test.ts b/src/parse-selector.test.ts index 277ac16..4a53a82 100644 --- a/src/parse-selector.test.ts +++ b/src/parse-selector.test.ts @@ -5,6 +5,7 @@ import type { AttributeSelector, ClassSelector, IdSelector, + LangSelector, NthSelector, PseudoClassSelector, PseudoElementSelector, @@ -297,9 +298,10 @@ describe('Selector Nodes', () => { const node = parse_selector(':lang(en)') const selector = node.first_child! as Selector const pseudoClass = selector.first_child! - const langNode = pseudoClass.first_child! + const langNode = pseudoClass.first_child! as LangSelector expect(langNode.type).toBe(LANG_SELECTOR) expect(langNode.length).toBeGreaterThan(0) + expect(langNode.name).toBe('en') }) }) }) @@ -468,8 +470,9 @@ describe('Selector Nodes', () => { test('UNIVERSAL_SELECTOR type_name', () => { const node = parse_selector('*') const selector = node.first_child! as Selector - const universalSelector = selector.first_child! + const universalSelector = selector.first_child! as UniversalSelector expect(universalSelector.type_name).toBe('UniversalSelector') + expect(universalSelector.name).toBe('*') }) test('NESTING_SELECTOR type_name', () => { @@ -1742,7 +1745,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.type).toBe(NTH_SELECTOR) - expect(nthNode.nth_a).toBeUndefined() + expect(nthNode.nth_a).toBeNull() expect(nthNode.nth_b).toBe('3') }) @@ -1750,7 +1753,7 @@ describe('Selector Nodes', () => { const root = parse_selector(':nth-child(-5)') const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector - expect(nthNode.nth_a).toBeUndefined() + expect(nthNode.nth_a).toBeNull() expect(nthNode.nth_b).toBe('-5') }) @@ -1758,7 +1761,7 @@ describe('Selector Nodes', () => { const root = parse_selector(':nth-child(0)') const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector - expect(nthNode.nth_a).toBeUndefined() + expect(nthNode.nth_a).toBeNull() expect(nthNode.nth_b).toBe('0') }) }) @@ -1769,7 +1772,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('odd') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) test('even keyword', () => { @@ -1777,7 +1780,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('even') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) }) @@ -1787,7 +1790,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('n') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) test('+n', () => { @@ -1795,7 +1798,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('+n') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) test('-n', () => { @@ -1803,7 +1806,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('-n') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) }) @@ -1813,7 +1816,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('2n') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) test('-3n', () => { @@ -1821,7 +1824,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('-3n') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) test('+5n', () => { @@ -1829,7 +1832,7 @@ describe('Selector Nodes', () => { const pseudoClass = root.first_child!.first_child! const nthNode = pseudoClass.first_child! as NthSelector expect(nthNode.nth_a).toBe('+5n') - expect(nthNode.nth_b).toBeUndefined() + expect(nthNode.nth_b).toBeNull() }) }) @@ -1916,7 +1919,7 @@ describe('Selector Nodes', () => { const anplusb = nthOfNode.first_child! as NthSelector expect(anplusb.type).toBe(NTH_SELECTOR) expect(anplusb.nth_a).toBe('2n') - expect(anplusb.nth_b).toBeUndefined() + expect(anplusb.nth_b).toBeNull() // Second child is the selector list const selectorList = (nthOfNode as unknown as WithChildren).children[1]