@projectwallace/css-parser 0.8.2 → 0.8.4

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/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { parse } from './parse';
2
2
  export { parse_selector } from './parse-selector';
3
3
  export { parse_atrule_prelude } from './parse-atrule-prelude';
4
+ export { parse_declaration } from './parse-declaration';
4
5
  export { parse_value } from './parse-value';
5
6
  export { tokenize } from './tokenize';
6
7
  export { walk, traverse, SKIP, BREAK } from './walk';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { parse } from './parse.js';
2
2
  export { parse_selector } from './parse-selector.js';
3
3
  export { parse_atrule_prelude } from './parse-atrule-prelude.js';
4
+ export { parse_declaration } from './parse-declaration.js';
4
5
  export { parse_value } from './parse-value.js';
5
6
  export { tokenize } from './tokenize.js';
6
7
  export { BREAK, SKIP, traverse, walk } from './walk.js';
@@ -189,13 +189,7 @@ class ANplusBParser {
189
189
  this.lexer.pos = skip_whitespace_forward(this.source, this.lexer.pos, this.expr_end);
190
190
  }
191
191
  create_anplusb_node(start, a_start, a_end, b_start, b_end) {
192
- const node = this.arena.create_node(
193
- NTH_SELECTOR,
194
- start,
195
- this.lexer.pos - start,
196
- this.lexer.line,
197
- 1
198
- );
192
+ const node = this.arena.create_node(NTH_SELECTOR, start, this.lexer.pos - start, this.lexer.line, 1);
199
193
  if (a_end > a_start) {
200
194
  this.arena.set_content_start_delta(node, a_start - start);
201
195
  this.arena.set_content_length(node, a_end - a_start);
@@ -16,12 +16,16 @@ class AtRulePreludeParser {
16
16
  this.lexer = new Lexer(source, false);
17
17
  this.prelude_end = 0;
18
18
  }
19
- // Parse an at-rule prelude into nodes based on the at-rule type
19
+ // Parse an at-rule prelude into nodes (standalone use)
20
20
  parse_prelude(at_rule_name, start, end, line = 1, column = 1) {
21
21
  this.prelude_end = end;
22
22
  this.lexer.pos = start;
23
23
  this.lexer.line = line;
24
24
  this.lexer.column = column;
25
+ return this.parse_prelude_dispatch(at_rule_name);
26
+ }
27
+ // Dispatch to appropriate parser based on at-rule type
28
+ parse_prelude_dispatch(at_rule_name) {
25
29
  if (str_equals("media", at_rule_name)) {
26
30
  return this.parse_media_query_list();
27
31
  } else if (str_equals("container", at_rule_name)) {
@@ -59,13 +63,7 @@ class AtRulePreludeParser {
59
63
  return nodes;
60
64
  }
61
65
  create_node(type, start, end) {
62
- return this.arena.create_node(
63
- type,
64
- start,
65
- end - start,
66
- this.lexer.token_line,
67
- this.lexer.token_column
68
- );
66
+ return this.arena.create_node(type, start, end - start, this.lexer.token_line, this.lexer.token_column);
69
67
  }
70
68
  is_and_or_not(str) {
71
69
  return str_equals("and", str) || str_equals("or", str) || str_equals("not", str);
@@ -0,0 +1,7 @@
1
+ import { CSSNode } from './css-node';
2
+ /**
3
+ * Parse a CSS declaration string and return an AST
4
+ * @param source - The CSS declaration to parse (e.g., "color: red", "margin: 10px !important")
5
+ * @returns The DECLARATION CSSNode
6
+ */
7
+ export declare function parse_declaration(source: string): CSSNode;
@@ -0,0 +1,114 @@
1
+ import { Lexer } from './lexer.js';
2
+ import { CSSDataArena, DECLARATION, FLAG_IMPORTANT } from './arena.js';
3
+ import { ValueParser } from './parse-value.js';
4
+ import { TOKEN_IDENT, TOKEN_COLON, TOKEN_EOF, TOKEN_SEMICOLON, TOKEN_RIGHT_BRACE, TOKEN_LEFT_BRACE, TOKEN_DELIM } from './token-types.js';
5
+ import { trim_boundaries } from './parse-utils.js';
6
+ import { CSSNode } from './css-node.js';
7
+
8
+ class DeclarationParser {
9
+ arena;
10
+ source;
11
+ value_parser;
12
+ constructor(arena, source, parse_values = true) {
13
+ this.arena = arena;
14
+ this.source = source;
15
+ this.value_parser = parse_values ? new ValueParser(arena, source) : null;
16
+ }
17
+ // Parse a declaration range into a declaration node (standalone use)
18
+ parse_declaration(start, end, line = 1, column = 1) {
19
+ const lexer = new Lexer(this.source, false);
20
+ lexer.pos = start;
21
+ lexer.line = line;
22
+ lexer.column = column;
23
+ lexer.next_token_fast(true);
24
+ return this.parse_declaration_with_lexer(lexer, end);
25
+ }
26
+ // Parse a declaration using a provided lexer (used by Parser to avoid re-tokenization)
27
+ parse_declaration_with_lexer(lexer, end) {
28
+ if (lexer.token_type !== TOKEN_IDENT) {
29
+ return null;
30
+ }
31
+ let prop_start = lexer.token_start;
32
+ let prop_end = lexer.token_end;
33
+ let decl_line = lexer.token_line;
34
+ let decl_column = lexer.token_column;
35
+ const saved = lexer.save_position();
36
+ lexer.next_token_fast(true);
37
+ if (lexer.token_type !== TOKEN_COLON) {
38
+ lexer.restore_position(saved);
39
+ return null;
40
+ }
41
+ lexer.next_token_fast(true);
42
+ let declaration = this.arena.create_node(
43
+ DECLARATION,
44
+ prop_start,
45
+ 0,
46
+ // length unknown yet
47
+ decl_line,
48
+ decl_column
49
+ );
50
+ this.arena.set_content_start_delta(declaration, 0);
51
+ this.arena.set_content_length(declaration, prop_end - prop_start);
52
+ let value_start = lexer.token_start;
53
+ let value_start_line = lexer.token_line;
54
+ let value_start_column = lexer.token_column;
55
+ let value_end = value_start;
56
+ let has_important = false;
57
+ let last_end = lexer.token_end;
58
+ while (lexer.token_type !== TOKEN_EOF && lexer.token_start < end) {
59
+ let token_type = lexer.token_type;
60
+ if (token_type === TOKEN_SEMICOLON) break;
61
+ if (token_type === TOKEN_RIGHT_BRACE) break;
62
+ if (token_type === TOKEN_LEFT_BRACE) {
63
+ lexer.restore_position(saved);
64
+ return null;
65
+ }
66
+ if (token_type === TOKEN_DELIM && this.source[lexer.token_start] === "!") {
67
+ value_end = lexer.token_start;
68
+ let next_type = lexer.next_token_fast(true);
69
+ if (next_type === TOKEN_IDENT) {
70
+ has_important = true;
71
+ last_end = lexer.token_end;
72
+ lexer.next_token_fast(true);
73
+ break;
74
+ }
75
+ }
76
+ last_end = lexer.token_end;
77
+ value_end = last_end;
78
+ lexer.next_token_fast(true);
79
+ }
80
+ let trimmed = trim_boundaries(this.source, value_start, value_end);
81
+ if (trimmed) {
82
+ this.arena.set_value_start_delta(declaration, trimmed[0] - prop_start);
83
+ this.arena.set_value_length(declaration, trimmed[1] - trimmed[0]);
84
+ if (this.value_parser) {
85
+ let valueNodes = this.value_parser.parse_value(value_start, trimmed[1], value_start_line, value_start_column);
86
+ this.arena.append_children(declaration, valueNodes);
87
+ }
88
+ } else {
89
+ this.arena.set_value_start_delta(declaration, value_start - prop_start);
90
+ this.arena.set_value_length(declaration, 0);
91
+ }
92
+ if (has_important) {
93
+ this.arena.set_flag(declaration, FLAG_IMPORTANT);
94
+ }
95
+ if (lexer.token_type === TOKEN_SEMICOLON) {
96
+ last_end = lexer.token_end;
97
+ lexer.next_token_fast(true);
98
+ }
99
+ this.arena.set_length(declaration, last_end - prop_start);
100
+ return declaration;
101
+ }
102
+ }
103
+ function parse_declaration(source) {
104
+ const arena = new CSSDataArena(CSSDataArena.capacity_for_source(source.length));
105
+ const decl_parser = new DeclarationParser(arena, source);
106
+ const decl_index = decl_parser.parse_declaration(0, source.length);
107
+ if (decl_index === null) {
108
+ const empty = arena.create_node(DECLARATION, 0, 0, 1, 1);
109
+ return new CSSNode(arena, source, empty);
110
+ }
111
+ return new CSSNode(arena, source, decl_index);
112
+ }
113
+
114
+ export { DeclarationParser, parse_declaration };
@@ -17,7 +17,7 @@ class SelectorParser {
17
17
  this.lexer = new Lexer(source, false);
18
18
  this.selector_end = 0;
19
19
  }
20
- // Parse a selector range into selector nodes
20
+ // Parse a selector range into selector nodes (standalone use)
21
21
  // Always returns a NODE_SELECTOR_LIST with selector components as children
22
22
  parse_selector(start, end, line = 1, column = 1, allow_relative = true) {
23
23
  this.selector_end = end;
@@ -58,13 +58,7 @@ class SelectorParser {
58
58
  }
59
59
  }
60
60
  if (selectors.length >= 1) {
61
- let list_node = this.arena.create_node(
62
- SELECTOR_LIST,
63
- list_start,
64
- this.lexer.pos - list_start,
65
- list_line,
66
- list_column
67
- );
61
+ let list_node = this.arena.create_node(SELECTOR_LIST, list_start, this.lexer.pos - list_start, list_line, list_column);
68
62
  this.arena.append_children(list_node, selectors);
69
63
  return list_node;
70
64
  }
@@ -547,13 +541,7 @@ class SelectorParser {
547
541
  let selector_list = this.parse_selector_list();
548
542
  this.selector_end = saved_selector_end;
549
543
  this.lexer.restore_position(saved);
550
- let of_node = this.arena.create_node(
551
- NTH_OF_SELECTOR,
552
- start,
553
- end - start,
554
- this.lexer.line,
555
- 1
556
- );
544
+ let of_node = this.arena.create_node(NTH_OF_SELECTOR, start, end - start, this.lexer.line, 1);
557
545
  if (anplusb_node !== null && selector_list !== null) {
558
546
  this.arena.set_first_child(of_node, anplusb_node);
559
547
  this.arena.set_next_sibling(anplusb_node, selector_list);
@@ -580,13 +568,7 @@ class SelectorParser {
580
568
  return -1;
581
569
  }
582
570
  create_node(type, start, end) {
583
- let node = this.arena.create_node(
584
- type,
585
- start,
586
- end - start,
587
- this.lexer.line,
588
- this.lexer.column
589
- );
571
+ let node = this.arena.create_node(type, start, end - start, this.lexer.line, this.lexer.column);
590
572
  this.arena.set_content_start_delta(node, 0);
591
573
  this.arena.set_content_length(node, end - start);
592
574
  return node;
@@ -15,13 +15,17 @@ class ValueParser {
15
15
  this.lexer = new Lexer(source, false);
16
16
  this.value_end = 0;
17
17
  }
18
- // Parse a declaration value range into value nodes
18
+ // Parse a declaration value range into value nodes (standalone use)
19
19
  // Returns array of value node indices
20
20
  parse_value(start, end, start_line, start_column) {
21
21
  this.value_end = end;
22
22
  this.lexer.pos = start;
23
23
  this.lexer.line = start_line;
24
24
  this.lexer.column = start_column;
25
+ return this.parse_value_tokens();
26
+ }
27
+ // Core token parsing logic
28
+ parse_value_tokens() {
25
29
  let nodes = [];
26
30
  while (this.lexer.pos < this.value_end) {
27
31
  this.lexer.next_token_fast(false);
@@ -77,13 +81,7 @@ class ValueParser {
77
81
  }
78
82
  }
79
83
  create_node(node_type, start, end) {
80
- let node = this.arena.create_node(
81
- node_type,
82
- start,
83
- end - start,
84
- this.lexer.token_line,
85
- this.lexer.token_column
86
- );
84
+ let node = this.arena.create_node(node_type, start, end - start, this.lexer.token_line, this.lexer.token_column);
87
85
  this.arena.set_content_length(node, end - start);
88
86
  return node;
89
87
  }
@@ -154,9 +152,7 @@ class ValueParser {
154
152
  let token_type = this.lexer.token_type;
155
153
  if (token_type === TOKEN_EOF) break;
156
154
  if (this.lexer.token_start >= this.value_end) break;
157
- if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) {
158
- paren_depth++;
159
- } else if (token_type === TOKEN_RIGHT_PAREN) {
155
+ if (token_type === TOKEN_RIGHT_PAREN) {
160
156
  paren_depth--;
161
157
  if (paren_depth === 0) {
162
158
  content_end = this.lexer.token_start;
package/dist/parse.js CHANGED
@@ -1,10 +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_IMPORTANT, AT_RULE } from './arena.js';
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
- import { ValueParser } from './parse-value.js';
5
4
  import { SelectorParser } from './parse-selector.js';
6
5
  import { AtRulePreludeParser } from './parse-atrule-prelude.js';
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';
6
+ import { DeclarationParser } from './parse-declaration.js';
7
+ import { TOKEN_EOF, TOKEN_AT_KEYWORD, TOKEN_LEFT_BRACE, TOKEN_RIGHT_BRACE, TOKEN_IDENT, TOKEN_SEMICOLON } from './token-types.js';
8
8
  import { trim_boundaries } from './parse-utils.js';
9
9
 
10
10
  let DECLARATION_AT_RULES = /* @__PURE__ */ new Set(["font-face", "font-feature-values", "page", "property", "counter-style"]);
@@ -13,9 +13,9 @@ class Parser {
13
13
  source;
14
14
  lexer;
15
15
  arena;
16
- value_parser;
17
16
  selector_parser;
18
17
  prelude_parser;
18
+ declaration_parser;
19
19
  parse_values_enabled;
20
20
  parse_selectors_enabled;
21
21
  parse_atrule_preludes_enabled;
@@ -29,9 +29,9 @@ class Parser {
29
29
  this.lexer = new Lexer(source, skip_comments);
30
30
  let capacity = CSSDataArena.capacity_for_source(source.length);
31
31
  this.arena = new CSSDataArena(capacity);
32
- this.value_parser = this.parse_values_enabled ? new ValueParser(this.arena, source) : null;
33
32
  this.selector_parser = this.parse_selectors_enabled ? new SelectorParser(this.arena, source) : null;
34
33
  this.prelude_parser = this.parse_atrule_preludes_enabled ? new AtRulePreludeParser(this.arena, source) : null;
34
+ this.declaration_parser = new DeclarationParser(this.arena, source, this.parse_values_enabled);
35
35
  }
36
36
  // Get the arena (for internal/advanced use only)
37
37
  get_arena() {
@@ -56,13 +56,7 @@ class Parser {
56
56
  // Parse the entire stylesheet and return the root CSSNode
57
57
  parse() {
58
58
  this.next_token();
59
- let stylesheet = this.arena.create_node(
60
- STYLESHEET,
61
- 0,
62
- this.source.length,
63
- 1,
64
- 1
65
- );
59
+ let stylesheet = this.arena.create_node(STYLESHEET, 0, this.source.length, 1, 1);
66
60
  let rules = [];
67
61
  while (!this.is_eof()) {
68
62
  let rule = this.parse_rule();
@@ -177,13 +171,7 @@ class Parser {
177
171
  return selectorNode;
178
172
  }
179
173
  }
180
- let selector = this.arena.create_node(
181
- SELECTOR_LIST,
182
- selector_start,
183
- last_end - selector_start,
184
- selector_line,
185
- selector_column
186
- );
174
+ let selector = this.arena.create_node(SELECTOR_LIST, selector_start, last_end - selector_start, selector_line, selector_column);
187
175
  return selector;
188
176
  }
189
177
  // Parse a declaration: property: value;
@@ -191,72 +179,7 @@ class Parser {
191
179
  if (this.peek_type() !== TOKEN_IDENT) {
192
180
  return null;
193
181
  }
194
- let prop_start = this.lexer.token_start;
195
- let prop_end = this.lexer.token_end;
196
- let decl_line = this.lexer.token_line;
197
- let decl_column = this.lexer.token_column;
198
- const saved = this.lexer.save_position();
199
- this.next_token();
200
- if (this.peek_type() !== TOKEN_COLON) {
201
- this.lexer.restore_position(saved);
202
- return null;
203
- }
204
- this.next_token();
205
- let declaration = this.arena.create_node(
206
- DECLARATION,
207
- prop_start,
208
- 0,
209
- // length unknown yet
210
- decl_line,
211
- decl_column
212
- );
213
- this.arena.set_content_start_delta(declaration, 0);
214
- this.arena.set_content_length(declaration, prop_end - prop_start);
215
- let value_start = this.lexer.token_start;
216
- let value_start_line = this.lexer.token_line;
217
- let value_start_column = this.lexer.token_column;
218
- let value_end = value_start;
219
- let has_important = false;
220
- let last_end = this.lexer.token_end;
221
- while (!this.is_eof()) {
222
- let token_type = this.peek_type();
223
- if (token_type === TOKEN_SEMICOLON || token_type === TOKEN_RIGHT_BRACE) break;
224
- if (token_type === TOKEN_LEFT_BRACE) {
225
- this.lexer.restore_position(saved);
226
- return null;
227
- }
228
- if (token_type === TOKEN_DELIM && this.source[this.lexer.token_start] === "!") {
229
- value_end = this.lexer.token_start;
230
- let next_type = this.lexer.next_token_fast();
231
- if (next_type === TOKEN_IDENT) {
232
- has_important = true;
233
- last_end = this.lexer.token_end;
234
- this.next_token();
235
- break;
236
- }
237
- }
238
- last_end = this.lexer.token_end;
239
- value_end = last_end;
240
- this.next_token();
241
- }
242
- let trimmed = trim_boundaries(this.source, value_start, value_end);
243
- if (trimmed) {
244
- this.arena.set_value_start_delta(declaration, trimmed[0] - prop_start);
245
- this.arena.set_value_length(declaration, trimmed[1] - trimmed[0]);
246
- if (this.parse_values_enabled && this.value_parser) {
247
- let valueNodes = this.value_parser.parse_value(value_start, trimmed[1], value_start_line, value_start_column);
248
- this.arena.append_children(declaration, valueNodes);
249
- }
250
- }
251
- if (has_important) {
252
- this.arena.set_flag(declaration, FLAG_IMPORTANT);
253
- }
254
- if (this.peek_type() === TOKEN_SEMICOLON) {
255
- last_end = this.lexer.token_end;
256
- this.next_token();
257
- }
258
- this.arena.set_length(declaration, last_end - prop_start);
259
- return declaration;
182
+ return this.declaration_parser.parse_declaration_with_lexer(this.lexer, this.source.length);
260
183
  }
261
184
  // Parse an at-rule: @media, @import, @font-face, etc.
262
185
  parse_atrule() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectwallace/css-parser",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",
@@ -35,6 +35,10 @@
35
35
  "types": "./dist/parse-atrule-prelude.d.ts",
36
36
  "import": "./dist/parse-atrule-prelude.js"
37
37
  },
38
+ "./parse-declaration": {
39
+ "types": "./dist/parse-declaration.d.ts",
40
+ "import": "./dist/parse-declaration.js"
41
+ },
38
42
  "./parse-value": {
39
43
  "types": "./dist/parse-value.d.ts",
40
44
  "import": "./dist/parse-value.js"