@projectwallace/css-parser 0.8.5 → 0.8.6

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/dist/arena.js CHANGED
@@ -1,4 +1,4 @@
1
- let BYTES_PER_NODE = 32;
1
+ let BYTES_PER_NODE = 36;
2
2
  const STYLESHEET = 1;
3
3
  const STYLE_RULE = 2;
4
4
  const AT_RULE = 3;
@@ -37,6 +37,7 @@ const LAYER_NAME = 37;
37
37
  const PRELUDE_OPERATOR = 38;
38
38
  const FLAG_IMPORTANT = 1 << 0;
39
39
  const FLAG_HAS_ERROR = 1 << 1;
40
+ const FLAG_LENGTH_OVERFLOW = 1 << 2;
40
41
  const FLAG_HAS_BLOCK = 1 << 3;
41
42
  const FLAG_HAS_DECLARATIONS = 1 << 5;
42
43
  const FLAG_HAS_PARENS = 1 << 6;
@@ -59,6 +60,8 @@ class CSSDataArena {
59
60
  // Number of nodes currently allocated
60
61
  growth_count;
61
62
  // Number of times the arena has grown
63
+ overflow_lengths;
64
+ // Stores actual lengths for nodes > 65535 chars
62
65
  // Growth multiplier when capacity is exceeded
63
66
  static GROWTH_FACTOR = 1.3;
64
67
  // Estimated nodes per KB of CSS (based on real-world data)
@@ -71,6 +74,7 @@ class CSSDataArena {
71
74
  this.growth_count = 0;
72
75
  this.buffer = new ArrayBuffer(initial_capacity * BYTES_PER_NODE);
73
76
  this.view = new DataView(this.buffer);
77
+ this.overflow_lengths = /* @__PURE__ */ new Map();
74
78
  }
75
79
  // Calculate recommended initial capacity based on CSS source size
76
80
  static capacity_for_source(source_length) {
@@ -109,6 +113,12 @@ class CSSDataArena {
109
113
  }
110
114
  // Read length in source
111
115
  get_length(node_index) {
116
+ if (this.has_flag(node_index, FLAG_LENGTH_OVERFLOW)) {
117
+ const overflow_length = this.overflow_lengths.get(node_index);
118
+ if (overflow_length !== void 0) {
119
+ return overflow_length;
120
+ }
121
+ }
112
122
  return this.view.getUint16(this.node_offset(node_index) + 2, true);
113
123
  }
114
124
  // Read content start offset (stored as delta from startOffset)
@@ -123,11 +133,11 @@ class CSSDataArena {
123
133
  }
124
134
  // Read attribute operator (for NODE_SELECTOR_ATTRIBUTE)
125
135
  get_attr_operator(node_index) {
126
- return this.view.getUint8(this.node_offset(node_index) + 30);
136
+ return this.view.getUint8(this.node_offset(node_index) + 32);
127
137
  }
128
138
  // Read attribute flags (for NODE_SELECTOR_ATTRIBUTE)
129
139
  get_attr_flags(node_index) {
130
- return this.view.getUint8(this.node_offset(node_index) + 31);
140
+ return this.view.getUint8(this.node_offset(node_index) + 33);
131
141
  }
132
142
  // Read first child index (0 = no children)
133
143
  get_first_child(node_index) {
@@ -143,7 +153,7 @@ class CSSDataArena {
143
153
  }
144
154
  // Read start column
145
155
  get_start_column(node_index) {
146
- return this.view.getUint16(this.node_offset(node_index) + 28, true);
156
+ return this.view.getUint32(this.node_offset(node_index) + 28, true);
147
157
  }
148
158
  // Read value start offset (stored as delta from startOffset, declaration value / at-rule prelude)
149
159
  get_value_start(node_index) {
@@ -164,13 +174,15 @@ class CSSDataArena {
164
174
  set_flags(node_index, flags) {
165
175
  this.view.setUint8(this.node_offset(node_index) + 1, flags);
166
176
  }
167
- // Write start offset in source
168
- set_start_offset(node_index, offset) {
169
- this.view.setUint32(this.node_offset(node_index) + 12, offset, true);
170
- }
171
177
  // Write length in source
172
178
  set_length(node_index, length) {
173
- this.view.setUint16(this.node_offset(node_index) + 2, length, true);
179
+ if (length > 65535) {
180
+ this.view.setUint16(this.node_offset(node_index) + 2, 65535, true);
181
+ this.set_flag(node_index, FLAG_LENGTH_OVERFLOW);
182
+ this.overflow_lengths.set(node_index, length);
183
+ } else {
184
+ this.view.setUint16(this.node_offset(node_index) + 2, length, true);
185
+ }
174
186
  }
175
187
  // Write content start delta (offset from startOffset)
176
188
  set_content_start_delta(node_index, delta) {
@@ -182,11 +194,11 @@ class CSSDataArena {
182
194
  }
183
195
  // Write attribute operator (for NODE_SELECTOR_ATTRIBUTE)
184
196
  set_attr_operator(node_index, operator) {
185
- this.view.setUint8(this.node_offset(node_index) + 30, operator);
197
+ this.view.setUint8(this.node_offset(node_index) + 32, operator);
186
198
  }
187
199
  // Write attribute flags (for NODE_SELECTOR_ATTRIBUTE)
188
200
  set_attr_flags(node_index, flags) {
189
- this.view.setUint8(this.node_offset(node_index) + 31, flags);
201
+ this.view.setUint8(this.node_offset(node_index) + 33, flags);
190
202
  }
191
203
  // Write first child index
192
204
  set_first_child(node_index, childIndex) {
@@ -196,14 +208,6 @@ class CSSDataArena {
196
208
  set_next_sibling(node_index, siblingIndex) {
197
209
  this.view.setUint32(this.node_offset(node_index) + 8, siblingIndex, true);
198
210
  }
199
- // Write start line
200
- set_start_line(node_index, line) {
201
- this.view.setUint32(this.node_offset(node_index) + 24, line, true);
202
- }
203
- // Write start column
204
- set_start_column(node_index, column) {
205
- this.view.setUint16(this.node_offset(node_index) + 28, column, true);
206
- }
207
211
  // Write value start delta (offset from startOffset, declaration value / at-rule prelude)
208
212
  set_value_start_delta(node_index, delta) {
209
213
  this.view.setUint16(this.node_offset(node_index) + 18, delta, true);
@@ -233,10 +237,10 @@ class CSSDataArena {
233
237
  this.count++;
234
238
  const offset = node_index * BYTES_PER_NODE;
235
239
  this.view.setUint8(offset, type);
236
- this.view.setUint16(offset + 2, length, true);
237
240
  this.view.setUint32(offset + 12, start_offset, true);
238
241
  this.view.setUint32(offset + 24, start_line, true);
239
- this.view.setUint16(offset + 28, start_column, true);
242
+ this.view.setUint32(offset + 28, start_column, true);
243
+ this.set_length(node_index, length);
240
244
  return node_index;
241
245
  }
242
246
  // --- Tree Building Helpers ---
@@ -275,4 +279,4 @@ class CSSDataArena {
275
279
  }
276
280
  }
277
281
 
278
- 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, CSSDataArena, DECLARATION, DIMENSION, FLAG_HAS_BLOCK, FLAG_HAS_DECLARATIONS, FLAG_HAS_ERROR, FLAG_HAS_PARENS, 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 };
282
+ 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, CSSDataArena, DECLARATION, DIMENSION, FLAG_HAS_BLOCK, FLAG_HAS_DECLARATIONS, FLAG_HAS_ERROR, FLAG_HAS_PARENS, FLAG_IMPORTANT, FLAG_LENGTH_OVERFLOW, 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 };
@@ -202,25 +202,12 @@ export declare class CSSNode {
202
202
  /** Get text of first compound selector (no node allocation) */
203
203
  get first_compound_text(): string;
204
204
  /**
205
- * Clone this node as a mutable plain JavaScript object
206
- *
207
- * Extracts all properties from the arena into a plain object with children as an array.
208
- * The resulting object can be freely modified.
205
+ * Clone this node as a mutable plain JavaScript object with children as arrays.
206
+ * See API.md for examples.
209
207
  *
210
208
  * @param options - Cloning configuration
211
209
  * @param options.deep - Recursively clone children (default: true)
212
210
  * @param options.locations - Include line/column/start/length (default: false)
213
- * @returns Plain object with children as array
214
- *
215
- * @example
216
- * const ast = parse('div { color: red; }')
217
- * const decl = ast.first_child.block.first_child
218
- * const plain = decl.clone()
219
- *
220
- * // Access children as array
221
- * plain.children.length
222
- * plain.children[0]
223
- * plain.children.push(newChild)
224
211
  */
225
212
  clone(options?: CloneOptions): PlainCSSNode;
226
213
  }
package/dist/css-node.js CHANGED
@@ -454,25 +454,12 @@ class CSSNode {
454
454
  }
455
455
  // --- Node Cloning ---
456
456
  /**
457
- * Clone this node as a mutable plain JavaScript object
458
- *
459
- * Extracts all properties from the arena into a plain object with children as an array.
460
- * The resulting object can be freely modified.
457
+ * Clone this node as a mutable plain JavaScript object with children as arrays.
458
+ * See API.md for examples.
461
459
  *
462
460
  * @param options - Cloning configuration
463
461
  * @param options.deep - Recursively clone children (default: true)
464
462
  * @param options.locations - Include line/column/start/length (default: false)
465
- * @returns Plain object with children as array
466
- *
467
- * @example
468
- * const ast = parse('div { color: red; }')
469
- * const decl = ast.first_child.block.first_child
470
- * const plain = decl.clone()
471
- *
472
- * // Access children as array
473
- * plain.children.length
474
- * plain.children[0]
475
- * plain.children.push(newChild)
476
463
  */
477
464
  clone(options = {}) {
478
465
  const { deep = true, locations = false } = options;
package/dist/index.d.ts CHANGED
@@ -5,9 +5,10 @@ export { parse_declaration } from './parse-declaration';
5
5
  export { parse_value } from './parse-value';
6
6
  export { tokenize } from './tokenize';
7
7
  export { walk, traverse, SKIP, BREAK } from './walk';
8
+ export { is_custom, is_vendor_prefixed, str_equals, str_starts_with, str_index_of } from './string-utils';
8
9
  export { type ParserOptions } from './parse';
9
10
  export { CSSNode, type CSSNodeType, TYPE_NAMES, type CloneOptions, type PlainCSSNode } from './css-node';
10
- export type { LexerPosition } from './lexer';
11
+ export type { LexerPosition } from './tokenize';
11
12
  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';
12
13
  export * from './constants';
13
14
  export * from './token-types';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { parse_declaration } from './parse-declaration.js';
5
5
  export { parse_value } from './parse-value.js';
6
6
  export { tokenize } from './tokenize.js';
7
7
  export { BREAK, SKIP, traverse, walk } from './walk.js';
8
+ export { is_custom, is_vendor_prefixed, str_equals, str_index_of, str_starts_with } from './string-utils.js';
8
9
  export { CSSNode, TYPE_NAMES } from './css-node.js';
9
10
  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';
10
11
  export { NODE_TYPES } from './constants.js';
@@ -1,4 +1,4 @@
1
- import { Lexer } from './lexer.js';
1
+ import { Lexer } from './tokenize.js';
2
2
  import { NTH_SELECTOR, CSSDataArena } from './arena.js';
3
3
  import { TOKEN_IDENT, TOKEN_DELIM, TOKEN_DIMENSION, TOKEN_NUMBER } from './token-types.js';
4
4
  import { str_equals, CHAR_MINUS_HYPHEN, CHAR_PLUS, str_index_of } from './string-utils.js';
@@ -1,4 +1,4 @@
1
- import { Lexer } from './lexer.js';
1
+ import { Lexer } from './tokenize.js';
2
2
  import { CSSDataArena, PRELUDE_OPERATOR, MEDIA_TYPE, MEDIA_QUERY, MEDIA_FEATURE, IDENTIFIER, CONTAINER_QUERY, SUPPORTS_QUERY, LAYER_NAME, URL } from './arena.js';
3
3
  import { TOKEN_COMMA, TOKEN_IDENT, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_WHITESPACE, TOKEN_URL, TOKEN_FUNCTION, TOKEN_STRING, TOKEN_EOF } from './token-types.js';
4
4
  import { str_equals } from './string-utils.js';
@@ -1,4 +1,4 @@
1
- import { Lexer } from './lexer.js';
1
+ import { Lexer } from './tokenize.js';
2
2
  import { CSSDataArena, DECLARATION, FLAG_IMPORTANT } from './arena.js';
3
3
  import { ValueParser } from './parse-value.js';
4
4
  import { TOKEN_IDENT, TOKEN_COLON, TOKEN_EOF, TOKEN_SEMICOLON, TOKEN_RIGHT_BRACE, TOKEN_LEFT_BRACE, TOKEN_DELIM } from './token-types.js';
@@ -1,4 +1,4 @@
1
- import { Lexer } from './lexer.js';
1
+ import { Lexer } from './tokenize.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';
@@ -1,4 +1,4 @@
1
- import { Lexer } from './lexer.js';
1
+ import { Lexer } from './tokenize.js';
2
2
  import { CSSDataArena, OPERATOR, HASH, STRING, DIMENSION, NUMBER, IDENTIFIER, URL, FUNCTION, PARENTHESIS } from './arena.js';
3
3
  import { TOKEN_EOF, TOKEN_LEFT_PAREN, TOKEN_COMMA, TOKEN_DELIM, TOKEN_FUNCTION, TOKEN_HASH, TOKEN_STRING, TOKEN_DIMENSION, TOKEN_PERCENTAGE, TOKEN_NUMBER, TOKEN_IDENT, TOKEN_RIGHT_PAREN } from './token-types.js';
4
4
  import { is_whitespace, CHAR_PLUS, CHAR_MINUS_HYPHEN, CHAR_ASTERISK, CHAR_FORWARD_SLASH, str_equals } from './string-utils.js';
package/dist/parse.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Lexer } from './lexer.js';
1
+ import { Lexer } from './tokenize.js';
2
2
  import { CSSDataArena, STYLESHEET, STYLE_RULE, FLAG_HAS_BLOCK, BLOCK, FLAG_HAS_DECLARATIONS, SELECTOR_LIST, AT_RULE } from './arena.js';
3
3
  import { CSSNode } from './css-node.js';
4
4
  import { SelectorParser } from './parse-selector.js';
@@ -67,3 +67,17 @@ export declare function str_index_of(str: string, searchChar: string): number;
67
67
  */
68
68
  export declare function is_vendor_prefixed(text: string): boolean;
69
69
  export declare function is_vendor_prefixed(source: string, start: number, end: number): boolean;
70
+ /**
71
+ * Check if a string is a CSS custom property (starts with --)
72
+ *
73
+ * @param str - The string to check
74
+ * @returns true if the string starts with -- (custom property)
75
+ *
76
+ * Examples:
77
+ * - `--primary-color` → true
78
+ * - `--my-var` → true
79
+ * - `-webkit-transform` → false (vendor prefix, not custom)
80
+ * - `border-radius` → false (standard property)
81
+ * - `color` → false
82
+ */
83
+ export declare function is_custom(str: string): boolean;
@@ -109,5 +109,9 @@ function is_vendor_prefixed(source, start, end) {
109
109
  }
110
110
  return false;
111
111
  }
112
+ function is_custom(str) {
113
+ if (str.length < 3) return false;
114
+ return str.charCodeAt(0) === CHAR_MINUS_HYPHEN && str.charCodeAt(1) === CHAR_MINUS_HYPHEN;
115
+ }
112
116
 
113
- export { CHAR_AMPERSAND, CHAR_ASTERISK, CHAR_CARET, CHAR_CARRIAGE_RETURN, CHAR_COLON, CHAR_DOLLAR, CHAR_DOUBLE_QUOTE, CHAR_EQUALS, CHAR_FORM_FEED, CHAR_FORWARD_SLASH, CHAR_GREATER_THAN, CHAR_MINUS_HYPHEN, CHAR_NEWLINE, CHAR_PERIOD, CHAR_PIPE, CHAR_PLUS, CHAR_SINGLE_QUOTE, CHAR_SPACE, CHAR_TAB, CHAR_TILDE, is_combinator, is_digit, is_vendor_prefixed, is_whitespace, str_equals, str_index_of, str_starts_with };
117
+ export { CHAR_AMPERSAND, CHAR_ASTERISK, CHAR_CARET, CHAR_CARRIAGE_RETURN, CHAR_COLON, CHAR_DOLLAR, CHAR_DOUBLE_QUOTE, CHAR_EQUALS, CHAR_FORM_FEED, CHAR_FORWARD_SLASH, CHAR_GREATER_THAN, CHAR_MINUS_HYPHEN, CHAR_NEWLINE, CHAR_PERIOD, CHAR_PIPE, CHAR_PLUS, CHAR_SINGLE_QUOTE, CHAR_SPACE, CHAR_TAB, CHAR_TILDE, is_combinator, is_custom, is_digit, is_vendor_prefixed, is_whitespace, str_equals, str_index_of, str_starts_with };
@@ -1,4 +1,14 @@
1
- import type { Token } from './token-types';
1
+ import { type Token, type TokenType } from './token-types';
2
+ export interface LexerPosition {
3
+ pos: number;
4
+ line: number;
5
+ column: number;
6
+ token_type: TokenType;
7
+ token_start: number;
8
+ token_end: number;
9
+ token_line: number;
10
+ token_column: number;
11
+ }
2
12
  /**
3
13
  * Tokenize CSS source code
4
14
  * @param source - The CSS source code to tokenize
package/dist/tokenize.js CHANGED
@@ -1,6 +1,432 @@
1
- import { Lexer } from './lexer.js';
2
- import { TOKEN_EOF } from './token-types.js';
1
+ import { is_whitespace, is_newline, is_digit, is_ident_start, is_hex_digit, is_ident_char } from './char-types.js';
2
+ import { TOKEN_EOF, TOKEN_LEFT_BRACE, TOKEN_RIGHT_BRACE, TOKEN_COLON, TOKEN_SEMICOLON, TOKEN_COMMA, TOKEN_LEFT_BRACKET, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_CDO, TOKEN_CDC, TOKEN_DELIM, TOKEN_WHITESPACE, TOKEN_COMMENT, TOKEN_STRING, TOKEN_BAD_STRING, TOKEN_PERCENTAGE, TOKEN_DIMENSION, TOKEN_NUMBER, TOKEN_FUNCTION, TOKEN_IDENT, TOKEN_AT_KEYWORD, TOKEN_HASH } from './token-types.js';
3
3
 
4
+ const CHAR_LEFT_BRACE = 123;
5
+ const CHAR_RIGHT_BRACE = 125;
6
+ const CHAR_COLON = 58;
7
+ const CHAR_SEMICOLON = 59;
8
+ const CHAR_COMMA = 44;
9
+ const CHAR_LEFT_BRACKET = 91;
10
+ const CHAR_RIGHT_BRACKET = 93;
11
+ const CHAR_LEFT_PAREN = 40;
12
+ const CHAR_RIGHT_PAREN = 41;
13
+ const CHAR_FORWARD_SLASH = 47;
14
+ const CHAR_ASTERISK = 42;
15
+ const CHAR_DOUBLE_QUOTE = 34;
16
+ const CHAR_SINGLE_QUOTE = 39;
17
+ const CHAR_DOT = 46;
18
+ const CHAR_LESS_THAN = 60;
19
+ const CHAR_EXCLAMATION = 33;
20
+ const CHAR_HYPHEN = 45;
21
+ const CHAR_GREATER_THAN = 62;
22
+ const CHAR_AT_SIGN = 64;
23
+ const CHAR_HASH = 35;
24
+ const CHAR_BACKSLASH = 92;
25
+ const CHAR_PLUS = 43;
26
+ const CHAR_PERCENT = 37;
27
+ const CHAR_LOWERCASE_E = 101;
28
+ const CHAR_UPPERCASE_E = 69;
29
+ const CHAR_CARRIAGE_RETURN = 13;
30
+ const CHAR_LINE_FEED = 10;
31
+ class Lexer {
32
+ source;
33
+ pos;
34
+ line;
35
+ column;
36
+ skip_comments;
37
+ // Current token properties (avoiding object allocation)
38
+ token_type;
39
+ token_start;
40
+ token_end;
41
+ token_line;
42
+ token_column;
43
+ constructor(source, skip_comments = false) {
44
+ this.source = source;
45
+ this.pos = 0;
46
+ this.line = 1;
47
+ this.column = 1;
48
+ this.skip_comments = skip_comments;
49
+ this.token_type = TOKEN_EOF;
50
+ this.token_start = 0;
51
+ this.token_end = 0;
52
+ this.token_line = 1;
53
+ this.token_column = 1;
54
+ }
55
+ // Fast token advancing without object allocation (for internal parser use)
56
+ next_token_fast(skip_whitespace = false) {
57
+ if (skip_whitespace) {
58
+ while (this.pos < this.source.length) {
59
+ let ch2 = this.source.charCodeAt(this.pos);
60
+ if (!is_whitespace(ch2) && !is_newline(ch2)) break;
61
+ this.advance();
62
+ }
63
+ }
64
+ if (this.pos >= this.source.length) {
65
+ return this.make_token(TOKEN_EOF, this.pos, this.pos);
66
+ }
67
+ let ch = this.source.charCodeAt(this.pos);
68
+ let start = this.pos;
69
+ let start_line = this.line;
70
+ let start_column = this.column;
71
+ if (ch === CHAR_LEFT_BRACE) {
72
+ this.advance();
73
+ return this.make_token(TOKEN_LEFT_BRACE, start, this.pos, start_line, start_column);
74
+ }
75
+ if (ch === CHAR_RIGHT_BRACE) {
76
+ this.advance();
77
+ return this.make_token(TOKEN_RIGHT_BRACE, start, this.pos, start_line, start_column);
78
+ }
79
+ if (ch === CHAR_COLON) {
80
+ this.advance();
81
+ return this.make_token(TOKEN_COLON, start, this.pos, start_line, start_column);
82
+ }
83
+ if (ch === CHAR_SEMICOLON) {
84
+ this.advance();
85
+ return this.make_token(TOKEN_SEMICOLON, start, this.pos, start_line, start_column);
86
+ }
87
+ if (ch === CHAR_COMMA) {
88
+ this.advance();
89
+ return this.make_token(TOKEN_COMMA, start, this.pos, start_line, start_column);
90
+ }
91
+ if (ch === CHAR_LEFT_BRACKET) {
92
+ this.advance();
93
+ return this.make_token(TOKEN_LEFT_BRACKET, start, this.pos, start_line, start_column);
94
+ }
95
+ if (ch === CHAR_RIGHT_BRACKET) {
96
+ this.advance();
97
+ return this.make_token(TOKEN_RIGHT_BRACKET, start, this.pos, start_line, start_column);
98
+ }
99
+ if (ch === CHAR_LEFT_PAREN) {
100
+ this.advance();
101
+ return this.make_token(TOKEN_LEFT_PAREN, start, this.pos, start_line, start_column);
102
+ }
103
+ if (ch === CHAR_RIGHT_PAREN) {
104
+ this.advance();
105
+ return this.make_token(TOKEN_RIGHT_PAREN, start, this.pos, start_line, start_column);
106
+ }
107
+ if (is_whitespace(ch) || is_newline(ch)) {
108
+ return this.consume_whitespace(start_line, start_column);
109
+ }
110
+ if (ch === CHAR_FORWARD_SLASH && this.peek() === CHAR_ASTERISK) {
111
+ if (this.skip_comments) {
112
+ this.advance(2);
113
+ while (this.pos < this.source.length - 1) {
114
+ let ch2 = this.source.charCodeAt(this.pos);
115
+ if (ch2 === CHAR_ASTERISK && this.source.charCodeAt(this.pos + 1) === CHAR_FORWARD_SLASH) {
116
+ this.advance(2);
117
+ break;
118
+ }
119
+ this.advance();
120
+ }
121
+ return this.next_token_fast(skip_whitespace);
122
+ }
123
+ return this.consume_comment(start_line, start_column);
124
+ }
125
+ if (ch === CHAR_DOUBLE_QUOTE || ch === CHAR_SINGLE_QUOTE) {
126
+ return this.consume_string(ch, start_line, start_column);
127
+ }
128
+ if (is_digit(ch)) {
129
+ return this.consume_number(start_line, start_column);
130
+ }
131
+ if (ch === CHAR_DOT && is_digit(this.peek())) {
132
+ return this.consume_number(start_line, start_column);
133
+ }
134
+ if (ch === CHAR_LESS_THAN && this.pos + 3 < this.source.length) {
135
+ if (this.source.charCodeAt(this.pos + 1) === CHAR_EXCLAMATION && this.source.charCodeAt(this.pos + 2) === CHAR_HYPHEN && this.source.charCodeAt(this.pos + 3) === CHAR_HYPHEN) {
136
+ this.advance(4);
137
+ return this.make_token(TOKEN_CDO, start, this.pos, start_line, start_column);
138
+ }
139
+ }
140
+ if (ch === CHAR_HYPHEN && this.pos + 2 < this.source.length) {
141
+ if (this.source.charCodeAt(this.pos + 1) === CHAR_HYPHEN && this.source.charCodeAt(this.pos + 2) === CHAR_GREATER_THAN) {
142
+ this.advance(3);
143
+ return this.make_token(TOKEN_CDC, start, this.pos, start_line, start_column);
144
+ }
145
+ }
146
+ if (ch === CHAR_AT_SIGN) {
147
+ return this.consume_at_keyword(start_line, start_column);
148
+ }
149
+ if (ch === CHAR_HASH) {
150
+ return this.consume_hash(start_line, start_column);
151
+ }
152
+ if (is_ident_start(ch)) {
153
+ return this.consume_ident_or_function(start_line, start_column);
154
+ }
155
+ if (ch === CHAR_HYPHEN) {
156
+ let next = this.peek();
157
+ if (is_ident_start(next) || next === CHAR_HYPHEN) {
158
+ return this.consume_ident_or_function(start_line, start_column);
159
+ }
160
+ }
161
+ if (ch === CHAR_BACKSLASH) {
162
+ let next = this.peek();
163
+ if (next !== 0 && !is_newline(next)) {
164
+ return this.consume_ident_or_function(start_line, start_column);
165
+ }
166
+ }
167
+ if (ch === CHAR_HYPHEN || ch === CHAR_PLUS) {
168
+ let next = this.peek();
169
+ if (is_digit(next) || next === CHAR_DOT && is_digit(this.peek(2))) {
170
+ return this.consume_number(start_line, start_column);
171
+ }
172
+ }
173
+ this.advance();
174
+ return this.make_token(TOKEN_DELIM, start, this.pos, start_line, start_column);
175
+ }
176
+ consume_whitespace(start_line, start_column) {
177
+ let start = this.pos;
178
+ while (this.pos < this.source.length) {
179
+ let ch = this.source.charCodeAt(this.pos);
180
+ if (!is_whitespace(ch) && !is_newline(ch)) break;
181
+ this.advance();
182
+ }
183
+ return this.make_token(TOKEN_WHITESPACE, start, this.pos, start_line, start_column);
184
+ }
185
+ consume_comment(start_line, start_column) {
186
+ let start = this.pos;
187
+ this.advance(2);
188
+ while (this.pos < this.source.length - 1) {
189
+ let ch = this.source.charCodeAt(this.pos);
190
+ if (ch === CHAR_ASTERISK && this.source.charCodeAt(this.pos + 1) === CHAR_FORWARD_SLASH) {
191
+ this.advance(2);
192
+ break;
193
+ }
194
+ this.advance();
195
+ }
196
+ return this.make_token(TOKEN_COMMENT, start, this.pos, start_line, start_column);
197
+ }
198
+ consume_string(quote, start_line, start_column) {
199
+ let start = this.pos;
200
+ this.advance();
201
+ while (this.pos < this.source.length) {
202
+ let ch = this.source.charCodeAt(this.pos);
203
+ if (ch === quote) {
204
+ this.advance();
205
+ return this.make_token(TOKEN_STRING, start, this.pos, start_line, start_column);
206
+ }
207
+ if (is_newline(ch)) {
208
+ return this.make_token(TOKEN_BAD_STRING, start, this.pos, start_line, start_column);
209
+ }
210
+ if (ch === CHAR_BACKSLASH) {
211
+ this.advance();
212
+ if (this.pos < this.source.length) {
213
+ let next = this.source.charCodeAt(this.pos);
214
+ if (is_hex_digit(next)) {
215
+ this.consume_hex_escape();
216
+ } else if (!is_newline(next)) {
217
+ this.advance();
218
+ } else {
219
+ this.advance();
220
+ }
221
+ }
222
+ continue;
223
+ }
224
+ this.advance();
225
+ }
226
+ return this.make_token(TOKEN_BAD_STRING, start, this.pos, start_line, start_column);
227
+ }
228
+ consume_hex_escape() {
229
+ let count = 0;
230
+ while (count < 6 && this.pos < this.source.length) {
231
+ let ch = this.source.charCodeAt(this.pos);
232
+ if (!is_hex_digit(ch)) break;
233
+ this.advance();
234
+ count++;
235
+ }
236
+ if (this.pos < this.source.length) {
237
+ let ch = this.source.charCodeAt(this.pos);
238
+ if (is_whitespace(ch) || is_newline(ch)) {
239
+ this.advance();
240
+ }
241
+ }
242
+ }
243
+ consume_number(start_line, start_column) {
244
+ let start = this.pos;
245
+ let ch = this.source.charCodeAt(this.pos);
246
+ if (ch === CHAR_PLUS || ch === CHAR_HYPHEN) {
247
+ this.advance();
248
+ }
249
+ while (this.pos < this.source.length && is_digit(this.source.charCodeAt(this.pos))) {
250
+ this.advance();
251
+ }
252
+ if (this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_DOT && this.pos + 1 < this.source.length && is_digit(this.source.charCodeAt(this.pos + 1))) {
253
+ this.advance();
254
+ while (this.pos < this.source.length && is_digit(this.source.charCodeAt(this.pos))) {
255
+ this.advance();
256
+ }
257
+ }
258
+ if (this.pos < this.source.length) {
259
+ let ch2 = this.source.charCodeAt(this.pos);
260
+ if (ch2 === CHAR_LOWERCASE_E || ch2 === CHAR_UPPERCASE_E) {
261
+ let next = this.peek();
262
+ if (is_digit(next) || (next === CHAR_PLUS || next === CHAR_HYPHEN) && is_digit(this.peek(2))) {
263
+ this.advance();
264
+ if (this.pos < this.source.length) {
265
+ let sign = this.source.charCodeAt(this.pos);
266
+ if (sign === CHAR_PLUS || sign === CHAR_HYPHEN) {
267
+ this.advance();
268
+ }
269
+ }
270
+ while (this.pos < this.source.length && is_digit(this.source.charCodeAt(this.pos))) {
271
+ this.advance();
272
+ }
273
+ }
274
+ }
275
+ }
276
+ if (this.pos < this.source.length) {
277
+ let ch2 = this.source.charCodeAt(this.pos);
278
+ if (ch2 === CHAR_PERCENT) {
279
+ this.advance();
280
+ return this.make_token(TOKEN_PERCENTAGE, start, this.pos, start_line, start_column);
281
+ }
282
+ if (is_ident_start(ch2) || ch2 === CHAR_HYPHEN && is_ident_start(this.peek())) {
283
+ while (this.pos < this.source.length && is_ident_char(this.source.charCodeAt(this.pos))) {
284
+ this.advance();
285
+ }
286
+ return this.make_token(TOKEN_DIMENSION, start, this.pos, start_line, start_column);
287
+ }
288
+ }
289
+ return this.make_token(TOKEN_NUMBER, start, this.pos, start_line, start_column);
290
+ }
291
+ consume_ident_or_function(start_line, start_column) {
292
+ let start = this.pos;
293
+ while (this.pos < this.source.length) {
294
+ let ch = this.source.charCodeAt(this.pos);
295
+ if (ch === CHAR_BACKSLASH) {
296
+ if (this.pos + 1 >= this.source.length) break;
297
+ let next = this.source.charCodeAt(this.pos + 1);
298
+ if (is_newline(next)) break;
299
+ this.advance();
300
+ if (is_hex_digit(next)) {
301
+ this.advance();
302
+ for (let i = 0; i < 5 && this.pos < this.source.length; i++) {
303
+ if (!is_hex_digit(this.source.charCodeAt(this.pos))) break;
304
+ this.advance();
305
+ }
306
+ if (this.pos < this.source.length) {
307
+ let ws = this.source.charCodeAt(this.pos);
308
+ if (is_whitespace(ws) || is_newline(ws)) {
309
+ this.advance();
310
+ }
311
+ }
312
+ } else {
313
+ this.advance();
314
+ }
315
+ } else if (is_ident_char(ch)) {
316
+ this.advance();
317
+ } else {
318
+ break;
319
+ }
320
+ }
321
+ if (this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_LEFT_PAREN) {
322
+ this.advance();
323
+ return this.make_token(TOKEN_FUNCTION, start, this.pos, start_line, start_column);
324
+ }
325
+ return this.make_token(TOKEN_IDENT, start, this.pos, start_line, start_column);
326
+ }
327
+ consume_at_keyword(start_line, start_column) {
328
+ let start = this.pos;
329
+ this.advance();
330
+ while (this.pos < this.source.length && is_ident_char(this.source.charCodeAt(this.pos))) {
331
+ this.advance();
332
+ }
333
+ return this.make_token(TOKEN_AT_KEYWORD, start, this.pos, start_line, start_column);
334
+ }
335
+ consume_hash(start_line, start_column) {
336
+ let start = this.pos;
337
+ this.advance();
338
+ while (this.pos < this.source.length && is_ident_char(this.source.charCodeAt(this.pos))) {
339
+ this.advance();
340
+ }
341
+ return this.make_token(TOKEN_HASH, start, this.pos, start_line, start_column);
342
+ }
343
+ advance(count = 1) {
344
+ if (count === 1) {
345
+ if (this.pos >= this.source.length) return;
346
+ let ch = this.source.charCodeAt(this.pos);
347
+ this.pos++;
348
+ if (is_newline(ch)) {
349
+ if (ch === CHAR_CARRIAGE_RETURN && this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_LINE_FEED) {
350
+ this.pos++;
351
+ }
352
+ this.line++;
353
+ this.column = 1;
354
+ } else {
355
+ this.column++;
356
+ }
357
+ return;
358
+ }
359
+ for (let i = 0; i < count; i++) {
360
+ if (this.pos >= this.source.length) break;
361
+ let ch = this.source.charCodeAt(this.pos);
362
+ this.pos++;
363
+ if (is_newline(ch)) {
364
+ if (ch === CHAR_CARRIAGE_RETURN && this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_LINE_FEED) {
365
+ this.pos++;
366
+ i++;
367
+ }
368
+ this.line++;
369
+ this.column = 1;
370
+ } else {
371
+ this.column++;
372
+ }
373
+ }
374
+ }
375
+ peek(offset = 1) {
376
+ let index = this.pos + offset;
377
+ if (index >= this.source.length) return 0;
378
+ return this.source.charCodeAt(index);
379
+ }
380
+ make_token(type, start, end, line = this.line, column = this.column) {
381
+ this.token_type = type;
382
+ this.token_start = start;
383
+ this.token_end = end;
384
+ this.token_line = line;
385
+ this.token_column = column;
386
+ return type;
387
+ }
388
+ // Public API: returns Token object for backwards compatibility
389
+ next_token(skip_whitespace = false) {
390
+ this.next_token_fast(skip_whitespace);
391
+ return {
392
+ type: this.token_type,
393
+ start: this.token_start,
394
+ end: this.token_end,
395
+ line: this.token_line,
396
+ column: this.token_column
397
+ };
398
+ }
399
+ /**
400
+ * Save complete lexer state for backtracking
401
+ * @returns Object containing all lexer state
402
+ */
403
+ save_position() {
404
+ return {
405
+ pos: this.pos,
406
+ line: this.line,
407
+ column: this.column,
408
+ token_type: this.token_type,
409
+ token_start: this.token_start,
410
+ token_end: this.token_end,
411
+ token_line: this.token_line,
412
+ token_column: this.token_column
413
+ };
414
+ }
415
+ /**
416
+ * Restore lexer state from saved position
417
+ * @param saved The saved position to restore
418
+ */
419
+ restore_position(saved) {
420
+ this.pos = saved.pos;
421
+ this.line = saved.line;
422
+ this.column = saved.column;
423
+ this.token_type = saved.token_type;
424
+ this.token_start = saved.token_start;
425
+ this.token_end = saved.token_end;
426
+ this.token_line = saved.token_line;
427
+ this.token_column = saved.token_column;
428
+ }
429
+ }
4
430
  function* tokenize(source, skip_comments = true) {
5
431
  const lexer = new Lexer(source, skip_comments);
6
432
  while (true) {
@@ -12,4 +438,4 @@ function* tokenize(source, skip_comments = true) {
12
438
  }
13
439
  }
14
440
 
15
- export { tokenize };
441
+ export { Lexer, tokenize };
package/dist/walk.d.ts CHANGED
@@ -3,34 +3,12 @@ export declare const SKIP: unique symbol;
3
3
  export declare const BREAK: unique symbol;
4
4
  type WalkCallback = (node: CSSNode, depth: number) => void | typeof SKIP | typeof BREAK;
5
5
  /**
6
- * Walk the AST in depth-first order, calling the callback for each node
6
+ * Walk the AST in depth-first order, calling the callback for each node.
7
+ * Return SKIP to skip children, BREAK to stop traversal. See API.md for examples.
7
8
  *
8
9
  * @param node - The root node to start walking from
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.
10
+ * @param callback - Function called for each node. Receives the node and its depth (0 for root).
11
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
- * })
34
12
  */
35
13
  export declare function walk(node: CSSNode, callback: WalkCallback, depth?: number): boolean;
36
14
  type WalkEnterLeaveCallback = (node: CSSNode) => void | typeof SKIP | typeof BREAK;
@@ -39,36 +17,11 @@ interface WalkEnterLeaveOptions {
39
17
  leave?: WalkEnterLeaveCallback;
40
18
  }
41
19
  /**
42
- * Walk the AST in depth-first order, calling enter before visiting children and leave after
20
+ * Walk the AST in depth-first order, calling enter before visiting children and leave after.
21
+ * Return SKIP in enter to skip children (leave still called), BREAK to stop (leave NOT called). See API.md for examples.
43
22
  *
44
23
  * @param node - The root node to start walking from
45
24
  * @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
- * })
72
25
  */
73
26
  export declare function traverse(node: CSSNode, { enter, leave }?: WalkEnterLeaveOptions): boolean;
74
27
  export {};
package/dist/walk.js CHANGED
@@ -1,5 +1,5 @@
1
- const SKIP = Symbol("SKIP");
2
- const BREAK = Symbol("BREAK");
1
+ const SKIP = /* @__PURE__ */ Symbol("SKIP");
2
+ const BREAK = /* @__PURE__ */ Symbol("BREAK");
3
3
  function walk(node, callback, depth = 0) {
4
4
  const result = callback(node, depth);
5
5
  if (result === BREAK) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectwallace/css-parser",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "High-performance CSS lexer and parser, optimized for CSS inspection and analysis",
5
5
  "author": "Bart Veneman <bart@projectwallace.com>",
6
6
  "license": "MIT",
@@ -70,6 +70,7 @@
70
70
  "ast"
71
71
  ],
72
72
  "devDependencies": {
73
+ "@codecov/vite-plugin": "^1.9.1",
73
74
  "@projectwallace/preset-oxlint": "^0.0.7",
74
75
  "@types/node": "^24.10.1",
75
76
  "@vitest/coverage-v8": "^4.0.8",
@@ -84,5 +85,10 @@
84
85
  "typescript": "^5.9.3",
85
86
  "vite": "^7.2.2",
86
87
  "vitest": "^4.0.8"
88
+ },
89
+ "overrides": {
90
+ "@codecov/vite-plugin": {
91
+ "vite": "$vite"
92
+ }
87
93
  }
88
94
  }
package/dist/lexer.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import { type TokenType } from './token-types';
2
- export interface LexerPosition {
3
- pos: number;
4
- line: number;
5
- column: number;
6
- token_type: TokenType;
7
- token_start: number;
8
- token_end: number;
9
- token_line: number;
10
- token_column: number;
11
- }
package/dist/lexer.js DELETED
@@ -1,431 +0,0 @@
1
- import { is_whitespace, is_newline, is_digit, is_ident_start, is_hex_digit, is_ident_char } from './char-types.js';
2
- import { TOKEN_EOF, TOKEN_LEFT_BRACE, TOKEN_RIGHT_BRACE, TOKEN_COLON, TOKEN_SEMICOLON, TOKEN_COMMA, TOKEN_LEFT_BRACKET, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_CDO, TOKEN_CDC, TOKEN_DELIM, TOKEN_WHITESPACE, TOKEN_COMMENT, TOKEN_STRING, TOKEN_BAD_STRING, TOKEN_PERCENTAGE, TOKEN_DIMENSION, TOKEN_NUMBER, TOKEN_FUNCTION, TOKEN_IDENT, TOKEN_AT_KEYWORD, TOKEN_HASH } from './token-types.js';
3
-
4
- const CHAR_LEFT_BRACE = 123;
5
- const CHAR_RIGHT_BRACE = 125;
6
- const CHAR_COLON = 58;
7
- const CHAR_SEMICOLON = 59;
8
- const CHAR_COMMA = 44;
9
- const CHAR_LEFT_BRACKET = 91;
10
- const CHAR_RIGHT_BRACKET = 93;
11
- const CHAR_LEFT_PAREN = 40;
12
- const CHAR_RIGHT_PAREN = 41;
13
- const CHAR_FORWARD_SLASH = 47;
14
- const CHAR_ASTERISK = 42;
15
- const CHAR_DOUBLE_QUOTE = 34;
16
- const CHAR_SINGLE_QUOTE = 39;
17
- const CHAR_DOT = 46;
18
- const CHAR_LESS_THAN = 60;
19
- const CHAR_EXCLAMATION = 33;
20
- const CHAR_HYPHEN = 45;
21
- const CHAR_GREATER_THAN = 62;
22
- const CHAR_AT_SIGN = 64;
23
- const CHAR_HASH = 35;
24
- const CHAR_BACKSLASH = 92;
25
- const CHAR_PLUS = 43;
26
- const CHAR_PERCENT = 37;
27
- const CHAR_LOWERCASE_E = 101;
28
- const CHAR_UPPERCASE_E = 69;
29
- const CHAR_CARRIAGE_RETURN = 13;
30
- const CHAR_LINE_FEED = 10;
31
- class Lexer {
32
- source;
33
- pos;
34
- line;
35
- column;
36
- skip_comments;
37
- // Current token properties (avoiding object allocation)
38
- token_type;
39
- token_start;
40
- token_end;
41
- token_line;
42
- token_column;
43
- constructor(source, skip_comments = false) {
44
- this.source = source;
45
- this.pos = 0;
46
- this.line = 1;
47
- this.column = 1;
48
- this.skip_comments = skip_comments;
49
- this.token_type = TOKEN_EOF;
50
- this.token_start = 0;
51
- this.token_end = 0;
52
- this.token_line = 1;
53
- this.token_column = 1;
54
- }
55
- // Fast token advancing without object allocation (for internal parser use)
56
- next_token_fast(skip_whitespace = false) {
57
- if (skip_whitespace) {
58
- while (this.pos < this.source.length) {
59
- let ch2 = this.source.charCodeAt(this.pos);
60
- if (!is_whitespace(ch2) && !is_newline(ch2)) break;
61
- this.advance();
62
- }
63
- }
64
- if (this.pos >= this.source.length) {
65
- return this.make_token(TOKEN_EOF, this.pos, this.pos);
66
- }
67
- let ch = this.source.charCodeAt(this.pos);
68
- let start = this.pos;
69
- let start_line = this.line;
70
- let start_column = this.column;
71
- if (ch === CHAR_LEFT_BRACE) {
72
- this.advance();
73
- return this.make_token(TOKEN_LEFT_BRACE, start, this.pos, start_line, start_column);
74
- }
75
- if (ch === CHAR_RIGHT_BRACE) {
76
- this.advance();
77
- return this.make_token(TOKEN_RIGHT_BRACE, start, this.pos, start_line, start_column);
78
- }
79
- if (ch === CHAR_COLON) {
80
- this.advance();
81
- return this.make_token(TOKEN_COLON, start, this.pos, start_line, start_column);
82
- }
83
- if (ch === CHAR_SEMICOLON) {
84
- this.advance();
85
- return this.make_token(TOKEN_SEMICOLON, start, this.pos, start_line, start_column);
86
- }
87
- if (ch === CHAR_COMMA) {
88
- this.advance();
89
- return this.make_token(TOKEN_COMMA, start, this.pos, start_line, start_column);
90
- }
91
- if (ch === CHAR_LEFT_BRACKET) {
92
- this.advance();
93
- return this.make_token(TOKEN_LEFT_BRACKET, start, this.pos, start_line, start_column);
94
- }
95
- if (ch === CHAR_RIGHT_BRACKET) {
96
- this.advance();
97
- return this.make_token(TOKEN_RIGHT_BRACKET, start, this.pos, start_line, start_column);
98
- }
99
- if (ch === CHAR_LEFT_PAREN) {
100
- this.advance();
101
- return this.make_token(TOKEN_LEFT_PAREN, start, this.pos, start_line, start_column);
102
- }
103
- if (ch === CHAR_RIGHT_PAREN) {
104
- this.advance();
105
- return this.make_token(TOKEN_RIGHT_PAREN, start, this.pos, start_line, start_column);
106
- }
107
- if (is_whitespace(ch) || is_newline(ch)) {
108
- return this.consume_whitespace(start_line, start_column);
109
- }
110
- if (ch === CHAR_FORWARD_SLASH && this.peek() === CHAR_ASTERISK) {
111
- if (this.skip_comments) {
112
- this.advance(2);
113
- while (this.pos < this.source.length - 1) {
114
- let ch2 = this.source.charCodeAt(this.pos);
115
- if (ch2 === CHAR_ASTERISK && this.source.charCodeAt(this.pos + 1) === CHAR_FORWARD_SLASH) {
116
- this.advance(2);
117
- break;
118
- }
119
- this.advance();
120
- }
121
- return this.next_token_fast(skip_whitespace);
122
- }
123
- return this.consume_comment(start_line, start_column);
124
- }
125
- if (ch === CHAR_DOUBLE_QUOTE || ch === CHAR_SINGLE_QUOTE) {
126
- return this.consume_string(ch, start_line, start_column);
127
- }
128
- if (is_digit(ch)) {
129
- return this.consume_number(start_line, start_column);
130
- }
131
- if (ch === CHAR_DOT && is_digit(this.peek())) {
132
- return this.consume_number(start_line, start_column);
133
- }
134
- if (ch === CHAR_LESS_THAN && this.pos + 3 < this.source.length) {
135
- if (this.source.charCodeAt(this.pos + 1) === CHAR_EXCLAMATION && this.source.charCodeAt(this.pos + 2) === CHAR_HYPHEN && this.source.charCodeAt(this.pos + 3) === CHAR_HYPHEN) {
136
- this.advance(4);
137
- return this.make_token(TOKEN_CDO, start, this.pos, start_line, start_column);
138
- }
139
- }
140
- if (ch === CHAR_HYPHEN && this.pos + 2 < this.source.length) {
141
- if (this.source.charCodeAt(this.pos + 1) === CHAR_HYPHEN && this.source.charCodeAt(this.pos + 2) === CHAR_GREATER_THAN) {
142
- this.advance(3);
143
- return this.make_token(TOKEN_CDC, start, this.pos, start_line, start_column);
144
- }
145
- }
146
- if (ch === CHAR_AT_SIGN) {
147
- return this.consume_at_keyword(start_line, start_column);
148
- }
149
- if (ch === CHAR_HASH) {
150
- return this.consume_hash(start_line, start_column);
151
- }
152
- if (is_ident_start(ch)) {
153
- return this.consume_ident_or_function(start_line, start_column);
154
- }
155
- if (ch === CHAR_HYPHEN) {
156
- let next = this.peek();
157
- if (is_ident_start(next) || next === CHAR_HYPHEN) {
158
- return this.consume_ident_or_function(start_line, start_column);
159
- }
160
- }
161
- if (ch === CHAR_BACKSLASH) {
162
- let next = this.peek();
163
- if (next !== 0 && !is_newline(next)) {
164
- return this.consume_ident_or_function(start_line, start_column);
165
- }
166
- }
167
- if (ch === CHAR_HYPHEN || ch === CHAR_PLUS) {
168
- let next = this.peek();
169
- if (is_digit(next) || next === CHAR_DOT && is_digit(this.peek(2))) {
170
- return this.consume_number(start_line, start_column);
171
- }
172
- }
173
- this.advance();
174
- return this.make_token(TOKEN_DELIM, start, this.pos, start_line, start_column);
175
- }
176
- consume_whitespace(start_line, start_column) {
177
- let start = this.pos;
178
- while (this.pos < this.source.length) {
179
- let ch = this.source.charCodeAt(this.pos);
180
- if (!is_whitespace(ch) && !is_newline(ch)) break;
181
- this.advance();
182
- }
183
- return this.make_token(TOKEN_WHITESPACE, start, this.pos, start_line, start_column);
184
- }
185
- consume_comment(start_line, start_column) {
186
- let start = this.pos;
187
- this.advance(2);
188
- while (this.pos < this.source.length - 1) {
189
- let ch = this.source.charCodeAt(this.pos);
190
- if (ch === CHAR_ASTERISK && this.source.charCodeAt(this.pos + 1) === CHAR_FORWARD_SLASH) {
191
- this.advance(2);
192
- break;
193
- }
194
- this.advance();
195
- }
196
- return this.make_token(TOKEN_COMMENT, start, this.pos, start_line, start_column);
197
- }
198
- consume_string(quote, start_line, start_column) {
199
- let start = this.pos;
200
- this.advance();
201
- while (this.pos < this.source.length) {
202
- let ch = this.source.charCodeAt(this.pos);
203
- if (ch === quote) {
204
- this.advance();
205
- return this.make_token(TOKEN_STRING, start, this.pos, start_line, start_column);
206
- }
207
- if (is_newline(ch)) {
208
- return this.make_token(TOKEN_BAD_STRING, start, this.pos, start_line, start_column);
209
- }
210
- if (ch === CHAR_BACKSLASH) {
211
- this.advance();
212
- if (this.pos < this.source.length) {
213
- let next = this.source.charCodeAt(this.pos);
214
- if (is_hex_digit(next)) {
215
- this.consume_hex_escape();
216
- } else if (!is_newline(next)) {
217
- this.advance();
218
- } else {
219
- this.advance();
220
- }
221
- }
222
- continue;
223
- }
224
- this.advance();
225
- }
226
- return this.make_token(TOKEN_BAD_STRING, start, this.pos, start_line, start_column);
227
- }
228
- consume_hex_escape() {
229
- let count = 0;
230
- while (count < 6 && this.pos < this.source.length) {
231
- let ch = this.source.charCodeAt(this.pos);
232
- if (!is_hex_digit(ch)) break;
233
- this.advance();
234
- count++;
235
- }
236
- if (this.pos < this.source.length) {
237
- let ch = this.source.charCodeAt(this.pos);
238
- if (is_whitespace(ch) || is_newline(ch)) {
239
- this.advance();
240
- }
241
- }
242
- }
243
- consume_number(start_line, start_column) {
244
- let start = this.pos;
245
- let ch = this.source.charCodeAt(this.pos);
246
- if (ch === CHAR_PLUS || ch === CHAR_HYPHEN) {
247
- this.advance();
248
- }
249
- while (this.pos < this.source.length && is_digit(this.source.charCodeAt(this.pos))) {
250
- this.advance();
251
- }
252
- if (this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_DOT && this.pos + 1 < this.source.length && is_digit(this.source.charCodeAt(this.pos + 1))) {
253
- this.advance();
254
- while (this.pos < this.source.length && is_digit(this.source.charCodeAt(this.pos))) {
255
- this.advance();
256
- }
257
- }
258
- if (this.pos < this.source.length) {
259
- let ch2 = this.source.charCodeAt(this.pos);
260
- if (ch2 === CHAR_LOWERCASE_E || ch2 === CHAR_UPPERCASE_E) {
261
- let next = this.peek();
262
- if (is_digit(next) || (next === CHAR_PLUS || next === CHAR_HYPHEN) && is_digit(this.peek(2))) {
263
- this.advance();
264
- if (this.pos < this.source.length) {
265
- let sign = this.source.charCodeAt(this.pos);
266
- if (sign === CHAR_PLUS || sign === CHAR_HYPHEN) {
267
- this.advance();
268
- }
269
- }
270
- while (this.pos < this.source.length && is_digit(this.source.charCodeAt(this.pos))) {
271
- this.advance();
272
- }
273
- }
274
- }
275
- }
276
- if (this.pos < this.source.length) {
277
- let ch2 = this.source.charCodeAt(this.pos);
278
- if (ch2 === CHAR_PERCENT) {
279
- this.advance();
280
- return this.make_token(TOKEN_PERCENTAGE, start, this.pos, start_line, start_column);
281
- }
282
- if (is_ident_start(ch2) || ch2 === CHAR_HYPHEN && is_ident_start(this.peek())) {
283
- while (this.pos < this.source.length && is_ident_char(this.source.charCodeAt(this.pos))) {
284
- this.advance();
285
- }
286
- return this.make_token(TOKEN_DIMENSION, start, this.pos, start_line, start_column);
287
- }
288
- }
289
- return this.make_token(TOKEN_NUMBER, start, this.pos, start_line, start_column);
290
- }
291
- consume_ident_or_function(start_line, start_column) {
292
- let start = this.pos;
293
- while (this.pos < this.source.length) {
294
- let ch = this.source.charCodeAt(this.pos);
295
- if (ch === CHAR_BACKSLASH) {
296
- if (this.pos + 1 >= this.source.length) break;
297
- let next = this.source.charCodeAt(this.pos + 1);
298
- if (is_newline(next)) break;
299
- this.advance();
300
- if (is_hex_digit(next)) {
301
- this.advance();
302
- for (let i = 0; i < 5 && this.pos < this.source.length; i++) {
303
- if (!is_hex_digit(this.source.charCodeAt(this.pos))) break;
304
- this.advance();
305
- }
306
- if (this.pos < this.source.length) {
307
- let ws = this.source.charCodeAt(this.pos);
308
- if (is_whitespace(ws) || is_newline(ws)) {
309
- this.advance();
310
- }
311
- }
312
- } else {
313
- this.advance();
314
- }
315
- } else if (is_ident_char(ch)) {
316
- this.advance();
317
- } else {
318
- break;
319
- }
320
- }
321
- if (this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_LEFT_PAREN) {
322
- this.advance();
323
- return this.make_token(TOKEN_FUNCTION, start, this.pos, start_line, start_column);
324
- }
325
- return this.make_token(TOKEN_IDENT, start, this.pos, start_line, start_column);
326
- }
327
- consume_at_keyword(start_line, start_column) {
328
- let start = this.pos;
329
- this.advance();
330
- while (this.pos < this.source.length && is_ident_char(this.source.charCodeAt(this.pos))) {
331
- this.advance();
332
- }
333
- return this.make_token(TOKEN_AT_KEYWORD, start, this.pos, start_line, start_column);
334
- }
335
- consume_hash(start_line, start_column) {
336
- let start = this.pos;
337
- this.advance();
338
- while (this.pos < this.source.length && is_ident_char(this.source.charCodeAt(this.pos))) {
339
- this.advance();
340
- }
341
- return this.make_token(TOKEN_HASH, start, this.pos, start_line, start_column);
342
- }
343
- advance(count = 1) {
344
- if (count === 1) {
345
- if (this.pos >= this.source.length) return;
346
- let ch = this.source.charCodeAt(this.pos);
347
- this.pos++;
348
- if (is_newline(ch)) {
349
- if (ch === CHAR_CARRIAGE_RETURN && this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_LINE_FEED) {
350
- this.pos++;
351
- }
352
- this.line++;
353
- this.column = 1;
354
- } else {
355
- this.column++;
356
- }
357
- return;
358
- }
359
- for (let i = 0; i < count; i++) {
360
- if (this.pos >= this.source.length) break;
361
- let ch = this.source.charCodeAt(this.pos);
362
- this.pos++;
363
- if (is_newline(ch)) {
364
- if (ch === CHAR_CARRIAGE_RETURN && this.pos < this.source.length && this.source.charCodeAt(this.pos) === CHAR_LINE_FEED) {
365
- this.pos++;
366
- i++;
367
- }
368
- this.line++;
369
- this.column = 1;
370
- } else {
371
- this.column++;
372
- }
373
- }
374
- }
375
- peek(offset = 1) {
376
- let index = this.pos + offset;
377
- if (index >= this.source.length) return 0;
378
- return this.source.charCodeAt(index);
379
- }
380
- make_token(type, start, end, line = this.line, column = this.column) {
381
- this.token_type = type;
382
- this.token_start = start;
383
- this.token_end = end;
384
- this.token_line = line;
385
- this.token_column = column;
386
- return type;
387
- }
388
- // Public API: returns Token object for backwards compatibility
389
- next_token(skip_whitespace = false) {
390
- this.next_token_fast(skip_whitespace);
391
- return {
392
- type: this.token_type,
393
- start: this.token_start,
394
- end: this.token_end,
395
- line: this.token_line,
396
- column: this.token_column
397
- };
398
- }
399
- /**
400
- * Save complete lexer state for backtracking
401
- * @returns Object containing all lexer state
402
- */
403
- save_position() {
404
- return {
405
- pos: this.pos,
406
- line: this.line,
407
- column: this.column,
408
- token_type: this.token_type,
409
- token_start: this.token_start,
410
- token_end: this.token_end,
411
- token_line: this.token_line,
412
- token_column: this.token_column
413
- };
414
- }
415
- /**
416
- * Restore lexer state from saved position
417
- * @param saved The saved position to restore
418
- */
419
- restore_position(saved) {
420
- this.pos = saved.pos;
421
- this.line = saved.line;
422
- this.column = saved.column;
423
- this.token_type = saved.token_type;
424
- this.token_start = saved.token_start;
425
- this.token_end = saved.token_end;
426
- this.token_line = saved.token_line;
427
- this.token_column = saved.token_column;
428
- }
429
- }
430
-
431
- export { Lexer };