Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)`
Expand Down
227 changes: 227 additions & 0 deletions src/css-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
})
48 changes: 48 additions & 0 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
[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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading