From 150c76253a0e31eba83900b55a050cc306c9936d Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 7 Dec 2025 22:07:49 +0100 Subject: [PATCH] feat: add `type_name` to cssnode for easier debugging --- API.md | 36 +++++++ src/css-node.test.ts | 227 +++++++++++++++++++++++++++++++++++++++++++ src/css-node.ts | 48 +++++++++ src/index.ts | 2 +- 4 files changed, 312 insertions(+), 1 deletion(-) diff --git a/API.md b/API.md index acf03e0..015b385 100644 --- a/API.md +++ b/API.md @@ -32,6 +32,7 @@ function parse(source: string, options?: ParserOptions): CSSNode `CSSNode` - Root stylesheet node with the following properties: - `type` - Node type constant (e.g., `NODE_STYLESHEET`, `NODE_STYLE_RULE`) +- `type_name` - Human-readable type name (e.g., `'stylesheet'`, `'style_rule'`) - `text` - Full text of the node from source - `name` - Property name, at-rule name, or layer name - `property` - Alias for `name` (for declarations) @@ -259,6 +260,41 @@ console.log(nestedRule.type) // NODE_STYLE_RULE console.log(nestedRule.block.is_empty) // false ``` +### Example 8: Using type_name for Debugging + +The `type_name` property provides human-readable type names for easier debugging: + +```typescript +import { parse, TYPE_NAMES } from '@projectwallace/css-parser' + +const ast = parse('.foo { color: red; }') + +// Using type_name directly on nodes +for (let node of ast) { + console.log(`${node.type_name}: ${node.text}`) +} +// Output: +// style_rule: .foo { color: red; } +// selector_list: .foo +// selector_class: .foo +// block: color: red +// declaration: color: red +// value_keyword: red + +// Useful for logging and error messages +const rule = ast.first_child +console.log(`Processing ${rule.type_name}`) // "Processing style_rule" + +// TYPE_NAMES export for custom type checking +import { NODE_DECLARATION } from '@projectwallace/css-parser' +console.log(TYPE_NAMES[NODE_DECLARATION]) // 'declaration' + +// Compare strings instead of numeric constants +if (node.type_name === 'declaration') { + console.log(`Property: ${node.property}, Value: ${node.value}`) +} +``` + --- ## `parse_selector(source)` diff --git a/src/css-node.test.ts b/src/css-node.test.ts index 49ac462..7cdf9c5 100644 --- a/src/css-node.test.ts +++ b/src/css-node.test.ts @@ -391,4 +391,231 @@ describe('CSSNode', () => { expect(media.has_declarations).toBe(false) }) }) + + describe('type_name property', () => { + test('should return stylesheet for root node', () => { + const source = 'body { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + + expect(root.type_name).toBe('stylesheet') + }) + + test('should return style_rule for style rules', () => { + const source = 'body { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + + expect(rule.type_name).toBe('rule') + }) + + test('should return declaration for declarations', () => { + const source = 'body { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + + expect(decl.type_name).toBe('declaration') + }) + + test('should return at_rule for at-rules', () => { + const source = '@media screen { body { color: red; } }' + const parser = new Parser(source) + const root = parser.parse() + const media = root.first_child! + + expect(media.type_name).toBe('atrule') + }) + + test('should return selector_list for selector lists', () => { + const source = 'body { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + + expect(selectorList.type_name).toBe('selectorlist') + }) + + test('should return selector_type for type selectors', () => { + const source = 'div { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + + expect(typeSelector.type_name).toBe('type-selector') + }) + + test('should return selector_class for class selectors', () => { + const source = '.foo { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const classSelector = selector.first_child! + + expect(classSelector.type_name).toBe('class-selector') + }) + + test('should return selector_id for ID selectors', () => { + const source = '#bar { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const idSelector = selector.first_child! + + expect(idSelector.type_name).toBe('id-selector') + }) + + test('should return selector_universal for universal selectors', () => { + const source = '* { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const universalSelector = selector.first_child! + + expect(universalSelector.type_name).toBe('universal-selector') + }) + + test('should return selector_attribute for attribute selectors', () => { + const source = '[href] { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const attrSelector = selector.first_child! + + expect(attrSelector.type_name).toBe('attribute-selector') + }) + + test('should return selector_pseudo_class for pseudo-class selectors', () => { + const source = ':hover { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const pseudoClass = selector.first_child! + + expect(pseudoClass.type_name).toBe('pseudoclass-selector') + }) + + test('should return selector_pseudo_element for pseudo-element selectors', () => { + const source = '::before { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const pseudoElement = selector.first_child! + + expect(pseudoElement.type_name).toBe('pseudoelement-selector') + }) + + test('should return selector_combinator for combinators', () => { + const source = 'div > span { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const combinator = selector.first_child!.next_sibling! + + expect(combinator.type_name).toBe('selector-combinator') + }) + + test('should return value_keyword for keyword values', () => { + const source = 'body { color: red; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + const value = decl.first_child! + + expect(value.type_name).toBe('keyword') + }) + + test('should return value_number for numeric values', () => { + const source = 'body { opacity: 0.5; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + const value = decl.first_child! + + expect(value.type_name).toBe('number') + }) + + test('should return value_dimension for dimension values', () => { + const source = 'body { width: 100px; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + const value = decl.first_child! + + expect(value.type_name).toBe('dimension') + }) + + test('should return value_string for string values', () => { + const source = 'body { content: "hello"; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + const value = decl.first_child! + + expect(value.type_name).toBe('string') + }) + + test('should return value_color for color values', () => { + const source = 'body { color: #ff0000; }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + const value = decl.first_child! + + expect(value.type_name).toBe('color') + }) + + test('should return value_function for function values', () => { + const source = 'body { width: calc(100% - 20px); }' + const parser = new Parser(source) + const root = parser.parse() + const rule = root.first_child! + const block = rule.block! + const decl = block.first_child! + const value = decl.first_child! + + expect(value.type_name).toBe('function') + }) + + test('should return prelude_media_query for media query preludes', () => { + const source = '@media screen and (min-width: 768px) { body { color: red; } }' + const parser = new Parser(source) + const root = parser.parse() + const media = root.first_child! + const prelude = media.first_child! + + expect(prelude.type_name).toBe('media-query') + }) + }) }) diff --git a/src/css-node.ts b/src/css-node.ts index 5649454..8ff6abc 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -51,6 +51,49 @@ import { import { CHAR_MINUS_HYPHEN, CHAR_PLUS, is_whitespace } from './string-utils' import { parse_dimension } from './parse-utils' +// Type name lookup table - maps numeric type to human-readable string +export const TYPE_NAMES: Record = { + [NODE_STYLESHEET]: 'stylesheet', + [NODE_STYLE_RULE]: 'rule', + [NODE_AT_RULE]: 'atrule', + [NODE_DECLARATION]: 'declaration', + [NODE_SELECTOR]: 'selector', + [NODE_COMMENT]: 'comment', + [NODE_BLOCK]: 'block', + [NODE_VALUE_KEYWORD]: 'keyword', + [NODE_VALUE_NUMBER]: 'number', + [NODE_VALUE_DIMENSION]: 'dimension', + [NODE_VALUE_STRING]: 'string', + [NODE_VALUE_COLOR]: 'color', + [NODE_VALUE_FUNCTION]: 'function', + [NODE_VALUE_OPERATOR]: 'operator', + [NODE_VALUE_PARENTHESIS]: 'parenthesis', + [NODE_SELECTOR_LIST]: 'selectorlist', + [NODE_SELECTOR_TYPE]: 'type-selector', + [NODE_SELECTOR_CLASS]: 'class-selector', + [NODE_SELECTOR_ID]: 'id-selector', + [NODE_SELECTOR_ATTRIBUTE]: 'attribute-selector', + [NODE_SELECTOR_PSEUDO_CLASS]: 'pseudoclass-selector', + [NODE_SELECTOR_PSEUDO_ELEMENT]: 'pseudoelement-selector', + [NODE_SELECTOR_COMBINATOR]: 'selector-combinator', + [NODE_SELECTOR_UNIVERSAL]: 'universal-selector', + [NODE_SELECTOR_NESTING]: 'nesting-selector', + [NODE_SELECTOR_NTH]: 'nth-selector', + [NODE_SELECTOR_NTH_OF]: 'nth-of-selector', + [NODE_SELECTOR_LANG]: 'lang-selector', + [NODE_PRELUDE_MEDIA_QUERY]: 'media-query', + [NODE_PRELUDE_MEDIA_FEATURE]: 'media-feature', + [NODE_PRELUDE_MEDIA_TYPE]: 'media-type', + [NODE_PRELUDE_CONTAINER_QUERY]: 'container-query', + [NODE_PRELUDE_SUPPORTS_QUERY]: 'supports-query', + [NODE_PRELUDE_LAYER_NAME]: 'layer-name', + [NODE_PRELUDE_IDENTIFIER]: 'identifier', + [NODE_PRELUDE_OPERATOR]: 'operator', + [NODE_PRELUDE_IMPORT_URL]: 'import-url', + [NODE_PRELUDE_IMPORT_LAYER]: 'import-layer', + [NODE_PRELUDE_IMPORT_SUPPORTS]: 'import-supports', +} as const + // Node type constants (numeric for performance) export type CSSNodeType = | typeof NODE_STYLESHEET @@ -114,6 +157,11 @@ export class CSSNode { return this.arena.get_type(this.index) as CSSNodeType } + // Get node type as human-readable string + get type_name(): string { + return TYPE_NAMES[this.type] || 'unknown' + } + // Get the full text of this node from source get text(): string { let start = this.arena.get_start_offset(this.index) diff --git a/src/index.ts b/src/index.ts index 32184eb..3d1f530 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ export { walk, traverse } from './walk' export { type ParserOptions } from './parse' // Types -export { CSSNode, type CSSNodeType } from './css-node' +export { CSSNode, type CSSNodeType, TYPE_NAMES } from './css-node' export type { LexerPosition } from './lexer' export {