From 452f7d24632caf18e8cf3b98086eac7062200cb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 07:14:56 +0000 Subject: [PATCH 1/2] perf: use switch for CSSNode type_name instead of object lookup Co-Authored-By: Claude Sonnet 5.5 --- src/css-node.test.ts | 20 +++++++++ src/css-node.ts | 99 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/css-node.test.ts diff --git a/src/css-node.test.ts b/src/css-node.test.ts new file mode 100644 index 0000000..77dc210 --- /dev/null +++ b/src/css-node.test.ts @@ -0,0 +1,20 @@ +import { describe, test, expect } from 'vitest' +import { parse } from './parse' +import { TYPE_NAMES } from './css-node' + +describe('CSSNode type_name', () => { + test('matches TYPE_NAMES for every known type', () => { + const node = parse('a { color: red }') + const proto = Object.getPrototypeOf(node) + const getter = Object.getOwnPropertyDescriptor(proto, 'type_name')!.get! + for (const [type, name] of Object.entries(TYPE_NAMES)) { + expect(getter.call({ type: Number(type) })).toBe(name) + } + }) + + test('returns unknown for an unknown type', () => { + const node = parse('a { color: red }') + const getter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), 'type_name')!.get! + expect(getter.call({ type: 9999 })).toBe('unknown') + }) +}) diff --git a/src/css-node.ts b/src/css-node.ts index 6bb5a17..8d5b524 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -327,7 +327,104 @@ export class CSSNode { /** Get node type as human-readable string */ get type_name(): TypeName { - return TYPE_NAMES[this.type] || 'unknown' + // A switch compiles to a jump table, which is faster than an object lookup + // NOTE: keep in sync with TYPE_NAMES (enforced by a test) + switch (this.type) { + case STYLESHEET: + return 'StyleSheet' + case STYLE_RULE: + return 'Rule' + case AT_RULE: + return 'Atrule' + case DECLARATION: + return 'Declaration' + case SELECTOR: + return 'Selector' + case COMMENT: + return 'Comment' + case BLOCK: + return 'Block' + case RAW: + return 'Raw' + case IDENTIFIER: + return 'Identifier' + case NUMBER: + return 'Number' + case DIMENSION: + return 'Dimension' + case STRING: + return 'String' + case HASH: + return 'Hash' + case FUNCTION: + return 'Function' + case OPERATOR: + return 'Operator' + case PARENTHESIS: + return 'Parentheses' + case URL: + return 'Url' + case UNICODE_RANGE: + return 'UnicodeRange' + case VALUE: + return 'Value' + case SELECTOR_LIST: + return 'SelectorList' + case TYPE_SELECTOR: + return 'TypeSelector' + case CLASS_SELECTOR: + return 'ClassSelector' + case ID_SELECTOR: + return 'IdSelector' + case ATTRIBUTE_SELECTOR: + return 'AttributeSelector' + case PSEUDO_CLASS_SELECTOR: + return 'PseudoClassSelector' + case PSEUDO_ELEMENT_SELECTOR: + return 'PseudoElementSelector' + case COMBINATOR: + return 'Combinator' + case UNIVERSAL_SELECTOR: + return 'UniversalSelector' + case NESTING_SELECTOR: + return 'NestingSelector' + case NTH_SELECTOR: + return 'Nth' + case NTH_OF_SELECTOR: + return 'NthOf' + case LANG_SELECTOR: + return 'Lang' + case MEDIA_QUERY: + return 'MediaQuery' + case MEDIA_FEATURE: + return 'Feature' + case MEDIA_TYPE: + return 'MediaType' + case CONTAINER_QUERY: + return 'ContainerQuery' + case SUPPORTS_QUERY: + return 'SupportsQuery' + case SUPPORTS_DECLARATION: + return 'SupportsDeclaration' + case LAYER_NAME: + return 'Layer' + case PRELUDE_OPERATOR: + return 'Operator' + case FEATURE_RANGE: + return 'MediaFeatureRange' + case AT_RULE_PRELUDE: + return 'AtrulePrelude' + case PRELUDE_SELECTORLIST: + return 'PreludeSelectorList' + case RATIO: + return 'Ratio' + case IF_BRANCH: + return 'IfBranch' + case IF_CONDITION: + return 'IfCondition' + default: + return 'unknown' + } } /** Get the full text of this node from source */ From d2eb86c6c34d1afd10bb86d977455e0c0b4012b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 07:17:45 +0000 Subject: [PATCH 2/2] feat!: remove TYPE_NAMES export type_name is now backed solely by a switch; use node.type_name instead of TYPE_NAMES[node.type]. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 6 +-- src/css-node.test.ts | 20 --------- src/css-node.ts | 100 +++++++++++++++++++++---------------------- src/index.ts | 2 +- 4 files changed, 52 insertions(+), 76 deletions(-) delete mode 100644 src/css-node.test.ts diff --git a/README.md b/README.md index 1c36c3a..97ce78c 100644 --- a/README.md +++ b/README.md @@ -215,14 +215,14 @@ for (const token of tokenize('a { color: red }')) { ### Node types, type guards and `clone()` -Node type constants (`STYLE_RULE`, `DECLARATION`, `DIMENSION`, …), `TYPE_NAMES` and `NODE_TYPES` are exported from the root, as are type guards such as `is_declaration()` and `is_dimension()` that narrow a node to its specific type. `node.clone({ deep?, locations? })` returns a mutable plain-object copy of a node. +Node type constants (`STYLE_RULE`, `DECLARATION`, `DIMENSION`, …) and `NODE_TYPES` are exported from the root, as are type guards such as `is_declaration()` and `is_dimension()` that narrow a node to its specific type. `node.clone({ deep?, locations? })` returns a mutable plain-object copy of a node. ```typescript -import { parse, is_declaration, TYPE_NAMES, DECLARATION } from '@projectwallace/css-parser' +import { parse, is_declaration } from '@projectwallace/css-parser' const decl = parse('a { color: red }').first_child.block.first_child is_declaration(decl) // true -TYPE_NAMES[DECLARATION] // "Declaration" +decl.type_name // "Declaration" decl.clone({ deep: false }).property // "color" ``` diff --git a/src/css-node.test.ts b/src/css-node.test.ts deleted file mode 100644 index 77dc210..0000000 --- a/src/css-node.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, test, expect } from 'vitest' -import { parse } from './parse' -import { TYPE_NAMES } from './css-node' - -describe('CSSNode type_name', () => { - test('matches TYPE_NAMES for every known type', () => { - const node = parse('a { color: red }') - const proto = Object.getPrototypeOf(node) - const getter = Object.getOwnPropertyDescriptor(proto, 'type_name')!.get! - for (const [type, name] of Object.entries(TYPE_NAMES)) { - expect(getter.call({ type: Number(type) })).toBe(name) - } - }) - - test('returns unknown for an unknown type', () => { - const node = parse('a { color: red }') - const getter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), 'type_name')!.get! - expect(getter.call({ type: 9999 })).toBe('unknown') - }) -}) diff --git a/src/css-node.ts b/src/css-node.ts index 8d5b524..4f634c3 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -72,57 +72,54 @@ import { } from './string-utils' import { dimension_number_end } from './parse-dimension' -// Type name lookup table - maps numeric type to CSSTree-compatible strings -export const TYPE_NAMES = { - [STYLESHEET]: 'StyleSheet', - [STYLE_RULE]: 'Rule', - [AT_RULE]: 'Atrule', - [DECLARATION]: 'Declaration', - [SELECTOR]: 'Selector', - [COMMENT]: 'Comment', - [BLOCK]: 'Block', - [RAW]: 'Raw', - [IDENTIFIER]: 'Identifier', - [NUMBER]: 'Number', - [DIMENSION]: 'Dimension', - [STRING]: 'String', - [HASH]: 'Hash', - [FUNCTION]: 'Function', - [OPERATOR]: 'Operator', - [PARENTHESIS]: 'Parentheses', - [URL]: 'Url', - [UNICODE_RANGE]: 'UnicodeRange', - [VALUE]: 'Value', - [SELECTOR_LIST]: 'SelectorList', - [TYPE_SELECTOR]: 'TypeSelector', - [CLASS_SELECTOR]: 'ClassSelector', - [ID_SELECTOR]: 'IdSelector', - [ATTRIBUTE_SELECTOR]: 'AttributeSelector', - [PSEUDO_CLASS_SELECTOR]: 'PseudoClassSelector', - [PSEUDO_ELEMENT_SELECTOR]: 'PseudoElementSelector', - [COMBINATOR]: 'Combinator', - [UNIVERSAL_SELECTOR]: 'UniversalSelector', - [NESTING_SELECTOR]: 'NestingSelector', - [NTH_SELECTOR]: 'Nth', - [NTH_OF_SELECTOR]: 'NthOf', - [LANG_SELECTOR]: 'Lang', - [MEDIA_QUERY]: 'MediaQuery', - [MEDIA_FEATURE]: 'Feature', - [MEDIA_TYPE]: 'MediaType', - [CONTAINER_QUERY]: 'ContainerQuery', - [SUPPORTS_QUERY]: 'SupportsQuery', - [SUPPORTS_DECLARATION]: 'SupportsDeclaration', - [LAYER_NAME]: 'Layer', - [PRELUDE_OPERATOR]: 'Operator', - [FEATURE_RANGE]: 'MediaFeatureRange', - [AT_RULE_PRELUDE]: 'AtrulePrelude', - [PRELUDE_SELECTORLIST]: 'PreludeSelectorList', - [RATIO]: 'Ratio', - [IF_BRANCH]: 'IfBranch', - [IF_CONDITION]: 'IfCondition', -} as const - -export type TypeName = (typeof TYPE_NAMES)[keyof typeof TYPE_NAMES] | 'unknown' +// CSSTree-compatible node type names +export type TypeName = + | 'StyleSheet' + | 'Rule' + | 'Atrule' + | 'Declaration' + | 'Selector' + | 'Comment' + | 'Block' + | 'Raw' + | 'Identifier' + | 'Number' + | 'Dimension' + | 'String' + | 'Hash' + | 'Function' + | 'Operator' + | 'Parentheses' + | 'Url' + | 'UnicodeRange' + | 'Value' + | 'SelectorList' + | 'TypeSelector' + | 'ClassSelector' + | 'IdSelector' + | 'AttributeSelector' + | 'PseudoClassSelector' + | 'PseudoElementSelector' + | 'Combinator' + | 'UniversalSelector' + | 'NestingSelector' + | 'Nth' + | 'NthOf' + | 'Lang' + | 'MediaQuery' + | 'Feature' + | 'MediaType' + | 'ContainerQuery' + | 'SupportsQuery' + | 'SupportsDeclaration' + | 'Layer' + | 'MediaFeatureRange' + | 'AtrulePrelude' + | 'PreludeSelectorList' + | 'Ratio' + | 'IfBranch' + | 'IfCondition' + | 'unknown' // Node type constants (numeric for performance) export type CSSNodeType = @@ -328,7 +325,6 @@ export class CSSNode { /** Get node type as human-readable string */ get type_name(): TypeName { // A switch compiles to a jump table, which is faster than an object lookup - // NOTE: keep in sync with TYPE_NAMES (enforced by a test) switch (this.type) { case STYLESHEET: return 'StyleSheet' diff --git a/src/index.ts b/src/index.ts index 015420f..190bc6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export { export { type ParserOptions } from './parse' // Types -export { TYPE_NAMES, type CloneOptions, type PlainCSSNode } from './css-node' +export { type CloneOptions, type PlainCSSNode } from './css-node' export type { LexerPosition, CommentInfo } from './tokenize' // Constants - imported from dedicated constants file