@projectwallace/css-parser 0.1.0 → 0.3.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.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # CSS Parser
2
2
 
3
+ > [!WARNING]
4
+ > This is a very experimental CSS parser. Expect several bugs and inconveniences!
5
+
3
6
  **High-performance CSS parser optimized for static analysis and formatting**
4
7
 
5
8
  Built for speed and efficiency, this parser handles large CSS files with minimal memory overhead and blazing-fast parse times. Designed with a data-oriented architecture using a single contiguous memory arena for zero allocations during parsing.
@@ -10,18 +13,7 @@ Built for speed and efficiency, this parser handles large CSS files with minimal
10
13
  - **Error recovery** - Continues parsing on malformed CSS
11
14
  - **Comment preservation** - Comments stored as first-class AST nodes
12
15
  - **Location tracking** - Line, column, offset, and length for all nodes
13
- - **Vendor prefix detection** - Automatic detection of `-webkit-`, `-moz-`, etc.
14
- - **Structured parsing** - Deep parsing of selectors, values, and at-rule preludes
15
-
16
- ## Performance
17
-
18
- - **Tiny install size**
19
- - **Zero allocations during parsing** - all memory allocated upfront based on real world heuristics, which also helps prevent garbage collection running often
20
- - **Cache-friendly data layout** - contiguous memory for sequential access
21
- - **First-class comment and location support** - while still being performant because analysis requires constant access to lines and columns
22
- - **No syntax validation** - focusing only on the raw data we can skip expensive syntax files and MDN data syncs
23
-
24
- This parser was heavily influenced by [CSSTree](https://github.com/csstree/csstree), one of the most robust CSS parsers available.
16
+ - **Built-in vendor prefix detection** - Automatic detection of `-webkit-`, `-moz-`, etc. for selectors, values, properties and more
25
17
 
26
18
  ## Installation
27
19
 
@@ -63,10 +55,24 @@ for (const rule of ast) {
63
55
  }
64
56
  ```
65
57
 
58
+ ## Performance
59
+
60
+ - **Tiny install size**
61
+ - **Zero allocations during parsing** - all memory allocated upfront based on real world heuristics, which also helps prevent garbage collection running often
62
+ - **Cache-friendly data layout** - contiguous memory for sequential access powered by concepts or Data Oriented Design
63
+ - **First-class comment and location support** - while still being performant because analysis requires constant access to lines and columns
64
+ - **No syntax validation** - focusing only on the raw data we can skip expensive syntax files and MDN data syncs
65
+
66
+ This parser was heavily influenced by [CSSTree](https://github.com/csstree/csstree), one of the most robust CSS parsers available.
67
+
66
68
  ## Documentation
67
69
 
68
70
  See [API.md](./API.md) for complete documentation of all parser functions and options.
69
71
 
72
+ ## Non-goals
73
+
74
+ - **No syntax validation** - this parser does not try to validate your CSS structure. Everything can be anything
75
+
70
76
  ## License
71
77
 
72
78
  MIT
package/dist/arena.d.ts CHANGED
@@ -36,6 +36,7 @@ export declare const FLAG_IMPORTANT: number;
36
36
  export declare const FLAG_HAS_ERROR: number;
37
37
  export declare const FLAG_LENGTH_OVERFLOW: number;
38
38
  export declare const FLAG_HAS_BLOCK: number;
39
+ export declare const FLAG_VENDOR_PREFIXED: number;
39
40
  export declare class CSSDataArena {
40
41
  private buffer;
41
42
  private view;
@@ -59,6 +60,7 @@ export declare class CSSDataArena {
59
60
  get_last_child(node_index: number): number;
60
61
  get_next_sibling(node_index: number): number;
61
62
  get_start_line(node_index: number): number;
63
+ get_start_column(node_index: number): number;
62
64
  get_value_start(node_index: number): number;
63
65
  get_value_length(node_index: number): number;
64
66
  set_type(node_index: number, type: number): void;
@@ -71,6 +73,7 @@ export declare class CSSDataArena {
71
73
  set_last_child(node_index: number, childIndex: number): void;
72
74
  set_next_sibling(node_index: number, siblingIndex: number): void;
73
75
  set_start_line(node_index: number, line: number): void;
76
+ set_start_column(node_index: number, column: number): void;
74
77
  set_value_start(node_index: number, offset: number): void;
75
78
  set_value_length(node_index: number, length: number): void;
76
79
  private grow;
@@ -1,5 +1,5 @@
1
1
  import { L as Lexer, q as TOKEN_COMMA, T as TOKEN_IDENT, t as TOKEN_LEFT_PAREN, u as TOKEN_RIGHT_PAREN, l as TOKEN_WHITESPACE, f as TOKEN_URL, a as TOKEN_FUNCTION, d as TOKEN_STRING, y as TOKEN_EOF } from './lexer-DXablYMZ.js';
2
- import { O as str_equals, E as NODE_PRELUDE_OPERATOR, y as NODE_PRELUDE_MEDIA_TYPE, w as NODE_PRELUDE_MEDIA_QUERY, x as NODE_PRELUDE_MEDIA_FEATURE, M as trim_boundaries, D as NODE_PRELUDE_IDENTIFIER, z as NODE_PRELUDE_CONTAINER_QUERY, A as NODE_PRELUDE_SUPPORTS_QUERY, B as NODE_PRELUDE_LAYER_NAME, F as NODE_PRELUDE_IMPORT_URL, G as NODE_PRELUDE_IMPORT_LAYER, H as NODE_PRELUDE_IMPORT_SUPPORTS, P as CHAR_SPACE, Q as CHAR_TAB, R as CHAR_NEWLINE, S as CHAR_CARRIAGE_RETURN, T as CHAR_FORM_FEED } from './string-utils-B-rJII-E.js';
2
+ import { Q as str_equals, E as NODE_PRELUDE_OPERATOR, y as NODE_PRELUDE_MEDIA_TYPE, w as NODE_PRELUDE_MEDIA_QUERY, x as NODE_PRELUDE_MEDIA_FEATURE, P as trim_boundaries, D as NODE_PRELUDE_IDENTIFIER, z as NODE_PRELUDE_CONTAINER_QUERY, A as NODE_PRELUDE_SUPPORTS_QUERY, B as NODE_PRELUDE_LAYER_NAME, F as NODE_PRELUDE_IMPORT_URL, G as NODE_PRELUDE_IMPORT_LAYER, H as NODE_PRELUDE_IMPORT_SUPPORTS, R as CHAR_SPACE, S as CHAR_TAB, T as CHAR_NEWLINE, U as CHAR_CARRIAGE_RETURN, V as CHAR_FORM_FEED } from './string-utils-5j619JDp.js';
3
3
 
4
4
  class AtRulePreludeParser {
5
5
  lexer;
@@ -13,10 +13,11 @@ class AtRulePreludeParser {
13
13
  this.prelude_end = 0;
14
14
  }
15
15
  // Parse an at-rule prelude into nodes based on the at-rule type
16
- parse_prelude(at_rule_name, start, end, line = 1) {
16
+ parse_prelude(at_rule_name, start, end, line = 1, column = 1) {
17
17
  this.prelude_end = end;
18
18
  this.lexer.pos = start;
19
19
  this.lexer.line = line;
20
+ this.lexer.column = column;
20
21
  if (str_equals("media", at_rule_name)) {
21
22
  return this.parse_media_query_list();
22
23
  } else if (str_equals("container", at_rule_name)) {
@@ -59,6 +60,7 @@ class AtRulePreludeParser {
59
60
  parse_single_media_query() {
60
61
  let query_start = this.lexer.pos;
61
62
  let query_line = this.lexer.line;
63
+ this.lexer.column;
62
64
  this.skip_whitespace();
63
65
  if (this.lexer.pos >= this.prelude_end) return null;
64
66
  let token_start = this.lexer.pos;
@@ -98,6 +100,7 @@ class AtRulePreludeParser {
98
100
  this.arena.set_start_offset(media_type, this.lexer.token_start);
99
101
  this.arena.set_length(media_type, this.lexer.token_end - this.lexer.token_start);
100
102
  this.arena.set_start_line(media_type, this.lexer.token_line);
103
+ this.arena.set_start_column(media_type, this.lexer.token_column);
101
104
  components.push(media_type);
102
105
  }
103
106
  } else {
@@ -5,7 +5,7 @@ export declare class AtRulePreludeParser {
5
5
  private source;
6
6
  private prelude_end;
7
7
  constructor(arena: CSSDataArena, source: string);
8
- parse_prelude(at_rule_name: string, start: number, end: number, line?: number): number[];
8
+ parse_prelude(at_rule_name: string, start: number, end: number, line?: number, column?: number): number[];
9
9
  private parse_media_query_list;
10
10
  private is_and_or_not;
11
11
  private parse_single_media_query;
@@ -21,6 +21,7 @@ export declare class CSSNode {
21
21
  get values(): CSSNode[];
22
22
  get value_count(): number;
23
23
  get line(): number;
24
+ get column(): number;
24
25
  get offset(): number;
25
26
  get length(): number;
26
27
  get first_child(): CSSNode | null;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { parse } from './parse.js';
2
2
  export { parse_selector } from './parse-selector.js';
3
3
  export { parse_atrule_prelude } from './parse-atrule-prelude.js';
4
4
  export { tokenize } from './tokenize.js';
5
- export { C as CSSNode, I as FLAG_IMPORTANT, a as NODE_AT_RULE, b as NODE_COMMENT, c as NODE_DECLARATION, z as NODE_PRELUDE_CONTAINER_QUERY, D as NODE_PRELUDE_IDENTIFIER, G as NODE_PRELUDE_IMPORT_LAYER, H as NODE_PRELUDE_IMPORT_SUPPORTS, F as NODE_PRELUDE_IMPORT_URL, B as NODE_PRELUDE_LAYER_NAME, x as NODE_PRELUDE_MEDIA_FEATURE, w as NODE_PRELUDE_MEDIA_QUERY, y as NODE_PRELUDE_MEDIA_TYPE, E as NODE_PRELUDE_OPERATOR, A as NODE_PRELUDE_SUPPORTS_QUERY, d as NODE_SELECTOR, q as NODE_SELECTOR_ATTRIBUTE, o as NODE_SELECTOR_CLASS, t as NODE_SELECTOR_COMBINATOR, p as NODE_SELECTOR_ID, m as NODE_SELECTOR_LIST, v as NODE_SELECTOR_NESTING, r as NODE_SELECTOR_PSEUDO_CLASS, s as NODE_SELECTOR_PSEUDO_ELEMENT, n as NODE_SELECTOR_TYPE, u as NODE_SELECTOR_UNIVERSAL, e as NODE_STYLESHEET, N as NODE_STYLE_RULE, j as NODE_VALUE_COLOR, h as NODE_VALUE_DIMENSION, k as NODE_VALUE_FUNCTION, f as NODE_VALUE_KEYWORD, g as NODE_VALUE_NUMBER, l as NODE_VALUE_OPERATOR, i as NODE_VALUE_STRING } from './string-utils-B-rJII-E.js';
5
+ export { C as CSSNode, I as FLAG_IMPORTANT, a as NODE_AT_RULE, b as NODE_COMMENT, c as NODE_DECLARATION, z as NODE_PRELUDE_CONTAINER_QUERY, D as NODE_PRELUDE_IDENTIFIER, G as NODE_PRELUDE_IMPORT_LAYER, H as NODE_PRELUDE_IMPORT_SUPPORTS, F as NODE_PRELUDE_IMPORT_URL, B as NODE_PRELUDE_LAYER_NAME, x as NODE_PRELUDE_MEDIA_FEATURE, w as NODE_PRELUDE_MEDIA_QUERY, y as NODE_PRELUDE_MEDIA_TYPE, E as NODE_PRELUDE_OPERATOR, A as NODE_PRELUDE_SUPPORTS_QUERY, d as NODE_SELECTOR, q as NODE_SELECTOR_ATTRIBUTE, o as NODE_SELECTOR_CLASS, t as NODE_SELECTOR_COMBINATOR, p as NODE_SELECTOR_ID, m as NODE_SELECTOR_LIST, v as NODE_SELECTOR_NESTING, r as NODE_SELECTOR_PSEUDO_CLASS, s as NODE_SELECTOR_PSEUDO_ELEMENT, n as NODE_SELECTOR_TYPE, u as NODE_SELECTOR_UNIVERSAL, e as NODE_STYLESHEET, N as NODE_STYLE_RULE, j as NODE_VALUE_COLOR, h as NODE_VALUE_DIMENSION, k as NODE_VALUE_FUNCTION, f as NODE_VALUE_KEYWORD, g as NODE_VALUE_NUMBER, l as NODE_VALUE_OPERATOR, i as NODE_VALUE_STRING } from './string-utils-5j619JDp.js';
6
6
  export { b as TOKEN_AT_KEYWORD, e as TOKEN_BAD_STRING, g as TOKEN_BAD_URL, n as TOKEN_CDC, m as TOKEN_CDO, o as TOKEN_COLON, q as TOKEN_COMMA, x as TOKEN_COMMENT, h as TOKEN_DELIM, k as TOKEN_DIMENSION, y as TOKEN_EOF, a as TOKEN_FUNCTION, c as TOKEN_HASH, T as TOKEN_IDENT, v as TOKEN_LEFT_BRACE, r as TOKEN_LEFT_BRACKET, t as TOKEN_LEFT_PAREN, i as TOKEN_NUMBER, j as TOKEN_PERCENTAGE, w as TOKEN_RIGHT_BRACE, s as TOKEN_RIGHT_BRACKET, u as TOKEN_RIGHT_PAREN, p as TOKEN_SEMICOLON, d as TOKEN_STRING, f as TOKEN_URL, l as TOKEN_WHITESPACE } from './lexer-DXablYMZ.js';
7
7
 
8
8
  function walk(node, callback, depth = 0) {
@@ -1,5 +1,5 @@
1
- import { K as CSSDataArena, C as CSSNode } from './string-utils-B-rJII-E.js';
2
- import { A as AtRulePreludeParser } from './at-rule-prelude-parser-DlqYQAYH.js';
1
+ import { K as CSSDataArena, C as CSSNode } from './string-utils-5j619JDp.js';
2
+ import { A as AtRulePreludeParser } from './at-rule-prelude-parser-pzWB6S0z.js';
3
3
 
4
4
  function parse_atrule_prelude(at_rule_name, prelude) {
5
5
  const arena = new CSSDataArena(CSSDataArena.capacity_for_source(prelude.length));
@@ -1,5 +1,5 @@
1
- import { K as CSSDataArena, d as NODE_SELECTOR, C as CSSNode } from './string-utils-B-rJII-E.js';
2
- import { S as SelectorParser } from './selector-parser-2b3tGyri.js';
1
+ import { K as CSSDataArena, d as NODE_SELECTOR, C as CSSNode } from './string-utils-5j619JDp.js';
2
+ import { S as SelectorParser } from './selector-parser-CwNLN7Us.js';
3
3
 
4
4
  function parse_selector(source) {
5
5
  const arena = new CSSDataArena(CSSDataArena.capacity_for_source(source.length));
package/dist/parse.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { L as Lexer, y as TOKEN_EOF, q as TOKEN_COMMA, h as TOKEN_DELIM, a as TOKEN_FUNCTION, c as TOKEN_HASH, d as TOKEN_STRING, k as TOKEN_DIMENSION, j as TOKEN_PERCENTAGE, i as TOKEN_NUMBER, T as TOKEN_IDENT, t as TOKEN_LEFT_PAREN, u as TOKEN_RIGHT_PAREN, b as TOKEN_AT_KEYWORD, v as TOKEN_LEFT_BRACE, w as TOKEN_RIGHT_BRACE, o as TOKEN_COLON, p as TOKEN_SEMICOLON } from './lexer-DXablYMZ.js';
2
- import { J as is_whitespace, f as NODE_VALUE_KEYWORD, g as NODE_VALUE_NUMBER, h as NODE_VALUE_DIMENSION, i as NODE_VALUE_STRING, j as NODE_VALUE_COLOR, l as NODE_VALUE_OPERATOR, k as NODE_VALUE_FUNCTION, K as CSSDataArena, e as NODE_STYLESHEET, C as CSSNode, N as NODE_STYLE_RULE, L as FLAG_HAS_BLOCK, d as NODE_SELECTOR, c as NODE_DECLARATION, M as trim_boundaries, I as FLAG_IMPORTANT, a as NODE_AT_RULE } from './string-utils-B-rJII-E.js';
3
- import { S as SelectorParser } from './selector-parser-2b3tGyri.js';
4
- import { A as AtRulePreludeParser } from './at-rule-prelude-parser-DlqYQAYH.js';
2
+ import { J as is_whitespace, f as NODE_VALUE_KEYWORD, g as NODE_VALUE_NUMBER, h as NODE_VALUE_DIMENSION, i as NODE_VALUE_STRING, j as NODE_VALUE_COLOR, l as NODE_VALUE_OPERATOR, k as NODE_VALUE_FUNCTION, K as CSSDataArena, e as NODE_STYLESHEET, C as CSSNode, N as NODE_STYLE_RULE, L as FLAG_HAS_BLOCK, d as NODE_SELECTOR, c as NODE_DECLARATION, M as is_vendor_prefixed, O as FLAG_VENDOR_PREFIXED, P as trim_boundaries, I as FLAG_IMPORTANT, a as NODE_AT_RULE } from './string-utils-5j619JDp.js';
3
+ import { S as SelectorParser } from './selector-parser-CwNLN7Us.js';
4
+ import { A as AtRulePreludeParser } from './at-rule-prelude-parser-pzWB6S0z.js';
5
5
 
6
6
  const CH_PLUS = 43;
7
7
  const CH_MINUS = 45;
@@ -240,6 +240,7 @@ class Parser {
240
240
  this.arena.set_start_offset(stylesheet, 0);
241
241
  this.arena.set_length(stylesheet, this.source.length);
242
242
  this.arena.set_start_line(stylesheet, 1);
243
+ this.arena.set_start_column(stylesheet, 1);
243
244
  while (!this.is_eof()) {
244
245
  let rule = this.parse_rule();
245
246
  if (rule !== null) {
@@ -265,9 +266,11 @@ class Parser {
265
266
  if (this.is_eof()) return null;
266
267
  let rule_start = this.lexer.token_start;
267
268
  let rule_line = this.lexer.token_line;
269
+ let rule_column = this.lexer.token_column;
268
270
  let style_rule = this.arena.create_node();
269
271
  this.arena.set_type(style_rule, NODE_STYLE_RULE);
270
272
  this.arena.set_start_line(style_rule, rule_line);
273
+ this.arena.set_start_column(style_rule, rule_column);
271
274
  let selector = this.parse_selector();
272
275
  if (selector !== null) {
273
276
  this.arena.append_child(style_rule, selector);
@@ -313,13 +316,14 @@ class Parser {
313
316
  if (this.is_eof()) return null;
314
317
  let selector_start = this.lexer.token_start;
315
318
  let selector_line = this.lexer.token_line;
319
+ let selector_column = this.lexer.token_column;
316
320
  let last_end = this.lexer.token_end;
317
321
  while (!this.is_eof() && this.peek_type() !== TOKEN_LEFT_BRACE) {
318
322
  last_end = this.lexer.token_end;
319
323
  this.next_token();
320
324
  }
321
325
  if (this.parse_selectors_enabled && this.selector_parser) {
322
- let selectorNode = this.selector_parser.parse_selector(selector_start, last_end, selector_line);
326
+ let selectorNode = this.selector_parser.parse_selector(selector_start, last_end, selector_line, selector_column);
323
327
  if (selectorNode !== null) {
324
328
  return selectorNode;
325
329
  }
@@ -327,6 +331,7 @@ class Parser {
327
331
  let selector = this.arena.create_node();
328
332
  this.arena.set_type(selector, NODE_SELECTOR);
329
333
  this.arena.set_start_line(selector, selector_line);
334
+ this.arena.set_start_column(selector, selector_column);
330
335
  this.arena.set_start_offset(selector, selector_start);
331
336
  this.arena.set_length(selector, last_end - selector_start);
332
337
  return selector;
@@ -339,6 +344,7 @@ class Parser {
339
344
  let prop_start = this.lexer.token_start;
340
345
  let prop_end = this.lexer.token_end;
341
346
  let decl_line = this.lexer.token_line;
347
+ let decl_column = this.lexer.token_column;
342
348
  this.next_token();
343
349
  if (this.peek_type() !== TOKEN_COLON) {
344
350
  return null;
@@ -347,9 +353,13 @@ class Parser {
347
353
  let declaration = this.arena.create_node();
348
354
  this.arena.set_type(declaration, NODE_DECLARATION);
349
355
  this.arena.set_start_line(declaration, decl_line);
356
+ this.arena.set_start_column(declaration, decl_column);
350
357
  this.arena.set_start_offset(declaration, prop_start);
351
358
  this.arena.set_content_start(declaration, prop_start);
352
359
  this.arena.set_content_length(declaration, prop_end - prop_start);
360
+ if (is_vendor_prefixed(this.source, prop_start, prop_end)) {
361
+ this.arena.set_flag(declaration, FLAG_VENDOR_PREFIXED);
362
+ }
353
363
  let value_start = this.lexer.token_start;
354
364
  let value_end = value_start;
355
365
  let has_important = false;
@@ -402,6 +412,7 @@ class Parser {
402
412
  }
403
413
  let at_rule_start = this.lexer.token_start;
404
414
  let at_rule_line = this.lexer.token_line;
415
+ let at_rule_column = this.lexer.token_column;
405
416
  let at_rule_name = this.source.substring(this.lexer.token_start + 1, this.lexer.token_end);
406
417
  let name_start = this.lexer.token_start + 1;
407
418
  let name_length = at_rule_name.length;
@@ -409,6 +420,7 @@ class Parser {
409
420
  let at_rule = this.arena.create_node();
410
421
  this.arena.set_type(at_rule, NODE_AT_RULE);
411
422
  this.arena.set_start_line(at_rule, at_rule_line);
423
+ this.arena.set_start_column(at_rule, at_rule_column);
412
424
  this.arena.set_start_offset(at_rule, at_rule_start);
413
425
  this.arena.set_content_start(at_rule, name_start);
414
426
  this.arena.set_content_length(at_rule, name_length);
@@ -423,7 +435,7 @@ class Parser {
423
435
  this.arena.set_value_start(at_rule, trimmed[0]);
424
436
  this.arena.set_value_length(at_rule, trimmed[1] - trimmed[0]);
425
437
  if (this.prelude_parser) {
426
- let prelude_nodes = this.prelude_parser.parse_prelude(at_rule_name, trimmed[0], trimmed[1], at_rule_line);
438
+ let prelude_nodes = this.prelude_parser.parse_prelude(at_rule_name, trimmed[0], trimmed[1], at_rule_line, at_rule_column);
427
439
  for (let prelude_node of prelude_nodes) {
428
440
  this.arena.append_child(at_rule, prelude_node);
429
441
  }
@@ -1,5 +1,5 @@
1
1
  import { L as Lexer, q as TOKEN_COMMA, y as TOKEN_EOF, l as TOKEN_WHITESPACE, a as TOKEN_FUNCTION, o as TOKEN_COLON, r as TOKEN_LEFT_BRACKET, h as TOKEN_DELIM, c as TOKEN_HASH, T as TOKEN_IDENT, s as TOKEN_RIGHT_BRACKET, t as TOKEN_LEFT_PAREN, u as TOKEN_RIGHT_PAREN } from './lexer-DXablYMZ.js';
2
- import { d as NODE_SELECTOR, m as NODE_SELECTOR_LIST, J as is_whitespace, o as NODE_SELECTOR_CLASS, q as NODE_SELECTOR_ATTRIBUTE, M as trim_boundaries, s as NODE_SELECTOR_PSEUDO_ELEMENT, r as NODE_SELECTOR_PSEUDO_CLASS, n as NODE_SELECTOR_TYPE, p as NODE_SELECTOR_ID, u as NODE_SELECTOR_UNIVERSAL, v as NODE_SELECTOR_NESTING, t as NODE_SELECTOR_COMBINATOR } from './string-utils-B-rJII-E.js';
2
+ import { d as NODE_SELECTOR, m as NODE_SELECTOR_LIST, J as is_whitespace, o as NODE_SELECTOR_CLASS, q as NODE_SELECTOR_ATTRIBUTE, P as trim_boundaries, s as NODE_SELECTOR_PSEUDO_ELEMENT, r as NODE_SELECTOR_PSEUDO_CLASS, M as is_vendor_prefixed, O as FLAG_VENDOR_PREFIXED, n as NODE_SELECTOR_TYPE, p as NODE_SELECTOR_ID, u as NODE_SELECTOR_UNIVERSAL, v as NODE_SELECTOR_NESTING, t as NODE_SELECTOR_COMBINATOR } from './string-utils-5j619JDp.js';
3
3
 
4
4
  class SelectorParser {
5
5
  lexer;
@@ -14,10 +14,11 @@ class SelectorParser {
14
14
  }
15
15
  // Parse a selector range into selector nodes
16
16
  // Always returns a NODE_SELECTOR wrapper with detailed selector nodes as children
17
- parse_selector(start, end, line = 1) {
17
+ parse_selector(start, end, line = 1, column = 1) {
18
18
  this.selector_end = end;
19
19
  this.lexer.pos = start;
20
20
  this.lexer.line = line;
21
+ this.lexer.column = column;
21
22
  let innerSelector = this.parse_selector_list();
22
23
  if (innerSelector === null) {
23
24
  return null;
@@ -27,6 +28,7 @@ class SelectorParser {
27
28
  this.arena.set_start_offset(selectorWrapper, start);
28
29
  this.arena.set_length(selectorWrapper, end - start);
29
30
  this.arena.set_start_line(selectorWrapper, line);
31
+ this.arena.set_start_column(selectorWrapper, column);
30
32
  this.arena.set_first_child(selectorWrapper, innerSelector);
31
33
  this.arena.set_last_child(selectorWrapper, innerSelector);
32
34
  return selectorWrapper;
@@ -60,6 +62,7 @@ class SelectorParser {
60
62
  this.arena.set_start_offset(list_node, list_start);
61
63
  this.arena.set_length(list_node, this.lexer.pos - list_start);
62
64
  this.arena.set_start_line(list_node, this.lexer.line);
65
+ this.arena.set_start_column(list_node, this.lexer.column);
63
66
  this.arena.set_first_child(list_node, selectors[0]);
64
67
  this.arena.set_last_child(list_node, selectors[selectors.length - 1]);
65
68
  for (let i = 0; i < selectors.length - 1; i++) {
@@ -210,6 +213,7 @@ class SelectorParser {
210
213
  this.arena.set_start_offset(node, dot_pos);
211
214
  this.arena.set_length(node, this.lexer.token_end - dot_pos);
212
215
  this.arena.set_start_line(node, this.lexer.line);
216
+ this.arena.set_start_column(node, this.lexer.column);
213
217
  this.arena.set_content_start(node, this.lexer.token_start);
214
218
  this.arena.set_content_length(node, this.lexer.token_end - this.lexer.token_start);
215
219
  return node;
@@ -236,6 +240,7 @@ class SelectorParser {
236
240
  this.arena.set_start_offset(node, start);
237
241
  this.arena.set_length(node, end - start);
238
242
  this.arena.set_start_line(node, this.lexer.line);
243
+ this.arena.set_start_column(node, this.lexer.column);
239
244
  let trimmed = trim_boundaries(this.source, start + 1, end - 1);
240
245
  if (trimmed) {
241
246
  this.arena.set_content_start(node, trimmed[0]);
@@ -258,8 +263,12 @@ class SelectorParser {
258
263
  this.arena.set_start_offset(node, start);
259
264
  this.arena.set_length(node, this.lexer.token_end - start);
260
265
  this.arena.set_start_line(node, this.lexer.line);
266
+ this.arena.set_start_column(node, this.lexer.column);
261
267
  this.arena.set_content_start(node, this.lexer.token_start);
262
268
  this.arena.set_content_length(node, this.lexer.token_end - this.lexer.token_start);
269
+ if (is_vendor_prefixed(this.source, this.lexer.token_start, this.lexer.token_end)) {
270
+ this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
271
+ }
263
272
  return node;
264
273
  } else if (token_type === TOKEN_FUNCTION) {
265
274
  return this.parse_pseudo_function_after_colon(start, is_pseudo_element);
@@ -294,8 +303,12 @@ class SelectorParser {
294
303
  this.arena.set_start_offset(node, start);
295
304
  this.arena.set_length(node, end - start);
296
305
  this.arena.set_start_line(node, this.lexer.line);
306
+ this.arena.set_start_column(node, this.lexer.column);
297
307
  this.arena.set_content_start(node, func_name_start);
298
308
  this.arena.set_content_length(node, func_name_end - func_name_start);
309
+ if (is_vendor_prefixed(this.source, func_name_start, func_name_end)) {
310
+ this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
311
+ }
299
312
  return node;
300
313
  }
301
314
  // Create simple selector nodes
@@ -305,6 +318,7 @@ class SelectorParser {
305
318
  this.arena.set_start_offset(node, start);
306
319
  this.arena.set_length(node, end - start);
307
320
  this.arena.set_start_line(node, this.lexer.line);
321
+ this.arena.set_start_column(node, this.lexer.column);
308
322
  this.arena.set_content_start(node, start);
309
323
  this.arena.set_content_length(node, end - start);
310
324
  return node;
@@ -315,6 +329,7 @@ class SelectorParser {
315
329
  this.arena.set_start_offset(node, start);
316
330
  this.arena.set_length(node, end - start);
317
331
  this.arena.set_start_line(node, this.lexer.line);
332
+ this.arena.set_start_column(node, this.lexer.column);
318
333
  this.arena.set_content_start(node, start + 1);
319
334
  this.arena.set_content_length(node, end - start - 1);
320
335
  return node;
@@ -325,6 +340,7 @@ class SelectorParser {
325
340
  this.arena.set_start_offset(node, start);
326
341
  this.arena.set_length(node, end - start);
327
342
  this.arena.set_start_line(node, this.lexer.line);
343
+ this.arena.set_start_column(node, this.lexer.column);
328
344
  this.arena.set_content_start(node, start);
329
345
  this.arena.set_content_length(node, end - start);
330
346
  return node;
@@ -335,6 +351,7 @@ class SelectorParser {
335
351
  this.arena.set_start_offset(node, start);
336
352
  this.arena.set_length(node, end - start);
337
353
  this.arena.set_start_line(node, this.lexer.line);
354
+ this.arena.set_start_column(node, this.lexer.column);
338
355
  this.arena.set_content_start(node, start);
339
356
  this.arena.set_content_length(node, end - start);
340
357
  return node;
@@ -345,6 +362,7 @@ class SelectorParser {
345
362
  this.arena.set_start_offset(node, start);
346
363
  this.arena.set_length(node, end - start);
347
364
  this.arena.set_start_line(node, this.lexer.line);
365
+ this.arena.set_start_column(node, this.lexer.column);
348
366
  this.arena.set_content_start(node, start);
349
367
  this.arena.set_content_length(node, end - start);
350
368
  return node;
@@ -5,7 +5,7 @@ export declare class SelectorParser {
5
5
  private source;
6
6
  private selector_end;
7
7
  constructor(arena: CSSDataArena, source: string);
8
- parse_selector(start: number, end: number, line?: number): number | null;
8
+ parse_selector(start: number, end: number, line?: number, column?: number): number | null;
9
9
  private parse_selector_list;
10
10
  private parse_complex_selector;
11
11
  private parse_compound_selector;
@@ -36,6 +36,7 @@ const NODE_PRELUDE_IMPORT_SUPPORTS = 40;
36
36
  const FLAG_IMPORTANT = 1 << 0;
37
37
  const FLAG_HAS_ERROR = 1 << 1;
38
38
  const FLAG_HAS_BLOCK = 1 << 3;
39
+ const FLAG_VENDOR_PREFIXED = 1 << 4;
39
40
  class CSSDataArena {
40
41
  buffer;
41
42
  view;
@@ -114,6 +115,10 @@ class CSSDataArena {
114
115
  get_start_line(node_index) {
115
116
  return this.view.getUint32(this.node_offset(node_index) + 32, true);
116
117
  }
118
+ // Read start column
119
+ get_start_column(node_index) {
120
+ return this.view.getUint16(this.node_offset(node_index) + 42, true);
121
+ }
117
122
  // Read value start offset (declaration value / at-rule prelude)
118
123
  get_value_start(node_index) {
119
124
  return this.view.getUint32(this.node_offset(node_index) + 36, true);
@@ -163,6 +168,10 @@ class CSSDataArena {
163
168
  set_start_line(node_index, line) {
164
169
  this.view.setUint32(this.node_offset(node_index) + 32, line, true);
165
170
  }
171
+ // Write start column
172
+ set_start_column(node_index, column) {
173
+ this.view.setUint16(this.node_offset(node_index) + 42, column, true);
174
+ }
166
175
  // Write value start offset (declaration value / at-rule prelude)
167
176
  set_value_start(node_index, offset) {
168
177
  this.view.setUint32(this.node_offset(node_index) + 36, offset, true);
@@ -282,11 +291,9 @@ class CSSNode {
282
291
  get is_important() {
283
292
  return this.arena.has_flag(this.index, FLAG_IMPORTANT);
284
293
  }
285
- // Check if this has a vendor prefix (lazy computation for performance)
294
+ // Check if this has a vendor prefix (flag-based for performance)
286
295
  get is_vendor_prefixed() {
287
- const name = this.name;
288
- if (!name) return false;
289
- return name.startsWith("-webkit-") || name.startsWith("-moz-") || name.startsWith("-ms-") || name.startsWith("-o-");
296
+ return this.arena.has_flag(this.index, FLAG_VENDOR_PREFIXED);
290
297
  }
291
298
  // Check if this node has an error
292
299
  get has_error() {
@@ -325,6 +332,10 @@ class CSSNode {
325
332
  get line() {
326
333
  return this.arena.get_start_line(this.index);
327
334
  }
335
+ // Get start column number
336
+ get column() {
337
+ return this.arena.get_start_column(this.index);
338
+ }
328
339
  // Get start offset in source
329
340
  get offset() {
330
341
  return this.arena.get_start_offset(this.index);
@@ -377,6 +388,7 @@ const CHAR_CARRIAGE_RETURN = 13;
377
388
  const CHAR_FORM_FEED = 12;
378
389
  const CHAR_FORWARD_SLASH = 47;
379
390
  const CHAR_ASTERISK = 42;
391
+ const CHAR_MINUS_HYPHEN = 45;
380
392
  function is_whitespace(ch) {
381
393
  return ch === CHAR_SPACE || ch === CHAR_TAB || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED;
382
394
  }
@@ -436,5 +448,19 @@ function str_equals(a, b) {
436
448
  }
437
449
  return true;
438
450
  }
451
+ function is_vendor_prefixed(source, start, end) {
452
+ if (source.charCodeAt(start) !== CHAR_MINUS_HYPHEN) {
453
+ return false;
454
+ }
455
+ if (source.charCodeAt(start + 1) === CHAR_MINUS_HYPHEN) {
456
+ return false;
457
+ }
458
+ let length = end - start;
459
+ if (length < 3) {
460
+ return false;
461
+ }
462
+ let secondHyphenPos = source.indexOf("-", start + 2);
463
+ return secondHyphenPos !== -1 && secondHyphenPos < end;
464
+ }
439
465
 
440
- export { NODE_PRELUDE_SUPPORTS_QUERY as A, NODE_PRELUDE_LAYER_NAME as B, CSSNode as C, NODE_PRELUDE_IDENTIFIER as D, NODE_PRELUDE_OPERATOR as E, NODE_PRELUDE_IMPORT_URL as F, NODE_PRELUDE_IMPORT_LAYER as G, NODE_PRELUDE_IMPORT_SUPPORTS as H, FLAG_IMPORTANT as I, is_whitespace as J, CSSDataArena as K, FLAG_HAS_BLOCK as L, trim_boundaries as M, NODE_STYLE_RULE as N, str_equals as O, CHAR_SPACE as P, CHAR_TAB as Q, CHAR_NEWLINE as R, CHAR_CARRIAGE_RETURN as S, CHAR_FORM_FEED as T, NODE_AT_RULE as a, NODE_COMMENT as b, NODE_DECLARATION as c, NODE_SELECTOR as d, NODE_STYLESHEET as e, NODE_VALUE_KEYWORD as f, NODE_VALUE_NUMBER as g, NODE_VALUE_DIMENSION as h, NODE_VALUE_STRING as i, NODE_VALUE_COLOR as j, NODE_VALUE_FUNCTION as k, NODE_VALUE_OPERATOR as l, NODE_SELECTOR_LIST as m, NODE_SELECTOR_TYPE as n, NODE_SELECTOR_CLASS as o, NODE_SELECTOR_ID as p, NODE_SELECTOR_ATTRIBUTE as q, NODE_SELECTOR_PSEUDO_CLASS as r, NODE_SELECTOR_PSEUDO_ELEMENT as s, NODE_SELECTOR_COMBINATOR as t, NODE_SELECTOR_UNIVERSAL as u, NODE_SELECTOR_NESTING as v, NODE_PRELUDE_MEDIA_QUERY as w, NODE_PRELUDE_MEDIA_FEATURE as x, NODE_PRELUDE_MEDIA_TYPE as y, NODE_PRELUDE_CONTAINER_QUERY as z };
466
+ export { NODE_PRELUDE_SUPPORTS_QUERY as A, NODE_PRELUDE_LAYER_NAME as B, CSSNode as C, NODE_PRELUDE_IDENTIFIER as D, NODE_PRELUDE_OPERATOR as E, NODE_PRELUDE_IMPORT_URL as F, NODE_PRELUDE_IMPORT_LAYER as G, NODE_PRELUDE_IMPORT_SUPPORTS as H, FLAG_IMPORTANT as I, is_whitespace as J, CSSDataArena as K, FLAG_HAS_BLOCK as L, is_vendor_prefixed as M, NODE_STYLE_RULE as N, FLAG_VENDOR_PREFIXED as O, trim_boundaries as P, str_equals as Q, CHAR_SPACE as R, CHAR_TAB as S, CHAR_NEWLINE as T, CHAR_CARRIAGE_RETURN as U, CHAR_FORM_FEED as V, NODE_AT_RULE as a, NODE_COMMENT as b, NODE_DECLARATION as c, NODE_SELECTOR as d, NODE_STYLESHEET as e, NODE_VALUE_KEYWORD as f, NODE_VALUE_NUMBER as g, NODE_VALUE_DIMENSION as h, NODE_VALUE_STRING as i, NODE_VALUE_COLOR as j, NODE_VALUE_FUNCTION as k, NODE_VALUE_OPERATOR as l, NODE_SELECTOR_LIST as m, NODE_SELECTOR_TYPE as n, NODE_SELECTOR_CLASS as o, NODE_SELECTOR_ID as p, NODE_SELECTOR_ATTRIBUTE as q, NODE_SELECTOR_PSEUDO_CLASS as r, NODE_SELECTOR_PSEUDO_ELEMENT as s, NODE_SELECTOR_COMBINATOR as t, NODE_SELECTOR_UNIVERSAL as u, NODE_SELECTOR_NESTING as v, NODE_PRELUDE_MEDIA_QUERY as w, NODE_PRELUDE_MEDIA_FEATURE as x, NODE_PRELUDE_MEDIA_TYPE as y, NODE_PRELUDE_CONTAINER_QUERY as z };
@@ -5,6 +5,7 @@ export declare const CHAR_CARRIAGE_RETURN = 13;
5
5
  export declare const CHAR_FORM_FEED = 12;
6
6
  export declare const CHAR_FORWARD_SLASH = 47;
7
7
  export declare const CHAR_ASTERISK = 42;
8
+ export declare const CHAR_MINUS_HYPHEN = 45;
8
9
  /**
9
10
  * Check if a character code is whitespace (space, tab, newline, CR, or FF)
10
11
  */
@@ -27,3 +28,25 @@ export declare function trim_boundaries(source: string, start: number, end: numb
27
28
  * @param b Compare string
28
29
  */
29
30
  export declare function str_equals(a: string, b: string): boolean;
31
+ /**
32
+ * Check if a string range has a vendor prefix
33
+ *
34
+ * @param source - The source string
35
+ * @param start - Start offset in source
36
+ * @param end - End offset in source
37
+ * @returns true if the range starts with a vendor prefix (-webkit-, -moz-, -ms-, -o-)
38
+ *
39
+ * Detects vendor prefixes by checking:
40
+ * 1. Starts with a single hyphen (not --)
41
+ * 2. Contains at least 3 characters (shortest is -o-)
42
+ * 3. Has a second hyphen after the vendor name
43
+ *
44
+ * Examples:
45
+ * - `-webkit-transform` → true
46
+ * - `-moz-appearance` → true
47
+ * - `-ms-filter` → true
48
+ * - `-o-border-image` → true
49
+ * - `--custom-property` → false (CSS custom property)
50
+ * - `border-radius` → false (doesn't start with hyphen)
51
+ */
52
+ export declare function is_vendor_prefixed(source: string, start: number, end: number): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectwallace/css-parser",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
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",