@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.
- package/.rush/temp/chunked-rush-logs/quark-parser.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark-parser.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/index.ts +12 -0
- package/package.json +39 -0
- package/rush-logs/quark-parser.apply-exports.cache.log +1 -0
- package/rush-logs/quark-parser.apply-exports.log +1 -0
- package/rush-logs/quark-parser.build_package-metas.cache.log +1 -0
- package/rush-logs/quark-parser.build_package-metas.log +1 -0
- package/src/error.ts +24 -0
- package/src/parser.ts +1482 -0
- package/src/tables.ts +77 -0
- package/src/tokenizer.ts +443 -0
- package/src/types.ts +497 -0
- package/support/docs/README.md +443 -0
- package/support/package-meta.json +33 -0
- package/support/tests/grammar-docs.test.ts +109 -0
- package/support/tests/parser-at-rules.test.ts +430 -0
- package/support/tests/parser-declarations.test.ts +152 -0
- package/support/tests/parser-edge-cases.test.ts +296 -0
- package/support/tests/parser-expressions.test.ts +413 -0
- package/support/tests/parser-real-world.test.ts +429 -0
- package/support/tests/parser-selectors.test.ts +169 -0
- package/support/tests/tokenizer.test.ts +268 -0
- 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.
|