@projectwallace/css-parser 0.1.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 +72 -0
- package/dist/arena.d.ts +84 -0
- package/dist/at-rule-prelude-parser-DlqYQAYH.js +464 -0
- package/dist/at-rule-prelude-parser.d.ts +24 -0
- package/dist/char-types.d.ts +7 -0
- package/dist/css-node.d.ts +31 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +28 -0
- package/dist/lexer-DXablYMZ.js +479 -0
- package/dist/lexer.d.ts +27 -0
- package/dist/parse-atrule-prelude.d.ts +8 -0
- package/dist/parse-atrule-prelude.js +11 -0
- package/dist/parse-selector.d.ts +7 -0
- package/dist/parse-selector.js +19 -0
- package/dist/parse.d.ts +9 -0
- package/dist/parse.js +512 -0
- package/dist/parser.d.ts +34 -0
- package/dist/selector-parser-2b3tGyri.js +365 -0
- package/dist/selector-parser.d.ts +25 -0
- package/dist/string-utils-B-rJII-E.js +440 -0
- package/dist/string-utils.d.ts +29 -0
- package/dist/token-types.d.ts +34 -0
- package/dist/tokenize.d.ts +8 -0
- package/dist/tokenize.js +14 -0
- package/dist/value-parser.d.ts +19 -0
- package/dist/walk.d.ts +20 -0
- package/package.json +78 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
let BYTES_PER_NODE = 44;
|
|
2
|
+
const NODE_STYLESHEET = 1;
|
|
3
|
+
const NODE_STYLE_RULE = 2;
|
|
4
|
+
const NODE_AT_RULE = 3;
|
|
5
|
+
const NODE_DECLARATION = 4;
|
|
6
|
+
const NODE_SELECTOR = 5;
|
|
7
|
+
const NODE_COMMENT = 6;
|
|
8
|
+
const NODE_VALUE_KEYWORD = 10;
|
|
9
|
+
const NODE_VALUE_NUMBER = 11;
|
|
10
|
+
const NODE_VALUE_DIMENSION = 12;
|
|
11
|
+
const NODE_VALUE_STRING = 13;
|
|
12
|
+
const NODE_VALUE_COLOR = 14;
|
|
13
|
+
const NODE_VALUE_FUNCTION = 15;
|
|
14
|
+
const NODE_VALUE_OPERATOR = 16;
|
|
15
|
+
const NODE_SELECTOR_LIST = 20;
|
|
16
|
+
const NODE_SELECTOR_TYPE = 21;
|
|
17
|
+
const NODE_SELECTOR_CLASS = 22;
|
|
18
|
+
const NODE_SELECTOR_ID = 23;
|
|
19
|
+
const NODE_SELECTOR_ATTRIBUTE = 24;
|
|
20
|
+
const NODE_SELECTOR_PSEUDO_CLASS = 25;
|
|
21
|
+
const NODE_SELECTOR_PSEUDO_ELEMENT = 26;
|
|
22
|
+
const NODE_SELECTOR_COMBINATOR = 27;
|
|
23
|
+
const NODE_SELECTOR_UNIVERSAL = 28;
|
|
24
|
+
const NODE_SELECTOR_NESTING = 29;
|
|
25
|
+
const NODE_PRELUDE_MEDIA_QUERY = 30;
|
|
26
|
+
const NODE_PRELUDE_MEDIA_FEATURE = 31;
|
|
27
|
+
const NODE_PRELUDE_MEDIA_TYPE = 32;
|
|
28
|
+
const NODE_PRELUDE_CONTAINER_QUERY = 33;
|
|
29
|
+
const NODE_PRELUDE_SUPPORTS_QUERY = 34;
|
|
30
|
+
const NODE_PRELUDE_LAYER_NAME = 35;
|
|
31
|
+
const NODE_PRELUDE_IDENTIFIER = 36;
|
|
32
|
+
const NODE_PRELUDE_OPERATOR = 37;
|
|
33
|
+
const NODE_PRELUDE_IMPORT_URL = 38;
|
|
34
|
+
const NODE_PRELUDE_IMPORT_LAYER = 39;
|
|
35
|
+
const NODE_PRELUDE_IMPORT_SUPPORTS = 40;
|
|
36
|
+
const FLAG_IMPORTANT = 1 << 0;
|
|
37
|
+
const FLAG_HAS_ERROR = 1 << 1;
|
|
38
|
+
const FLAG_HAS_BLOCK = 1 << 3;
|
|
39
|
+
class CSSDataArena {
|
|
40
|
+
buffer;
|
|
41
|
+
view;
|
|
42
|
+
capacity;
|
|
43
|
+
// Number of nodes that can fit
|
|
44
|
+
count;
|
|
45
|
+
// Number of nodes currently allocated
|
|
46
|
+
// Growth multiplier when capacity is exceeded
|
|
47
|
+
static GROWTH_FACTOR = 1.3;
|
|
48
|
+
// Estimated nodes per KB of CSS (based on real-world data)
|
|
49
|
+
static NODES_PER_KB = 60;
|
|
50
|
+
// Buffer to avoid frequent growth (15%)
|
|
51
|
+
static CAPACITY_BUFFER = 1.15;
|
|
52
|
+
constructor(initial_capacity = 1024) {
|
|
53
|
+
this.capacity = initial_capacity;
|
|
54
|
+
this.count = 1;
|
|
55
|
+
this.buffer = new ArrayBuffer(initial_capacity * BYTES_PER_NODE);
|
|
56
|
+
this.view = new DataView(this.buffer);
|
|
57
|
+
}
|
|
58
|
+
// Calculate recommended initial capacity based on CSS source size
|
|
59
|
+
static capacity_for_source(source_length) {
|
|
60
|
+
let size_in_kb = source_length / 1024;
|
|
61
|
+
let estimated_nodes = Math.ceil(size_in_kb * CSSDataArena.NODES_PER_KB);
|
|
62
|
+
let capacity = Math.ceil(estimated_nodes * CSSDataArena.CAPACITY_BUFFER);
|
|
63
|
+
return Math.max(16, capacity);
|
|
64
|
+
}
|
|
65
|
+
// Get the number of nodes currently in the arena
|
|
66
|
+
get_count() {
|
|
67
|
+
return this.count;
|
|
68
|
+
}
|
|
69
|
+
// Get the capacity (max nodes without reallocation)
|
|
70
|
+
get_capacity() {
|
|
71
|
+
return this.capacity;
|
|
72
|
+
}
|
|
73
|
+
// Calculate byte offset for a node
|
|
74
|
+
node_offset(node_index) {
|
|
75
|
+
return node_index * BYTES_PER_NODE;
|
|
76
|
+
}
|
|
77
|
+
// Read node type
|
|
78
|
+
get_type(node_index) {
|
|
79
|
+
return this.view.getUint8(this.node_offset(node_index));
|
|
80
|
+
}
|
|
81
|
+
// Read node flags
|
|
82
|
+
get_flags(node_index) {
|
|
83
|
+
return this.view.getUint8(this.node_offset(node_index) + 1);
|
|
84
|
+
}
|
|
85
|
+
// Read start offset in source
|
|
86
|
+
get_start_offset(node_index) {
|
|
87
|
+
return this.view.getUint32(this.node_offset(node_index) + 4, true);
|
|
88
|
+
}
|
|
89
|
+
// Read length in source
|
|
90
|
+
get_length(node_index) {
|
|
91
|
+
return this.view.getUint16(this.node_offset(node_index) + 8, true);
|
|
92
|
+
}
|
|
93
|
+
// Read content start offset
|
|
94
|
+
get_content_start(node_index) {
|
|
95
|
+
return this.view.getUint32(this.node_offset(node_index) + 12, true);
|
|
96
|
+
}
|
|
97
|
+
// Read content length
|
|
98
|
+
get_content_length(node_index) {
|
|
99
|
+
return this.view.getUint16(this.node_offset(node_index) + 16, true);
|
|
100
|
+
}
|
|
101
|
+
// Read first child index (0 = no children)
|
|
102
|
+
get_first_child(node_index) {
|
|
103
|
+
return this.view.getUint32(this.node_offset(node_index) + 20, true);
|
|
104
|
+
}
|
|
105
|
+
// Read last child index (0 = no children)
|
|
106
|
+
get_last_child(node_index) {
|
|
107
|
+
return this.view.getUint32(this.node_offset(node_index) + 24, true);
|
|
108
|
+
}
|
|
109
|
+
// Read next sibling index (0 = no sibling)
|
|
110
|
+
get_next_sibling(node_index) {
|
|
111
|
+
return this.view.getUint32(this.node_offset(node_index) + 28, true);
|
|
112
|
+
}
|
|
113
|
+
// Read start line
|
|
114
|
+
get_start_line(node_index) {
|
|
115
|
+
return this.view.getUint32(this.node_offset(node_index) + 32, true);
|
|
116
|
+
}
|
|
117
|
+
// Read value start offset (declaration value / at-rule prelude)
|
|
118
|
+
get_value_start(node_index) {
|
|
119
|
+
return this.view.getUint32(this.node_offset(node_index) + 36, true);
|
|
120
|
+
}
|
|
121
|
+
// Read value length
|
|
122
|
+
get_value_length(node_index) {
|
|
123
|
+
return this.view.getUint16(this.node_offset(node_index) + 40, true);
|
|
124
|
+
}
|
|
125
|
+
// --- Write Methods ---
|
|
126
|
+
// Write node type
|
|
127
|
+
set_type(node_index, type) {
|
|
128
|
+
this.view.setUint8(this.node_offset(node_index), type);
|
|
129
|
+
}
|
|
130
|
+
// Write node flags
|
|
131
|
+
set_flags(node_index, flags) {
|
|
132
|
+
this.view.setUint8(this.node_offset(node_index) + 1, flags);
|
|
133
|
+
}
|
|
134
|
+
// Write start offset in source
|
|
135
|
+
set_start_offset(node_index, offset) {
|
|
136
|
+
this.view.setUint32(this.node_offset(node_index) + 4, offset, true);
|
|
137
|
+
}
|
|
138
|
+
// Write length in source
|
|
139
|
+
set_length(node_index, length) {
|
|
140
|
+
this.view.setUint16(this.node_offset(node_index) + 8, length, true);
|
|
141
|
+
}
|
|
142
|
+
// Write content start offset
|
|
143
|
+
set_content_start(node_index, offset) {
|
|
144
|
+
this.view.setUint32(this.node_offset(node_index) + 12, offset, true);
|
|
145
|
+
}
|
|
146
|
+
// Write content length
|
|
147
|
+
set_content_length(node_index, length) {
|
|
148
|
+
this.view.setUint16(this.node_offset(node_index) + 16, length, true);
|
|
149
|
+
}
|
|
150
|
+
// Write first child index
|
|
151
|
+
set_first_child(node_index, childIndex) {
|
|
152
|
+
this.view.setUint32(this.node_offset(node_index) + 20, childIndex, true);
|
|
153
|
+
}
|
|
154
|
+
// Write last child index
|
|
155
|
+
set_last_child(node_index, childIndex) {
|
|
156
|
+
this.view.setUint32(this.node_offset(node_index) + 24, childIndex, true);
|
|
157
|
+
}
|
|
158
|
+
// Write next sibling index
|
|
159
|
+
set_next_sibling(node_index, siblingIndex) {
|
|
160
|
+
this.view.setUint32(this.node_offset(node_index) + 28, siblingIndex, true);
|
|
161
|
+
}
|
|
162
|
+
// Write start line
|
|
163
|
+
set_start_line(node_index, line) {
|
|
164
|
+
this.view.setUint32(this.node_offset(node_index) + 32, line, true);
|
|
165
|
+
}
|
|
166
|
+
// Write value start offset (declaration value / at-rule prelude)
|
|
167
|
+
set_value_start(node_index, offset) {
|
|
168
|
+
this.view.setUint32(this.node_offset(node_index) + 36, offset, true);
|
|
169
|
+
}
|
|
170
|
+
// Write value length
|
|
171
|
+
set_value_length(node_index, length) {
|
|
172
|
+
this.view.setUint16(this.node_offset(node_index) + 40, length, true);
|
|
173
|
+
}
|
|
174
|
+
// --- Node Creation ---
|
|
175
|
+
// Grow the arena by 1.3x when capacity is exceeded
|
|
176
|
+
grow() {
|
|
177
|
+
let new_capacity = Math.ceil(this.capacity * CSSDataArena.GROWTH_FACTOR);
|
|
178
|
+
let new_buffer = new ArrayBuffer(new_capacity * BYTES_PER_NODE);
|
|
179
|
+
new Uint8Array(new_buffer).set(new Uint8Array(this.buffer));
|
|
180
|
+
this.buffer = new_buffer;
|
|
181
|
+
this.view = new DataView(new_buffer);
|
|
182
|
+
this.capacity = new_capacity;
|
|
183
|
+
}
|
|
184
|
+
// Allocate a new node and return its index
|
|
185
|
+
// The node is zero-initialized by default (ArrayBuffer guarantees this)
|
|
186
|
+
// Automatically grows the arena if capacity is exceeded
|
|
187
|
+
create_node() {
|
|
188
|
+
if (this.count >= this.capacity) {
|
|
189
|
+
this.grow();
|
|
190
|
+
}
|
|
191
|
+
let node_index = this.count;
|
|
192
|
+
this.count++;
|
|
193
|
+
return node_index;
|
|
194
|
+
}
|
|
195
|
+
// --- Tree Building Helpers ---
|
|
196
|
+
// Add a child node to a parent node
|
|
197
|
+
// This appends to the end of the child list using the sibling chain
|
|
198
|
+
// O(1) operation using lastChild pointer
|
|
199
|
+
append_child(parentIndex, childIndex) {
|
|
200
|
+
let last_child = this.get_last_child(parentIndex);
|
|
201
|
+
if (last_child === 0) {
|
|
202
|
+
this.set_first_child(parentIndex, childIndex);
|
|
203
|
+
this.set_last_child(parentIndex, childIndex);
|
|
204
|
+
} else {
|
|
205
|
+
this.set_next_sibling(last_child, childIndex);
|
|
206
|
+
this.set_last_child(parentIndex, childIndex);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Check if a node has any children
|
|
210
|
+
has_children(node_index) {
|
|
211
|
+
return this.get_first_child(node_index) !== 0;
|
|
212
|
+
}
|
|
213
|
+
// Check if a node has a next sibling
|
|
214
|
+
has_next_sibling(node_index) {
|
|
215
|
+
return this.get_next_sibling(node_index) !== 0;
|
|
216
|
+
}
|
|
217
|
+
// --- Flag Management Helpers ---
|
|
218
|
+
// Set a specific flag bit (doesn't clear other flags)
|
|
219
|
+
set_flag(node_index, flag) {
|
|
220
|
+
let current_flags = this.get_flags(node_index);
|
|
221
|
+
this.set_flags(node_index, current_flags | flag);
|
|
222
|
+
}
|
|
223
|
+
// Clear a specific flag bit (doesn't affect other flags)
|
|
224
|
+
clear_flag(node_index, flag) {
|
|
225
|
+
let current_flags = this.get_flags(node_index);
|
|
226
|
+
this.set_flags(node_index, current_flags & ~flag);
|
|
227
|
+
}
|
|
228
|
+
// Check if a specific flag is set
|
|
229
|
+
has_flag(node_index, flag) {
|
|
230
|
+
return (this.get_flags(node_index) & flag) !== 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class CSSNode {
|
|
235
|
+
arena;
|
|
236
|
+
source;
|
|
237
|
+
index;
|
|
238
|
+
constructor(arena, source, index) {
|
|
239
|
+
this.arena = arena;
|
|
240
|
+
this.source = source;
|
|
241
|
+
this.index = index;
|
|
242
|
+
}
|
|
243
|
+
// Get the node index (for internal use)
|
|
244
|
+
get_index() {
|
|
245
|
+
return this.index;
|
|
246
|
+
}
|
|
247
|
+
// Get node type as number (for performance)
|
|
248
|
+
get type() {
|
|
249
|
+
return this.arena.get_type(this.index);
|
|
250
|
+
}
|
|
251
|
+
// Get the full text of this node from source
|
|
252
|
+
get text() {
|
|
253
|
+
let start = this.arena.get_start_offset(this.index);
|
|
254
|
+
let length = this.arena.get_length(this.index);
|
|
255
|
+
return this.source.substring(start, start + length);
|
|
256
|
+
}
|
|
257
|
+
// Get the "content" text (property name for declarations, at-rule name for at-rules, layer name for import layers)
|
|
258
|
+
get name() {
|
|
259
|
+
let start = this.arena.get_content_start(this.index);
|
|
260
|
+
let length = this.arena.get_content_length(this.index);
|
|
261
|
+
if (length === 0) return "";
|
|
262
|
+
return this.source.substring(start, start + length);
|
|
263
|
+
}
|
|
264
|
+
// Alias for name (for declarations: "color" in "color: blue")
|
|
265
|
+
// More semantic than `name` for declaration nodes
|
|
266
|
+
get property() {
|
|
267
|
+
return this.name;
|
|
268
|
+
}
|
|
269
|
+
// Get the value text (for declarations: "blue" in "color: blue")
|
|
270
|
+
get value() {
|
|
271
|
+
let start = this.arena.get_value_start(this.index);
|
|
272
|
+
let length = this.arena.get_value_length(this.index);
|
|
273
|
+
if (length === 0) return null;
|
|
274
|
+
return this.source.substring(start, start + length);
|
|
275
|
+
}
|
|
276
|
+
// Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
|
|
277
|
+
// This is an alias for `value` to make at-rule usage more semantic
|
|
278
|
+
get prelude() {
|
|
279
|
+
return this.value;
|
|
280
|
+
}
|
|
281
|
+
// Check if this declaration has !important
|
|
282
|
+
get is_important() {
|
|
283
|
+
return this.arena.has_flag(this.index, FLAG_IMPORTANT);
|
|
284
|
+
}
|
|
285
|
+
// Check if this has a vendor prefix (lazy computation for performance)
|
|
286
|
+
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-");
|
|
290
|
+
}
|
|
291
|
+
// Check if this node has an error
|
|
292
|
+
get has_error() {
|
|
293
|
+
return this.arena.has_flag(this.index, FLAG_HAS_ERROR);
|
|
294
|
+
}
|
|
295
|
+
// Check if this at-rule has a prelude
|
|
296
|
+
get has_prelude() {
|
|
297
|
+
return this.arena.get_value_length(this.index) > 0;
|
|
298
|
+
}
|
|
299
|
+
// Check if this rule has a block { }
|
|
300
|
+
get has_block() {
|
|
301
|
+
return this.arena.has_flag(this.index, FLAG_HAS_BLOCK);
|
|
302
|
+
}
|
|
303
|
+
// --- Value Node Access (for declarations) ---
|
|
304
|
+
// Get array of parsed value nodes (for declarations only)
|
|
305
|
+
get values() {
|
|
306
|
+
let result = [];
|
|
307
|
+
let child = this.first_child;
|
|
308
|
+
while (child) {
|
|
309
|
+
result.push(child);
|
|
310
|
+
child = child.next_sibling;
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
// Get count of value nodes
|
|
315
|
+
get value_count() {
|
|
316
|
+
let count = 0;
|
|
317
|
+
let child = this.first_child;
|
|
318
|
+
while (child) {
|
|
319
|
+
count++;
|
|
320
|
+
child = child.next_sibling;
|
|
321
|
+
}
|
|
322
|
+
return count;
|
|
323
|
+
}
|
|
324
|
+
// Get start line number
|
|
325
|
+
get line() {
|
|
326
|
+
return this.arena.get_start_line(this.index);
|
|
327
|
+
}
|
|
328
|
+
// Get start offset in source
|
|
329
|
+
get offset() {
|
|
330
|
+
return this.arena.get_start_offset(this.index);
|
|
331
|
+
}
|
|
332
|
+
// Get length in source
|
|
333
|
+
get length() {
|
|
334
|
+
return this.arena.get_length(this.index);
|
|
335
|
+
}
|
|
336
|
+
// --- Tree Traversal ---
|
|
337
|
+
// Get first child node
|
|
338
|
+
get first_child() {
|
|
339
|
+
let child_index = this.arena.get_first_child(this.index);
|
|
340
|
+
if (child_index === 0) return null;
|
|
341
|
+
return new CSSNode(this.arena, this.source, child_index);
|
|
342
|
+
}
|
|
343
|
+
// Get next sibling node
|
|
344
|
+
get next_sibling() {
|
|
345
|
+
let sibling_index = this.arena.get_next_sibling(this.index);
|
|
346
|
+
if (sibling_index === 0) return null;
|
|
347
|
+
return new CSSNode(this.arena, this.source, sibling_index);
|
|
348
|
+
}
|
|
349
|
+
// Check if this node has children
|
|
350
|
+
get has_children() {
|
|
351
|
+
return this.arena.has_children(this.index);
|
|
352
|
+
}
|
|
353
|
+
// Get all children as an array
|
|
354
|
+
get children() {
|
|
355
|
+
let result = [];
|
|
356
|
+
let child = this.first_child;
|
|
357
|
+
while (child) {
|
|
358
|
+
result.push(child);
|
|
359
|
+
child = child.next_sibling;
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
// Make CSSNode iterable over its children
|
|
364
|
+
*[Symbol.iterator]() {
|
|
365
|
+
let child = this.first_child;
|
|
366
|
+
while (child) {
|
|
367
|
+
yield child;
|
|
368
|
+
child = child.next_sibling;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const CHAR_SPACE = 32;
|
|
374
|
+
const CHAR_TAB = 9;
|
|
375
|
+
const CHAR_NEWLINE = 10;
|
|
376
|
+
const CHAR_CARRIAGE_RETURN = 13;
|
|
377
|
+
const CHAR_FORM_FEED = 12;
|
|
378
|
+
const CHAR_FORWARD_SLASH = 47;
|
|
379
|
+
const CHAR_ASTERISK = 42;
|
|
380
|
+
function is_whitespace(ch) {
|
|
381
|
+
return ch === CHAR_SPACE || ch === CHAR_TAB || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED;
|
|
382
|
+
}
|
|
383
|
+
function trim_boundaries(source, start, end) {
|
|
384
|
+
while (start < end) {
|
|
385
|
+
let ch = source.charCodeAt(start);
|
|
386
|
+
if (is_whitespace(ch)) {
|
|
387
|
+
start++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (ch === CHAR_FORWARD_SLASH && start + 1 < end && source.charCodeAt(start + 1) === CHAR_ASTERISK) {
|
|
391
|
+
start += 2;
|
|
392
|
+
while (start < end) {
|
|
393
|
+
if (source.charCodeAt(start) === CHAR_ASTERISK && start + 1 < end && source.charCodeAt(start + 1) === CHAR_FORWARD_SLASH) {
|
|
394
|
+
start += 2;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
start++;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
while (end > start) {
|
|
404
|
+
let ch = source.charCodeAt(end - 1);
|
|
405
|
+
if (is_whitespace(ch)) {
|
|
406
|
+
end--;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (end >= 2 && ch === CHAR_FORWARD_SLASH && source.charCodeAt(end - 2) === CHAR_ASTERISK) {
|
|
410
|
+
end -= 2;
|
|
411
|
+
while (end > start) {
|
|
412
|
+
if (end >= 2 && source.charCodeAt(end - 2) === CHAR_FORWARD_SLASH && source.charCodeAt(end - 1) === CHAR_ASTERISK) {
|
|
413
|
+
end -= 2;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
end--;
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
if (start >= end) return null;
|
|
423
|
+
return [start, end];
|
|
424
|
+
}
|
|
425
|
+
function str_equals(a, b) {
|
|
426
|
+
if (a.length !== b.length) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
for (let i = 0; i < a.length; i++) {
|
|
430
|
+
let ca = a.charCodeAt(i);
|
|
431
|
+
let cb = b.charCodeAt(i);
|
|
432
|
+
if (cb >= 65 && cb <= 90) cb |= 32;
|
|
433
|
+
if (ca !== cb) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
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 };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const CHAR_SPACE = 32;
|
|
2
|
+
export declare const CHAR_TAB = 9;
|
|
3
|
+
export declare const CHAR_NEWLINE = 10;
|
|
4
|
+
export declare const CHAR_CARRIAGE_RETURN = 13;
|
|
5
|
+
export declare const CHAR_FORM_FEED = 12;
|
|
6
|
+
export declare const CHAR_FORWARD_SLASH = 47;
|
|
7
|
+
export declare const CHAR_ASTERISK = 42;
|
|
8
|
+
/**
|
|
9
|
+
* Check if a character code is whitespace (space, tab, newline, CR, or FF)
|
|
10
|
+
*/
|
|
11
|
+
export declare function is_whitespace(ch: number): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Trim whitespace and comments from both ends of a string range
|
|
14
|
+
*
|
|
15
|
+
* @param source - The source string
|
|
16
|
+
* @param start - Start offset in source
|
|
17
|
+
* @param end - End offset in source
|
|
18
|
+
* @returns [trimmed_start, trimmed_end] or null if all whitespace/comments
|
|
19
|
+
*
|
|
20
|
+
* Skips whitespace (space, tab, newline, CR, FF) and CSS comments from both ends
|
|
21
|
+
* of the specified range. Returns the trimmed boundaries or null if the range
|
|
22
|
+
* contains only whitespace and comments.
|
|
23
|
+
*/
|
|
24
|
+
export declare function trim_boundaries(source: string, start: number, end: number): [number, number] | null;
|
|
25
|
+
/**
|
|
26
|
+
* @param a Base string, MUST be lowercase!
|
|
27
|
+
* @param b Compare string
|
|
28
|
+
*/
|
|
29
|
+
export declare function str_equals(a: string, b: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const TOKEN_IDENT = 1;
|
|
2
|
+
export declare const TOKEN_FUNCTION = 2;
|
|
3
|
+
export declare const TOKEN_AT_KEYWORD = 3;
|
|
4
|
+
export declare const TOKEN_HASH = 4;
|
|
5
|
+
export declare const TOKEN_STRING = 5;
|
|
6
|
+
export declare const TOKEN_BAD_STRING = 6;
|
|
7
|
+
export declare const TOKEN_URL = 7;
|
|
8
|
+
export declare const TOKEN_BAD_URL = 8;
|
|
9
|
+
export declare const TOKEN_DELIM = 9;
|
|
10
|
+
export declare const TOKEN_NUMBER = 10;
|
|
11
|
+
export declare const TOKEN_PERCENTAGE = 11;
|
|
12
|
+
export declare const TOKEN_DIMENSION = 12;
|
|
13
|
+
export declare const TOKEN_WHITESPACE = 13;
|
|
14
|
+
export declare const TOKEN_CDO = 14;
|
|
15
|
+
export declare const TOKEN_CDC = 15;
|
|
16
|
+
export declare const TOKEN_COLON = 16;
|
|
17
|
+
export declare const TOKEN_SEMICOLON = 17;
|
|
18
|
+
export declare const TOKEN_COMMA = 18;
|
|
19
|
+
export declare const TOKEN_LEFT_BRACKET = 19;
|
|
20
|
+
export declare const TOKEN_RIGHT_BRACKET = 20;
|
|
21
|
+
export declare const TOKEN_LEFT_PAREN = 21;
|
|
22
|
+
export declare const TOKEN_RIGHT_PAREN = 22;
|
|
23
|
+
export declare const TOKEN_LEFT_BRACE = 23;
|
|
24
|
+
export declare const TOKEN_RIGHT_BRACE = 24;
|
|
25
|
+
export declare const TOKEN_COMMENT = 25;
|
|
26
|
+
export declare const TOKEN_EOF = 26;
|
|
27
|
+
export type TokenType = typeof TOKEN_IDENT | typeof TOKEN_FUNCTION | typeof TOKEN_AT_KEYWORD | typeof TOKEN_HASH | typeof TOKEN_STRING | typeof TOKEN_BAD_STRING | typeof TOKEN_URL | typeof TOKEN_BAD_URL | typeof TOKEN_DELIM | typeof TOKEN_NUMBER | typeof TOKEN_PERCENTAGE | typeof TOKEN_DIMENSION | typeof TOKEN_WHITESPACE | typeof TOKEN_CDO | typeof TOKEN_CDC | typeof TOKEN_COLON | typeof TOKEN_SEMICOLON | typeof TOKEN_COMMA | typeof TOKEN_LEFT_BRACKET | typeof TOKEN_RIGHT_BRACKET | typeof TOKEN_LEFT_PAREN | typeof TOKEN_RIGHT_PAREN | typeof TOKEN_LEFT_BRACE | typeof TOKEN_RIGHT_BRACE | typeof TOKEN_COMMENT | typeof TOKEN_EOF;
|
|
28
|
+
export type Token = {
|
|
29
|
+
type: TokenType;
|
|
30
|
+
start: number;
|
|
31
|
+
end: number;
|
|
32
|
+
line: number;
|
|
33
|
+
column: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Token } from './token-types';
|
|
2
|
+
/**
|
|
3
|
+
* Tokenize CSS source code
|
|
4
|
+
* @param source - The CSS source code to tokenize
|
|
5
|
+
* @param skip_comments - Whether to skip comment tokens (default: true)
|
|
6
|
+
* @yields CSS tokens
|
|
7
|
+
*/
|
|
8
|
+
export declare function tokenize(source: string, skip_comments?: boolean): Generator<Token, void, undefined>;
|
package/dist/tokenize.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { L as Lexer, y as TOKEN_EOF } from './lexer-DXablYMZ.js';
|
|
2
|
+
|
|
3
|
+
function* tokenize(source, skip_comments = true) {
|
|
4
|
+
const lexer = new Lexer(source, skip_comments);
|
|
5
|
+
while (true) {
|
|
6
|
+
const token = lexer.next_token();
|
|
7
|
+
if (!token || token.type === TOKEN_EOF) {
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
yield token;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { tokenize };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CSSDataArena } from './arena';
|
|
2
|
+
export declare class ValueParser {
|
|
3
|
+
private lexer;
|
|
4
|
+
private arena;
|
|
5
|
+
private source;
|
|
6
|
+
private value_end;
|
|
7
|
+
constructor(arena: CSSDataArena, source: string);
|
|
8
|
+
parse_value(start: number, end: number): number[];
|
|
9
|
+
private is_whitespace_token;
|
|
10
|
+
private parse_value_node;
|
|
11
|
+
private create_keyword_node;
|
|
12
|
+
private create_number_node;
|
|
13
|
+
private create_dimension_node;
|
|
14
|
+
private create_string_node;
|
|
15
|
+
private create_color_node;
|
|
16
|
+
private create_operator_node;
|
|
17
|
+
private parse_operator_node;
|
|
18
|
+
private parse_function_node;
|
|
19
|
+
}
|
package/dist/walk.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CSSNode } from './css-node';
|
|
2
|
+
type WalkCallback = (node: CSSNode, depth: number) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Walk the AST in depth-first order, calling the callback for each node
|
|
5
|
+
* @param node - The root node to start walking from
|
|
6
|
+
* @param callback - Function to call for each node visited. Receives the node and its depth (0 for root)
|
|
7
|
+
*/
|
|
8
|
+
export declare function walk(node: CSSNode, callback: WalkCallback, depth?: number): void;
|
|
9
|
+
type WalkEnterLeaveCallback = (node: CSSNode) => void;
|
|
10
|
+
interface WalkEnterLeaveOptions {
|
|
11
|
+
enter?: WalkEnterLeaveCallback;
|
|
12
|
+
leave?: WalkEnterLeaveCallback;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Walk the AST in depth-first order, calling enter before visiting children and leave after
|
|
16
|
+
* @param node - The root node to start walking from
|
|
17
|
+
* @param options - Object with optional enter and leave callback functions
|
|
18
|
+
*/
|
|
19
|
+
export declare function walk_enter_leave(node: CSSNode, { enter, leave }?: WalkEnterLeaveOptions): void;
|
|
20
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@projectwallace/css-parser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance CSS lexer and parser, optimized for CSS inspection and analysis",
|
|
5
|
+
"author": "Bart Veneman <bat@projectwallace.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/projectwallace/css-parser.git"
|
|
10
|
+
},
|
|
11
|
+
"issues": "https://github.com/projectwallace/css-parser/issues",
|
|
12
|
+
"homepage": "https://github.com/projectwallace/css-parser",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./tokenizer": {
|
|
23
|
+
"types": "./dist/tokenize.d.ts",
|
|
24
|
+
"import": "./dist/tokenize.js"
|
|
25
|
+
},
|
|
26
|
+
"./parse": {
|
|
27
|
+
"types": "./dist/parse.d.ts",
|
|
28
|
+
"import": "./dist/parse.js"
|
|
29
|
+
},
|
|
30
|
+
"./parse-selector": {
|
|
31
|
+
"types": "./dist/parse-selector.d.ts",
|
|
32
|
+
"import": "./dist/parse-selector.js"
|
|
33
|
+
},
|
|
34
|
+
"./parse-atrule-prelude": {
|
|
35
|
+
"types": "./dist/parse-atrule-prelude.d.ts",
|
|
36
|
+
"import": "./dist/parse-atrule-prelude.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "vitest",
|
|
44
|
+
"test-coverage": "vitest --coverage",
|
|
45
|
+
"test-build": "npm run build && vitest run --config vitest.config.build.ts",
|
|
46
|
+
"build": "vite build && tsc --project tsconfig.build.json",
|
|
47
|
+
"benchmark": "npm run build && node benchmark/index.ts",
|
|
48
|
+
"benchmark:memory": "npm run build && node --expose-gc benchmark/memory.ts",
|
|
49
|
+
"lint": "oxlint --config .oxlintrc.json",
|
|
50
|
+
"lint-package": "publint",
|
|
51
|
+
"check": "tsc --noEmit",
|
|
52
|
+
"knip": "knip"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"css",
|
|
56
|
+
"parser",
|
|
57
|
+
"lexer",
|
|
58
|
+
"tokenizer",
|
|
59
|
+
"ast"
|
|
60
|
+
],
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@projectwallace/preset-oxlint": "^0.0.7",
|
|
63
|
+
"@types/node": "^24.10.1",
|
|
64
|
+
"@vitest/coverage-v8": "^4.0.8",
|
|
65
|
+
"bootstrap": "^5.3.8",
|
|
66
|
+
"css-tree": "^3.1.0",
|
|
67
|
+
"knip": "^5.69.1",
|
|
68
|
+
"oxlint": "^1.28.0",
|
|
69
|
+
"postcss": "^8.5.6",
|
|
70
|
+
"prettier": "^3.6.2",
|
|
71
|
+
"publint": "^0.3.15",
|
|
72
|
+
"tailwindcss": "^2.2.8",
|
|
73
|
+
"tinybench": "^2.9.0",
|
|
74
|
+
"typescript": "^5.9.3",
|
|
75
|
+
"vite": "^7.2.2",
|
|
76
|
+
"vitest": "^4.0.8"
|
|
77
|
+
}
|
|
78
|
+
}
|