Skip to content

Commit 92b69b3

Browse files
authored
fix: when parse_* is false, add RAW nodes to AST (#155)
- src/parse.ts: Selector fallback now creates a RAW node when parse_selectors_enabled is false, instead of always using SELECTOR_LIST - src/parse-declaration.ts: Added RAW import and an else branch that creates a RAW node as a child of DECLARATION when value_parser is null (parse_values: false) - src/css-node.ts: Updated the value getter to only return first_child when it's a VALUE node (not RAW), keeping declaration.value returning the raw text string when parse_values: false - src/parse-options.test.ts: Updated 9 test assertions — has_children: false → true for parse_values:false cases, SELECTOR_LIST → RAW for parse_selectors:false cases, plus added first_child?.type === RAW assertion - src/walk.test.ts: Updated 19 expected type arrays — SELECTOR_LIST → RAW for all parse_selectors: false cases, and added RAW after DECLARATION for all parse_values: false cases some extras - expose `ATTR_FLAG_NAMES` and `ATTR_OPERATOR_NAMES` publicly
1 parent 00b8452 commit 92b69b3

10 files changed

Lines changed: 218 additions & 121 deletions

File tree

src/api.test.ts

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
FUNCTION,
1414
ATTRIBUTE_SELECTOR,
1515
} from './arena'
16-
import { PlainCSSNode } from './css-node'
16+
import { CSSNode, PlainCSSNode } from './css-node'
1717

1818
describe('CSSNode', () => {
1919
describe('iteration', () => {
@@ -65,7 +65,8 @@ describe('CSSNode', () => {
6565
const importRule = root.first_child!
6666
const children = [...importRule]
6767

68-
expect(children).toHaveLength(0)
68+
// RAW prelude node is created when parse_atrule_preludes is false
69+
expect(children).toHaveLength(1)
6970
})
7071
})
7172

@@ -208,7 +209,7 @@ describe('CSSNode', () => {
208209
expect(rule.has_prelude).toBe(true) // has selector
209210

210211
expect(declaration.has_prelude).toBe(false)
211-
expect(declaration.value).toBe('red')
212+
expect((declaration.value as PlainCSSNode).text).toBe('red')
212213
})
213214
})
214215

@@ -771,7 +772,7 @@ describe('CSSNode', () => {
771772
expect(clone.text).toBe('color: red;')
772773
expect(clone.name).toBe(undefined)
773774
expect(clone.property).toBe('color')
774-
expect(clone.value).toBe('red')
775+
expect((clone.value as PlainCSSNode).text).toBe('red')
775776
expect(clone.children).toBeUndefined()
776777
})
777778

@@ -841,7 +842,7 @@ describe('CSSNode', () => {
841842
expect(clone.type_name).toBe('Declaration')
842843
expect(clone.property).toBe('color')
843844
expect(clone.name).toBe(undefined)
844-
expect(clone.value).toBe('red')
845+
expect((clone.value as CSSNode)?.text).toBe('red')
845846
expect(clone.is_important).toBe(true)
846847
})
847848

@@ -896,53 +897,53 @@ describe('CSSNode', () => {
896897
expect(clone.attr_flags).toBeDefined()
897898
})
898899

899-
test('returns human-readable attribute operator strings', () => {
900-
const operators = [
901-
{ selector: '[attr]', expected: null },
902-
{ selector: '[attr="val"]', expected: '=' },
903-
{ selector: '[attr~="val"]', expected: '~=' },
904-
{ selector: '[attr|="val"]', expected: '|=' },
905-
{ selector: '[attr^="val"]', expected: '^=' },
906-
{ selector: '[attr$="val"]', expected: '$=' },
907-
{ selector: '[attr*="val"]', expected: '*=' },
908-
]
909-
910-
for (const { selector, expected } of operators) {
911-
const ast = parse_selector(selector)
912-
const attribute = ast.first_child!.first_child!
913-
const clone = attribute.clone({ deep: false })
900+
test('returns human-readable attribute operator strings', () => {
901+
const operators = [
902+
{ selector: '[attr]', expected: null },
903+
{ selector: '[attr="val"]', expected: '=' },
904+
{ selector: '[attr~="val"]', expected: '~=' },
905+
{ selector: '[attr|="val"]', expected: '|=' },
906+
{ selector: '[attr^="val"]', expected: '^=' },
907+
{ selector: '[attr$="val"]', expected: '$=' },
908+
{ selector: '[attr*="val"]', expected: '*=' },
909+
]
910+
911+
for (const { selector, expected } of operators) {
912+
const ast = parse_selector(selector)
913+
const attribute = ast.first_child!.first_child!
914+
const clone = attribute.clone({ deep: false })
915+
916+
expect(clone.attr_operator).toBe(expected)
917+
}
918+
})
914919

915-
expect(clone.attr_operator).toBe(expected)
916-
}
917-
})
920+
test('returns human-readable attribute flag strings', () => {
921+
const flags = [
922+
{ selector: '[attr="val"]', expected: null },
923+
{ selector: '[attr="val" i]', expected: 'i' },
924+
{ selector: '[attr="val" s]', expected: 's' },
925+
]
918926

919-
test('returns human-readable attribute flag strings', () => {
920-
const flags = [
921-
{ selector: '[attr="val"]', expected: null },
922-
{ selector: '[attr="val" i]', expected: 'i' },
923-
{ selector: '[attr="val" s]', expected: 's' },
924-
]
927+
for (const { selector, expected } of flags) {
928+
const ast = parse_selector(selector)
929+
const attribute = ast.first_child!.first_child!
930+
const clone = attribute.clone({ deep: false })
925931

926-
for (const { selector, expected } of flags) {
927-
const ast = parse_selector(selector)
928-
const attribute = ast.first_child!.first_child!
929-
const clone = attribute.clone({ deep: false })
930-
931-
expect(clone.attr_flags).toBe(expected)
932-
}
933-
})
932+
expect(clone.attr_flags).toBe(expected)
933+
}
934+
})
934935

935-
test('extracts nth selector properties', () => {
936-
const ast = parse_selector(':nth-child(2n+1)')
937-
const selector = ast.first_child!
938-
const pseudo = selector.first_child!
939-
const nth = pseudo.first_child!
936+
test('extracts nth selector properties', () => {
937+
const ast = parse_selector(':nth-child(2n+1)')
938+
const selector = ast.first_child!
939+
const pseudo = selector.first_child!
940+
const nth = pseudo.first_child!
940941

941-
const clone = nth.clone({ deep: false })
942+
const clone = nth.clone({ deep: false })
942943

943-
expect(clone.type).toBe(NTH_SELECTOR)
944-
expect(clone.nth_a).toBe('2n')
945-
expect(clone.nth_b).toBe('+1')
944+
expect(clone.type).toBe(NTH_SELECTOR)
945+
expect(clone.nth_a).toBe('2n')
946+
expect(clone.nth_b).toBe('+1')
946947
})
947948
})
948949

src/arena.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const DECLARATION = 4
5151
export const SELECTOR = 5
5252
export const COMMENT = 6
5353
export const BLOCK = 7 // Block container for declarations and nested rules
54+
export const RAW = 8 // Unparsed nodes
5455

5556
// Value node type constants (for declaration values)
5657
export const IDENTIFIER = 10 // identifier: red, auto, inherit

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SELECTOR,
1010
COMMENT,
1111
BLOCK,
12+
RAW,
1213
IDENTIFIER,
1314
NUMBER,
1415
DIMENSION,
@@ -62,6 +63,7 @@ export {
6263
SELECTOR,
6364
COMMENT,
6465
BLOCK,
66+
RAW,
6567
IDENTIFIER,
6668
NUMBER,
6769
DIMENSION,
@@ -117,6 +119,7 @@ export const NODE_TYPES = {
117119
SELECTOR,
118120
COMMENT,
119121
BLOCK,
122+
RAW,
120123
// Value nodes
121124
IDENTIFIER,
122125
NUMBER,

src/css-node.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// CSSNode - Ergonomic wrapper over arena node indices
22
import type { CSSDataArena } from './arena'
33
import {
4+
RAW,
45
STYLESHEET,
56
STYLE_RULE,
67
AT_RULE,
@@ -71,6 +72,7 @@ export const TYPE_NAMES = {
7172
[SELECTOR]: 'Selector',
7273
[COMMENT]: 'Comment',
7374
[BLOCK]: 'Block',
75+
[RAW]: 'Raw',
7476
[IDENTIFIER]: 'Identifier',
7577
[NUMBER]: 'Number',
7678
[DIMENSION]: 'Dimension',
@@ -135,6 +137,7 @@ export type CSSNodeType =
135137
| typeof SELECTOR
136138
| typeof COMMENT
137139
| typeof BLOCK
140+
| typeof RAW
138141
| typeof IDENTIFIER
139142
| typeof NUMBER
140143
| typeof DIMENSION
@@ -356,7 +359,7 @@ export class CSSNode {
356359
get prelude(): CSSNode | null | undefined {
357360
if (this.type === AT_RULE) {
358361
let first = this.first_child
359-
if (first && first.type === AT_RULE_PRELUDE) {
362+
if (first?.type === AT_RULE_PRELUDE || first?.type === RAW) {
360363
return first
361364
}
362365
return null

src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ export { is_custom, is_vendor_prefixed, str_equals, str_starts_with, str_index_o
1515
export { type ParserOptions } from './parse'
1616

1717
// Types
18-
export { CSSNode, type CSSNodeType, TYPE_NAMES, type CloneOptions, type PlainCSSNode } from './css-node'
18+
export {
19+
CSSNode,
20+
type CSSNodeType,
21+
TYPE_NAMES,
22+
type CloneOptions,
23+
type PlainCSSNode,
24+
ATTR_FLAG_NAMES,
25+
ATTR_OPERATOR_NAMES,
26+
} from './css-node'
1927
export type { LexerPosition, CommentInfo } from './tokenize'
2028

2129
export {

src/parse-declaration.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Declaration Parser - Parses CSS declarations into structured AST nodes
22
import { Lexer } from './tokenize'
3-
import { CSSDataArena, DECLARATION, FLAG_IMPORTANT, FLAG_BROWSERHACK } from './arena'
3+
import { CSSDataArena, DECLARATION, RAW, FLAG_IMPORTANT, FLAG_BROWSERHACK } from './arena'
44
import { ValueParser } from './parse-value'
55
import { is_vendor_prefixed } from './string-utils'
66
import {
@@ -119,11 +119,7 @@ export class DeclarationParser {
119119
}
120120

121121
// Expect identifier, at-keyword, or hash token (property name) - whitespace already skipped by caller
122-
if (
123-
lexer.token_type !== TOKEN_IDENT &&
124-
lexer.token_type !== TOKEN_AT_KEYWORD &&
125-
lexer.token_type !== TOKEN_HASH
126-
) {
122+
if (lexer.token_type !== TOKEN_IDENT && lexer.token_type !== TOKEN_AT_KEYWORD && lexer.token_type !== TOKEN_HASH) {
127123
return null
128124
}
129125

@@ -228,6 +224,10 @@ export class DeclarationParser {
228224

229225
// Link VALUE node as single child of the declaration
230226
this.arena.append_children(declaration, [valueNode])
227+
} else {
228+
// Create RAW node for unparsed value text
229+
let rawNode = this.arena.create_node(RAW, trimmed[0], trimmed[1] - trimmed[0], value_start_line, value_start_column)
230+
this.arena.append_children(declaration, [rawNode])
231231
}
232232
} else {
233233
// Empty value - set zero-length value field so node.value returns "" instead of null

0 commit comments

Comments
 (0)