@projectwallace/css-parser 0.7.2 → 0.8.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/dist/arena.js CHANGED
@@ -1,4 +1,4 @@
1
- let BYTES_PER_NODE = 40;
1
+ let BYTES_PER_NODE = 32;
2
2
  const STYLESHEET = 1;
3
3
  const STYLE_RULE = 2;
4
4
  const AT_RULE = 3;
@@ -38,7 +38,6 @@ const PRELUDE_OPERATOR = 38;
38
38
  const FLAG_IMPORTANT = 1 << 0;
39
39
  const FLAG_HAS_ERROR = 1 << 1;
40
40
  const FLAG_HAS_BLOCK = 1 << 3;
41
- const FLAG_VENDOR_PREFIXED = 1 << 4;
42
41
  const FLAG_HAS_DECLARATIONS = 1 << 5;
43
42
  const FLAG_HAS_PARENS = 1 << 6;
44
43
  const ATTR_OPERATOR_NONE = 0;
@@ -99,59 +98,55 @@ class CSSDataArena {
99
98
  }
100
99
  // Read start offset in source
101
100
  get_start_offset(node_index) {
102
- return this.view.getUint32(this.node_offset(node_index) + 4, true);
101
+ return this.view.getUint32(this.node_offset(node_index) + 12, true);
103
102
  }
104
103
  // Read length in source
105
104
  get_length(node_index) {
106
- return this.view.getUint16(this.node_offset(node_index) + 8, true);
105
+ return this.view.getUint16(this.node_offset(node_index) + 2, true);
107
106
  }
108
107
  // Read content start offset (stored as delta from startOffset)
109
108
  get_content_start(node_index) {
110
109
  const startOffset = this.get_start_offset(node_index);
111
- const delta = this.view.getUint16(this.node_offset(node_index) + 12, true);
110
+ const delta = this.view.getUint16(this.node_offset(node_index) + 16, true);
112
111
  return startOffset + delta;
113
112
  }
114
113
  // Read content length
115
114
  get_content_length(node_index) {
116
- return this.view.getUint16(this.node_offset(node_index) + 14, true);
115
+ return this.view.getUint16(this.node_offset(node_index) + 20, true);
117
116
  }
118
117
  // Read attribute operator (for NODE_SELECTOR_ATTRIBUTE)
119
118
  get_attr_operator(node_index) {
120
- return this.view.getUint8(this.node_offset(node_index) + 2);
119
+ return this.view.getUint8(this.node_offset(node_index) + 30);
121
120
  }
122
121
  // Read attribute flags (for NODE_SELECTOR_ATTRIBUTE)
123
122
  get_attr_flags(node_index) {
124
- return this.view.getUint8(this.node_offset(node_index) + 3);
123
+ return this.view.getUint8(this.node_offset(node_index) + 31);
125
124
  }
126
125
  // Read first child index (0 = no children)
127
126
  get_first_child(node_index) {
128
- return this.view.getUint32(this.node_offset(node_index) + 20, true);
129
- }
130
- // Read last child index (0 = no children)
131
- get_last_child(node_index) {
132
- return this.view.getUint32(this.node_offset(node_index) + 24, true);
127
+ return this.view.getUint32(this.node_offset(node_index) + 4, true);
133
128
  }
134
129
  // Read next sibling index (0 = no sibling)
135
130
  get_next_sibling(node_index) {
136
- return this.view.getUint32(this.node_offset(node_index) + 28, true);
131
+ return this.view.getUint32(this.node_offset(node_index) + 8, true);
137
132
  }
138
133
  // Read start line
139
134
  get_start_line(node_index) {
140
- return this.view.getUint32(this.node_offset(node_index) + 32, true);
135
+ return this.view.getUint32(this.node_offset(node_index) + 24, true);
141
136
  }
142
137
  // Read start column
143
138
  get_start_column(node_index) {
144
- return this.view.getUint16(this.node_offset(node_index) + 36, true);
139
+ return this.view.getUint16(this.node_offset(node_index) + 28, true);
145
140
  }
146
141
  // Read value start offset (stored as delta from startOffset, declaration value / at-rule prelude)
147
142
  get_value_start(node_index) {
148
143
  const startOffset = this.get_start_offset(node_index);
149
- const delta = this.view.getUint16(this.node_offset(node_index) + 16, true);
144
+ const delta = this.view.getUint16(this.node_offset(node_index) + 18, true);
150
145
  return startOffset + delta;
151
146
  }
152
147
  // Read value length
153
148
  get_value_length(node_index) {
154
- return this.view.getUint16(this.node_offset(node_index) + 18, true);
149
+ return this.view.getUint16(this.node_offset(node_index) + 22, true);
155
150
  }
156
151
  // --- Write Methods ---
157
152
  // Write node type
@@ -164,55 +159,51 @@ class CSSDataArena {
164
159
  }
165
160
  // Write start offset in source
166
161
  set_start_offset(node_index, offset) {
167
- this.view.setUint32(this.node_offset(node_index) + 4, offset, true);
162
+ this.view.setUint32(this.node_offset(node_index) + 12, offset, true);
168
163
  }
169
164
  // Write length in source
170
165
  set_length(node_index, length) {
171
- this.view.setUint16(this.node_offset(node_index) + 8, length, true);
166
+ this.view.setUint16(this.node_offset(node_index) + 2, length, true);
172
167
  }
173
168
  // Write content start delta (offset from startOffset)
174
169
  set_content_start_delta(node_index, delta) {
175
- this.view.setUint16(this.node_offset(node_index) + 12, delta, true);
170
+ this.view.setUint16(this.node_offset(node_index) + 16, delta, true);
176
171
  }
177
172
  // Write content length
178
173
  set_content_length(node_index, length) {
179
- this.view.setUint16(this.node_offset(node_index) + 14, length, true);
174
+ this.view.setUint16(this.node_offset(node_index) + 20, length, true);
180
175
  }
181
176
  // Write attribute operator (for NODE_SELECTOR_ATTRIBUTE)
182
177
  set_attr_operator(node_index, operator) {
183
- this.view.setUint8(this.node_offset(node_index) + 2, operator);
178
+ this.view.setUint8(this.node_offset(node_index) + 30, operator);
184
179
  }
185
180
  // Write attribute flags (for NODE_SELECTOR_ATTRIBUTE)
186
181
  set_attr_flags(node_index, flags) {
187
- this.view.setUint8(this.node_offset(node_index) + 3, flags);
182
+ this.view.setUint8(this.node_offset(node_index) + 31, flags);
188
183
  }
189
184
  // Write first child index
190
185
  set_first_child(node_index, childIndex) {
191
- this.view.setUint32(this.node_offset(node_index) + 20, childIndex, true);
192
- }
193
- // Write last child index
194
- set_last_child(node_index, childIndex) {
195
- this.view.setUint32(this.node_offset(node_index) + 24, childIndex, true);
186
+ this.view.setUint32(this.node_offset(node_index) + 4, childIndex, true);
196
187
  }
197
188
  // Write next sibling index
198
189
  set_next_sibling(node_index, siblingIndex) {
199
- this.view.setUint32(this.node_offset(node_index) + 28, siblingIndex, true);
190
+ this.view.setUint32(this.node_offset(node_index) + 8, siblingIndex, true);
200
191
  }
201
192
  // Write start line
202
193
  set_start_line(node_index, line) {
203
- this.view.setUint32(this.node_offset(node_index) + 32, line, true);
194
+ this.view.setUint32(this.node_offset(node_index) + 24, line, true);
204
195
  }
205
196
  // Write start column
206
197
  set_start_column(node_index, column) {
207
- this.view.setUint16(this.node_offset(node_index) + 36, column, true);
198
+ this.view.setUint16(this.node_offset(node_index) + 28, column, true);
208
199
  }
209
200
  // Write value start delta (offset from startOffset, declaration value / at-rule prelude)
210
201
  set_value_start_delta(node_index, delta) {
211
- this.view.setUint16(this.node_offset(node_index) + 16, delta, true);
202
+ this.view.setUint16(this.node_offset(node_index) + 18, delta, true);
212
203
  }
213
204
  // Write value length
214
205
  set_value_length(node_index, length) {
215
- this.view.setUint16(this.node_offset(node_index) + 18, length, true);
206
+ this.view.setUint16(this.node_offset(node_index) + 22, length, true);
216
207
  }
217
208
  // --- Node Creation ---
218
209
  // Grow the arena by 1.3x when capacity is exceeded
@@ -234,10 +225,10 @@ class CSSDataArena {
234
225
  this.count++;
235
226
  const offset = node_index * BYTES_PER_NODE;
236
227
  this.view.setUint8(offset, type);
237
- this.view.setUint32(offset + 4, start_offset, true);
238
- this.view.setUint16(offset + 8, length, true);
239
- this.view.setUint32(offset + 32, start_line, true);
240
- this.view.setUint16(offset + 36, start_column, true);
228
+ this.view.setUint16(offset + 2, length, true);
229
+ this.view.setUint32(offset + 12, start_offset, true);
230
+ this.view.setUint32(offset + 24, start_line, true);
231
+ this.view.setUint16(offset + 28, start_column, true);
241
232
  return node_index;
242
233
  }
243
234
  // --- Tree Building Helpers ---
@@ -246,8 +237,7 @@ class CSSDataArena {
246
237
  append_children(parent_index, children) {
247
238
  if (children.length === 0) return;
248
239
  const offset = this.node_offset(parent_index);
249
- this.view.setUint32(offset + 20, children[0], true);
250
- this.view.setUint32(offset + 24, children[children.length - 1], true);
240
+ this.view.setUint32(offset + 4, children[0], true);
251
241
  for (let i = 0; i < children.length - 1; i++) {
252
242
  this.set_next_sibling(children[i], children[i + 1]);
253
243
  }
@@ -277,4 +267,4 @@ class CSSDataArena {
277
267
  }
278
268
  }
279
269
 
280
- 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_VENDOR_PREFIXED, 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 };
270
+ 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 };
@@ -47,7 +47,7 @@ export interface CloneOptions {
47
47
  */
48
48
  deep?: boolean;
49
49
  /**
50
- * Include location information (line, column, offset, length)
50
+ * Include location information (line, column, start, length)
51
51
  * @default false
52
52
  */
53
53
  locations?: boolean;
@@ -71,8 +71,9 @@ export type PlainCSSNode = {
71
71
  nth_b?: string | null;
72
72
  line?: number;
73
73
  column?: number;
74
- offset?: number;
74
+ start?: number;
75
75
  length?: number;
76
+ end?: number;
76
77
  };
77
78
  export declare class CSSNode {
78
79
  private arena;
@@ -86,6 +87,7 @@ export declare class CSSNode {
86
87
  get name(): string;
87
88
  get property(): string;
88
89
  get value(): string | number | null;
90
+ get value_as_number(): number | null;
89
91
  get prelude(): string | null;
90
92
  get attr_operator(): number;
91
93
  get attr_flags(): number;
@@ -102,8 +104,9 @@ export declare class CSSNode {
102
104
  get value_count(): number;
103
105
  get line(): number;
104
106
  get column(): number;
105
- get offset(): number;
107
+ get start(): number;
106
108
  get length(): number;
109
+ get end(): number;
107
110
  get first_child(): CSSNode | null;
108
111
  get next_sibling(): CSSNode | null;
109
112
  get has_next(): boolean;
@@ -128,7 +131,7 @@ export declare class CSSNode {
128
131
  *
129
132
  * @param options - Cloning configuration
130
133
  * @param options.deep - Recursively clone children (default: true)
131
- * @param options.locations - Include line/column/offset/length (default: false)
134
+ * @param options.locations - Include line/column/start/length (default: false)
132
135
  * @returns Plain object with children as array
133
136
  *
134
137
  * @example
package/dist/css-node.js CHANGED
@@ -1,5 +1,5 @@
1
- import { URL, STRING, DIMENSION, NUMBER, DECLARATION, FLAG_IMPORTANT, FLAG_VENDOR_PREFIXED, FLAG_HAS_ERROR, FLAG_HAS_BLOCK, FLAG_HAS_DECLARATIONS, STYLE_RULE, BLOCK, AT_RULE, COMMENT, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, FLAG_HAS_PARENS, NTH_SELECTOR, NTH_OF_SELECTOR, SELECTOR_LIST, SELECTOR, COMBINATOR, ATTRIBUTE_SELECTOR, PRELUDE_OPERATOR, LAYER_NAME, SUPPORTS_QUERY, CONTAINER_QUERY, MEDIA_TYPE, MEDIA_FEATURE, MEDIA_QUERY, LANG_SELECTOR, NESTING_SELECTOR, UNIVERSAL_SELECTOR, ID_SELECTOR, CLASS_SELECTOR, TYPE_SELECTOR, PARENTHESIS, OPERATOR, FUNCTION, HASH, IDENTIFIER, STYLESHEET } from './arena.js';
2
- import { is_whitespace, CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils.js';
1
+ import { DIMENSION, NUMBER, URL, STRING, DECLARATION, FLAG_IMPORTANT, IDENTIFIER, FUNCTION, AT_RULE, PSEUDO_ELEMENT_SELECTOR, PSEUDO_CLASS_SELECTOR, FLAG_HAS_ERROR, FLAG_HAS_BLOCK, FLAG_HAS_DECLARATIONS, STYLE_RULE, BLOCK, COMMENT, FLAG_HAS_PARENS, NTH_SELECTOR, NTH_OF_SELECTOR, SELECTOR_LIST, SELECTOR, COMBINATOR, ATTRIBUTE_SELECTOR, PRELUDE_OPERATOR, LAYER_NAME, SUPPORTS_QUERY, CONTAINER_QUERY, MEDIA_TYPE, MEDIA_FEATURE, MEDIA_QUERY, LANG_SELECTOR, NESTING_SELECTOR, UNIVERSAL_SELECTOR, ID_SELECTOR, CLASS_SELECTOR, TYPE_SELECTOR, PARENTHESIS, OPERATOR, HASH, STYLESHEET } from './arena.js';
2
+ import { str_starts_with, is_vendor_prefixed, is_whitespace, CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils.js';
3
3
  import { parse_dimension } from './parse-utils.js';
4
4
 
5
5
  const TYPE_NAMES = {
@@ -85,15 +85,21 @@ class CSSNode {
85
85
  // For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
86
86
  // For URL nodes with unquoted URL: returns the URL content without quotes
87
87
  get value() {
88
- if (this.type === URL) {
89
- const firstChild = this.first_child;
88
+ let { type, text } = this;
89
+ if (type === DIMENSION) {
90
+ return parse_dimension(text).value;
91
+ }
92
+ if (type === NUMBER) {
93
+ return Number.parseFloat(this.text);
94
+ }
95
+ if (type === URL) {
96
+ let firstChild = this.first_child;
90
97
  if (firstChild && firstChild.type === STRING) {
91
98
  return firstChild.text;
92
99
  }
93
- const text = this.text;
94
- if (text.startsWith("url(")) {
95
- const openParen = text.indexOf("(");
96
- const closeParen = text.lastIndexOf(")");
100
+ if (str_starts_with(text, "url(")) {
101
+ let openParen = text.indexOf("(");
102
+ let closeParen = text.lastIndexOf(")");
97
103
  if (openParen !== -1 && closeParen !== -1 && closeParen > openParen) {
98
104
  let content = text.substring(openParen + 1, closeParen).trim();
99
105
  return content;
@@ -102,14 +108,21 @@ class CSSNode {
102
108
  return text;
103
109
  }
104
110
  }
105
- if (this.type === DIMENSION || this.type === NUMBER) {
106
- return parse_dimension(this.text).value;
107
- }
108
111
  let start = this.arena.get_value_start(this.index);
109
112
  let length = this.arena.get_value_length(this.index);
110
113
  if (length === 0) return null;
111
114
  return this.source.substring(start, start + length);
112
115
  }
116
+ get value_as_number() {
117
+ let text = this.text;
118
+ if (this.type === NUMBER) {
119
+ return Number.parseFloat(text);
120
+ }
121
+ if (this.type === DIMENSION) {
122
+ return parse_dimension(text).value;
123
+ }
124
+ return null;
125
+ }
113
126
  // Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
114
127
  // This is an alias for `value` to make at-rule usage more semantic
115
128
  get prelude() {
@@ -136,9 +149,23 @@ class CSSNode {
136
149
  if (this.type !== DECLARATION) return null;
137
150
  return this.arena.has_flag(this.index, FLAG_IMPORTANT);
138
151
  }
139
- // Check if this has a vendor prefix (flag-based for performance)
152
+ // Check if this has a vendor prefix (computed on-demand)
140
153
  get is_vendor_prefixed() {
141
- return this.arena.has_flag(this.index, FLAG_VENDOR_PREFIXED);
154
+ switch (this.type) {
155
+ case DECLARATION:
156
+ return is_vendor_prefixed(this.name);
157
+ case PSEUDO_CLASS_SELECTOR:
158
+ case PSEUDO_ELEMENT_SELECTOR:
159
+ return is_vendor_prefixed(this.name);
160
+ case AT_RULE:
161
+ return is_vendor_prefixed(this.name);
162
+ case FUNCTION:
163
+ return is_vendor_prefixed(this.name);
164
+ case IDENTIFIER:
165
+ return is_vendor_prefixed(this.text);
166
+ default:
167
+ return false;
168
+ }
142
169
  }
143
170
  // Check if this node has an error
144
171
  get has_error() {
@@ -221,13 +248,18 @@ class CSSNode {
221
248
  return this.arena.get_start_column(this.index);
222
249
  }
223
250
  // Get start offset in source
224
- get offset() {
251
+ get start() {
225
252
  return this.arena.get_start_offset(this.index);
226
253
  }
227
254
  // Get length in source
228
255
  get length() {
229
256
  return this.arena.get_length(this.index);
230
257
  }
258
+ // Get end offset in source
259
+ // End is not stored, must be calculated
260
+ get end() {
261
+ return this.start + this.length;
262
+ }
231
263
  // --- Tree Traversal ---
232
264
  // Get first child node
233
265
  get first_child() {
@@ -399,8 +431,8 @@ class CSSNode {
399
431
  let child = this.first_child;
400
432
  while (child) {
401
433
  if (child.type === COMBINATOR) break;
402
- if (start === -1) start = child.offset;
403
- end = child.offset + child.length;
434
+ if (start === -1) start = child.start;
435
+ end = child.start + child.length;
404
436
  child = child.next_sibling;
405
437
  }
406
438
  if (start === -1) return "";
@@ -415,7 +447,7 @@ class CSSNode {
415
447
  *
416
448
  * @param options - Cloning configuration
417
449
  * @param options.deep - Recursively clone children (default: true)
418
- * @param options.locations - Include line/column/offset/length (default: false)
450
+ * @param options.locations - Include line/column/start/length (default: false)
419
451
  * @returns Plain object with children as array
420
452
  *
421
453
  * @example
@@ -459,8 +491,9 @@ class CSSNode {
459
491
  if (locations) {
460
492
  plain.line = this.line;
461
493
  plain.column = this.column;
462
- plain.offset = this.offset;
494
+ plain.start = this.start;
463
495
  plain.length = this.length;
496
+ plain.end = this.end;
464
497
  }
465
498
  if (deep) {
466
499
  for (let child of this.children) {
@@ -1,7 +1,7 @@
1
1
  import { Lexer } from './lexer.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
- import { CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils.js';
4
+ import { str_equals, CHAR_MINUS_HYPHEN, CHAR_PLUS, str_index_of } from './string-utils.js';
5
5
  import { skip_whitespace_forward } from './parse-utils.js';
6
6
  import { CSSNode } from './css-node.js';
7
7
 
@@ -36,8 +36,8 @@ class ANplusBParser {
36
36
  }
37
37
  this.lexer.next_token_fast(true);
38
38
  if (this.lexer.token_type === TOKEN_IDENT) {
39
- const text = this.source.substring(this.lexer.token_start, this.lexer.token_end).toLowerCase();
40
- if (text === "odd" || text === "even") {
39
+ const text = this.source.substring(this.lexer.token_start, this.lexer.token_end);
40
+ if (str_equals("odd", text) || str_equals("even", text)) {
41
41
  a_start = this.lexer.token_start;
42
42
  a_end = this.lexer.token_end;
43
43
  return this.create_anplusb_node(node_start, a_start, a_end, 0, 0);
@@ -118,7 +118,7 @@ class ANplusBParser {
118
118
  }
119
119
  if (this.lexer.token_type === TOKEN_DIMENSION) {
120
120
  const token_text = this.source.substring(this.lexer.token_start, this.lexer.token_end);
121
- const n_index = token_text.toLowerCase().indexOf("n");
121
+ const n_index = str_index_of(token_text, "n");
122
122
  if (n_index !== -1) {
123
123
  a_start = this.lexer.token_start;
124
124
  a_end = this.lexer.token_start + n_index + 1;
@@ -1,8 +1,8 @@
1
1
  import { Lexer } from './lexer.js';
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_VENDOR_PREFIXED, FLAG_HAS_PARENS, LANG_SELECTOR, NTH_OF_SELECTOR } from './arena.js';
2
+ import { CSSDataArena, SELECTOR_LIST, SELECTOR, COMBINATOR, NESTING_SELECTOR, ID_SELECTOR, TYPE_SELECTOR, UNIVERSAL_SELECTOR, CLASS_SELECTOR, ATTRIBUTE_SELECTOR, ATTR_OPERATOR_NONE, ATTR_FLAG_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, PSEUDO_ELEMENT_SELECTOR, PSEUDO_CLASS_SELECTOR, FLAG_HAS_PARENS, LANG_SELECTOR, NTH_OF_SELECTOR } from './arena.js';
3
3
  import { TOKEN_COMMA, TOKEN_DELIM, TOKEN_EOF, TOKEN_WHITESPACE, TOKEN_FUNCTION, TOKEN_COLON, TOKEN_LEFT_BRACKET, TOKEN_HASH, TOKEN_IDENT, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_STRING } from './token-types.js';
4
4
  import { skip_whitespace_and_comments_forward, skip_whitespace_and_comments_backward, skip_whitespace_forward } from './parse-utils.js';
5
- import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, is_vendor_prefixed } from './string-utils.js';
5
+ import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, str_equals } from './string-utils.js';
6
6
  import { ANplusBParser } from './parse-anplusb.js';
7
7
  import { CSSNode } from './css-node.js';
8
8
 
@@ -44,7 +44,6 @@ class SelectorParser {
44
44
  next_sibling = this.arena.get_next_sibling(last_component);
45
45
  }
46
46
  this.arena.set_first_child(selector_wrapper, complex_selector);
47
- this.arena.set_last_child(selector_wrapper, last_component);
48
47
  selectors.push(selector_wrapper);
49
48
  }
50
49
  this.skip_whitespace();
@@ -422,9 +421,6 @@ class SelectorParser {
422
421
  let node = this.create_node(is_pseudo_element ? PSEUDO_ELEMENT_SELECTOR : PSEUDO_CLASS_SELECTOR, start, this.lexer.token_end);
423
422
  this.arena.set_content_start_delta(node, this.lexer.token_start - start);
424
423
  this.arena.set_content_length(node, this.lexer.token_end - this.lexer.token_start);
425
- if (is_vendor_prefixed(this.source, this.lexer.token_start, this.lexer.token_end)) {
426
- this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
427
- }
428
424
  return node;
429
425
  } else if (token_type === TOKEN_FUNCTION) {
430
426
  return this.parse_pseudo_function_after_colon(start, is_pseudo_element);
@@ -462,29 +458,24 @@ class SelectorParser {
462
458
  this.arena.set_content_start_delta(node, func_name_start - start);
463
459
  this.arena.set_content_length(node, func_name_end - func_name_start);
464
460
  this.arena.set_flag(node, FLAG_HAS_PARENS);
465
- if (is_vendor_prefixed(this.source, func_name_start, func_name_end)) {
466
- this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
467
- }
468
461
  if (content_end > content_start) {
469
- let func_name = this.source.substring(func_name_start, func_name_end).toLowerCase();
470
- if (this.is_nth_pseudo(func_name)) {
462
+ let func_name_substr = this.source.substring(func_name_start, func_name_end);
463
+ if (this.is_nth_pseudo(func_name_substr)) {
471
464
  let child = this.parse_nth_expression(content_start, content_end);
472
465
  if (child !== null) {
473
466
  this.arena.set_first_child(node, child);
474
- this.arena.set_last_child(node, child);
475
467
  }
476
- } else if (func_name === "lang") {
468
+ } else if (str_equals("lang", func_name_substr)) {
477
469
  this.parse_lang_identifiers(content_start, content_end, node);
478
470
  } else {
479
471
  let saved_selector_end = this.selector_end;
480
472
  const saved = this.lexer.save_position();
481
- let allow_relative = func_name === "has";
473
+ let allow_relative = str_equals("has", func_name_substr);
482
474
  let child_selector = this.parse_selector(content_start, content_end, this.lexer.line, this.lexer.column, allow_relative);
483
475
  this.selector_end = saved_selector_end;
484
476
  this.lexer.restore_position(saved);
485
477
  if (child_selector !== null) {
486
478
  this.arena.set_first_child(node, child_selector);
487
- this.arena.set_last_child(node, child_selector);
488
479
  }
489
480
  }
490
481
  }
@@ -492,7 +483,7 @@ class SelectorParser {
492
483
  }
493
484
  // Check if pseudo-class name is an nth-* pseudo
494
485
  is_nth_pseudo(name) {
495
- return name === "nth-child" || name === "nth-last-child" || name === "nth-of-type" || name === "nth-last-of-type" || name === "nth-col" || name === "nth-last-col";
486
+ return str_equals("nth-child", name) || str_equals("nth-last-child", name) || str_equals("nth-of-type", name) || str_equals("nth-last-of-type", name) || str_equals("nth-col", name) || str_equals("nth-last-col", name);
496
487
  }
497
488
  // Parse :lang() content - comma-separated language identifiers
498
489
  // Accepts both quoted strings: :lang("en", "fr") and unquoted: :lang(en, fr)
@@ -531,9 +522,6 @@ class SelectorParser {
531
522
  if (first_child !== null) {
532
523
  this.arena.set_first_child(parent_node, first_child);
533
524
  }
534
- if (last_child !== null) {
535
- this.arena.set_last_child(parent_node, last_child);
536
- }
537
525
  this.selector_end = saved_selector_end;
538
526
  this.lexer.restore_position(saved);
539
527
  }
@@ -562,11 +550,9 @@ class SelectorParser {
562
550
  );
563
551
  if (anplusb_node !== null && selector_list !== null) {
564
552
  this.arena.set_first_child(of_node, anplusb_node);
565
- this.arena.set_last_child(of_node, selector_list);
566
553
  this.arena.set_next_sibling(anplusb_node, selector_list);
567
554
  } else if (anplusb_node !== null) {
568
555
  this.arena.set_first_child(of_node, anplusb_node);
569
- this.arena.set_last_child(of_node, anplusb_node);
570
556
  }
571
557
  return of_node;
572
558
  } else {
@@ -1,7 +1,7 @@
1
1
  import { Lexer } from './lexer.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
- import { is_whitespace, CHAR_PLUS, CHAR_MINUS_HYPHEN, CHAR_ASTERISK, CHAR_FORWARD_SLASH } from './string-utils.js';
4
+ import { is_whitespace, CHAR_PLUS, CHAR_MINUS_HYPHEN, CHAR_ASTERISK, CHAR_FORWARD_SLASH, str_equals } from './string-utils.js';
5
5
  import { CSSNode } from './css-node.js';
6
6
 
7
7
  class ValueParser {
@@ -98,9 +98,9 @@ class ValueParser {
98
98
  }
99
99
  parse_function_node(start, end) {
100
100
  let name_end = end - 1;
101
- let func_name = this.source.substring(start, name_end).toLowerCase();
101
+ let func_name_substr = this.source.substring(start, name_end);
102
102
  let node = this.arena.create_node(
103
- func_name === "url" ? URL : FUNCTION,
103
+ str_equals("url", func_name_substr) ? URL : FUNCTION,
104
104
  start,
105
105
  0,
106
106
  // length unknown yet
@@ -109,7 +109,7 @@ class ValueParser {
109
109
  );
110
110
  this.arena.set_content_start_delta(node, 0);
111
111
  this.arena.set_content_length(node, name_end - start);
112
- if (func_name === "url" || func_name === "src") {
112
+ if (str_equals("url", func_name_substr) || str_equals("src", func_name_substr)) {
113
113
  let save_pos = this.lexer.save_position();
114
114
  this.lexer.next_token_fast(false);
115
115
  while (this.is_whitespace_inline() && this.lexer.pos < this.value_end) {
package/dist/parse.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { Lexer } from './lexer.js';
2
- import { CSSDataArena, STYLESHEET, STYLE_RULE, FLAG_HAS_BLOCK, BLOCK, FLAG_HAS_DECLARATIONS, SELECTOR_LIST, DECLARATION, FLAG_VENDOR_PREFIXED, FLAG_IMPORTANT, AT_RULE } from './arena.js';
2
+ import { CSSDataArena, STYLESHEET, STYLE_RULE, FLAG_HAS_BLOCK, BLOCK, FLAG_HAS_DECLARATIONS, SELECTOR_LIST, DECLARATION, FLAG_IMPORTANT, AT_RULE } from './arena.js';
3
3
  import { CSSNode } from './css-node.js';
4
4
  import { ValueParser } from './parse-value.js';
5
5
  import { SelectorParser } from './parse-selector.js';
6
6
  import { AtRulePreludeParser } from './parse-atrule-prelude.js';
7
7
  import { TOKEN_EOF, TOKEN_AT_KEYWORD, TOKEN_LEFT_BRACE, TOKEN_RIGHT_BRACE, TOKEN_IDENT, TOKEN_COLON, TOKEN_SEMICOLON, TOKEN_DELIM } from './token-types.js';
8
- import { is_vendor_prefixed } from './string-utils.js';
9
8
  import { trim_boundaries } from './parse-utils.js';
10
9
 
11
10
  let DECLARATION_AT_RULES = /* @__PURE__ */ new Set(["font-face", "font-feature-values", "page", "property", "counter-style"]);
@@ -213,9 +212,6 @@ class Parser {
213
212
  );
214
213
  this.arena.set_content_start_delta(declaration, 0);
215
214
  this.arena.set_content_length(declaration, prop_end - prop_start);
216
- if (is_vendor_prefixed(this.source, prop_start, prop_end)) {
217
- this.arena.set_flag(declaration, FLAG_VENDOR_PREFIXED);
218
- }
219
215
  let value_start = this.lexer.token_start;
220
216
  let value_end = value_start;
221
217
  let has_important = false;
@@ -23,6 +23,27 @@ export declare const CHAR_COLON = 58;
23
23
  * @param b Compare string
24
24
  */
25
25
  export declare function str_equals(a: string, b: string): boolean;
26
+ /**
27
+ * Case-insensitive ASCII prefix check without allocations
28
+ * Returns true if string `str` starts with prefix (case-insensitive)
29
+ *
30
+ * IMPORTANT: prefix MUST be lowercase for correct comparison
31
+ *
32
+ * @param str - The string to check
33
+ * @param prefix - The lowercase prefix to match against
34
+ */
35
+ export declare function str_starts_with(str: string, prefix: string): boolean;
36
+ /**
37
+ * Case-insensitive character/substring search without allocations
38
+ * Returns the index of the first occurrence of searchChar (case-insensitive)
39
+ *
40
+ * IMPORTANT: searchChar MUST be lowercase for correct comparison
41
+ *
42
+ * @param str - The string to search in
43
+ * @param searchChar - The lowercase character/substring to find
44
+ * @returns The index of the first match, or -1 if not found
45
+ */
46
+ export declare function str_index_of(str: string, searchChar: string): number;
26
47
  /**
27
48
  * Check if a string range has a vendor prefix
28
49
  *
@@ -44,4 +65,5 @@ export declare function str_equals(a: string, b: string): boolean;
44
65
  * - `--custom-property` → false (CSS custom property)
45
66
  * - `border-radius` → false (doesn't start with hyphen)
46
67
  */
68
+ export declare function is_vendor_prefixed(text: string): boolean;
47
69
  export declare function is_vendor_prefixed(source: string, start: number, end: number): boolean;
@@ -41,7 +41,57 @@ function str_equals(a, b) {
41
41
  }
42
42
  return true;
43
43
  }
44
+ function str_starts_with(str, prefix) {
45
+ if (str.length < prefix.length) {
46
+ return false;
47
+ }
48
+ for (let i = 0; i < prefix.length; i++) {
49
+ let ca = str.charCodeAt(i);
50
+ let cb = prefix.charCodeAt(i);
51
+ if (ca >= 65 && ca <= 90) ca |= 32;
52
+ if (ca !== cb) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+ function str_index_of(str, searchChar) {
59
+ if (searchChar.length === 0) {
60
+ return -1;
61
+ }
62
+ if (searchChar.length === 1) {
63
+ const searchCode = searchChar.charCodeAt(0);
64
+ for (let i = 0; i < str.length; i++) {
65
+ let ca = str.charCodeAt(i);
66
+ if (ca >= 65 && ca <= 90) ca |= 32;
67
+ if (ca === searchCode) {
68
+ return i;
69
+ }
70
+ }
71
+ return -1;
72
+ }
73
+ for (let i = 0; i <= str.length - searchChar.length; i++) {
74
+ let match = true;
75
+ for (let j = 0; j < searchChar.length; j++) {
76
+ let ca = str.charCodeAt(i + j);
77
+ let cb = searchChar.charCodeAt(j);
78
+ if (ca >= 65 && ca <= 90) ca |= 32;
79
+ if (ca !== cb) {
80
+ match = false;
81
+ break;
82
+ }
83
+ }
84
+ if (match) {
85
+ return i;
86
+ }
87
+ }
88
+ return -1;
89
+ }
44
90
  function is_vendor_prefixed(source, start, end) {
91
+ if (start === void 0 || end === void 0) {
92
+ start = 0;
93
+ end = source.length;
94
+ }
45
95
  if (source.charCodeAt(start) !== CHAR_MINUS_HYPHEN) {
46
96
  return false;
47
97
  }
@@ -60,4 +110,4 @@ function is_vendor_prefixed(source, start, end) {
60
110
  return false;
61
111
  }
62
112
 
63
- 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 };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectwallace/css-parser",
3
- "version": "0.7.2",
3
+ "version": "0.8.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",