@jxsuite/runtime 1.0.0 → 1.2.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/dist/runtime.js +429 -50
- package/dist/runtime.js.map +9 -8
- package/package.json +4 -3
- package/src/expression.ts +523 -28
- package/src/runtime.ts +189 -11
- package/src/statements.ts +212 -0
package/src/runtime.ts
CHANGED
|
@@ -21,19 +21,23 @@ import {
|
|
|
21
21
|
onEffectCleanup,
|
|
22
22
|
reactive,
|
|
23
23
|
ref,
|
|
24
|
+
toRaw,
|
|
24
25
|
} from "@vue/reactivity";
|
|
25
26
|
import { evaluateExpression, isMutating } from "./expression.ts";
|
|
26
27
|
import type { DynamicClass, JxEventHandler, JxPath, JxRenderOptions, JxScope } from "./types.ts";
|
|
27
28
|
import {
|
|
29
|
+
hasStructuredBody,
|
|
28
30
|
isExpressionDef,
|
|
29
31
|
isFunctionDef,
|
|
30
32
|
isJsonObject,
|
|
31
33
|
isMappedArray,
|
|
34
|
+
isNamedFormulaDef,
|
|
32
35
|
isPrototypeDef,
|
|
33
36
|
isRef as isRefValue,
|
|
34
37
|
isServerFnDef,
|
|
35
38
|
paramNames,
|
|
36
39
|
} from "@jxsuite/schema/guards";
|
|
40
|
+
import { runStatements } from "./statements.ts";
|
|
37
41
|
import type {
|
|
38
42
|
JxAttributeValue,
|
|
39
43
|
JxClassDef,
|
|
@@ -259,6 +263,55 @@ function canvasAttrName(el: HTMLElement, key: string): string {
|
|
|
259
263
|
return key;
|
|
260
264
|
}
|
|
261
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Studio-canvas prop-binding markers. When on, `bindProperty` stamps `data-jx-bound-prop="<key>"`
|
|
268
|
+
* on elements whose `textContent` is an invertible single-key state binding — the pure template
|
|
269
|
+
* `"${state.key}"` or `{ "$ref": "#/state/key" }`. The studio canvas uses the marker to offer
|
|
270
|
+
* inline editing of component-instance prop values; the rendered text must BE the prop value
|
|
271
|
+
* verbatim, so mixed templates and deep paths are never stamped. Off (the default) leaves
|
|
272
|
+
* production rendering untouched; the studio sets it for design/edit (not preview — see
|
|
273
|
+
* iframe-render).
|
|
274
|
+
*/
|
|
275
|
+
let _stampPropBindings = false;
|
|
276
|
+
export function setStampPropBindings(on: boolean) {
|
|
277
|
+
_stampPropBindings = on;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The single invertible template form: the whole string is exactly one `${state.key}` access. */
|
|
281
|
+
const PURE_STATE_TEMPLATE = /^\s*\$\{\s*state\.(\w+)\s*\}\s*$/;
|
|
282
|
+
|
|
283
|
+
/** The state key of an invertible `textContent` binding, or null (mixed template, deep path). */
|
|
284
|
+
function boundPropKey(key: string, val: unknown, state: JxScope): string | null {
|
|
285
|
+
if (!_stampPropBindings || key !== "textContent") {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
let sub: string | null = null;
|
|
289
|
+
if (isRefObj(val)) {
|
|
290
|
+
if (!val.$ref.startsWith("#/state/")) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const rest = val.$ref.slice("#/state/".length);
|
|
294
|
+
sub = rest && !rest.includes("/") ? rest : null;
|
|
295
|
+
} else if (typeof val === "string") {
|
|
296
|
+
sub = PURE_STATE_TEMPLATE.exec(val)?.[1] ?? null;
|
|
297
|
+
}
|
|
298
|
+
if (!sub) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
// Only plain writable data entries are per-instance props. Computed/signal entries live as refs
|
|
302
|
+
// In the raw scope (auto-unwrapped by the reactive proxy), and function/object entries (handlers,
|
|
303
|
+
// $prototype instances) are not prop-overridable either — an instance $props write against any of
|
|
304
|
+
// Them would clobber the definition's behavior, so their bindings are never marked editable.
|
|
305
|
+
const stored = (toRaw(state) as Record<string, unknown>)[sub];
|
|
306
|
+
if (isRef(stored) || typeof stored === "function") {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
if (typeof stored === "object" && stored !== null) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return sub;
|
|
313
|
+
}
|
|
314
|
+
|
|
262
315
|
/**
|
|
263
316
|
* Build the reactive scope (state) from the document using the five-shape detection algorithm.
|
|
264
317
|
*
|
|
@@ -356,7 +409,24 @@ export async function buildScope(
|
|
|
356
409
|
for (const [key, def] of Object.entries(defs)) {
|
|
357
410
|
if (isExpressionDef(def)) {
|
|
358
411
|
const node = def.$expression;
|
|
359
|
-
if (
|
|
412
|
+
if (isNamedFormulaDef(def)) {
|
|
413
|
+
// A named formula: callable with positional args mapped onto its declared parameters.
|
|
414
|
+
const params = def.parameters as (string | { name?: string; default?: unknown })[];
|
|
415
|
+
state[key] = (...argValues: unknown[]) => {
|
|
416
|
+
const args: Record<string, unknown> = {};
|
|
417
|
+
for (const [i, p] of params.entries()) {
|
|
418
|
+
const name = typeof p === "string" ? p : (p?.name ?? "");
|
|
419
|
+
if (!name) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
args[name] =
|
|
423
|
+
argValues[i] === undefined && typeof p === "object" && p !== null && "default" in p
|
|
424
|
+
? p.default
|
|
425
|
+
: argValues[i];
|
|
426
|
+
}
|
|
427
|
+
return evaluateExpression(node, state, null, { args });
|
|
428
|
+
};
|
|
429
|
+
} else if (isMutating(node.operator)) {
|
|
360
430
|
const handler: JxEventHandler = (s, event) => evaluateExpression(node, s, event);
|
|
361
431
|
state[key] = handler;
|
|
362
432
|
} else {
|
|
@@ -367,7 +437,33 @@ export async function buildScope(
|
|
|
367
437
|
|
|
368
438
|
// Third pass: $prototype: "Function" entries
|
|
369
439
|
for (const [key, def] of Object.entries(defs)) {
|
|
370
|
-
if (
|
|
440
|
+
if (hasStructuredBody(def)) {
|
|
441
|
+
// Structured body (spec §20): with parameters → a callable mapping positional args onto
|
|
442
|
+
// $args names (invoked via the call operator); without → an event handler.
|
|
443
|
+
const { body } = def;
|
|
444
|
+
if (Array.isArray(def.parameters) && def.parameters.length > 0) {
|
|
445
|
+
const params = def.parameters as (string | { name?: string; default?: unknown })[];
|
|
446
|
+
state[key] = (...argValues: unknown[]) => {
|
|
447
|
+
const args: Record<string, unknown> = {};
|
|
448
|
+
for (const [i, p] of params.entries()) {
|
|
449
|
+
const name = typeof p === "string" ? p : (p?.name ?? "");
|
|
450
|
+
if (!name) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
args[name] =
|
|
454
|
+
argValues[i] === undefined && typeof p === "object" && p !== null && "default" in p
|
|
455
|
+
? p.default
|
|
456
|
+
: argValues[i];
|
|
457
|
+
}
|
|
458
|
+
return runStatements(body, state, null, { args });
|
|
459
|
+
};
|
|
460
|
+
} else {
|
|
461
|
+
const handler: JxEventHandler = (s, event) => {
|
|
462
|
+
void runStatements(body, s, event ?? null);
|
|
463
|
+
};
|
|
464
|
+
state[key] = handler;
|
|
465
|
+
}
|
|
466
|
+
} else if (isFunctionDef(def)) {
|
|
371
467
|
state[key] = await resolveFunction(def, state, key, base);
|
|
372
468
|
}
|
|
373
469
|
}
|
|
@@ -476,7 +572,7 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
476
572
|
|
|
477
573
|
let fn: ((...args: unknown[]) => unknown) | undefined;
|
|
478
574
|
|
|
479
|
-
if (def.body) {
|
|
575
|
+
if (typeof def.body === "string") {
|
|
480
576
|
const params = resolveParamNames(def);
|
|
481
577
|
fn = new Function(...params, def.body) as (...args: unknown[]) => unknown;
|
|
482
578
|
Object.defineProperty(fn, "name", {
|
|
@@ -518,7 +614,7 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
518
614
|
const hasParams = (def.parameters ?? def.arguments ?? []).length > 0;
|
|
519
615
|
let isComputed = false;
|
|
520
616
|
if (!hasParams) {
|
|
521
|
-
if (def.body) {
|
|
617
|
+
if (typeof def.body === "string") {
|
|
522
618
|
isComputed = /\breturn\b/.test(def.body);
|
|
523
619
|
} else if (fn) {
|
|
524
620
|
isComputed = fn.length <= 1 && /\breturn\b/.test(fn.toString());
|
|
@@ -715,8 +811,17 @@ function applyProperties(el: HTMLElement, def: JxElement, state: JxScope) {
|
|
|
715
811
|
}
|
|
716
812
|
continue;
|
|
717
813
|
}
|
|
814
|
+
// Event handler: inline $prototype: "Function" with a structured body (spec §20)
|
|
815
|
+
if (hasStructuredBody(val)) {
|
|
816
|
+
const { body } = val;
|
|
817
|
+
const scope = state;
|
|
818
|
+
el.addEventListener(key.slice(2), (e) => {
|
|
819
|
+
void runStatements(body, scope, e);
|
|
820
|
+
});
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
718
823
|
// Event handler: inline $prototype: "Function"
|
|
719
|
-
if (isFunctionDef(val) && val.body) {
|
|
824
|
+
if (isFunctionDef(val) && typeof val.body === "string") {
|
|
720
825
|
const params = resolveParamNames(val);
|
|
721
826
|
const fn = new Function(...params, val.body) as (s: JxScope, e: Event) => unknown;
|
|
722
827
|
const scope = state;
|
|
@@ -744,6 +849,10 @@ function applyProperties(el: HTMLElement, def: JxElement, state: JxScope) {
|
|
|
744
849
|
*/
|
|
745
850
|
function bindProperty(el: HTMLElement, key: string, val: unknown, state: JxScope) {
|
|
746
851
|
const target = el as unknown as Record<string, unknown>;
|
|
852
|
+
const boundProp = boundPropKey(key, val, state);
|
|
853
|
+
if (boundProp) {
|
|
854
|
+
el.dataset.jxBoundProp = boundProp;
|
|
855
|
+
}
|
|
747
856
|
if (isRefObj(val)) {
|
|
748
857
|
const refVal = val as { $ref: string };
|
|
749
858
|
if (key === "id") {
|
|
@@ -921,14 +1030,25 @@ export function applyStyle(
|
|
|
921
1030
|
if (key === "@--") {
|
|
922
1031
|
continue;
|
|
923
1032
|
} // Base canvas width, not a real media query
|
|
924
|
-
const
|
|
925
|
-
?
|
|
1033
|
+
const query = key.startsWith("@--")
|
|
1034
|
+
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
926
1035
|
: key.startsWith("@(")
|
|
927
|
-
?
|
|
928
|
-
:
|
|
1036
|
+
? key.slice(1)
|
|
1037
|
+
: null;
|
|
1038
|
+
const atRule = query === null ? key : `@media ${query}`;
|
|
929
1039
|
const scope = `[data-jx="${uid}"]`;
|
|
930
|
-
|
|
931
|
-
|
|
1040
|
+
const scheme = query === null ? null : pureSchemeOf(query);
|
|
1041
|
+
if (scheme) {
|
|
1042
|
+
// Dual-emit scheme rules: a media-guarded copy for auto plus a forced-attribute copy.
|
|
1043
|
+
// The forced root attribute wins over the OS preference.
|
|
1044
|
+
const { auto, forced } = schemeSelectors(scope, scheme);
|
|
1045
|
+
css += `${atRule} { ${auto} { ${toCSSText(rules)} } }\n`;
|
|
1046
|
+
emitMediaNested(atRule, auto, rules);
|
|
1047
|
+
emitNested(forced, rules);
|
|
1048
|
+
} else {
|
|
1049
|
+
css += `${atRule} { ${scope} { ${toCSSText(rules)} } }\n`;
|
|
1050
|
+
emitMediaNested(atRule, scope, rules);
|
|
1051
|
+
}
|
|
932
1052
|
}
|
|
933
1053
|
|
|
934
1054
|
const tag = document.createElement("style");
|
|
@@ -1974,6 +2094,64 @@ export function toCSSText(rules: Record<string, unknown> | object) {
|
|
|
1974
2094
|
.join("; ");
|
|
1975
2095
|
}
|
|
1976
2096
|
|
|
2097
|
+
// ─── Color Schemes ────────────────────────────────────────────────────────────
|
|
2098
|
+
|
|
2099
|
+
/**
|
|
2100
|
+
* Attribute on the root element (`<html>`) that forces a color scheme. Absent = auto (follow the OS
|
|
2101
|
+
* `prefers-color-scheme`). Values: "light" | "dark".
|
|
2102
|
+
*
|
|
2103
|
+
* @docs framework/concepts/color-schemes
|
|
2104
|
+
*/
|
|
2105
|
+
export const COLOR_SCHEME_ATTR = "data-color-scheme";
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* The localStorage key a site switcher persists the visitor's forced scheme under; read by the
|
|
2109
|
+
* compiler-injected pre-paint script. Values: "light" | "dark" (absent = auto).
|
|
2110
|
+
*
|
|
2111
|
+
* @docs framework/concepts/color-schemes
|
|
2112
|
+
*/
|
|
2113
|
+
export const COLOR_SCHEME_STORAGE_KEY = "jx-color-scheme";
|
|
2114
|
+
|
|
2115
|
+
/**
|
|
2116
|
+
* The scheme a _pure_ `prefers-color-scheme` media query targets. Compound queries (any other
|
|
2117
|
+
* conditions attached) return null — they are not eligible for forced-scheme dual emission.
|
|
2118
|
+
*
|
|
2119
|
+
* @param {string} query
|
|
2120
|
+
* @returns {"light" | "dark" | null}
|
|
2121
|
+
* @docs framework/concepts/color-schemes
|
|
2122
|
+
*/
|
|
2123
|
+
export function pureSchemeOf(query: string): "light" | "dark" | null {
|
|
2124
|
+
const m = /^\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)$/.exec(query.trim());
|
|
2125
|
+
return (m?.[1] as "light" | "dark") ?? null;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Selector pair for a scheme-conditional rule: `auto` applies inside the scheme's media query only
|
|
2130
|
+
* while no scheme is forced; `forced` applies unconditionally when the root attribute forces the
|
|
2131
|
+
* scheme. Guards are wrapped in `:where()` so specificity matches the unguarded selector and source
|
|
2132
|
+
* order decides the cascade.
|
|
2133
|
+
*
|
|
2134
|
+
* @param {string} selector
|
|
2135
|
+
* @param {"light" | "dark"} scheme
|
|
2136
|
+
* @returns {{ auto: string; forced: string }}
|
|
2137
|
+
* @docs framework/concepts/color-schemes
|
|
2138
|
+
*/
|
|
2139
|
+
export function schemeSelectors(
|
|
2140
|
+
selector: string,
|
|
2141
|
+
scheme: "light" | "dark",
|
|
2142
|
+
): { auto: string; forced: string } {
|
|
2143
|
+
if (selector === ":root" || selector === "html") {
|
|
2144
|
+
return {
|
|
2145
|
+
auto: `${selector}:where(:not([${COLOR_SCHEME_ATTR}]))`,
|
|
2146
|
+
forced: `${selector}:where([${COLOR_SCHEME_ATTR}="${scheme}"])`,
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
auto: `:where(:root:not([${COLOR_SCHEME_ATTR}])) ${selector}`,
|
|
2151
|
+
forced: `:where(:root[${COLOR_SCHEME_ATTR}="${scheme}"]) ${selector}`,
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1977
2155
|
// ─── Custom Element Registration ──────────────────────────────────────────────
|
|
1978
2156
|
|
|
1979
2157
|
let _rootMedia: Record<string, string> = {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Jx Statement Engine (spec §20) — structured function bodies.
|
|
4
|
+
*
|
|
5
|
+
* A Function entry's `body` may be a statement array instead of opaque JS source. This module is
|
|
6
|
+
* the statements counterpart to expression.ts, with the same dual shape: `runStatements`
|
|
7
|
+
* (interpreter) and `compileStatements` (JS emitter), shared by the runtime and every compiler
|
|
8
|
+
* target. Statement kinds reuse web-platform names only: bare expression nodes (§19), the JSON
|
|
9
|
+
* Schema `if`/`then`/`else` triple, `$switch`/`cases` in statement position, and WHATWG's
|
|
10
|
+
* `dispatchEvent` with `CustomEventInit` members.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
compileExpression,
|
|
15
|
+
compileOperandSource,
|
|
16
|
+
evaluateExpression,
|
|
17
|
+
evaluateOperand,
|
|
18
|
+
} from "./expression.ts";
|
|
19
|
+
|
|
20
|
+
import type { JxStatement } from "@jxsuite/schema/types";
|
|
21
|
+
import type { CompileOpts, ExpressionNode, IterCtx } from "./expression.ts";
|
|
22
|
+
import type { JxScope } from "./types.ts";
|
|
23
|
+
|
|
24
|
+
interface RunOpts {
|
|
25
|
+
/** Named-formula-style arguments bound for $args/<name> refs in the body. */
|
|
26
|
+
args?: Record<string, unknown>;
|
|
27
|
+
/** Dispatch target for dispatchEvent statements; defaults to event?.currentTarget. */
|
|
28
|
+
target?: EventTarget | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function statementKind(statement: JxStatement): "expression" | "if" | "switch" | "dispatch" {
|
|
32
|
+
if ("operator" in statement) {
|
|
33
|
+
return "expression";
|
|
34
|
+
}
|
|
35
|
+
if ("if" in statement) {
|
|
36
|
+
return "if";
|
|
37
|
+
}
|
|
38
|
+
if ("$switch" in statement) {
|
|
39
|
+
return "switch";
|
|
40
|
+
}
|
|
41
|
+
return "dispatch";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute a statement list sequentially against the reactive scope. A statement whose value is a
|
|
46
|
+
* thenable is awaited before the next runs (ECMA async/await semantics); purely synchronous bodies
|
|
47
|
+
* complete synchronously before the returned promise settles.
|
|
48
|
+
*/
|
|
49
|
+
export async function runStatements(
|
|
50
|
+
statements: JxStatement[],
|
|
51
|
+
state: JxScope,
|
|
52
|
+
event: Event | null,
|
|
53
|
+
opts: RunOpts = {},
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const iterCtx: IterCtx | undefined = opts.args ? { args: opts.args } : undefined;
|
|
56
|
+
for (const statement of statements) {
|
|
57
|
+
switch (statementKind(statement)) {
|
|
58
|
+
case "expression": {
|
|
59
|
+
const result = evaluateExpression(statement as ExpressionNode, state, event, iterCtx);
|
|
60
|
+
if (result && typeof (result as PromiseLike<unknown>).then === "function") {
|
|
61
|
+
await (result as PromiseLike<unknown>);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "if": {
|
|
66
|
+
const branch = statement as { if: unknown; then: JxStatement[]; else?: JxStatement[] };
|
|
67
|
+
const taken = evaluateOperand(branch.if, state, event, iterCtx) ? branch.then : branch.else;
|
|
68
|
+
if (Array.isArray(taken)) {
|
|
69
|
+
await runStatements(taken, state, event, opts);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "switch": {
|
|
74
|
+
const branch = statement as {
|
|
75
|
+
$switch: unknown;
|
|
76
|
+
cases: Record<string, JxStatement[]>;
|
|
77
|
+
default?: JxStatement[];
|
|
78
|
+
};
|
|
79
|
+
const key = String(evaluateOperand(branch.$switch, state, event, iterCtx));
|
|
80
|
+
const cases = branch.cases ?? {};
|
|
81
|
+
const taken = Object.hasOwn(cases, key) ? cases[key] : branch.default;
|
|
82
|
+
if (Array.isArray(taken)) {
|
|
83
|
+
await runStatements(taken, state, event, opts);
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "dispatch": {
|
|
88
|
+
const dispatch = statement as {
|
|
89
|
+
dispatchEvent: string;
|
|
90
|
+
detail?: unknown;
|
|
91
|
+
bubbles?: boolean;
|
|
92
|
+
composed?: boolean;
|
|
93
|
+
};
|
|
94
|
+
const target = opts.target ?? event?.currentTarget;
|
|
95
|
+
if (!target) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const init: CustomEventInit = {};
|
|
99
|
+
if ("detail" in dispatch) {
|
|
100
|
+
init.detail = evaluateOperand(dispatch.detail, state, event, iterCtx);
|
|
101
|
+
}
|
|
102
|
+
if (dispatch.bubbles !== undefined) {
|
|
103
|
+
init.bubbles = dispatch.bubbles;
|
|
104
|
+
}
|
|
105
|
+
if (dispatch.composed !== undefined) {
|
|
106
|
+
init.composed = dispatch.composed;
|
|
107
|
+
}
|
|
108
|
+
target.dispatchEvent(new CustomEvent(dispatch.dispatchEvent, init));
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
default: {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface StatementCompileOpts extends CompileOpts {
|
|
119
|
+
/** JS expression for the dispatchEvent target; defaults to `(event && event.currentTarget)`. */
|
|
120
|
+
dispatchTarget?: string;
|
|
121
|
+
/** Indentation prefix for emitted lines. */
|
|
122
|
+
indent?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compile a statement list to a JS statement block (no surrounding braces). Mirrors `runStatements`
|
|
127
|
+
* exactly; each statement kind lowers to its genuine ECMAScript form — expression statements,
|
|
128
|
+
* `if`/`else`, `switch` over the discriminant's string form, and `dispatchEvent(new
|
|
129
|
+
* CustomEvent(...))`.
|
|
130
|
+
*/
|
|
131
|
+
export function compileStatements(
|
|
132
|
+
statements: JxStatement[],
|
|
133
|
+
opts: StatementCompileOpts = {},
|
|
134
|
+
): string {
|
|
135
|
+
const indent = opts.indent ?? "";
|
|
136
|
+
const lines: string[] = [];
|
|
137
|
+
for (const statement of statements) {
|
|
138
|
+
switch (statementKind(statement)) {
|
|
139
|
+
case "expression": {
|
|
140
|
+
lines.push(`${indent}${compileExpression(statement as ExpressionNode, opts)};`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "if": {
|
|
144
|
+
const branch = statement as { if: unknown; then: JxStatement[]; else?: JxStatement[] };
|
|
145
|
+
const test = compileOperandSource(branch.if, opts);
|
|
146
|
+
const inner = { ...opts, indent: `${indent} ` };
|
|
147
|
+
lines.push(`${indent}if (${test}) {`, compileStatements(branch.then ?? [], inner));
|
|
148
|
+
if (branch.else) {
|
|
149
|
+
lines.push(`${indent}} else {`, compileStatements(branch.else, inner));
|
|
150
|
+
}
|
|
151
|
+
lines.push(`${indent}}`);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "switch": {
|
|
155
|
+
const branch = statement as {
|
|
156
|
+
$switch: unknown;
|
|
157
|
+
cases: Record<string, JxStatement[]>;
|
|
158
|
+
default?: JxStatement[];
|
|
159
|
+
};
|
|
160
|
+
const disc = compileOperandSource(branch.$switch, opts);
|
|
161
|
+
const inner = { ...opts, indent: `${indent} ` };
|
|
162
|
+
lines.push(`${indent}switch (String(${disc})) {`);
|
|
163
|
+
for (const [key, caseStatements] of Object.entries(branch.cases ?? {})) {
|
|
164
|
+
lines.push(
|
|
165
|
+
`${indent} case ${JSON.stringify(key)}: {`,
|
|
166
|
+
compileStatements(caseStatements, inner),
|
|
167
|
+
`${indent} break;`,
|
|
168
|
+
`${indent} }`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (branch.default) {
|
|
172
|
+
lines.push(
|
|
173
|
+
`${indent} default: {`,
|
|
174
|
+
compileStatements(branch.default, inner),
|
|
175
|
+
`${indent} }`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
lines.push(`${indent}}`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "dispatch": {
|
|
182
|
+
const dispatch = statement as {
|
|
183
|
+
dispatchEvent: string;
|
|
184
|
+
detail?: unknown;
|
|
185
|
+
bubbles?: boolean;
|
|
186
|
+
composed?: boolean;
|
|
187
|
+
};
|
|
188
|
+
const eventName = opts.eventParam ?? "event";
|
|
189
|
+
const target = opts.dispatchTarget ?? `(${eventName} && ${eventName}.currentTarget)`;
|
|
190
|
+
const init: string[] = [];
|
|
191
|
+
if ("detail" in dispatch) {
|
|
192
|
+
init.push(`detail: ${compileOperandSource(dispatch.detail, opts)}`);
|
|
193
|
+
}
|
|
194
|
+
if (dispatch.bubbles !== undefined) {
|
|
195
|
+
init.push(`bubbles: ${JSON.stringify(dispatch.bubbles)}`);
|
|
196
|
+
}
|
|
197
|
+
if (dispatch.composed !== undefined) {
|
|
198
|
+
init.push(`composed: ${JSON.stringify(dispatch.composed)}`);
|
|
199
|
+
}
|
|
200
|
+
const initSource = init.length > 0 ? `, { ${init.join(", ")} }` : "";
|
|
201
|
+
lines.push(
|
|
202
|
+
`${indent}${target}?.dispatchEvent(new CustomEvent(${JSON.stringify(dispatch.dispatchEvent)}${initSource}));`,
|
|
203
|
+
);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default: {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|