@projectwallace/css-parser 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,40 @@
1
- export { STYLESHEET, STYLE_RULE, AT_RULE, DECLARATION, SELECTOR, COMMENT, BLOCK, IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL, SELECTOR_LIST, TYPE_SELECTOR, CLASS_SELECTOR, ID_SELECTOR, ATTRIBUTE_SELECTOR, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, COMBINATOR, UNIVERSAL_SELECTOR, NESTING_SELECTOR, NTH_SELECTOR, NTH_OF_SELECTOR, LANG_SELECTOR, MEDIA_QUERY, MEDIA_FEATURE, MEDIA_TYPE, CONTAINER_QUERY, SUPPORTS_QUERY, LAYER_NAME, PRELUDE_OPERATOR, FLAG_IMPORTANT, ATTR_OPERATOR_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_NONE, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, } from './arena';
1
+ import { STYLESHEET, STYLE_RULE, AT_RULE, DECLARATION, SELECTOR, COMMENT, BLOCK, IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL, SELECTOR_LIST, TYPE_SELECTOR, CLASS_SELECTOR, ID_SELECTOR, ATTRIBUTE_SELECTOR, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, COMBINATOR, UNIVERSAL_SELECTOR, NESTING_SELECTOR, NTH_SELECTOR, NTH_OF_SELECTOR, LANG_SELECTOR, MEDIA_QUERY, MEDIA_FEATURE, MEDIA_TYPE, CONTAINER_QUERY, SUPPORTS_QUERY, LAYER_NAME, PRELUDE_OPERATOR, FLAG_IMPORTANT, ATTR_OPERATOR_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_NONE, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE } from './arena';
2
+ export { STYLESHEET, STYLE_RULE, AT_RULE, DECLARATION, SELECTOR, COMMENT, BLOCK, IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL, SELECTOR_LIST, TYPE_SELECTOR, CLASS_SELECTOR, ID_SELECTOR, ATTRIBUTE_SELECTOR, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, COMBINATOR, UNIVERSAL_SELECTOR, NESTING_SELECTOR, NTH_SELECTOR, NTH_OF_SELECTOR, LANG_SELECTOR, MEDIA_QUERY, MEDIA_FEATURE, MEDIA_TYPE, CONTAINER_QUERY, SUPPORTS_QUERY, LAYER_NAME, PRELUDE_OPERATOR, FLAG_IMPORTANT, ATTR_OPERATOR_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_NONE, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, };
3
+ export declare const NODE_TYPES: {
4
+ readonly STYLESHEET: 1;
5
+ readonly STYLE_RULE: 2;
6
+ readonly AT_RULE: 3;
7
+ readonly DECLARATION: 4;
8
+ readonly SELECTOR: 5;
9
+ readonly COMMENT: 6;
10
+ readonly BLOCK: 7;
11
+ readonly IDENTIFIER: 10;
12
+ readonly NUMBER: 11;
13
+ readonly DIMENSION: 12;
14
+ readonly STRING: 13;
15
+ readonly HASH: 14;
16
+ readonly FUNCTION: 15;
17
+ readonly OPERATOR: 16;
18
+ readonly PARENTHESIS: 17;
19
+ readonly URL: 18;
20
+ readonly SELECTOR_LIST: 20;
21
+ readonly TYPE_SELECTOR: 21;
22
+ readonly CLASS_SELECTOR: 22;
23
+ readonly ID_SELECTOR: 23;
24
+ readonly ATTRIBUTE_SELECTOR: 24;
25
+ readonly PSEUDO_CLASS_SELECTOR: 25;
26
+ readonly PSEUDO_ELEMENT_SELECTOR: 26;
27
+ readonly COMBINATOR: 27;
28
+ readonly UNIVERSAL_SELECTOR: 28;
29
+ readonly NESTING_SELECTOR: 29;
30
+ readonly NTH_SELECTOR: 30;
31
+ readonly NTH_OF_SELECTOR: 31;
32
+ readonly LANG_SELECTOR: 56;
33
+ readonly MEDIA_QUERY: 32;
34
+ readonly MEDIA_FEATURE: 33;
35
+ readonly MEDIA_TYPE: 34;
36
+ readonly CONTAINER_QUERY: 35;
37
+ readonly SUPPORTS_QUERY: 36;
38
+ readonly LAYER_NAME: 37;
39
+ readonly PRELUDE_OPERATOR: 38;
40
+ };
@@ -0,0 +1,47 @@
1
+ import { PRELUDE_OPERATOR, LAYER_NAME, SUPPORTS_QUERY, CONTAINER_QUERY, MEDIA_TYPE, MEDIA_FEATURE, MEDIA_QUERY, LANG_SELECTOR, NTH_OF_SELECTOR, NTH_SELECTOR, NESTING_SELECTOR, UNIVERSAL_SELECTOR, COMBINATOR, PSEUDO_ELEMENT_SELECTOR, PSEUDO_CLASS_SELECTOR, ATTRIBUTE_SELECTOR, ID_SELECTOR, CLASS_SELECTOR, TYPE_SELECTOR, SELECTOR_LIST, URL, PARENTHESIS, OPERATOR, FUNCTION, HASH, STRING, DIMENSION, NUMBER, IDENTIFIER, BLOCK, COMMENT, SELECTOR, DECLARATION, AT_RULE, STYLE_RULE, STYLESHEET } from './arena.js';
2
+ export { ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, ATTR_FLAG_NONE, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_NONE, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, FLAG_IMPORTANT } from './arena.js';
3
+
4
+ const NODE_TYPES = {
5
+ // Core nodes
6
+ STYLESHEET,
7
+ STYLE_RULE,
8
+ AT_RULE,
9
+ DECLARATION,
10
+ SELECTOR,
11
+ COMMENT,
12
+ BLOCK,
13
+ // Value nodes
14
+ IDENTIFIER,
15
+ NUMBER,
16
+ DIMENSION,
17
+ STRING,
18
+ HASH,
19
+ FUNCTION,
20
+ OPERATOR,
21
+ PARENTHESIS,
22
+ URL,
23
+ // Selector nodes
24
+ SELECTOR_LIST,
25
+ TYPE_SELECTOR,
26
+ CLASS_SELECTOR,
27
+ ID_SELECTOR,
28
+ ATTRIBUTE_SELECTOR,
29
+ PSEUDO_CLASS_SELECTOR,
30
+ PSEUDO_ELEMENT_SELECTOR,
31
+ COMBINATOR,
32
+ UNIVERSAL_SELECTOR,
33
+ NESTING_SELECTOR,
34
+ NTH_SELECTOR,
35
+ NTH_OF_SELECTOR,
36
+ LANG_SELECTOR,
37
+ // At-rule prelude nodes
38
+ MEDIA_QUERY,
39
+ MEDIA_FEATURE,
40
+ MEDIA_TYPE,
41
+ CONTAINER_QUERY,
42
+ SUPPORTS_QUERY,
43
+ LAYER_NAME,
44
+ PRELUDE_OPERATOR
45
+ };
46
+
47
+ export { ATTRIBUTE_SELECTOR, AT_RULE, BLOCK, CLASS_SELECTOR, COMBINATOR, COMMENT, CONTAINER_QUERY, DECLARATION, DIMENSION, FUNCTION, HASH, IDENTIFIER, ID_SELECTOR, LANG_SELECTOR, LAYER_NAME, MEDIA_FEATURE, MEDIA_QUERY, MEDIA_TYPE, NESTING_SELECTOR, NODE_TYPES, NTH_OF_SELECTOR, NTH_SELECTOR, NUMBER, OPERATOR, PARENTHESIS, PRELUDE_OPERATOR, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, SELECTOR, SELECTOR_LIST, STRING, STYLESHEET, STYLE_RULE, SUPPORTS_QUERY, TYPE_SELECTOR, UNIVERSAL_SELECTOR, URL };
@@ -80,48 +80,124 @@ export declare class CSSNode {
80
80
  private source;
81
81
  private index;
82
82
  constructor(arena: CSSDataArena, source: string, index: number);
83
- get_index(): number;
83
+ /** Get node type as number (for performance) */
84
84
  get type(): CSSNodeType;
85
+ /** Get node type as human-readable string */
85
86
  get type_name(): TypeName;
87
+ /** Get the full text of this node from source */
86
88
  get text(): string;
89
+ /** Get the "content" text (property name for declarations, at-rule name for at-rules, layer name for import layers) */
87
90
  get name(): string;
91
+ /**
92
+ * Alias for name (for declarations: "color" in "color: blue")
93
+ * More semantic than `name` for declaration nodes
94
+ */
88
95
  get property(): string;
96
+ /**
97
+ * Get the value text (for declarations: "blue" in "color: blue")
98
+ * For dimension/number nodes: returns the numeric value as a number
99
+ * For string nodes: returns the string content without quotes
100
+ * For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
101
+ * For URL nodes with unquoted URL: returns the URL content without quotes
102
+ */
89
103
  get value(): string | number | null;
104
+ /** Get the numeric value for NUMBER and DIMENSION nodes, or null for other node types */
90
105
  get value_as_number(): number | null;
106
+ /**
107
+ * Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
108
+ * This is an alias for `value` to make at-rule usage more semantic
109
+ */
91
110
  get prelude(): string | null;
111
+ /**
112
+ * Get the attribute operator (for attribute selectors: =, ~=, |=, ^=, $=, *=)
113
+ * Returns one of the ATTR_OPERATOR_* constants
114
+ */
92
115
  get attr_operator(): number;
116
+ /**
117
+ * Get the attribute flags (for attribute selectors: i, s)
118
+ * Returns one of the ATTR_FLAG_* constants
119
+ */
93
120
  get attr_flags(): number;
121
+ /** Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%") */
94
122
  get unit(): string | null;
123
+ /** Check if this declaration has !important */
95
124
  get is_important(): boolean | null;
125
+ /** Check if this has a vendor prefix (computed on-demand) */
96
126
  get is_vendor_prefixed(): boolean;
127
+ /** Check if this node has an error */
97
128
  get has_error(): boolean;
129
+ /** Check if this at-rule has a prelude */
98
130
  get has_prelude(): boolean;
131
+ /** Check if this rule has a block { } */
99
132
  get has_block(): boolean;
133
+ /** Check if this style rule has declarations */
100
134
  get has_declarations(): boolean;
135
+ /** Get the block node (for style rules and at-rules with blocks) */
101
136
  get block(): CSSNode | null;
137
+ /** Check if this block is empty (no declarations or rules, only comments allowed) */
102
138
  get is_empty(): boolean;
139
+ /** Get array of parsed value nodes (for declarations only) */
103
140
  get values(): CSSNode[];
104
- get value_count(): number;
141
+ /** Get start line number */
105
142
  get line(): number;
143
+ /** Get start column number */
106
144
  get column(): number;
145
+ /** Get start offset in source */
107
146
  get start(): number;
147
+ /** Get length in source */
108
148
  get length(): number;
149
+ /**
150
+ * Get end offset in source
151
+ * End is not stored, must be calculated
152
+ */
109
153
  get end(): number;
154
+ /** Get first child node */
110
155
  get first_child(): CSSNode | null;
156
+ /** Get next sibling node */
111
157
  get next_sibling(): CSSNode | null;
158
+ /** Check if this node has a next sibling */
112
159
  get has_next(): boolean;
160
+ /**
161
+ * Check if this node has children
162
+ * For pseudo-class/pseudo-element functions, returns true if FLAG_HAS_PARENS is set
163
+ * This allows formatters to distinguish :lang() from :hover
164
+ */
113
165
  get has_children(): boolean;
166
+ /** Get all children as an array */
114
167
  get children(): CSSNode[];
168
+ /** Make CSSNode iterable over its children */
115
169
  [Symbol.iterator](): Iterator<CSSNode>;
170
+ /** Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") */
116
171
  get nth_a(): string | null;
172
+ /** Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") */
117
173
  get nth_b(): string | null;
174
+ /** Get the An+B formula node from :nth-child(2n+1 of .foo) */
118
175
  get nth(): CSSNode | null;
176
+ /** Get the selector list from :nth-child(2n+1 of .foo) */
119
177
  get selector(): CSSNode | null;
178
+ /**
179
+ * Get selector list from pseudo-class functions
180
+ * Works for :is(.a), :not(.b), :has(.c), :where(.d), :nth-child(2n of .e)
181
+ */
120
182
  get selector_list(): CSSNode | null;
183
+ /**
184
+ * Iterator over first compound selector parts (zero allocation)
185
+ * Yields parts before the first combinator
186
+ */
121
187
  compound_parts(): IterableIterator<CSSNode>;
188
+ /**
189
+ * Get first compound selector as array
190
+ * Returns array of parts before first combinator
191
+ */
122
192
  get first_compound(): CSSNode[];
193
+ /**
194
+ * Split selector into compound selectors
195
+ * Returns array of compound arrays split by combinators
196
+ */
123
197
  get all_compounds(): CSSNode[][];
198
+ /** Check if selector is compound (no combinators) */
124
199
  get is_compound(): boolean;
200
+ /** Get text of first compound selector (no node allocation) */
125
201
  get first_compound_text(): string;
126
202
  /**
127
203
  * Clone this node as a mutable plain JavaScript object
package/dist/css-node.js CHANGED
@@ -49,41 +49,41 @@ class CSSNode {
49
49
  this.source = source;
50
50
  this.index = index;
51
51
  }
52
- // Get the node index (for internal use)
53
- get_index() {
54
- return this.index;
55
- }
56
- // Get node type as number (for performance)
52
+ /** Get node type as number (for performance) */
57
53
  get type() {
58
54
  return this.arena.get_type(this.index);
59
55
  }
60
- // Get node type as human-readable string
56
+ /** Get node type as human-readable string */
61
57
  get type_name() {
62
58
  return TYPE_NAMES[this.type] || "unknown";
63
59
  }
64
- // Get the full text of this node from source
60
+ /** Get the full text of this node from source */
65
61
  get text() {
66
62
  let start = this.arena.get_start_offset(this.index);
67
63
  let length = this.arena.get_length(this.index);
68
64
  return this.source.substring(start, start + length);
69
65
  }
70
- // Get the "content" text (property name for declarations, at-rule name for at-rules, layer name for import layers)
66
+ /** Get the "content" text (property name for declarations, at-rule name for at-rules, layer name for import layers) */
71
67
  get name() {
72
68
  let start = this.arena.get_content_start(this.index);
73
69
  let length = this.arena.get_content_length(this.index);
74
70
  if (length === 0) return "";
75
71
  return this.source.substring(start, start + length);
76
72
  }
77
- // Alias for name (for declarations: "color" in "color: blue")
78
- // More semantic than `name` for declaration nodes
73
+ /**
74
+ * Alias for name (for declarations: "color" in "color: blue")
75
+ * More semantic than `name` for declaration nodes
76
+ */
79
77
  get property() {
80
78
  return this.name;
81
79
  }
82
- // Get the value text (for declarations: "blue" in "color: blue")
83
- // For dimension/number nodes: returns the numeric value as a number
84
- // For string nodes: returns the string content without quotes
85
- // For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
86
- // For URL nodes with unquoted URL: returns the URL content without quotes
80
+ /**
81
+ * Get the value text (for declarations: "blue" in "color: blue")
82
+ * For dimension/number nodes: returns the numeric value as a number
83
+ * For string nodes: returns the string content without quotes
84
+ * For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
85
+ * For URL nodes with unquoted URL: returns the URL content without quotes
86
+ */
87
87
  get value() {
88
88
  let { type, text } = this;
89
89
  if (type === DIMENSION) {
@@ -113,6 +113,7 @@ class CSSNode {
113
113
  if (length === 0) return null;
114
114
  return this.source.substring(start, start + length);
115
115
  }
116
+ /** Get the numeric value for NUMBER and DIMENSION nodes, or null for other node types */
116
117
  get value_as_number() {
117
118
  let text = this.text;
118
119
  if (this.type === NUMBER) {
@@ -123,33 +124,39 @@ class CSSNode {
123
124
  }
124
125
  return null;
125
126
  }
126
- // Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
127
- // This is an alias for `value` to make at-rule usage more semantic
127
+ /**
128
+ * Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
129
+ * This is an alias for `value` to make at-rule usage more semantic
130
+ */
128
131
  get prelude() {
129
132
  let val = this.value;
130
133
  return typeof val === "string" ? val : null;
131
134
  }
132
- // Get the attribute operator (for attribute selectors: =, ~=, |=, ^=, $=, *=)
133
- // Returns one of the ATTR_OPERATOR_* constants
135
+ /**
136
+ * Get the attribute operator (for attribute selectors: =, ~=, |=, ^=, $=, *=)
137
+ * Returns one of the ATTR_OPERATOR_* constants
138
+ */
134
139
  get attr_operator() {
135
140
  return this.arena.get_attr_operator(this.index);
136
141
  }
137
- // Get the attribute flags (for attribute selectors: i, s)
138
- // Returns one of the ATTR_FLAG_* constants
142
+ /**
143
+ * Get the attribute flags (for attribute selectors: i, s)
144
+ * Returns one of the ATTR_FLAG_* constants
145
+ */
139
146
  get attr_flags() {
140
147
  return this.arena.get_attr_flags(this.index);
141
148
  }
142
- // Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%")
149
+ /** Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%") */
143
150
  get unit() {
144
151
  if (this.type !== DIMENSION) return null;
145
152
  return parse_dimension(this.text).unit;
146
153
  }
147
- // Check if this declaration has !important
154
+ /** Check if this declaration has !important */
148
155
  get is_important() {
149
156
  if (this.type !== DECLARATION) return null;
150
157
  return this.arena.has_flag(this.index, FLAG_IMPORTANT);
151
158
  }
152
- // Check if this has a vendor prefix (computed on-demand)
159
+ /** Check if this has a vendor prefix (computed on-demand) */
153
160
  get is_vendor_prefixed() {
154
161
  switch (this.type) {
155
162
  case DECLARATION:
@@ -167,23 +174,23 @@ class CSSNode {
167
174
  return false;
168
175
  }
169
176
  }
170
- // Check if this node has an error
177
+ /** Check if this node has an error */
171
178
  get has_error() {
172
179
  return this.arena.has_flag(this.index, FLAG_HAS_ERROR);
173
180
  }
174
- // Check if this at-rule has a prelude
181
+ /** Check if this at-rule has a prelude */
175
182
  get has_prelude() {
176
183
  return this.arena.get_value_length(this.index) > 0;
177
184
  }
178
- // Check if this rule has a block { }
185
+ /** Check if this rule has a block { } */
179
186
  get has_block() {
180
187
  return this.arena.has_flag(this.index, FLAG_HAS_BLOCK);
181
188
  }
182
- // Check if this style rule has declarations
189
+ /** Check if this style rule has declarations */
183
190
  get has_declarations() {
184
191
  return this.arena.has_flag(this.index, FLAG_HAS_DECLARATIONS);
185
192
  }
186
- // Get the block node (for style rules and at-rules with blocks)
193
+ /** Get the block node (for style rules and at-rules with blocks) */
187
194
  get block() {
188
195
  if (this.type === STYLE_RULE) {
189
196
  let first = this.first_child;
@@ -206,7 +213,7 @@ class CSSNode {
206
213
  }
207
214
  return null;
208
215
  }
209
- // Check if this block is empty (no declarations or rules, only comments allowed)
216
+ /** Check if this block is empty (no declarations or rules, only comments allowed) */
210
217
  get is_empty() {
211
218
  if (this.type !== BLOCK) return false;
212
219
  let child = this.first_child;
@@ -219,7 +226,7 @@ class CSSNode {
219
226
  return true;
220
227
  }
221
228
  // --- Value Node Access (for declarations) ---
222
- // Get array of parsed value nodes (for declarations only)
229
+ /** Get array of parsed value nodes (for declarations only) */
223
230
  get values() {
224
231
  let result = [];
225
232
  let child = this.first_child;
@@ -229,57 +236,52 @@ class CSSNode {
229
236
  }
230
237
  return result;
231
238
  }
232
- // Get count of value nodes
233
- get value_count() {
234
- let count = 0;
235
- let child = this.first_child;
236
- while (child) {
237
- count++;
238
- child = child.next_sibling;
239
- }
240
- return count;
241
- }
242
- // Get start line number
239
+ /** Get start line number */
243
240
  get line() {
244
241
  return this.arena.get_start_line(this.index);
245
242
  }
246
- // Get start column number
243
+ /** Get start column number */
247
244
  get column() {
248
245
  return this.arena.get_start_column(this.index);
249
246
  }
250
- // Get start offset in source
247
+ /** Get start offset in source */
251
248
  get start() {
252
249
  return this.arena.get_start_offset(this.index);
253
250
  }
254
- // Get length in source
251
+ /** Get length in source */
255
252
  get length() {
256
253
  return this.arena.get_length(this.index);
257
254
  }
258
- // Get end offset in source
259
- // End is not stored, must be calculated
255
+ /**
256
+ * Get end offset in source
257
+ * End is not stored, must be calculated
258
+ */
260
259
  get end() {
261
260
  return this.start + this.length;
262
261
  }
263
262
  // --- Tree Traversal ---
264
- // Get first child node
263
+ /** Get first child node */
265
264
  get first_child() {
266
265
  let child_index = this.arena.get_first_child(this.index);
267
266
  if (child_index === 0) return null;
268
267
  return new CSSNode(this.arena, this.source, child_index);
269
268
  }
270
- // Get next sibling node
269
+ /** Get next sibling node */
271
270
  get next_sibling() {
272
271
  let sibling_index = this.arena.get_next_sibling(this.index);
273
272
  if (sibling_index === 0) return null;
274
273
  return new CSSNode(this.arena, this.source, sibling_index);
275
274
  }
275
+ /** Check if this node has a next sibling */
276
276
  get has_next() {
277
277
  let sibling_index = this.arena.get_next_sibling(this.index);
278
278
  return sibling_index !== 0;
279
279
  }
280
- // Check if this node has children
281
- // For pseudo-class/pseudo-element functions, returns true if FLAG_HAS_PARENS is set
282
- // This allows formatters to distinguish :lang() from :hover
280
+ /**
281
+ * Check if this node has children
282
+ * For pseudo-class/pseudo-element functions, returns true if FLAG_HAS_PARENS is set
283
+ * This allows formatters to distinguish :lang() from :hover
284
+ */
283
285
  get has_children() {
284
286
  if (this.type === PSEUDO_CLASS_SELECTOR || this.type === PSEUDO_ELEMENT_SELECTOR) {
285
287
  if (this.arena.has_flag(this.index, FLAG_HAS_PARENS)) {
@@ -288,7 +290,7 @@ class CSSNode {
288
290
  }
289
291
  return this.arena.has_children(this.index);
290
292
  }
291
- // Get all children as an array
293
+ /** Get all children as an array */
292
294
  get children() {
293
295
  let result = [];
294
296
  let child = this.first_child;
@@ -298,7 +300,7 @@ class CSSNode {
298
300
  }
299
301
  return result;
300
302
  }
301
- // Make CSSNode iterable over its children
303
+ /** Make CSSNode iterable over its children */
302
304
  *[Symbol.iterator]() {
303
305
  let child = this.first_child;
304
306
  while (child) {
@@ -307,7 +309,7 @@ class CSSNode {
307
309
  }
308
310
  }
309
311
  // --- An+B Expression Helpers (for NODE_SELECTOR_NTH) ---
310
- // Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd")
312
+ /** Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") */
311
313
  get nth_a() {
312
314
  if (this.type !== NTH_SELECTOR) return null;
313
315
  let len = this.arena.get_content_length(this.index);
@@ -315,7 +317,7 @@ class CSSNode {
315
317
  let start = this.arena.get_content_start(this.index);
316
318
  return this.source.substring(start, start + len);
317
319
  }
318
- // Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1")
320
+ /** Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") */
319
321
  get nth_b() {
320
322
  if (this.type !== NTH_SELECTOR) return null;
321
323
  let len = this.arena.get_value_length(this.index);
@@ -339,20 +341,22 @@ class CSSNode {
339
341
  return value;
340
342
  }
341
343
  // --- Pseudo-Class Nth-Of Helpers (for NODE_SELECTOR_NTH_OF) ---
342
- // Get the An+B formula node from :nth-child(2n+1 of .foo)
344
+ /** Get the An+B formula node from :nth-child(2n+1 of .foo) */
343
345
  get nth() {
344
346
  if (this.type !== NTH_OF_SELECTOR) return null;
345
347
  return this.first_child;
346
348
  }
347
- // Get the selector list from :nth-child(2n+1 of .foo)
349
+ /** Get the selector list from :nth-child(2n+1 of .foo) */
348
350
  get selector() {
349
351
  if (this.type !== NTH_OF_SELECTOR) return null;
350
352
  let first = this.first_child;
351
353
  return first ? first.next_sibling : null;
352
354
  }
353
355
  // --- Pseudo-Class Selector List Helper ---
354
- // Get selector list from pseudo-class functions
355
- // Works for :is(.a), :not(.b), :has(.c), :where(.d), :nth-child(2n of .e)
356
+ /**
357
+ * Get selector list from pseudo-class functions
358
+ * Works for :is(.a), :not(.b), :has(.c), :where(.d), :nth-child(2n of .e)
359
+ */
356
360
  get selector_list() {
357
361
  if (this.type !== PSEUDO_CLASS_SELECTOR) return null;
358
362
  let child = this.first_child;
@@ -366,8 +370,10 @@ class CSSNode {
366
370
  return null;
367
371
  }
368
372
  // --- Compound Selector Helpers (for NODE_SELECTOR) ---
369
- // Iterator over first compound selector parts (zero allocation)
370
- // Yields parts before the first combinator
373
+ /**
374
+ * Iterator over first compound selector parts (zero allocation)
375
+ * Yields parts before the first combinator
376
+ */
371
377
  *compound_parts() {
372
378
  if (this.type !== SELECTOR) return;
373
379
  let child = this.first_child;
@@ -377,8 +383,10 @@ class CSSNode {
377
383
  child = child.next_sibling;
378
384
  }
379
385
  }
380
- // Get first compound selector as array
381
- // Returns array of parts before first combinator
386
+ /**
387
+ * Get first compound selector as array
388
+ * Returns array of parts before first combinator
389
+ */
382
390
  get first_compound() {
383
391
  if (this.type !== SELECTOR) return [];
384
392
  let result = [];
@@ -390,8 +398,10 @@ class CSSNode {
390
398
  }
391
399
  return result;
392
400
  }
393
- // Split selector into compound selectors
394
- // Returns array of compound arrays split by combinators
401
+ /**
402
+ * Split selector into compound selectors
403
+ * Returns array of compound arrays split by combinators
404
+ */
395
405
  get all_compounds() {
396
406
  if (this.type !== SELECTOR) return [];
397
407
  let compounds = [];
@@ -413,7 +423,7 @@ class CSSNode {
413
423
  }
414
424
  return compounds;
415
425
  }
416
- // Check if selector is compound (no combinators)
426
+ /** Check if selector is compound (no combinators) */
417
427
  get is_compound() {
418
428
  if (this.type !== SELECTOR) return false;
419
429
  let child = this.first_child;
@@ -423,7 +433,7 @@ class CSSNode {
423
433
  }
424
434
  return true;
425
435
  }
426
- // Get text of first compound selector (no node allocation)
436
+ /** Get text of first compound selector (no node allocation) */
427
437
  get first_compound_text() {
428
438
  if (this.type !== SELECTOR) return "";
429
439
  let start = -1;
package/dist/index.d.ts CHANGED
@@ -3,10 +3,11 @@ export { parse_selector } from './parse-selector';
3
3
  export { parse_atrule_prelude } from './parse-atrule-prelude';
4
4
  export { parse_value } from './parse-value';
5
5
  export { tokenize } from './tokenize';
6
- export { walk, traverse } from './walk';
6
+ export { walk, traverse, SKIP, BREAK } from './walk';
7
7
  export { type ParserOptions } from './parse';
8
8
  export { CSSNode, type CSSNodeType, TYPE_NAMES, type CloneOptions, type PlainCSSNode } from './css-node';
9
9
  export type { LexerPosition } from './lexer';
10
10
  export { ATTR_OPERATOR_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_NONE, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, } from './arena';
11
11
  export * from './constants';
12
12
  export * from './token-types';
13
+ export { NODE_TYPES } from './constants';
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ export { parse_selector } from './parse-selector.js';
3
3
  export { parse_atrule_prelude } from './parse-atrule-prelude.js';
4
4
  export { parse_value } from './parse-value.js';
5
5
  export { tokenize } from './tokenize.js';
6
- export { traverse, walk } from './walk.js';
6
+ export { BREAK, SKIP, traverse, walk } from './walk.js';
7
7
  export { CSSNode, TYPE_NAMES } from './css-node.js';
8
8
  export { ATTRIBUTE_SELECTOR, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, ATTR_FLAG_NONE, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_NONE, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, AT_RULE, BLOCK, CLASS_SELECTOR, COMBINATOR, COMMENT, CONTAINER_QUERY, DECLARATION, DIMENSION, FLAG_IMPORTANT, FUNCTION, HASH, IDENTIFIER, ID_SELECTOR, LANG_SELECTOR, LAYER_NAME, MEDIA_FEATURE, MEDIA_QUERY, MEDIA_TYPE, NESTING_SELECTOR, NTH_OF_SELECTOR, NTH_SELECTOR, NUMBER, OPERATOR, PARENTHESIS, PRELUDE_OPERATOR, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, SELECTOR, SELECTOR_LIST, STRING, STYLESHEET, STYLE_RULE, SUPPORTS_QUERY, TYPE_SELECTOR, UNIVERSAL_SELECTOR, URL } from './arena.js';
9
+ export { NODE_TYPES } from './constants.js';
9
10
  export { TOKEN_AT_KEYWORD, TOKEN_BAD_STRING, TOKEN_BAD_URL, TOKEN_CDC, TOKEN_CDO, TOKEN_COLON, TOKEN_COMMA, TOKEN_COMMENT, TOKEN_DELIM, TOKEN_DIMENSION, TOKEN_EOF, TOKEN_FUNCTION, TOKEN_HASH, TOKEN_IDENT, TOKEN_LEFT_BRACE, TOKEN_LEFT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_NUMBER, TOKEN_PERCENTAGE, TOKEN_RIGHT_BRACE, TOKEN_RIGHT_BRACKET, TOKEN_RIGHT_PAREN, TOKEN_SEMICOLON, TOKEN_STRING, TOKEN_URL, TOKEN_WHITESPACE } from './token-types.js';
@@ -2,7 +2,7 @@ import { Lexer } from './lexer.js';
2
2
  import { CSSDataArena, SELECTOR_LIST, SELECTOR, COMBINATOR, NESTING_SELECTOR, ID_SELECTOR, TYPE_SELECTOR, UNIVERSAL_SELECTOR, CLASS_SELECTOR, ATTRIBUTE_SELECTOR, ATTR_OPERATOR_NONE, ATTR_FLAG_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, PSEUDO_ELEMENT_SELECTOR, PSEUDO_CLASS_SELECTOR, FLAG_HAS_PARENS, LANG_SELECTOR, NTH_OF_SELECTOR } from './arena.js';
3
3
  import { TOKEN_COMMA, TOKEN_DELIM, TOKEN_EOF, TOKEN_WHITESPACE, TOKEN_FUNCTION, TOKEN_COLON, TOKEN_LEFT_BRACKET, TOKEN_HASH, TOKEN_IDENT, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_STRING } from './token-types.js';
4
4
  import { skip_whitespace_and_comments_forward, skip_whitespace_and_comments_backward, skip_whitespace_forward } from './parse-utils.js';
5
- import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, str_equals } from './string-utils.js';
5
+ import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_TAB, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, str_equals } from './string-utils.js';
6
6
  import { ANplusBParser } from './parse-anplusb.js';
7
7
  import { CSSNode } from './css-node.js';
8
8
 
@@ -108,7 +108,10 @@ class SelectorParser {
108
108
  }
109
109
  const saved = this.lexer.save_position();
110
110
  this.skip_whitespace();
111
- if (this.lexer.pos >= this.selector_end) break;
111
+ if (this.lexer.pos >= this.selector_end) {
112
+ this.lexer.restore_position(saved);
113
+ break;
114
+ }
112
115
  this.lexer.next_token_fast(false);
113
116
  let token_type = this.lexer.token_type;
114
117
  if (token_type === TOKEN_COMMA || this.lexer.pos >= this.selector_end) {
@@ -239,14 +242,17 @@ class SelectorParser {
239
242
  let has_whitespace = false;
240
243
  while (this.lexer.pos < this.selector_end) {
241
244
  let ch = this.source.charCodeAt(this.lexer.pos);
242
- if (ch === CHAR_SPACE || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED) {
245
+ if (ch === CHAR_SPACE || ch === CHAR_TAB || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED) {
243
246
  has_whitespace = true;
244
247
  this.lexer.pos++;
245
248
  } else {
246
249
  break;
247
250
  }
248
251
  }
249
- if (this.lexer.pos >= this.selector_end) return null;
252
+ if (this.lexer.pos >= this.selector_end) {
253
+ this.lexer.pos = whitespace_start;
254
+ return null;
255
+ }
250
256
  this.lexer.next_token_fast(false);
251
257
  if (this.lexer.token_type === TOKEN_DELIM) {
252
258
  let ch = this.source.charCodeAt(this.lexer.token_start);
@@ -258,7 +264,7 @@ class SelectorParser {
258
264
  this.lexer.pos = whitespace_start;
259
265
  while (this.lexer.pos < this.selector_end) {
260
266
  let ch = this.source.charCodeAt(this.lexer.pos);
261
- if (ch === CHAR_SPACE || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED) {
267
+ if (ch === CHAR_SPACE || ch === CHAR_TAB || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED) {
262
268
  this.lexer.pos++;
263
269
  } else {
264
270
  break;
package/dist/walk.d.ts CHANGED
@@ -1,20 +1,74 @@
1
1
  import type { CSSNode } from './css-node';
2
- type WalkCallback = (node: CSSNode, depth: number) => void;
2
+ export declare const SKIP: unique symbol;
3
+ export declare const BREAK: unique symbol;
4
+ type WalkCallback = (node: CSSNode, depth: number) => void | typeof SKIP | typeof BREAK;
3
5
  /**
4
6
  * Walk the AST in depth-first order, calling the callback for each node
7
+ *
5
8
  * @param node - The root node to start walking from
6
- * @param callback - Function to call for each node visited. Receives the node and its depth (0 for root)
9
+ * @param callback - Function to call for each node visited. Receives the node and its depth (0 for root).
10
+ * Return SKIP to skip children of current node, or BREAK to stop traversal entirely.
11
+ * @param depth - Starting depth (default: 0)
12
+ *
13
+ * @example
14
+ * import { parse, walk, SKIP, BREAK } from '@projectwallace/css-parser'
15
+ *
16
+ * const ast = parse('.a { .b { .c { color: red; } } }')
17
+ *
18
+ * // Skip nested rules
19
+ * walk(ast, (node) => {
20
+ * if (node.type === STYLE_RULE) {
21
+ * console.log(node.text)
22
+ * return SKIP // Don't visit nested rules
23
+ * }
24
+ * })
25
+ * // Output: .a { ... }, but not .b or .c
26
+ *
27
+ * // Stop on first declaration
28
+ * walk(ast, (node) => {
29
+ * if (node.type === DECLARATION) {
30
+ * console.log(node.name)
31
+ * return BREAK // Stop traversal
32
+ * }
33
+ * })
7
34
  */
8
- export declare function walk(node: CSSNode, callback: WalkCallback, depth?: number): void;
9
- type WalkEnterLeaveCallback = (node: CSSNode) => void;
35
+ export declare function walk(node: CSSNode, callback: WalkCallback, depth?: number): boolean;
36
+ type WalkEnterLeaveCallback = (node: CSSNode) => void | typeof SKIP | typeof BREAK;
10
37
  interface WalkEnterLeaveOptions {
11
38
  enter?: WalkEnterLeaveCallback;
12
39
  leave?: WalkEnterLeaveCallback;
13
40
  }
14
41
  /**
15
42
  * Walk the AST in depth-first order, calling enter before visiting children and leave after
43
+ *
16
44
  * @param node - The root node to start walking from
17
45
  * @param options - Object with optional enter and leave callback functions
46
+ * @param options.enter - Called before visiting children. Return SKIP to skip children (leave still called),
47
+ * or BREAK to stop traversal entirely (leave NOT called).
48
+ * @param options.leave - Called after visiting children. Return BREAK to stop traversal.
49
+ *
50
+ * @example
51
+ * import { parse, traverse, SKIP, BREAK } from '@projectwallace/css-parser'
52
+ *
53
+ * const ast = parse('@media screen { .a { color: red; } }')
54
+ *
55
+ * // Track context with skip
56
+ * let depth = 0
57
+ * traverse(ast, {
58
+ * enter(node) {
59
+ * depth++
60
+ * if (node.type === AT_RULE) {
61
+ * console.log('Entering media query at depth', depth)
62
+ * return SKIP // Skip contents but still call leave
63
+ * }
64
+ * },
65
+ * leave(node) {
66
+ * if (node.type === AT_RULE) {
67
+ * console.log('Leaving media query at depth', depth)
68
+ * }
69
+ * depth--
70
+ * }
71
+ * })
18
72
  */
19
- export declare function traverse(node: CSSNode, { enter, leave }?: WalkEnterLeaveOptions): void;
73
+ export declare function traverse(node: CSSNode, { enter, leave }?: WalkEnterLeaveOptions): boolean;
20
74
  export {};
package/dist/walk.js CHANGED
@@ -1,21 +1,45 @@
1
+ const SKIP = Symbol("SKIP");
2
+ const BREAK = Symbol("BREAK");
1
3
  function walk(node, callback, depth = 0) {
2
- callback(node, depth);
4
+ const result = callback(node, depth);
5
+ if (result === BREAK) {
6
+ return false;
7
+ }
8
+ if (result === SKIP) {
9
+ return true;
10
+ }
3
11
  let child = node.first_child;
4
12
  while (child) {
5
- walk(child, callback, depth + 1);
13
+ const should_continue = walk(child, callback, depth + 1);
14
+ if (!should_continue) {
15
+ return false;
16
+ }
6
17
  child = child.next_sibling;
7
18
  }
19
+ return true;
8
20
  }
9
21
  const NOOP = function() {
10
22
  };
11
23
  function traverse(node, { enter = NOOP, leave = NOOP } = {}) {
12
- enter(node);
13
- let child = node.first_child;
14
- while (child) {
15
- traverse(child, { enter, leave });
16
- child = child.next_sibling;
24
+ const enter_result = enter(node);
25
+ if (enter_result === BREAK) {
26
+ return false;
27
+ }
28
+ if (enter_result !== SKIP) {
29
+ let child = node.first_child;
30
+ while (child) {
31
+ const should_continue = traverse(child, { enter, leave });
32
+ if (!should_continue) {
33
+ return false;
34
+ }
35
+ child = child.next_sibling;
36
+ }
37
+ }
38
+ const leave_result = leave(node);
39
+ if (leave_result === BREAK) {
40
+ return false;
17
41
  }
18
- leave(node);
42
+ return true;
19
43
  }
20
44
 
21
- export { traverse, walk };
45
+ export { BREAK, SKIP, traverse, walk };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectwallace/css-parser",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "High-performance CSS lexer and parser, optimized for CSS inspection and analysis",
5
5
  "author": "Bart Veneman <bat@projectwallace.com>",
6
6
  "license": "MIT",