@excom/quark 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 (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.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 +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. package/tsconfig.json +5 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Quark expression evaluator.
3
+ *
4
+ * Evaluates `@excom/quark-parser` expression ASTs against a scope
5
+ * (built by `resolveVariable`). Replaces the old `new Function` + `with`
6
+ * path: no arbitrary JS, CSP-safe, methods limited to an allowlist of
7
+ * pure / read-only calls.
8
+ */
9
+ import { METHOD_ALLOWLIST } from "./language-tables";
10
+ import type {
11
+ Argument,
12
+ Expression,
13
+ Interpolation,
14
+ } from "@excom/quark-parser";
15
+ import { parseExpression } from "@excom/quark-parser";
16
+
17
+ export interface EvalContext {
18
+ /** Lazy name resolver (see `createScope` in variables.ts). */
19
+ scope: { lookup(name: string): unknown };
20
+ element: Element;
21
+ }
22
+
23
+ export class QuarkEvalError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = "QuarkEvalError";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Parse an expression source to an AST, memoized. Declaration values
32
+ * repeat a lot across elements / runs, so each unique source parses
33
+ * once.
34
+ */
35
+ const EXPRESSION_CACHE = new Map<string, Expression>();
36
+ export const getExpressionAst = (src: string): Expression => {
37
+ let ast = EXPRESSION_CACHE.get(src);
38
+ if (!ast) {
39
+ ast = parseExpression(src);
40
+ EXPRESSION_CACHE.set(src, ast);
41
+ }
42
+ return ast;
43
+ };
44
+
45
+ /**
46
+ * Collect referenced `$variable` names (prefixed with `$`, same shape
47
+ * as the old `extractVarNames` used by `resolveVariable`).
48
+ */
49
+ export const collectVariableNames = (expr: Expression): string[] => {
50
+ const names = new Set<string>();
51
+ const visit = (node: any): void => {
52
+ if (!node || typeof node !== "object") return;
53
+ if (Array.isArray(node)) {
54
+ node.forEach(visit);
55
+ return;
56
+ }
57
+ if (node.type === "variable") names.add("$" + node.name);
58
+ for (const key in node) {
59
+ if (key === "start" || key === "end" || key === "type") continue;
60
+ const value = node[key];
61
+ if (value && typeof value === "object") visit(value);
62
+ }
63
+ };
64
+ visit(expr);
65
+ return [...names];
66
+ };
67
+
68
+ /**
69
+ * Collect literal string args of `<callee>("name")` calls (`attr("x")`,
70
+ * `prop("x")`). Only static names can be observed (attr filters and
71
+ * property subscriptions come from them). Non-literal args set
72
+ * `hasNonLiteral` so callers can warn.
73
+ */
74
+ export const collectLiteralCalls = (
75
+ expr: Expression | null,
76
+ callee: string
77
+ ): { names: string[]; hasNonLiteral: boolean } => {
78
+ const names = new Set<string>();
79
+ let hasNonLiteral = false;
80
+ const visit = (node: any): void => {
81
+ if (!node || typeof node !== "object") return;
82
+ if (Array.isArray(node)) {
83
+ node.forEach(visit);
84
+ return;
85
+ }
86
+ if (
87
+ node.type === "function" &&
88
+ node.callee?.type === "identifier" &&
89
+ node.callee.name === callee
90
+ ) {
91
+ const arg = node.args?.[0]?.value;
92
+ if (arg?.type === "string" && arg.value != null) {
93
+ names.add(arg.value);
94
+ } else {
95
+ hasNonLiteral = true;
96
+ }
97
+ }
98
+ for (const key in node) {
99
+ if (key === "start" || key === "end" || key === "type") continue;
100
+ const value = node[key];
101
+ if (value && typeof value === "object") visit(value);
102
+ }
103
+ };
104
+ if (expr) visit(expr);
105
+ return { names: [...names], hasNonLiteral };
106
+ };
107
+
108
+ export const collectAttrCalls = (expr: Expression | null) =>
109
+ collectLiteralCalls(expr, "attr");
110
+ export const collectPropCalls = (expr: Expression | null) =>
111
+ collectLiteralCalls(expr, "prop");
112
+
113
+ const unescapeString = (raw: string): string =>
114
+ raw.includes("\\")
115
+ ? raw.replace(/\\(.)/g, (_, c: string) =>
116
+ c === "n" ? "\n" : c === "t" ? "\t" : c === "r" ? "\r" : c
117
+ )
118
+ : raw;
119
+
120
+ const interpolationToString = (value: any): string =>
121
+ value == null ? "" : String(value);
122
+
123
+ const truthy = (value: any): boolean => !!value;
124
+
125
+ export const evaluateExpression = (
126
+ expr: Expression,
127
+ context: EvalContext
128
+ ): any => {
129
+ return evaluate(expr, context);
130
+ };
131
+
132
+ const evaluate = (node: Expression, ctx: EvalContext): any => {
133
+ switch (node.type) {
134
+ case "string": {
135
+ if (node.value !== null) return unescapeString(node.value);
136
+ return node.parts
137
+ .map((part) =>
138
+ typeof part === "string"
139
+ ? unescapeString(part)
140
+ : interpolationToString(evaluate(part.expression, ctx))
141
+ )
142
+ .join("");
143
+ }
144
+ case "number":
145
+ return node.unit ? `${node.value}${node.unit}` : node.value;
146
+ case "color":
147
+ return node.value;
148
+ case "boolean":
149
+ return node.value;
150
+ case "null":
151
+ return null;
152
+ case "identifier":
153
+ // unknown bare identifiers throw inside `lookup`
154
+ return ctx.scope.lookup(node.name);
155
+ case "variable":
156
+ // `$bindings` never throw: unbound resolves to `undefined` (wipe)
157
+ return ctx.scope.lookup("$" + node.name);
158
+ case "parent_reference":
159
+ // Legacy behavior: `&` in an expression resolves to the element's tag
160
+ // name (used as a selector string).
161
+ return ctx.element.localName;
162
+ case "interpolation":
163
+ return evaluate(node.expression, ctx);
164
+ case "url":
165
+ return node.parts
166
+ .map((part) =>
167
+ typeof part === "string"
168
+ ? part
169
+ : interpolationToString(evaluate(part.expression, ctx))
170
+ )
171
+ .join("");
172
+ case "member": {
173
+ // Null propagation is built in: accessing a field of null/undefined
174
+ // resolves to undefined instead of erroring.
175
+ const object = evaluate(node.object, ctx);
176
+ if (object == null) return undefined;
177
+ return object[node.variable ? "$" + node.property : node.property];
178
+ }
179
+ case "index": {
180
+ const object = evaluate(node.object, ctx);
181
+ if (object == null) return undefined;
182
+ return object[evaluate(node.index, ctx)];
183
+ }
184
+ case "function":
185
+ return evaluateCall(node.callee, node.args, ctx);
186
+ case "if": {
187
+ // CSS-style `if($cond: a; else: b)`: first truthy arm wins.
188
+ for (const arm of node.arms) {
189
+ if (arm.condition === null || truthy(evaluate(arm.condition, ctx))) {
190
+ return evaluate(arm.value, ctx);
191
+ }
192
+ }
193
+ // No arm matched and no `else`: undefined = Quark no-op.
194
+ return undefined;
195
+ }
196
+ case "unary": {
197
+ const value = evaluate(node.argument, ctx);
198
+ if (node.operator === "-") return -value;
199
+ if (node.operator === "+") return +value;
200
+ return !truthy(value);
201
+ }
202
+ case "binary": {
203
+ const left = evaluate(node.left, ctx);
204
+ switch (node.operator) {
205
+ // Short-circuiting, JS semantics (matches the legacy eval behavior).
206
+ case "and":
207
+ return truthy(left) ? evaluate(node.right, ctx) : left;
208
+ case "or":
209
+ return truthy(left) ? left : evaluate(node.right, ctx);
210
+ }
211
+ const right = evaluate(node.right, ctx);
212
+ switch (node.operator) {
213
+ case "+":
214
+ return left + right;
215
+ case "-":
216
+ return left - right;
217
+ case "*":
218
+ return left * right;
219
+ case "/":
220
+ return left / right;
221
+ case "%":
222
+ return left % right;
223
+ // Loose equality preserves legacy `==` behavior in existing sheets.
224
+
225
+ case "==":
226
+ return left == right;
227
+
228
+ case "!=":
229
+ return left != right;
230
+ case "<":
231
+ return left < right;
232
+ case "<=":
233
+ return left <= right;
234
+ case ">":
235
+ return left > right;
236
+ case ">=":
237
+ return left >= right;
238
+ }
239
+ throw new QuarkEvalError(`Unknown operator "${node.operator}"`);
240
+ }
241
+ case "list":
242
+ return node.items.map((item) => evaluate(item, ctx));
243
+ case "map": {
244
+ const result: Record<string, any> = {};
245
+ for (const entry of node.entries) {
246
+ result[mapKeyToString(entry.key, ctx)] = evaluate(entry.value, ctx);
247
+ }
248
+ return result;
249
+ }
250
+ default:
251
+ throw new QuarkEvalError(
252
+ `Unsupported expression node "${(node as Expression).type}"`
253
+ );
254
+ }
255
+ };
256
+
257
+ /** Map keys: bare identifiers are literal strings (`(a: 1)` -> key "a"). */
258
+ const mapKeyToString = (key: Expression, ctx: EvalContext): string => {
259
+ if (key.type === "identifier") return key.name;
260
+ return interpolationToString(evaluate(key, ctx));
261
+ };
262
+
263
+ const evaluateCall = (
264
+ callee: Expression,
265
+ args: Argument[],
266
+ ctx: EvalContext
267
+ ): any => {
268
+ const argValues: any[] = [];
269
+ for (const arg of args) {
270
+ const value = evaluate(arg.value, ctx);
271
+ if (arg.spread) {
272
+ if (Array.isArray(value)) argValues.push(...value);
273
+ else argValues.push(value);
274
+ } else {
275
+ argValues.push(value);
276
+ }
277
+ }
278
+
279
+ // Method call on a value (dot accessor).
280
+ if (callee.type === "member") {
281
+ const object = evaluate(callee.object, ctx);
282
+ const method = callee.property;
283
+ // Null propagation: calling a method on null/undefined is a no-op.
284
+ if (object == null) return undefined;
285
+ const own =
286
+ typeof object === "object" &&
287
+ Object.prototype.hasOwnProperty.call(object, method);
288
+ if (own && typeof object[method] === "function") {
289
+ // Own-property functions are user-provided (e.g. @use module
290
+ // namespaces like `api.version()`), not built-in prototype methods.
291
+ return object[method](...argValues);
292
+ }
293
+ // Prototype methods (string/array/number/DOM) are allowlisted only
294
+ // (`ALLOWED_METHOD_NAMES` in language-tables.ts).
295
+ if (!METHOD_ALLOWLIST.has(method)) {
296
+ throw new QuarkEvalError(
297
+ `Method "${method}" is not allowed in Quark expressions. ` +
298
+ `Register a module function and call it instead.`
299
+ );
300
+ }
301
+ const fn = object[method];
302
+ if (typeof fn !== "function") {
303
+ throw new QuarkEvalError(`"${method}" is not a function`);
304
+ }
305
+ return fn.apply(object, argValues);
306
+ }
307
+
308
+ const fn = evaluate(callee, ctx);
309
+ if (typeof fn !== "function") {
310
+ throw new QuarkEvalError(
311
+ `"${callee.type === "identifier" ? callee.name : "<expression>"}" is not a function`
312
+ );
313
+ }
314
+ return fn(...argValues);
315
+ };
316
+
317
+ export type { Expression, Interpolation };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Form-control sync. Native `<input>`, `<option>`, and `<textarea>`
3
+ * markup is only the *default* (`value` / `checked` / `selected`, or
4
+ * textarea text). After the user edits (spec "dirty" flags), the live
5
+ * `.value` / `.checked` / `.selected` stop following those attrs, so a
6
+ * rule write would silently not show.
7
+ *
8
+ * The attribute stays the serializable source of truth. After a rule
9
+ * write, we push the same state onto the live property. Compare against
10
+ * the *live* value, not the attr, so a re-run that resolves to the
11
+ * current attr still restores a control the user has edited away.
12
+ *
13
+ * Custom elements are left alone (they own reflection). `<select>` has
14
+ * no `value` attr; write `selected` on its options.
15
+ */
16
+
17
+ /** Attribute keys synced to a live property, per native tag. */
18
+ export const FORM_CONTROL_ATTRIBUTES: Readonly<
19
+ Record<string, readonly string[]>
20
+ > = {
21
+ input: ["value", "checked"],
22
+ option: ["selected"],
23
+ };
24
+
25
+ /** Tags whose text is a default value mirrored to `.value`. */
26
+ export const TEXT_CONTROL_TAGS: readonly string[] = ["textarea"];
27
+
28
+ /** `true` when Quark syncs `name` on `element` (documented table above). */
29
+ export const isFormControlAttribute = (element: Element, name: string) =>
30
+ !!FORM_CONTROL_ATTRIBUTES[element.localName]?.includes(name);
31
+
32
+ /**
33
+ * After an attr write (`value` = the string, `null` = removed), push the
34
+ * same state onto the live property when it differs.
35
+ */
36
+ export const syncFormControlAttribute = (
37
+ element: Element,
38
+ name: string,
39
+ value: string | null
40
+ ) => {
41
+ if (!isFormControlAttribute(element, name)) return;
42
+ if (name === "value") {
43
+ const input = element as HTMLInputElement;
44
+ // a file input's value is read-only (assigning anything but "" throws)
45
+ if (input.type === "file") return;
46
+ const next = value ?? "";
47
+ if (input.value !== next) input.value = next;
48
+ return;
49
+ }
50
+ // `checked` / `selected`: presence attributes
51
+ const present = value !== null;
52
+ const control = element as HTMLInputElement & HTMLOptionElement;
53
+ if (control[name] !== present) control[name] = present;
54
+ };
55
+
56
+ /**
57
+ * After a text paint / wipe into a text control, mirror the new default
58
+ * onto `.value` when the live value has drifted.
59
+ */
60
+ export const syncTextControl = (target: Node, text: string) => {
61
+ if (
62
+ target.nodeType !== Node.ELEMENT_NODE ||
63
+ !TEXT_CONTROL_TAGS.includes((target as Element).localName)
64
+ ) {
65
+ return;
66
+ }
67
+ const control = target as HTMLTextAreaElement;
68
+ if (control.value !== text) control.value = text;
69
+ };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Runtime language tables, the prose-free half of `language.ts`. The
3
+ * engine imports only this module (evaluator method allowlist, selector
4
+ * classifier's pseudo-class support), so the docs in `language.ts` stay
5
+ * out of production. `language.ts` builds its documented rows on these
6
+ * tables; `language-docs.test.ts` keeps the two in step.
7
+ */
8
+
9
+ /**
10
+ * Prototype methods an expression may call via the dot accessor
11
+ * (`item.name.toLowerCase()`). Anything else throws. Own-property
12
+ * functions (`@use` namespaces, provided objects) are callable without
13
+ * being listed. Documented in `language.ts` `ALLOWED_METHODS`.
14
+ */
15
+ export const ALLOWED_METHOD_NAMES: readonly string[] = [
16
+ // string
17
+ "toLowerCase",
18
+ "toUpperCase",
19
+ "trim",
20
+ "split",
21
+ "charAt",
22
+ "startsWith",
23
+ "endsWith",
24
+ "padStart",
25
+ "padEnd",
26
+ "replace",
27
+ "replaceAll",
28
+ "toString",
29
+ // string + array
30
+ "includes",
31
+ "slice",
32
+ "indexOf",
33
+ "lastIndexOf",
34
+ "concat",
35
+ "at",
36
+ // array
37
+ "join",
38
+ "flat",
39
+ // number / date
40
+ "toFixed",
41
+ "toLocaleString",
42
+ "toLocaleDateString",
43
+ "toLocaleTimeString",
44
+ "toISOString",
45
+ // element
46
+ "getAttribute",
47
+ "hasAttribute",
48
+ "matches",
49
+ "closest",
50
+ ];
51
+
52
+ export const METHOD_ALLOWLIST: ReadonlySet<string> = new Set(
53
+ ALLOWED_METHOD_NAMES
54
+ );
55
+
56
+ export type PseudoClassKind =
57
+ | "logical"
58
+ | "relational"
59
+ | "structural"
60
+ | "attribute"
61
+ | "static"
62
+ | "unobserved";
63
+
64
+ export interface PseudoClassSupport {
65
+ kind: PseudoClassKind;
66
+ /** `attribute` kind: the attribute names the pseudo-class reflects. */
67
+ attributes?: readonly string[];
68
+ }
69
+
70
+ const attribute = (...attributes: string[]): PseudoClassSupport => ({
71
+ kind: "attribute",
72
+ attributes,
73
+ });
74
+
75
+ /**
76
+ * Pseudo-class name (no colon) → how Quark observes it.
77
+ * `selector-utils.ts` classifies from this table; anything missing is
78
+ * `unobserved` (first run only, warned at build). Every entry has a row
79
+ * in `language.ts` `PSEUDO_CLASSES`.
80
+ */
81
+ export const PSEUDO_CLASS_SUPPORT: Readonly<
82
+ Record<string, PseudoClassSupport>
83
+ > = {
84
+ // logical
85
+ is: { kind: "logical" },
86
+ where: { kind: "logical" },
87
+ not: { kind: "logical" },
88
+ // relational
89
+ has: { kind: "relational" },
90
+ // structural
91
+ "first-child": { kind: "structural" },
92
+ "last-child": { kind: "structural" },
93
+ "only-child": { kind: "structural" },
94
+ "nth-child": { kind: "structural" },
95
+ "nth-last-child": { kind: "structural" },
96
+ "first-of-type": { kind: "structural" },
97
+ "last-of-type": { kind: "structural" },
98
+ "only-of-type": { kind: "structural" },
99
+ "nth-of-type": { kind: "structural" },
100
+ "nth-last-of-type": { kind: "structural" },
101
+ empty: { kind: "structural" },
102
+ // attribute-backed
103
+ disabled: attribute("disabled"),
104
+ enabled: attribute("disabled"),
105
+ required: attribute("required"),
106
+ optional: attribute("required"),
107
+ "read-only": attribute("readonly", "disabled", "contenteditable"),
108
+ "read-write": attribute("readonly", "disabled", "contenteditable"),
109
+ "any-link": attribute("href"),
110
+ lang: attribute("lang"),
111
+ open: attribute("open"),
112
+ // static
113
+ scope: { kind: "static" },
114
+ root: { kind: "static" },
115
+ // unobserved (interaction / browser state with no attribute behind it)
116
+ hover: { kind: "unobserved" },
117
+ focus: { kind: "unobserved" },
118
+ "focus-within": { kind: "unobserved" },
119
+ "focus-visible": { kind: "unobserved" },
120
+ active: { kind: "unobserved" },
121
+ visited: { kind: "unobserved" },
122
+ link: { kind: "unobserved" },
123
+ target: { kind: "unobserved" },
124
+ checked: { kind: "unobserved" },
125
+ indeterminate: { kind: "unobserved" },
126
+ default: { kind: "unobserved" },
127
+ valid: { kind: "unobserved" },
128
+ invalid: { kind: "unobserved" },
129
+ "in-range": { kind: "unobserved" },
130
+ "out-of-range": { kind: "unobserved" },
131
+ "placeholder-shown": { kind: "unobserved" },
132
+ "popover-open": { kind: "unobserved" },
133
+ modal: { kind: "unobserved" },
134
+ fullscreen: { kind: "unobserved" },
135
+ defined: { kind: "unobserved" },
136
+ dir: { kind: "unobserved" },
137
+ };