@projectwallace/css-parser 0.4.0 → 0.6.0

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,13 +1,624 @@
1
- import { K as CSSDataArena, d as NODE_SELECTOR, C as CSSNode } from './string-utils-tMt2O9RW.js';
2
- import { S as SelectorParser } from './selector-parser-Wo-1tJbU.js';
1
+ import { L as Lexer, q as TOKEN_COMMA, h as TOKEN_DELIM, y as TOKEN_EOF, l as TOKEN_WHITESPACE, a as TOKEN_FUNCTION, o as TOKEN_COLON, r as TOKEN_LEFT_BRACKET, c as TOKEN_HASH, T as TOKEN_IDENT, s as TOKEN_RIGHT_BRACKET, t as TOKEN_LEFT_PAREN, u as TOKEN_RIGHT_PAREN, d as TOKEN_STRING } from './lexer-CtBKgfVv.js';
2
+ import { T as CSSDataArena, s as NODE_SELECTOR_LIST, C as CSSNode, j as NODE_SELECTOR, $ as CHAR_GREATER_THAN, a0 as CHAR_PLUS, a1 as CHAR_TILDE, a2 as CHAR_PERIOD, a3 as CHAR_ASTERISK, a4 as CHAR_AMPERSAND, a5 as is_whitespace, a6 as is_combinator, u as NODE_SELECTOR_CLASS, w as NODE_SELECTOR_ATTRIBUTE, a7 as skip_whitespace_and_comments_forward, a8 as skip_whitespace_and_comments_backward, a9 as CHAR_EQUALS, aa as CHAR_PIPE, ab as CHAR_CARET, ac as CHAR_DOLLAR, A as ATTR_OPERATOR_NONE, a as ATTR_OPERATOR_EQUAL, b as ATTR_OPERATOR_TILDE_EQUAL, c as ATTR_OPERATOR_PIPE_EQUAL, d as ATTR_OPERATOR_CARET_EQUAL, e as ATTR_OPERATOR_DOLLAR_EQUAL, f as ATTR_OPERATOR_STAR_EQUAL, ad as CHAR_SINGLE_QUOTE, ae as CHAR_DOUBLE_QUOTE, af as CHAR_COLON, y as NODE_SELECTOR_PSEUDO_ELEMENT, x as NODE_SELECTOR_PSEUDO_CLASS, X as is_vendor_prefixed, Y as FLAG_VENDOR_PREFIXED, _ as NODE_SELECTOR_LANG, ag as skip_whitespace_forward, F as NODE_SELECTOR_NTH_OF, t as NODE_SELECTOR_TYPE, v as NODE_SELECTOR_ID, B as NODE_SELECTOR_UNIVERSAL, D as NODE_SELECTOR_NESTING, z as NODE_SELECTOR_COMBINATOR } from './css-node-BzCSxoLM.js';
3
+ import { ANplusBParser } from './parse-anplusb.js';
3
4
 
5
+ class SelectorParser {
6
+ lexer;
7
+ arena;
8
+ source;
9
+ selector_end;
10
+ constructor(arena, source) {
11
+ this.arena = arena;
12
+ this.source = source;
13
+ this.lexer = new Lexer(source, false);
14
+ this.selector_end = 0;
15
+ }
16
+ // Parse a selector range into selector nodes
17
+ // Always returns a NODE_SELECTOR_LIST with selector components as children
18
+ parse_selector(start, end, line = 1, column = 1, allow_relative = false) {
19
+ this.selector_end = end;
20
+ this.lexer.pos = start;
21
+ this.lexer.line = line;
22
+ this.lexer.column = column;
23
+ return this.parse_selector_list(allow_relative);
24
+ }
25
+ // Parse comma-separated selectors
26
+ parse_selector_list(allow_relative = false) {
27
+ let selectors = [];
28
+ let list_start = this.lexer.pos;
29
+ let list_line = this.lexer.line;
30
+ let list_column = this.lexer.column;
31
+ while (this.lexer.pos < this.selector_end) {
32
+ let selector_start = this.lexer.pos;
33
+ let selector_line = this.lexer.line;
34
+ let selector_column = this.lexer.column;
35
+ let complex_selector = this.parse_complex_selector(allow_relative);
36
+ if (complex_selector !== null) {
37
+ let selector_wrapper = this.arena.create_node();
38
+ this.arena.set_type(selector_wrapper, NODE_SELECTOR);
39
+ this.arena.set_start_offset(selector_wrapper, selector_start);
40
+ this.arena.set_length(selector_wrapper, this.lexer.pos - selector_start);
41
+ this.arena.set_start_line(selector_wrapper, selector_line);
42
+ this.arena.set_start_column(selector_wrapper, selector_column);
43
+ let last_component = complex_selector;
44
+ while (this.arena.get_next_sibling(last_component) !== 0) {
45
+ last_component = this.arena.get_next_sibling(last_component);
46
+ }
47
+ this.arena.set_first_child(selector_wrapper, complex_selector);
48
+ this.arena.set_last_child(selector_wrapper, last_component);
49
+ selectors.push(selector_wrapper);
50
+ }
51
+ this.skip_whitespace();
52
+ if (this.lexer.pos >= this.selector_end) break;
53
+ this.lexer.next_token_fast(false);
54
+ let token_type = this.lexer.token_type;
55
+ if (token_type === TOKEN_COMMA) {
56
+ this.skip_whitespace();
57
+ continue;
58
+ } else {
59
+ break;
60
+ }
61
+ }
62
+ if (selectors.length >= 1) {
63
+ let list_node = this.arena.create_node();
64
+ this.arena.set_type(list_node, NODE_SELECTOR_LIST);
65
+ this.arena.set_start_offset(list_node, list_start);
66
+ this.arena.set_length(list_node, this.lexer.pos - list_start);
67
+ this.arena.set_start_line(list_node, list_line);
68
+ this.arena.set_start_column(list_node, list_column);
69
+ this.arena.set_first_child(list_node, selectors[0]);
70
+ this.arena.set_last_child(list_node, selectors[selectors.length - 1]);
71
+ for (let i = 0; i < selectors.length - 1; i++) {
72
+ this.arena.set_next_sibling(selectors[i], selectors[i + 1]);
73
+ }
74
+ return list_node;
75
+ }
76
+ return null;
77
+ }
78
+ // Parse a complex selector (with combinators)
79
+ // e.g., "div.class > p + span"
80
+ parse_complex_selector(allow_relative = false) {
81
+ let components = [];
82
+ this.skip_whitespace();
83
+ if (allow_relative && this.lexer.pos < this.selector_end) {
84
+ const saved = this.lexer.save_position();
85
+ this.lexer.next_token_fast(false);
86
+ let token_type = this.lexer.token_type;
87
+ if (token_type === TOKEN_DELIM) {
88
+ let ch = this.source.charCodeAt(this.lexer.token_start);
89
+ if (ch === CHAR_GREATER_THAN || ch === CHAR_PLUS || ch === CHAR_TILDE) {
90
+ let combinator = this.create_combinator(this.lexer.token_start, this.lexer.token_end);
91
+ components.push(combinator);
92
+ this.skip_whitespace();
93
+ } else {
94
+ this.lexer.restore_position(saved);
95
+ }
96
+ } else {
97
+ this.lexer.restore_position(saved);
98
+ }
99
+ }
100
+ while (this.lexer.pos < this.selector_end) {
101
+ if (this.lexer.pos >= this.selector_end) break;
102
+ let compound = this.parse_compound_selector();
103
+ if (compound !== null) {
104
+ components.push(compound);
105
+ } else {
106
+ break;
107
+ }
108
+ let combinator = this.try_parse_combinator();
109
+ if (combinator !== null) {
110
+ components.push(combinator);
111
+ this.skip_whitespace();
112
+ continue;
113
+ }
114
+ const saved = this.lexer.save_position();
115
+ this.skip_whitespace();
116
+ if (this.lexer.pos >= this.selector_end) break;
117
+ this.lexer.next_token_fast(false);
118
+ let token_type = this.lexer.token_type;
119
+ if (token_type === TOKEN_COMMA || this.lexer.pos >= this.selector_end) {
120
+ this.lexer.restore_position(saved);
121
+ break;
122
+ }
123
+ this.lexer.restore_position(saved);
124
+ break;
125
+ }
126
+ if (components.length === 0) return null;
127
+ for (let i = 0; i < components.length - 1; i++) {
128
+ let last_node = components[i];
129
+ while (this.arena.get_next_sibling(last_node) !== 0) {
130
+ last_node = this.arena.get_next_sibling(last_node);
131
+ }
132
+ this.arena.set_next_sibling(last_node, components[i + 1]);
133
+ }
134
+ return components[0];
135
+ }
136
+ // Parse a compound selector (no combinators)
137
+ // e.g., "div.class#id[attr]:hover"
138
+ parse_compound_selector() {
139
+ let parts = [];
140
+ while (this.lexer.pos < this.selector_end) {
141
+ const saved = this.lexer.save_position();
142
+ this.lexer.next_token_fast(false);
143
+ if (this.lexer.token_start >= this.selector_end) break;
144
+ let token_type = this.lexer.token_type;
145
+ if (token_type === TOKEN_EOF) break;
146
+ let part = this.parse_simple_selector();
147
+ if (part !== null) {
148
+ parts.push(part);
149
+ } else {
150
+ this.lexer.restore_position(saved);
151
+ break;
152
+ }
153
+ }
154
+ if (parts.length === 0) return null;
155
+ for (let i = 0; i < parts.length - 1; i++) {
156
+ this.arena.set_next_sibling(parts[i], parts[i + 1]);
157
+ }
158
+ return parts[0];
159
+ }
160
+ // Parse a simple selector (single component)
161
+ parse_simple_selector() {
162
+ let token_type = this.lexer.token_type;
163
+ let start = this.lexer.token_start;
164
+ let end = this.lexer.token_end;
165
+ switch (token_type) {
166
+ case TOKEN_IDENT:
167
+ return this.create_type_selector(start, end);
168
+ case TOKEN_HASH:
169
+ return this.create_id_selector(start, end);
170
+ case TOKEN_DELIM:
171
+ let ch = this.source.charCodeAt(start);
172
+ if (ch === CHAR_PERIOD) {
173
+ return this.parse_class_selector(start);
174
+ } else if (ch === CHAR_ASTERISK) {
175
+ return this.create_universal_selector(start, end);
176
+ } else if (ch === CHAR_AMPERSAND) {
177
+ return this.create_nesting_selector(start, end);
178
+ }
179
+ return null;
180
+ case TOKEN_LEFT_BRACKET:
181
+ return this.parse_attribute_selector(start);
182
+ case TOKEN_COLON:
183
+ return this.parse_pseudo(start);
184
+ case TOKEN_FUNCTION:
185
+ return this.parse_pseudo_function(start, end);
186
+ case TOKEN_WHITESPACE:
187
+ case TOKEN_COMMA:
188
+ return null;
189
+ default:
190
+ return null;
191
+ }
192
+ }
193
+ // Parse combinator (>, +, ~, or descendant space)
194
+ try_parse_combinator() {
195
+ let whitespace_start = this.lexer.pos;
196
+ let has_whitespace = false;
197
+ while (this.lexer.pos < this.selector_end) {
198
+ let ch = this.source.charCodeAt(this.lexer.pos);
199
+ if (is_whitespace(ch)) {
200
+ has_whitespace = true;
201
+ this.lexer.pos++;
202
+ } else {
203
+ break;
204
+ }
205
+ }
206
+ if (this.lexer.pos >= this.selector_end) return null;
207
+ this.lexer.next_token_fast(false);
208
+ if (this.lexer.token_type === TOKEN_DELIM) {
209
+ let ch = this.source.charCodeAt(this.lexer.token_start);
210
+ if (is_combinator(ch)) {
211
+ return this.create_combinator(this.lexer.token_start, this.lexer.token_end);
212
+ }
213
+ }
214
+ if (has_whitespace) {
215
+ this.lexer.pos = whitespace_start;
216
+ while (this.lexer.pos < this.selector_end) {
217
+ let ch = this.source.charCodeAt(this.lexer.pos);
218
+ if (is_whitespace(ch)) {
219
+ this.lexer.pos++;
220
+ } else {
221
+ break;
222
+ }
223
+ }
224
+ return this.create_combinator(whitespace_start, this.lexer.pos);
225
+ }
226
+ this.lexer.pos = whitespace_start;
227
+ return null;
228
+ }
229
+ // Parse class selector (.classname)
230
+ parse_class_selector(dot_pos) {
231
+ const saved = this.lexer.save_position();
232
+ this.lexer.next_token_fast(false);
233
+ if (this.lexer.token_type !== TOKEN_IDENT) {
234
+ this.lexer.restore_position(saved);
235
+ return null;
236
+ }
237
+ let node = this.arena.create_node();
238
+ this.arena.set_type(node, NODE_SELECTOR_CLASS);
239
+ this.arena.set_start_offset(node, dot_pos);
240
+ this.arena.set_length(node, this.lexer.token_end - dot_pos);
241
+ this.arena.set_start_line(node, this.lexer.line);
242
+ this.arena.set_start_column(node, this.lexer.column);
243
+ this.arena.set_content_start(node, this.lexer.token_start);
244
+ this.arena.set_content_length(node, this.lexer.token_end - this.lexer.token_start);
245
+ return node;
246
+ }
247
+ // Parse attribute selector ([attr], [attr=value], etc.)
248
+ parse_attribute_selector(start) {
249
+ let bracket_depth = 1;
250
+ let end = this.lexer.token_end;
251
+ let content_start = start + 1;
252
+ let content_end = content_start;
253
+ while (this.lexer.pos < this.selector_end && bracket_depth > 0) {
254
+ this.lexer.next_token_fast(false);
255
+ let token_type = this.lexer.token_type;
256
+ if (token_type === TOKEN_LEFT_BRACKET) {
257
+ bracket_depth++;
258
+ } else if (token_type === TOKEN_RIGHT_BRACKET) {
259
+ bracket_depth--;
260
+ if (bracket_depth === 0) {
261
+ content_end = this.lexer.token_start;
262
+ end = this.lexer.token_end;
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ let node = this.arena.create_node();
268
+ this.arena.set_type(node, NODE_SELECTOR_ATTRIBUTE);
269
+ this.arena.set_start_offset(node, start);
270
+ this.arena.set_length(node, end - start);
271
+ this.arena.set_start_line(node, this.lexer.line);
272
+ this.arena.set_start_column(node, this.lexer.column);
273
+ this.parse_attribute_content(node, content_start, content_end);
274
+ return node;
275
+ }
276
+ // Parse attribute content to extract name, operator, and value
277
+ parse_attribute_content(node, start, end) {
278
+ start = skip_whitespace_and_comments_forward(this.source, start, end);
279
+ end = skip_whitespace_and_comments_backward(this.source, end, start);
280
+ if (start >= end) return;
281
+ let name_start = start;
282
+ let name_end = start;
283
+ let operator_end = -1;
284
+ let value_start = -1;
285
+ let value_end = -1;
286
+ while (name_end < end) {
287
+ let ch2 = this.source.charCodeAt(name_end);
288
+ if (is_whitespace(ch2) || ch2 === CHAR_EQUALS || ch2 === CHAR_TILDE || ch2 === CHAR_PIPE || ch2 === CHAR_CARET || ch2 === CHAR_DOLLAR || ch2 === CHAR_ASTERISK) {
289
+ break;
290
+ }
291
+ name_end++;
292
+ }
293
+ if (name_end > name_start) {
294
+ this.arena.set_content_start(node, name_start);
295
+ this.arena.set_content_length(node, name_end - name_start);
296
+ }
297
+ let pos = skip_whitespace_and_comments_forward(this.source, name_end, end);
298
+ if (pos >= end) {
299
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_NONE);
300
+ return;
301
+ }
302
+ let ch1 = this.source.charCodeAt(pos);
303
+ if (ch1 === CHAR_EQUALS) {
304
+ operator_end = pos + 1;
305
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_EQUAL);
306
+ } else if (ch1 === CHAR_TILDE && pos + 1 < end && this.source.charCodeAt(pos + 1) === CHAR_EQUALS) {
307
+ operator_end = pos + 2;
308
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_TILDE_EQUAL);
309
+ } else if (ch1 === CHAR_PIPE && pos + 1 < end && this.source.charCodeAt(pos + 1) === CHAR_EQUALS) {
310
+ operator_end = pos + 2;
311
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_PIPE_EQUAL);
312
+ } else if (ch1 === CHAR_CARET && pos + 1 < end && this.source.charCodeAt(pos + 1) === CHAR_EQUALS) {
313
+ operator_end = pos + 2;
314
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_CARET_EQUAL);
315
+ } else if (ch1 === CHAR_DOLLAR && pos + 1 < end && this.source.charCodeAt(pos + 1) === CHAR_EQUALS) {
316
+ operator_end = pos + 2;
317
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_DOLLAR_EQUAL);
318
+ } else if (ch1 === CHAR_ASTERISK && pos + 1 < end && this.source.charCodeAt(pos + 1) === CHAR_EQUALS) {
319
+ operator_end = pos + 2;
320
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_STAR_EQUAL);
321
+ } else {
322
+ this.arena.set_attr_operator(node, ATTR_OPERATOR_NONE);
323
+ return;
324
+ }
325
+ pos = skip_whitespace_and_comments_forward(this.source, operator_end, end);
326
+ if (pos >= end) {
327
+ return;
328
+ }
329
+ value_start = pos;
330
+ let ch = this.source.charCodeAt(pos);
331
+ if (ch === CHAR_SINGLE_QUOTE || ch === CHAR_DOUBLE_QUOTE) {
332
+ let quote = ch;
333
+ value_start = pos;
334
+ pos++;
335
+ while (pos < end) {
336
+ let c = this.source.charCodeAt(pos);
337
+ if (c === quote) {
338
+ pos++;
339
+ break;
340
+ }
341
+ if (c === 92) {
342
+ pos += 2;
343
+ } else {
344
+ pos++;
345
+ }
346
+ }
347
+ value_end = pos;
348
+ } else {
349
+ while (pos < end) {
350
+ let c = this.source.charCodeAt(pos);
351
+ if (is_whitespace(c)) {
352
+ break;
353
+ }
354
+ pos++;
355
+ }
356
+ value_end = pos;
357
+ }
358
+ if (value_end > value_start) {
359
+ this.arena.set_value_start(node, value_start);
360
+ this.arena.set_value_length(node, value_end - value_start);
361
+ }
362
+ }
363
+ // Parse pseudo-class or pseudo-element (:hover, ::before)
364
+ parse_pseudo(start) {
365
+ const saved = this.lexer.save_position();
366
+ let is_pseudo_element = false;
367
+ if (this.lexer.pos < this.selector_end && this.source.charCodeAt(this.lexer.pos) === CHAR_COLON) {
368
+ is_pseudo_element = true;
369
+ this.lexer.pos++;
370
+ }
371
+ this.lexer.next_token_fast(false);
372
+ let token_type = this.lexer.token_type;
373
+ if (token_type === TOKEN_IDENT) {
374
+ let node = this.arena.create_node();
375
+ this.arena.set_type(node, is_pseudo_element ? NODE_SELECTOR_PSEUDO_ELEMENT : NODE_SELECTOR_PSEUDO_CLASS);
376
+ this.arena.set_start_offset(node, start);
377
+ this.arena.set_length(node, this.lexer.token_end - start);
378
+ this.arena.set_start_line(node, this.lexer.line);
379
+ this.arena.set_start_column(node, this.lexer.column);
380
+ this.arena.set_content_start(node, this.lexer.token_start);
381
+ this.arena.set_content_length(node, this.lexer.token_end - this.lexer.token_start);
382
+ if (is_vendor_prefixed(this.source, this.lexer.token_start, this.lexer.token_end)) {
383
+ this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
384
+ }
385
+ return node;
386
+ } else if (token_type === TOKEN_FUNCTION) {
387
+ return this.parse_pseudo_function_after_colon(start, is_pseudo_element);
388
+ }
389
+ this.lexer.restore_position(saved);
390
+ return null;
391
+ }
392
+ // Parse pseudo-class function (:nth-child(), :is(), etc.)
393
+ parse_pseudo_function(_start, _end) {
394
+ return null;
395
+ }
396
+ // Parse pseudo-class function after we've seen the colon
397
+ parse_pseudo_function_after_colon(start, is_pseudo_element) {
398
+ let func_name_start = this.lexer.token_start;
399
+ let func_name_end = this.lexer.token_end - 1;
400
+ let content_start = this.lexer.pos;
401
+ let content_end = content_start;
402
+ let paren_depth = 1;
403
+ let end = this.lexer.token_end;
404
+ while (this.lexer.pos < this.selector_end && paren_depth > 0) {
405
+ this.lexer.next_token_fast(false);
406
+ let token_type = this.lexer.token_type;
407
+ if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) {
408
+ paren_depth++;
409
+ } else if (token_type === TOKEN_RIGHT_PAREN) {
410
+ paren_depth--;
411
+ if (paren_depth === 0) {
412
+ content_end = this.lexer.token_start;
413
+ end = this.lexer.token_end;
414
+ break;
415
+ }
416
+ }
417
+ }
418
+ let node = this.arena.create_node();
419
+ this.arena.set_type(node, is_pseudo_element ? NODE_SELECTOR_PSEUDO_ELEMENT : NODE_SELECTOR_PSEUDO_CLASS);
420
+ this.arena.set_start_offset(node, start);
421
+ this.arena.set_length(node, end - start);
422
+ this.arena.set_start_line(node, this.lexer.line);
423
+ this.arena.set_start_column(node, this.lexer.column);
424
+ this.arena.set_content_start(node, func_name_start);
425
+ this.arena.set_content_length(node, func_name_end - func_name_start);
426
+ if (is_vendor_prefixed(this.source, func_name_start, func_name_end)) {
427
+ this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
428
+ }
429
+ if (content_end > content_start) {
430
+ let func_name = this.source.substring(func_name_start, func_name_end).toLowerCase();
431
+ if (this.is_nth_pseudo(func_name)) {
432
+ let child = this.parse_nth_expression(content_start, content_end);
433
+ if (child !== null) {
434
+ this.arena.set_first_child(node, child);
435
+ this.arena.set_last_child(node, child);
436
+ }
437
+ } else if (func_name === "lang") {
438
+ this.parse_lang_identifiers(content_start, content_end, node);
439
+ } else {
440
+ let saved_selector_end = this.selector_end;
441
+ const saved = this.lexer.save_position();
442
+ let allow_relative = func_name === "has";
443
+ let child_selector = this.parse_selector(content_start, content_end, this.lexer.line, this.lexer.column, allow_relative);
444
+ this.selector_end = saved_selector_end;
445
+ this.lexer.restore_position(saved);
446
+ if (child_selector !== null) {
447
+ this.arena.set_first_child(node, child_selector);
448
+ this.arena.set_last_child(node, child_selector);
449
+ }
450
+ }
451
+ }
452
+ return node;
453
+ }
454
+ // Check if pseudo-class name is an nth-* pseudo
455
+ is_nth_pseudo(name) {
456
+ return name === "nth-child" || name === "nth-last-child" || name === "nth-of-type" || name === "nth-last-of-type" || name === "nth-col" || name === "nth-last-col";
457
+ }
458
+ // Parse :lang() content - comma-separated language identifiers
459
+ // Accepts both quoted strings: :lang("en", "fr") and unquoted: :lang(en, fr)
460
+ parse_lang_identifiers(start, end, parent_node) {
461
+ let saved_selector_end = this.selector_end;
462
+ const saved = this.lexer.save_position();
463
+ this.lexer.pos = start;
464
+ this.selector_end = end;
465
+ let first_child = null;
466
+ let last_child = null;
467
+ while (this.lexer.pos < end) {
468
+ this.lexer.next_token_fast(false);
469
+ let token_type = this.lexer.token_type;
470
+ let token_start = this.lexer.token_start;
471
+ let token_end = this.lexer.token_end;
472
+ if (token_type === TOKEN_WHITESPACE) {
473
+ continue;
474
+ }
475
+ if (token_type === TOKEN_COMMA) {
476
+ continue;
477
+ }
478
+ if (token_type === TOKEN_STRING || token_type === TOKEN_IDENT) {
479
+ let lang_node = this.arena.create_node();
480
+ this.arena.set_type(lang_node, NODE_SELECTOR_LANG);
481
+ this.arena.set_start_offset(lang_node, token_start);
482
+ this.arena.set_length(lang_node, token_end - token_start);
483
+ this.arena.set_start_line(lang_node, this.lexer.line);
484
+ this.arena.set_start_column(lang_node, this.lexer.column);
485
+ if (first_child === null) {
486
+ first_child = lang_node;
487
+ }
488
+ if (last_child !== null) {
489
+ this.arena.set_next_sibling(last_child, lang_node);
490
+ }
491
+ last_child = lang_node;
492
+ }
493
+ if (this.lexer.pos >= end) {
494
+ break;
495
+ }
496
+ }
497
+ if (first_child !== null) {
498
+ this.arena.set_first_child(parent_node, first_child);
499
+ }
500
+ if (last_child !== null) {
501
+ this.arena.set_last_child(parent_node, last_child);
502
+ }
503
+ this.selector_end = saved_selector_end;
504
+ this.lexer.restore_position(saved);
505
+ }
506
+ // Parse An+B expression for nth-* pseudo-classes
507
+ // Handles both simple An+B and "An+B of S" syntax
508
+ parse_nth_expression(start, end) {
509
+ let of_index = this.find_of_keyword(start, end);
510
+ if (of_index !== -1) {
511
+ let anplusb_parser = new ANplusBParser(this.arena, this.source);
512
+ let anplusb_node = anplusb_parser.parse_anplusb(start, of_index, this.lexer.line);
513
+ let selector_start = of_index + 2;
514
+ selector_start = skip_whitespace_forward(this.source, selector_start, end);
515
+ let saved_selector_end = this.selector_end;
516
+ const saved = this.lexer.save_position();
517
+ this.selector_end = end;
518
+ this.lexer.pos = selector_start;
519
+ let selector_list = this.parse_selector_list();
520
+ this.selector_end = saved_selector_end;
521
+ this.lexer.restore_position(saved);
522
+ let of_node = this.arena.create_node();
523
+ this.arena.set_type(of_node, NODE_SELECTOR_NTH_OF);
524
+ this.arena.set_start_offset(of_node, start);
525
+ this.arena.set_length(of_node, end - start);
526
+ this.arena.set_start_line(of_node, this.lexer.line);
527
+ if (anplusb_node !== null && selector_list !== null) {
528
+ this.arena.set_first_child(of_node, anplusb_node);
529
+ this.arena.set_last_child(of_node, selector_list);
530
+ this.arena.set_next_sibling(anplusb_node, selector_list);
531
+ } else if (anplusb_node !== null) {
532
+ this.arena.set_first_child(of_node, anplusb_node);
533
+ this.arena.set_last_child(of_node, anplusb_node);
534
+ }
535
+ return of_node;
536
+ } else {
537
+ let anplusb_parser = new ANplusBParser(this.arena, this.source);
538
+ return anplusb_parser.parse_anplusb(start, end, this.lexer.line);
539
+ }
540
+ }
541
+ // Find the position of standalone "of" keyword
542
+ find_of_keyword(start, end) {
543
+ for (let i = start; i < end - 1; i++) {
544
+ if (this.source.charCodeAt(i) === 111 && this.source.charCodeAt(i + 1) === 102) {
545
+ let before_ok = i === start || is_whitespace(this.source.charCodeAt(i - 1));
546
+ let after_ok = i + 2 >= end || is_whitespace(this.source.charCodeAt(i + 2));
547
+ if (before_ok && after_ok) {
548
+ return i;
549
+ }
550
+ }
551
+ }
552
+ return -1;
553
+ }
554
+ // Create simple selector nodes
555
+ create_type_selector(start, end) {
556
+ let node = this.arena.create_node();
557
+ this.arena.set_type(node, NODE_SELECTOR_TYPE);
558
+ this.arena.set_start_offset(node, start);
559
+ this.arena.set_length(node, end - start);
560
+ this.arena.set_start_line(node, this.lexer.line);
561
+ this.arena.set_start_column(node, this.lexer.column);
562
+ this.arena.set_content_start(node, start);
563
+ this.arena.set_content_length(node, end - start);
564
+ return node;
565
+ }
566
+ create_id_selector(start, end) {
567
+ let node = this.arena.create_node();
568
+ this.arena.set_type(node, NODE_SELECTOR_ID);
569
+ this.arena.set_start_offset(node, start);
570
+ this.arena.set_length(node, end - start);
571
+ this.arena.set_start_line(node, this.lexer.line);
572
+ this.arena.set_start_column(node, this.lexer.column);
573
+ this.arena.set_content_start(node, start + 1);
574
+ this.arena.set_content_length(node, end - start - 1);
575
+ return node;
576
+ }
577
+ create_universal_selector(start, end) {
578
+ let node = this.arena.create_node();
579
+ this.arena.set_type(node, NODE_SELECTOR_UNIVERSAL);
580
+ this.arena.set_start_offset(node, start);
581
+ this.arena.set_length(node, end - start);
582
+ this.arena.set_start_line(node, this.lexer.line);
583
+ this.arena.set_start_column(node, this.lexer.column);
584
+ this.arena.set_content_start(node, start);
585
+ this.arena.set_content_length(node, end - start);
586
+ return node;
587
+ }
588
+ create_nesting_selector(start, end) {
589
+ let node = this.arena.create_node();
590
+ this.arena.set_type(node, NODE_SELECTOR_NESTING);
591
+ this.arena.set_start_offset(node, start);
592
+ this.arena.set_length(node, end - start);
593
+ this.arena.set_start_line(node, this.lexer.line);
594
+ this.arena.set_start_column(node, this.lexer.column);
595
+ this.arena.set_content_start(node, start);
596
+ this.arena.set_content_length(node, end - start);
597
+ return node;
598
+ }
599
+ create_combinator(start, end) {
600
+ let node = this.arena.create_node();
601
+ this.arena.set_type(node, NODE_SELECTOR_COMBINATOR);
602
+ this.arena.set_start_offset(node, start);
603
+ this.arena.set_length(node, end - start);
604
+ this.arena.set_start_line(node, this.lexer.line);
605
+ this.arena.set_start_column(node, this.lexer.column);
606
+ this.arena.set_content_start(node, start);
607
+ this.arena.set_content_length(node, end - start);
608
+ return node;
609
+ }
610
+ // Helper to skip whitespace
611
+ skip_whitespace() {
612
+ this.lexer.pos = skip_whitespace_forward(this.source, this.lexer.pos, this.selector_end);
613
+ }
614
+ }
4
615
  function parse_selector(source) {
5
616
  const arena = new CSSDataArena(CSSDataArena.capacity_for_source(source.length));
6
617
  const selector_parser = new SelectorParser(arena, source);
7
618
  const selector_index = selector_parser.parse_selector(0, source.length);
8
619
  if (selector_index === null) {
9
620
  const empty = arena.create_node();
10
- arena.set_type(empty, NODE_SELECTOR);
621
+ arena.set_type(empty, NODE_SELECTOR_LIST);
11
622
  arena.set_start_offset(empty, 0);
12
623
  arena.set_length(empty, 0);
13
624
  arena.set_start_line(empty, 1);
@@ -16,4 +627,4 @@ function parse_selector(source) {
16
627
  return new CSSNode(arena, source, selector_index);
17
628
  }
18
629
 
19
- export { parse_selector };
630
+ export { SelectorParser, parse_selector };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Parse a dimension string into numeric value and unit
3
+ *
4
+ * @param text - Dimension text like "100px", "50%", "1.5em"
5
+ * @returns Object with value (number) and unit (string)
6
+ *
7
+ * Examples:
8
+ * - "100px" → { value: 100, unit: "px" }
9
+ * - "50%" → { value: 50, unit: "%" }
10
+ * - "1.5em" → { value: 1.5, unit: "em" }
11
+ * - "-10rem" → { value: -10, unit: "rem" }
12
+ */
13
+ export declare function parse_dimension(text: string): {
14
+ value: number;
15
+ unit: string;
16
+ };
17
+ /**
18
+ * Skip whitespace forward from a position
19
+ *
20
+ * @param source - The source string
21
+ * @param pos - Starting position
22
+ * @param end - End boundary (exclusive)
23
+ * @returns New position after skipping whitespace
24
+ */
25
+ export declare function skip_whitespace_forward(source: string, pos: number, end: number): number;
26
+ /**
27
+ * Skip whitespace and comments forward from a position
28
+ *
29
+ * @param source - The source string
30
+ * @param pos - Starting position
31
+ * @param end - End boundary (exclusive)
32
+ * @returns New position after skipping whitespace/comments
33
+ */
34
+ export declare function skip_whitespace_and_comments_forward(source: string, pos: number, end: number): number;
35
+ /**
36
+ * Skip whitespace and comments backward from a position
37
+ *
38
+ * @param source - The source string
39
+ * @param pos - Starting position (exclusive, scanning backward from pos-1)
40
+ * @param start - Start boundary (inclusive, won't go before this)
41
+ * @returns New position after skipping whitespace/comments backward
42
+ */
43
+ export declare function skip_whitespace_and_comments_backward(source: string, pos: number, start: number): number;
44
+ /**
45
+ * Trim whitespace and comments from both ends of a string range
46
+ *
47
+ * @param source - The source string
48
+ * @param start - Start offset in source
49
+ * @param end - End offset in source
50
+ * @returns [trimmed_start, trimmed_end] or null if all whitespace/comments
51
+ *
52
+ * Skips whitespace (space, tab, newline, CR, FF) and CSS comments from both ends
53
+ * of the specified range. Returns the trimmed boundaries or null if the range
54
+ * contains only whitespace and comments.
55
+ */
56
+ export declare function trim_boundaries(source: string, start: number, end: number): [number, number] | null;