Skip to content

Commit f8a6517

Browse files
authored
feat: add type_name to cssnode for easier debugging (#26)
1 parent 711f964 commit f8a6517

File tree

4 files changed

+312
-1
lines changed

4 files changed

+312
-1
lines changed

API.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function parse(source: string, options?: ParserOptions): CSSNode
3232
`CSSNode` - Root stylesheet node with the following properties:
3333

3434
- `type` - Node type constant (e.g., `NODE_STYLESHEET`, `NODE_STYLE_RULE`)
35+
- `type_name` - Human-readable type name (e.g., `'stylesheet'`, `'style_rule'`)
3536
- `text` - Full text of the node from source
3637
- `name` - Property name, at-rule name, or layer name
3738
- `property` - Alias for `name` (for declarations)
@@ -259,6 +260,41 @@ console.log(nestedRule.type) // NODE_STYLE_RULE
259260
console.log(nestedRule.block.is_empty) // false
260261
```
261262

263+
### Example 8: Using type_name for Debugging
264+
265+
The `type_name` property provides human-readable type names for easier debugging:
266+
267+
```typescript
268+
import { parse, TYPE_NAMES } from '@projectwallace/css-parser'
269+
270+
const ast = parse('.foo { color: red; }')
271+
272+
// Using type_name directly on nodes
273+
for (let node of ast) {
274+
console.log(`${node.type_name}: ${node.text}`)
275+
}
276+
// Output:
277+
// style_rule: .foo { color: red; }
278+
// selector_list: .foo
279+
// selector_class: .foo
280+
// block: color: red
281+
// declaration: color: red
282+
// value_keyword: red
283+
284+
// Useful for logging and error messages
285+
const rule = ast.first_child
286+
console.log(`Processing ${rule.type_name}`) // "Processing style_rule"
287+
288+
// TYPE_NAMES export for custom type checking
289+
import { NODE_DECLARATION } from '@projectwallace/css-parser'
290+
console.log(TYPE_NAMES[NODE_DECLARATION]) // 'declaration'
291+
292+
// Compare strings instead of numeric constants
293+
if (node.type_name === 'declaration') {
294+
console.log(`Property: ${node.property}, Value: ${node.value}`)
295+
}
296+
```
297+
262298
---
263299

264300
## `parse_selector(source)`

src/css-node.test.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,231 @@ describe('CSSNode', () => {
391391
expect(media.has_declarations).toBe(false)
392392
})
393393
})
394+
395+
describe('type_name property', () => {
396+
test('should return stylesheet for root node', () => {
397+
const source = 'body { color: red; }'
398+
const parser = new Parser(source)
399+
const root = parser.parse()
400+
401+
expect(root.type_name).toBe('stylesheet')
402+
})
403+
404+
test('should return style_rule for style rules', () => {
405+
const source = 'body { color: red; }'
406+
const parser = new Parser(source)
407+
const root = parser.parse()
408+
const rule = root.first_child!
409+
410+
expect(rule.type_name).toBe('rule')
411+
})
412+
413+
test('should return declaration for declarations', () => {
414+
const source = 'body { color: red; }'
415+
const parser = new Parser(source)
416+
const root = parser.parse()
417+
const rule = root.first_child!
418+
const block = rule.block!
419+
const decl = block.first_child!
420+
421+
expect(decl.type_name).toBe('declaration')
422+
})
423+
424+
test('should return at_rule for at-rules', () => {
425+
const source = '@media screen { body { color: red; } }'
426+
const parser = new Parser(source)
427+
const root = parser.parse()
428+
const media = root.first_child!
429+
430+
expect(media.type_name).toBe('atrule')
431+
})
432+
433+
test('should return selector_list for selector lists', () => {
434+
const source = 'body { color: red; }'
435+
const parser = new Parser(source)
436+
const root = parser.parse()
437+
const rule = root.first_child!
438+
const selectorList = rule.first_child!
439+
440+
expect(selectorList.type_name).toBe('selectorlist')
441+
})
442+
443+
test('should return selector_type for type selectors', () => {
444+
const source = 'div { color: red; }'
445+
const parser = new Parser(source)
446+
const root = parser.parse()
447+
const rule = root.first_child!
448+
const selectorList = rule.first_child!
449+
const selector = selectorList.first_child!
450+
const typeSelector = selector.first_child!
451+
452+
expect(typeSelector.type_name).toBe('type-selector')
453+
})
454+
455+
test('should return selector_class for class selectors', () => {
456+
const source = '.foo { color: red; }'
457+
const parser = new Parser(source)
458+
const root = parser.parse()
459+
const rule = root.first_child!
460+
const selectorList = rule.first_child!
461+
const selector = selectorList.first_child!
462+
const classSelector = selector.first_child!
463+
464+
expect(classSelector.type_name).toBe('class-selector')
465+
})
466+
467+
test('should return selector_id for ID selectors', () => {
468+
const source = '#bar { color: red; }'
469+
const parser = new Parser(source)
470+
const root = parser.parse()
471+
const rule = root.first_child!
472+
const selectorList = rule.first_child!
473+
const selector = selectorList.first_child!
474+
const idSelector = selector.first_child!
475+
476+
expect(idSelector.type_name).toBe('id-selector')
477+
})
478+
479+
test('should return selector_universal for universal selectors', () => {
480+
const source = '* { color: red; }'
481+
const parser = new Parser(source)
482+
const root = parser.parse()
483+
const rule = root.first_child!
484+
const selectorList = rule.first_child!
485+
const selector = selectorList.first_child!
486+
const universalSelector = selector.first_child!
487+
488+
expect(universalSelector.type_name).toBe('universal-selector')
489+
})
490+
491+
test('should return selector_attribute for attribute selectors', () => {
492+
const source = '[href] { color: red; }'
493+
const parser = new Parser(source)
494+
const root = parser.parse()
495+
const rule = root.first_child!
496+
const selectorList = rule.first_child!
497+
const selector = selectorList.first_child!
498+
const attrSelector = selector.first_child!
499+
500+
expect(attrSelector.type_name).toBe('attribute-selector')
501+
})
502+
503+
test('should return selector_pseudo_class for pseudo-class selectors', () => {
504+
const source = ':hover { color: red; }'
505+
const parser = new Parser(source)
506+
const root = parser.parse()
507+
const rule = root.first_child!
508+
const selectorList = rule.first_child!
509+
const selector = selectorList.first_child!
510+
const pseudoClass = selector.first_child!
511+
512+
expect(pseudoClass.type_name).toBe('pseudoclass-selector')
513+
})
514+
515+
test('should return selector_pseudo_element for pseudo-element selectors', () => {
516+
const source = '::before { color: red; }'
517+
const parser = new Parser(source)
518+
const root = parser.parse()
519+
const rule = root.first_child!
520+
const selectorList = rule.first_child!
521+
const selector = selectorList.first_child!
522+
const pseudoElement = selector.first_child!
523+
524+
expect(pseudoElement.type_name).toBe('pseudoelement-selector')
525+
})
526+
527+
test('should return selector_combinator for combinators', () => {
528+
const source = 'div > span { color: red; }'
529+
const parser = new Parser(source)
530+
const root = parser.parse()
531+
const rule = root.first_child!
532+
const selectorList = rule.first_child!
533+
const selector = selectorList.first_child!
534+
const combinator = selector.first_child!.next_sibling!
535+
536+
expect(combinator.type_name).toBe('selector-combinator')
537+
})
538+
539+
test('should return value_keyword for keyword values', () => {
540+
const source = 'body { color: red; }'
541+
const parser = new Parser(source)
542+
const root = parser.parse()
543+
const rule = root.first_child!
544+
const block = rule.block!
545+
const decl = block.first_child!
546+
const value = decl.first_child!
547+
548+
expect(value.type_name).toBe('keyword')
549+
})
550+
551+
test('should return value_number for numeric values', () => {
552+
const source = 'body { opacity: 0.5; }'
553+
const parser = new Parser(source)
554+
const root = parser.parse()
555+
const rule = root.first_child!
556+
const block = rule.block!
557+
const decl = block.first_child!
558+
const value = decl.first_child!
559+
560+
expect(value.type_name).toBe('number')
561+
})
562+
563+
test('should return value_dimension for dimension values', () => {
564+
const source = 'body { width: 100px; }'
565+
const parser = new Parser(source)
566+
const root = parser.parse()
567+
const rule = root.first_child!
568+
const block = rule.block!
569+
const decl = block.first_child!
570+
const value = decl.first_child!
571+
572+
expect(value.type_name).toBe('dimension')
573+
})
574+
575+
test('should return value_string for string values', () => {
576+
const source = 'body { content: "hello"; }'
577+
const parser = new Parser(source)
578+
const root = parser.parse()
579+
const rule = root.first_child!
580+
const block = rule.block!
581+
const decl = block.first_child!
582+
const value = decl.first_child!
583+
584+
expect(value.type_name).toBe('string')
585+
})
586+
587+
test('should return value_color for color values', () => {
588+
const source = 'body { color: #ff0000; }'
589+
const parser = new Parser(source)
590+
const root = parser.parse()
591+
const rule = root.first_child!
592+
const block = rule.block!
593+
const decl = block.first_child!
594+
const value = decl.first_child!
595+
596+
expect(value.type_name).toBe('color')
597+
})
598+
599+
test('should return value_function for function values', () => {
600+
const source = 'body { width: calc(100% - 20px); }'
601+
const parser = new Parser(source)
602+
const root = parser.parse()
603+
const rule = root.first_child!
604+
const block = rule.block!
605+
const decl = block.first_child!
606+
const value = decl.first_child!
607+
608+
expect(value.type_name).toBe('function')
609+
})
610+
611+
test('should return prelude_media_query for media query preludes', () => {
612+
const source = '@media screen and (min-width: 768px) { body { color: red; } }'
613+
const parser = new Parser(source)
614+
const root = parser.parse()
615+
const media = root.first_child!
616+
const prelude = media.first_child!
617+
618+
expect(prelude.type_name).toBe('media-query')
619+
})
620+
})
394621
})

src/css-node.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,49 @@ import {
5151
import { CHAR_MINUS_HYPHEN, CHAR_PLUS, is_whitespace } from './string-utils'
5252
import { parse_dimension } from './parse-utils'
5353

54+
// Type name lookup table - maps numeric type to human-readable string
55+
export const TYPE_NAMES: Record<number, string> = {
56+
[NODE_STYLESHEET]: 'stylesheet',
57+
[NODE_STYLE_RULE]: 'rule',
58+
[NODE_AT_RULE]: 'atrule',
59+
[NODE_DECLARATION]: 'declaration',
60+
[NODE_SELECTOR]: 'selector',
61+
[NODE_COMMENT]: 'comment',
62+
[NODE_BLOCK]: 'block',
63+
[NODE_VALUE_KEYWORD]: 'keyword',
64+
[NODE_VALUE_NUMBER]: 'number',
65+
[NODE_VALUE_DIMENSION]: 'dimension',
66+
[NODE_VALUE_STRING]: 'string',
67+
[NODE_VALUE_COLOR]: 'color',
68+
[NODE_VALUE_FUNCTION]: 'function',
69+
[NODE_VALUE_OPERATOR]: 'operator',
70+
[NODE_VALUE_PARENTHESIS]: 'parenthesis',
71+
[NODE_SELECTOR_LIST]: 'selectorlist',
72+
[NODE_SELECTOR_TYPE]: 'type-selector',
73+
[NODE_SELECTOR_CLASS]: 'class-selector',
74+
[NODE_SELECTOR_ID]: 'id-selector',
75+
[NODE_SELECTOR_ATTRIBUTE]: 'attribute-selector',
76+
[NODE_SELECTOR_PSEUDO_CLASS]: 'pseudoclass-selector',
77+
[NODE_SELECTOR_PSEUDO_ELEMENT]: 'pseudoelement-selector',
78+
[NODE_SELECTOR_COMBINATOR]: 'selector-combinator',
79+
[NODE_SELECTOR_UNIVERSAL]: 'universal-selector',
80+
[NODE_SELECTOR_NESTING]: 'nesting-selector',
81+
[NODE_SELECTOR_NTH]: 'nth-selector',
82+
[NODE_SELECTOR_NTH_OF]: 'nth-of-selector',
83+
[NODE_SELECTOR_LANG]: 'lang-selector',
84+
[NODE_PRELUDE_MEDIA_QUERY]: 'media-query',
85+
[NODE_PRELUDE_MEDIA_FEATURE]: 'media-feature',
86+
[NODE_PRELUDE_MEDIA_TYPE]: 'media-type',
87+
[NODE_PRELUDE_CONTAINER_QUERY]: 'container-query',
88+
[NODE_PRELUDE_SUPPORTS_QUERY]: 'supports-query',
89+
[NODE_PRELUDE_LAYER_NAME]: 'layer-name',
90+
[NODE_PRELUDE_IDENTIFIER]: 'identifier',
91+
[NODE_PRELUDE_OPERATOR]: 'operator',
92+
[NODE_PRELUDE_IMPORT_URL]: 'import-url',
93+
[NODE_PRELUDE_IMPORT_LAYER]: 'import-layer',
94+
[NODE_PRELUDE_IMPORT_SUPPORTS]: 'import-supports',
95+
} as const
96+
5497
// Node type constants (numeric for performance)
5598
export type CSSNodeType =
5699
| typeof NODE_STYLESHEET
@@ -114,6 +157,11 @@ export class CSSNode {
114157
return this.arena.get_type(this.index) as CSSNodeType
115158
}
116159

160+
// Get node type as human-readable string
161+
get type_name(): string {
162+
return TYPE_NAMES[this.type] || 'unknown'
163+
}
164+
117165
// Get the full text of this node from source
118166
get text(): string {
119167
let start = this.arena.get_start_offset(this.index)

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export { walk, traverse } from './walk'
1212
export { type ParserOptions } from './parse'
1313

1414
// Types
15-
export { CSSNode, type CSSNodeType } from './css-node'
15+
export { CSSNode, type CSSNodeType, TYPE_NAMES } from './css-node'
1616
export type { LexerPosition } from './lexer'
1717

1818
export {

0 commit comments

Comments
 (0)