@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.
- package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark.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 +6 -0
- package/index.ts +4 -0
- package/language.ts +7 -0
- package/package.json +45 -0
- package/rush-logs/quark.apply-exports.cache.log +1 -0
- package/rush-logs/quark.apply-exports.log +1 -0
- package/rush-logs/quark.build_package-metas.cache.log +1 -0
- package/rush-logs/quark.build_package-metas.log +1 -0
- package/src/actions.ts +445 -0
- package/src/ast.ts +187 -0
- package/src/bindings.ts +115 -0
- package/src/builtin-modules.ts +401 -0
- package/src/constants.ts +47 -0
- package/src/cycle-check.ts +121 -0
- package/src/devtools-hook.ts +303 -0
- package/src/element-api.ts +109 -0
- package/src/evaluator.ts +317 -0
- package/src/form-controls.ts +69 -0
- package/src/language-tables.ts +137 -0
- package/src/language.ts +1225 -0
- package/src/observer.ts +110 -0
- package/src/paint.ts +305 -0
- package/src/properties.ts +818 -0
- package/src/props.ts +94 -0
- package/src/quark-element.ts +7 -0
- package/src/quark-internal.ts +365 -0
- package/src/quark.ts +817 -0
- package/src/resolvers.ts +867 -0
- package/src/rule.ts +800 -0
- package/src/scope-id.ts +47 -0
- package/src/selector-utils.ts +430 -0
- package/src/settle.ts +117 -0
- package/src/types.ts +155 -0
- package/src/utils.ts +79 -0
- package/src/variables.ts +359 -0
- package/support/demos/css-variables.html +18 -0
- package/support/demos/dispatch.html +20 -0
- package/support/demos/events.html +9 -0
- package/support/demos/iterate.html +13 -0
- package/support/demos/js-api.html +17 -0
- package/support/demos/provider-list.html +16 -0
- package/support/demos/toggle-content.html +14 -0
- package/support/demos/unset.html +12 -0
- package/support/demos/view-transition.html +36 -0
- package/support/docs/ATTRIBUTES.md +37 -0
- package/support/docs/AT_RULES.md +45 -0
- package/support/docs/BUILTINS.md +57 -0
- package/support/docs/CONTENT.md +40 -0
- package/support/docs/CSS_VARIABLES.md +16 -0
- package/support/docs/DECLARATIONS.md +26 -0
- package/support/docs/DELAY.md +37 -0
- package/support/docs/DIAGNOSTICS.md +32 -0
- package/support/docs/DISPATCH.md +81 -0
- package/support/docs/ELEMENT_PROPERTIES.md +34 -0
- package/support/docs/EXPRESSIONS.md +31 -0
- package/support/docs/INTERNAL.md +8 -0
- package/support/docs/JS_API.md +53 -0
- package/support/docs/JS_WRITES.md +36 -0
- package/support/docs/LIMITATIONS.md +26 -0
- package/support/docs/LOOP_GUARD.md +30 -0
- package/support/docs/METHODS.md +43 -0
- package/support/docs/MODULES.md +111 -0
- package/support/docs/NO_REVERSION.md +18 -0
- package/support/docs/ON.md +87 -0
- package/support/docs/REACTIVITY.md +24 -0
- package/support/docs/README.md +143 -0
- package/support/docs/SELECTORS.md +87 -0
- package/support/docs/SHEETS.md +67 -0
- package/support/docs/SYNTAX.md +36 -0
- package/support/docs/USE.md +24 -0
- package/support/docs/VALUES.md +27 -0
- package/support/docs/VARIABLES.md +47 -0
- package/support/docs/VIEW_TRANSITION.md +64 -0
- package/support/docs-sections.json +65 -0
- package/support/package-meta.json +208 -0
- package/support/scripts/build-language-docs.mjs +196 -0
- package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
- package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
- package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
- package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
- package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
- package/support/tests/actions.test.ts +437 -0
- package/support/tests/builtin-modules.test.ts +337 -0
- package/support/tests/builtins.test.ts +189 -0
- package/support/tests/css-variables.view.test.ts +35 -0
- package/support/tests/delay.test.ts +244 -0
- package/support/tests/devtools-hook.test.ts +496 -0
- package/support/tests/diagnostics.test.ts +224 -0
- package/support/tests/dispatch.view.test.ts +43 -0
- package/support/tests/element-api.test.ts +202 -0
- package/support/tests/evaluator.test.ts +237 -0
- package/support/tests/events.view.test.ts +40 -0
- package/support/tests/form-controls.test.ts +452 -0
- package/support/tests/helpers.ts +354 -0
- package/support/tests/internals.test.ts +568 -0
- package/support/tests/iterate.view.test.ts +40 -0
- package/support/tests/js-api.view.test.ts +45 -0
- package/support/tests/language-docs.test.ts +147 -0
- package/support/tests/lifecycle.test.ts +440 -0
- package/support/tests/loop-guard.test.ts +323 -0
- package/support/tests/provider-list.view.test.ts +47 -0
- package/support/tests/quark-features.test.ts +2887 -0
- package/support/tests/quark-with-providers.test.ts +169 -0
- package/support/tests/quark.test.ts +359 -0
- package/support/tests/resolvers.test.ts +346 -0
- package/support/tests/selector-analysis.test.ts +281 -0
- package/support/tests/selectors.test.ts +670 -0
- package/support/tests/toggle-content.view.test.ts +40 -0
- package/support/tests/unset.view.test.ts +42 -0
- package/support/tests/view-helpers.ts +81 -0
- package/support/tests/view-transition.test.ts +1069 -0
- package/support/tests/view-transition.view.test.ts +76 -0
- package/tsconfig.json +5 -0
package/src/evaluator.ts
ADDED
|
@@ -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
|
+
};
|