@excom/quark-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.
Files changed (32) hide show
  1. package/.rush/temp/chunked-rush-logs/quark-parser.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark-parser.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +5 -0
  11. package/index.ts +12 -0
  12. package/package.json +39 -0
  13. package/rush-logs/quark-parser.apply-exports.cache.log +1 -0
  14. package/rush-logs/quark-parser.apply-exports.log +1 -0
  15. package/rush-logs/quark-parser.build_package-metas.cache.log +1 -0
  16. package/rush-logs/quark-parser.build_package-metas.log +1 -0
  17. package/src/error.ts +24 -0
  18. package/src/parser.ts +1482 -0
  19. package/src/tables.ts +77 -0
  20. package/src/tokenizer.ts +443 -0
  21. package/src/types.ts +497 -0
  22. package/support/docs/README.md +443 -0
  23. package/support/package-meta.json +33 -0
  24. package/support/tests/grammar-docs.test.ts +109 -0
  25. package/support/tests/parser-at-rules.test.ts +430 -0
  26. package/support/tests/parser-declarations.test.ts +152 -0
  27. package/support/tests/parser-edge-cases.test.ts +296 -0
  28. package/support/tests/parser-expressions.test.ts +413 -0
  29. package/support/tests/parser-real-world.test.ts +429 -0
  30. package/support/tests/parser-selectors.test.ts +169 -0
  31. package/support/tests/tokenizer.test.ts +268 -0
  32. package/tsconfig.json +5 -0
@@ -0,0 +1,443 @@
1
+ # quark-parser
2
+
3
+ Fast, zero-dependency tokenizer and AST parser for the Quark language — the one grammar definition shared by the runtime, the formatter, and tooling.
4
+
5
+ ## Features
6
+
7
+ - **The whole Quark grammar** CSS-shaped statements and selectors, Quark's own at-rules, and structured expressions: dot / bracket accessors and CSS-style `if()`
8
+ - **Real AST** expressions are typed nodes, not token lists; every node carries source spans
9
+ - **Three entry points** `parse` (sheet), `parseExpression` (one value), `parseSelectorList` (one selector list)
10
+ - **Fast** single-pass charcode tokenizer, Pratt expression parser, no regexes on the hot path
11
+ - **Author-facing errors** `QuarkParseError` reports line / column
12
+ - **Grammar tables exported** operator precedence, attribute operators, selector pseudos, Quark's at-rules
13
+
14
+ ## Installation
15
+
16
+ <include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { parse, parseExpression, parseSelectorList, tokenize, QuarkParseError } from "@excom/quark-parser";
22
+
23
+ const ast = parse(`
24
+ provider-fetch[is-success] {
25
+ $items: prop("provision").body;
26
+ ul { content: iterate($items); }
27
+ [bind-label] { content: "Index: #{index}. ID: #{item.id}"; }
28
+ }
29
+ `);
30
+ // ast.type === "stylesheet"; ast.body[0].type === "rule"; ...
31
+
32
+ const expr = parseExpression(`iterate($items, ":scope > template", "id")`);
33
+ // expr.type === "function"
34
+ ```
35
+
36
+ ## Language reference
37
+
38
+ This section defines what **parses**. What the runtime does with a parsed sheet — declaration kinds, value keywords, built-in functions, allowed methods — is the `quark` package's documentation (one page per topic: declaration kinds, values, built-ins, allowed methods, selectors). Quark is a derivative of CSS: rules, selectors, and declarations carry over, the at-rules are its own, and a construct the engine never ran is a parse error rather than a statement the runtime skips (see *Rejected on purpose*).
39
+
40
+ Notation is EBNF: `=` defines, `|` alternates, `{ x }` repeats zero or more times, `[ x ]` is optional, `"x"` is literal text, and `ws` is whitespace. Every `quark` code block on this page is parsed by the package's tests; blocks marked *invalid* must fail.
41
+
42
+ ### Lexical structure
43
+
44
+ The tokenizer emits `ident`, `variable`, `at`, `string`, `number`, `hash`, `url`, and `punct` tokens. Whitespace is **not** a token: it sets a `ws` flag on the token that follows, and that flag decides descendant combinators, space-separated lists, and sign handling. Comments are `/* … */` only — a `//` raises `Line comments are not supported, use /* */`, so a sheet stays tokenizable by a CSS engine. They are collected separately and surface as `comment` statements between other statements; they never appear inside a selector or a value.
45
+
46
+ ```ebnf
47
+ ws = ( " " | "\t" | "\n" | "\r" | "\f" ) { " " | "\t" | "\n" | "\r" | "\f" }
48
+ comment = "/*" … "*/"
49
+ ident = ident-start { ident-char } | "-" ident-start { ident-char }
50
+ ident-start = letter | "_" | "\" any-char | non-ASCII
51
+ ident-char = ident-start | digit | "-"
52
+ variable = "$" ident-char { ident-char }
53
+ at = "@" ident
54
+ string = '"' { char | "\" any-char | interpolation } '"' | "'" { … } "'"
55
+ number = [ "-" | "+" ] ( digits [ "." digits ] | "." digits ) [ ( "e" | "E" ) [ "-" | "+" ] digits ] [ unit ]
56
+ unit = "%" | ident
57
+ hash = "#" ident-char { ident-char }
58
+ url = "url" "(" raw-text ")"
59
+ punct = "==" | "!=" | "<=" | ">=" | "::" | "*=" | "~=" | "^=" | "|=" | "$=" | "#{" | "..." | any-other-char
60
+ ```
61
+
62
+ - **Identifiers** may contain dashes, so `prevent-default`, `dangerous-html`, `-webkit-mask`, and `--accent` are single tokens. The leading `-` joins the identifier only where a value can start (see *Signs*). `and`, `or`, `not`, `true`, `false`, `null`, `if`, and `else` are ordinary identifiers that the parser recognizes by position.
63
+ - **Variables** keep their name without the `$`; the name may start with a digit.
64
+ - **Strings** keep escapes verbatim (`\"`, `\n`) and may contain `#{…}` interpolation, which is parsed as a nested value.
65
+ - **Numbers** split into value and unit: `10px` → `10` + `px`, `50%` → `50` + `%`. `2e3` is an exponent; `2em` is a unit.
66
+ - **Hash** tokens become a `color` node when every character is hex (`#ccc`, `#0f0f0f`) and an `identifier` named with the `#` otherwise.
67
+ - **Unquoted `url(…)`** is scanned raw when its contents hold no whitespace, quote, `$`, or `(`; otherwise `url(` tokenizes normally and parses as a function call.
68
+
69
+ #### Signs
70
+
71
+ `-` and `+` directly followed by a digit (or `.digit`) are part of the number when they stand where a value starts, or when whitespace precedes them and none follows — the CSS `margin: 10px -5px` convention. Everywhere else they are binary operators. The same start-of-value rule lets `-` begin an identifier.
72
+
73
+ ```quark
74
+ a {
75
+ sum: $x + 1; /* addition */
76
+ sum-tight: $x+1; /* addition */
77
+ list: $x +1; /* space list: $x, +1 */
78
+ spaced: 10px -5px;
79
+ vendor: -webkit-mask;
80
+ }
81
+ ```
82
+
83
+ ### Statements
84
+
85
+ ```ebnf
86
+ stylesheet = { statement }
87
+ statement = rule | declaration | at-rule | comment | ";"
88
+ rule = selector-list block
89
+ block = "{" { statement } "}"
90
+ declaration = key ":" value [ ";" ]
91
+ key = property | variable
92
+ property = ( ident | "*" ) { ident | "*" }
93
+ ```
94
+
95
+ - A declaration's `;` is optional before `}` and at end of input; stray `;` are skipped.
96
+ - Declarations are allowed at the top level of a sheet (a Quark extension; CSS has none).
97
+ - A variable key is a bare `$name`; a dot chain after it (`$sig.value:`, the former signal write) is a parse error that points at the owner-side forms (`@on` block on the owner, `element.quark.setProperty()` from JS).
98
+
99
+ ```quark
100
+ $app-theme: "dark";
101
+ main {
102
+ $count: 0;
103
+ data-theme: $app-theme;
104
+ [bind-count] { content: $count; }
105
+ @on click (target: "button") { $count: $count + 1; }
106
+ }
107
+ ```
108
+
109
+ #### Declaration or rule?
110
+
111
+ The parser scans ahead (skipping `(…)`, `[…]`, and `#{…}`) to the first top-level `{`, `;`, `}`, or end of input, noting the first top-level `:`.
112
+
113
+ | Terminator | Shape | Result |
114
+ | --- | --- | --- |
115
+ | `;` / `}` / end | has a top-level `:` | declaration |
116
+ | `;` / `}` / end | no `:` | parse error |
117
+ | `{` | `ident ":"` at the start **and** whitespace or `{` after the colon | parse error: a nested property block |
118
+ | `{` | anything else | rule |
119
+
120
+ So `a:hover { … }` is a rule, while `a: hover { … }` is the nested property syntax Quark rejects.
121
+
122
+ ### Selectors
123
+
124
+ ```ebnf
125
+ selector-list = selector { "," selector }
126
+ selector = compound { combinator compound }
127
+ combinator = ">" | "+" | "~" | ws
128
+ compound = simple { simple }
129
+ simple = ident | "*" | "." ident | "#" ident-chars | "&" [ ident ]
130
+ | attribute | pseudo-class | pseudo-element
131
+ attribute = "[" ident [ attr-op attr-value [ "i" | "s" ] ] "]"
132
+ attr-op = "=" | "*=" | "^=" | "$=" | "|=" | "~="
133
+ attr-value = string | ident | number
134
+ pseudo-class = ":" ident [ "(" ( selector-list | raw-text ) ")" ]
135
+ pseudo-element = "::" ident [ "(" raw-text ")" ]
136
+ ```
137
+
138
+ - Whitespace between two simple selectors is a descendant combinator; whitespace around `>`, `+`, `~` is ignored.
139
+ - `&` takes an adjacent identifier as its suffix (`&-open`, `&__title`); `&:hover` and `&[open]` are `&` followed by another simple selector.
140
+ - The argument of `:not`, `:is`, `:where`, `:has`, `:matches`, `:any`, `:-webkit-any`, `:-moz-any`, `:host`, `:host-context`, and `:current` parses as a selector list; every other pseudo argument is kept as raw text (`:nth-child(2n + 1)`). The `(` must follow the name directly.
141
+ - Attribute values are literals only — no expressions and no interpolation inside `[…]`.
142
+
143
+ ```quark
144
+ details[open] > summary, .card:not([is-loading]) [bind-status] { content: "Open"; }
145
+ li:nth-child(2n + 1)::before { content: "•"; }
146
+ .tab {
147
+ &-active { is-active: ""; }
148
+ &[aria-selected="true" i] { tabindex: "0"; }
149
+ }
150
+ ```
151
+
152
+ ### Expressions
153
+
154
+ Declaration values are expressions, not token lists. A value is a comma list of space lists of operator expressions:
155
+
156
+ ```ebnf
157
+ value = space-list { "," space-list } [ "," ]
158
+ space-list = expression { expression }
159
+ expression = unary { binary-op expression }
160
+ binary-op = "or" | "and" | "==" | "!=" | "<" | ">" | "<=" | ">=" | "+" | "-" | "*" | "/" | "%"
161
+ unary = ( "-" | "+" ) unary | "not" expression | postfix
162
+ postfix = primary { "." ident | "." variable | "[" expression "]" | "(" arguments ")" }
163
+ primary = number | string | color | "true" | "false" | "null" | variable | identifier | "&"
164
+ | interpolation | url | if-function | parens | bracket-list
165
+ parens = "(" ")" | "(" space-list ")" | "(" space-list { "," space-list } [ "," ] ")" | "(" map-entry { "," map-entry } [ "," ] ")"
166
+ map-entry = space-list ":" space-list
167
+ bracket-list = "[" { space-list [ "," ] } "]"
168
+ arguments = [ argument { "," argument } [ "," ] ]
169
+ argument = [ variable ":" ] space-list [ "..." ]
170
+ if-function = "if" "(" if-arm { ";" if-arm } [ ";" ] ")"
171
+ if-arm = ( expression | "else" ) ":" value
172
+ interpolation = "#{" value "}"
173
+ ```
174
+
175
+ #### Precedence
176
+
177
+ Lowest to highest. All binary operators are left-associative.
178
+
179
+ | Level | Operators |
180
+ | --- | --- |
181
+ | 1 | `or` |
182
+ | 2 | `and` |
183
+ | 3 | `not` (unary) |
184
+ | 4 | `==` `!=` |
185
+ | 5 | `<` `>` `<=` `>=` |
186
+ | 6 | `+` `-` |
187
+ | 7 | `*` `/` `%` |
188
+ | — | unary `-` `+` |
189
+ | — | `.` `[…]` `(…)` (postfix) |
190
+
191
+ `not` takes everything tighter than itself as its operand: `not $a == $b` is `not ($a == $b)`, while `not $a and $b` is `(not $a) and $b`. Parentheses group; a parenthesized expression keeps the parens in its span so sliced source re-parses.
192
+
193
+ ```quark
194
+ a {
195
+ ready: $a and not $b == $c or $d;
196
+ math: ($x + 1) * 2 % 5 - -$y;
197
+ text: "Total: " + $n + " / " + $total; /* `+` concatenates; prefer "Total: #{$n} / #{$total}" */
198
+ }
199
+ ```
200
+
201
+ #### Space lists
202
+
203
+ Two operands with no operator between them form a space-separated list, exactly as in CSS (`1px solid red`). A value can start with an identifier, variable, string, number, hash, `(`, `[`, `#{`, or `&`; the characters `, ; ) ] } { : !` always end a list. A `$variable` followed by `(` is therefore a list of two items, not a call — only identifiers, member chains, and interpolations are callable.
204
+
205
+ ```quark
206
+ a {
207
+ border: 1px solid $color;
208
+ pair: $x (1 + 2);
209
+ tags: "a", "b", "c";
210
+ }
211
+ ```
212
+
213
+ #### Accessors and calls
214
+
215
+ `.` reads a field (`$obj.field`, `item.name`), `.$name` reads a namespaced variable (`math.$pi`), and `[…]` indexes when the bracket is adjacent to its object (`$tags[$i]`, `$obj["key"]`). A space before `[` starts a bracket list instead. Calls take positional, named (`$name: value`), and spread (`$args...`) arguments; a call on a member chain is a method call.
216
+
217
+ ```quark
218
+ a {
219
+ title: item.meta.title.toUpperCase();
220
+ first: "#{$tags[0]} #{$obj["display-name"]}";
221
+ ns: math.$pi * math.round($r);
222
+ named: fetch-user($id: 7, $opts...);
223
+ self: closest(&);
224
+ chained: prop("provision").body.items[index].name;
225
+ }
226
+ ```
227
+
228
+ #### CSS-style `if()`
229
+
230
+ `if(` parses as a conditional when its parentheses contain a `:` at depth one; each arm is `condition: value` separated by `;`, and an optional `else:` arm must come last. A colon-less `if(a, b, c)` is an ordinary function call.
231
+
232
+ ```quark
233
+ a {
234
+ label: if($n == 0: "none"; $n == 1: "one"; else: "many");
235
+ style: if($active: "bold" "underline"; else: "normal");
236
+ size: if($big: 20; else: 10).toFixed(1);
237
+ fallback: if($a, $b, $c);
238
+ }
239
+ ```
240
+
241
+ #### Lists and maps
242
+
243
+ Comma and space lists nest; parentheses and square brackets give a list explicit boundaries. A parenthesized `key: value` sequence is a map.
244
+
245
+ ```quark
246
+ a {
247
+ csv: 1, 2, 3;
248
+ spaced: 1 2 3;
249
+ grouped: (1 2) (3 4);
250
+ bracketed: [1, 2, 3];
251
+ empty: ();
252
+ map: (name: "Ada", tags: ("a" "b"), nested: (x: 1));
253
+ interpolated: "Hello #{$user.name}!";
254
+ }
255
+ ```
256
+
257
+ #### Rejected syntax
258
+
259
+ JavaScript syntax that appears in legacy sheets is a parse error, with a message naming the alternative. Conditionals use `if()` or `ternary()`; null-safe access is runtime behavior (accessing a field of `null` / `undefined` yields `undefined`), not syntax.
260
+
261
+ ```quark invalid
262
+ a { x: $a ? $b : $c; }
263
+ ```
264
+
265
+ ```quark invalid
266
+ a { x: $a?.b; }
267
+ ```
268
+
269
+ ```quark invalid
270
+ a { x: $a ?? $b; }
271
+ ```
272
+
273
+ ```quark invalid
274
+ a { x: $a === $b; }
275
+ ```
276
+
277
+ ```quark invalid
278
+ a { x: $a || $b; }
279
+ ```
280
+
281
+ ```quark invalid
282
+ a { x: $a && $b; }
283
+ ```
284
+
285
+ ```quark invalid
286
+ a { @on click () => go(); }
287
+ ```
288
+
289
+ ```quark invalid
290
+ a { @on click go, prevent-default; }
291
+ ```
292
+
293
+ ```quark invalid
294
+ form { @off submit save; }
295
+ ```
296
+
297
+ ```quark invalid
298
+ button { @on click { @dispatch ping { } } }
299
+ ```
300
+
301
+ ```quark invalid
302
+ ul { @view-transition (types: "todo-change"); }
303
+ ```
304
+
305
+ ### At-rules
306
+
307
+ Quark's at-rules are the whole set — `"@use"`, `"@scope"`, `"@on"`, `"@dispatch"`, `"@command"`, `"@view-transition"`, `"@delay"`, `"@warn"`, `"@debug"`, `"@error"` — and each has a dedicated node. One table, `QUARK_AT_RULES`, types the parser's dispatch map, so any other name is a parse error: CSS's `@media` / `@supports` / `@keyframes` / `@font-face` / `@layer` and SCSS's control flow, mixins, and module rules are not part of the language.
308
+
309
+ ```ebnf
310
+ at-rule = "@use" string [ "as" ( ident | "*" ) ] [ ";" ]
311
+ | "@scope" block
312
+ | "@on" name-list [ options ] ( block | [ ";" ] )
313
+ | ( "@dispatch" | "@command" ) name-list [ options ] [ ";" ]
314
+ | "@view-transition" [ options ] block
315
+ | "@delay" value block
316
+ | ( "@warn" | "@debug" | "@error" ) value [ ";" ]
317
+ options = "(" [ option { "," option } ] ")"
318
+ option = ident [ ":" space-list ]
319
+ name-list = ( ident | string ) { "," ( ident | string ) }
320
+ ```
321
+
322
+ `@use` imports a JS module and takes no `with (…)` clause. `@scope` takes a block and no prelude. `@on` handlers live in the options group (`handle: fn` or `handle: (a, b)`); a bare expression after the event names — the handler list of earlier versions — is an error that points there, and so is an `@on` statement with neither options nor a block (nothing to do). `@off` is not part of the language (it was removed once `@on` gained options and blocks). `@dispatch` and `@command` are statements: a block is an error. `@view-transition` and `@delay` have no statement form: a missing block is an error, and so is a missing `@delay` duration.
323
+
324
+ ```quark
325
+ @use "/helpers.js" as *;
326
+ @use "/api-client.js" as api;
327
+ @scope {
328
+ #out { content: api.getAmount(); }
329
+ form {
330
+ @on submit (prevent-default, handle: api.save);
331
+ @on input, change (debounce: 300) { data-draft: event.target.value; }
332
+ @on keydown (key: "Escape", host: window) { is-open: none; }
333
+ @on click (target: "li[data-id]", once, handle: pick);
334
+ @on reset (prevent-default) {
335
+ data-draft: none;
336
+ @dispatch draft-cleared (detail: (at: event.timeStamp), target: "#status");
337
+ @command --refresh (target: "#preview");
338
+ }
339
+ }
340
+ button[data-copy] {
341
+ @on click {
342
+ data-copied: "";
343
+ @delay 2000 { data-copied: none; }
344
+ }
345
+ }
346
+ img:not([alt]) { @warn "img needs alt"; }
347
+ provider-fetch[is-success] {
348
+ @view-transition (types: "todo-change", timeout: 500) {
349
+ ul { content: iterate($todos, none, "id"); }
350
+ }
351
+ }
352
+ }
353
+ ```
354
+
355
+ ### Rejected on purpose
356
+
357
+ Quark keeps the CSS the engine runs and nothing else, so what it does not run does not parse. Every rejection names the construct, with the line and column.
358
+
359
+ | Construct | Message |
360
+ | --- | --- |
361
+ | an at-rule that is not Quark's own | `@media is not a Quark at-rule` |
362
+ | `%placeholder` selectors | `Placeholder selectors are not supported` |
363
+ | `#{…}` outside a string — a selector, a property name, an attribute value | `Interpolation is only supported inside strings` |
364
+ | `!important`, `!default`, `!global` | `!important is not supported` |
365
+ | nested property blocks | `Nested property blocks are not supported` |
366
+ | a `@use` configuration | `@use does not take a with clause` |
367
+
368
+ ```quark invalid
369
+ @media (width < 600px) { nav { is-compact: ""; } }
370
+ ```
371
+
372
+ ```quark invalid
373
+ @each $name, $glyph in $icons { .icon-#{$name} { content: $glyph; } }
374
+ ```
375
+
376
+ ```quark invalid
377
+ %error-message { content: $message; }
378
+ ```
379
+
380
+ ```quark invalid
381
+ .icon-#{$name} { content: $glyph; }
382
+ ```
383
+
384
+ ```quark invalid
385
+ li[data-id=#{$id}] { is-current: ""; }
386
+ ```
387
+
388
+ ```quark invalid
389
+ a { border-#{$side}-radius: 3px; }
390
+ ```
391
+
392
+ ```quark invalid
393
+ a { color: red !important; }
394
+ ```
395
+
396
+ ```quark invalid
397
+ a { font: { size: 1rem; } }
398
+ ```
399
+
400
+ ```quark invalid
401
+ @use "/theme.js" with ($accent: "red");
402
+ ```
403
+
404
+ ### AST
405
+
406
+ `types.ts` is the contract. Every node has `type`, `start`, and `end` (offsets into the source), so consumers slice original text instead of re-serializing.
407
+
408
+ | Source | Node `type` |
409
+ | --- | --- |
410
+ | sheet | `stylesheet` → `body: Statement[]` |
411
+ | `selector { … }` | `rule` → `selector: selector_list`, `block` |
412
+ | `key: value;` | `declaration` → `property` (a `property` with a `name`, or a `variable`), `value` |
413
+ | `@use "/x.js" as api;` | `atrule` → `name: "use"`, `url`, `namespace` (`null` = derived from the url, `"*"` = global) |
414
+ | `@scope { … }` | `atrule` → `name: "scope"`, `block` |
415
+ | `@on click, submit (once, handle: a) { … }` / `@on click (handle: a);` | `atrule` → `name: "on"`, `events: event_name[]` (each `name`, `quoted`), `options: listener_option[]` (each `name`, `value: Expression \| null` — `null` for a flag), `block` (`null` in the statement form) |
416
+ | `@dispatch cart-add (detail: $d);` / `@command --refresh (target: "#x");` | `atrule` → `name: "dispatch" \| "command"`, `names: event_name[]`, `options: listener_option[]` |
417
+ | `@view-transition (types: "t") { … }` | `atrule` → `name: "view-transition"`, `options: listener_option[]` (same node as `@on`'s), `block` |
418
+ | `@delay 2000 { … }` | `atrule` → `name: "delay"`, `duration: Expression`, `block` |
419
+ | `@warn "…";` / `@debug $x;` / `@error "…";` | `atrule` → `name: "warn" \| "debug" \| "error"`, `value: Expression` |
420
+ | comment | `comment` → `text` |
421
+
422
+ Expression nodes: `string` (`parts`, `value`), `number` (`value`, `unit`), `color`, `boolean`, `null`, `identifier`, `variable`, `parent_reference` (`&`), `interpolation`, `url`, `function` (`callee`, `args`), `if` (`arms`), `member` (`object`, `property`, `variable`), `index`, `unary`, `binary`, `list` (`separator`, `brackets`, `parens`), `map`.
423
+
424
+ Selector parts: `type_selector`, `class_selector`, `id_selector`, `attribute_selector`, `pseudo_class_selector`, `pseudo_element_selector`, `parent_selector`, `combinator`.
425
+
426
+ ### Grammar tables
427
+
428
+ The data-driven parts of the grammar are exported so tooling never re-types them:
429
+
430
+ | Export | Holds |
431
+ | --- | --- |
432
+ | `BINARY_BP` | binary operator → binding power (the precedence table above) |
433
+ | `NOT_BP` | binding power of unary `not` |
434
+ | `ATTR_OPERATORS` | attribute selector operators |
435
+ | `SELECTOR_PSEUDOS` | pseudo-classes whose argument is a selector list |
436
+ | `QUARK_AT_RULES` | Quark's at-rule names; every other name is rejected |
437
+
438
+ ## Design notes
439
+
440
+ - Single-pass, charcode-based tokenizer (no regexes on the hot path). Whitespace is a flag on tokens, not a token; comments are collected separately so expression parsing never has to skip them.
441
+ - Recursive-descent statement parser + Pratt expression parser. Node naming loosely follows `salesforce-ux/scss-parser` (`stylesheet`, `rule`, `declaration`, `atrule`, `function`, `variable`, ...), but values are structured expression nodes rather than token lists.
442
+ - Every node carries `start` / `end` source offsets. `QuarkParseError` reports line / column.
443
+ - `QUARK_AT_RULES` types the parser's dispatch map, so an at-rule is one table entry plus one method, and the same table rejects every other name.