@jxsuite/runtime 0.22.1 → 0.23.1
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 +277 -9
- package/dist/runtime.js.map +5 -4
- package/package.json +3 -2
- package/src/expression.js +469 -0
- package/src/runtime.js +49 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
],
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
|
-
".": "./src/runtime.js"
|
|
17
|
+
".": "./src/runtime.js",
|
|
18
|
+
"./expression": "./src/expression.js"
|
|
18
19
|
},
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"provenance": true
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jx Declarative Expression Engine (spec §19)
|
|
3
|
+
*
|
|
4
|
+
* Shared module used by both runtime (evaluateExpression) and compiler (compileExpression).
|
|
5
|
+
*
|
|
6
|
+
* @module expression
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @typedef {{ operator: string; target: any; value?: any; initial?: any }} ExpressionNode */
|
|
10
|
+
|
|
11
|
+
const MUTATING_OPS = new Set([
|
|
12
|
+
"=",
|
|
13
|
+
"+=",
|
|
14
|
+
"-=",
|
|
15
|
+
"*=",
|
|
16
|
+
"/=",
|
|
17
|
+
"push",
|
|
18
|
+
"pop",
|
|
19
|
+
"shift",
|
|
20
|
+
"unshift",
|
|
21
|
+
"splice",
|
|
22
|
+
]);
|
|
23
|
+
const UNARY_OPS = new Set(["!", "-"]);
|
|
24
|
+
const BINARY_OPS = new Set([
|
|
25
|
+
"+",
|
|
26
|
+
"-",
|
|
27
|
+
"*",
|
|
28
|
+
"/",
|
|
29
|
+
"%",
|
|
30
|
+
"===",
|
|
31
|
+
"!==",
|
|
32
|
+
"<",
|
|
33
|
+
"<=",
|
|
34
|
+
">",
|
|
35
|
+
">=",
|
|
36
|
+
"&&",
|
|
37
|
+
"||",
|
|
38
|
+
]);
|
|
39
|
+
const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/="]);
|
|
40
|
+
const ARRAY_METHOD_OPS = new Set(["push", "pop", "shift", "unshift", "splice"]);
|
|
41
|
+
const AGGREGATE_OPS = new Set(["reduce", "map", "filter"]);
|
|
42
|
+
|
|
43
|
+
export const BLESSED_OPERATORS = new Set([
|
|
44
|
+
...MUTATING_OPS,
|
|
45
|
+
...UNARY_OPS,
|
|
46
|
+
...BINARY_OPS,
|
|
47
|
+
...AGGREGATE_OPS,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} op
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isMutating(op) {
|
|
55
|
+
return MUTATING_OPS.has(op);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Runtime Evaluation ──────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an operand to its runtime value.
|
|
62
|
+
*
|
|
63
|
+
* @param {any} operand
|
|
64
|
+
* @param {Record<string, any>} state
|
|
65
|
+
* @param {Event | null} event
|
|
66
|
+
* @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
|
|
67
|
+
* @returns {any}
|
|
68
|
+
*/
|
|
69
|
+
function resolveOperand(operand, state, event, iterCtx) {
|
|
70
|
+
if (operand === null || operand === undefined) return operand;
|
|
71
|
+
|
|
72
|
+
// Nested expression node
|
|
73
|
+
if (typeof operand === "object" && !Array.isArray(operand) && "operator" in operand) {
|
|
74
|
+
return evaluateExpression(operand, state, event, iterCtx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// $ref pointer
|
|
78
|
+
if (typeof operand === "object" && !Array.isArray(operand) && "$ref" in operand) {
|
|
79
|
+
return resolveExprRef(operand.$ref, state, event, iterCtx);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Array of operands (e.g., splice args)
|
|
83
|
+
if (Array.isArray(operand)) {
|
|
84
|
+
return operand.map((o) => resolveOperand(o, state, event, iterCtx));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Literal
|
|
88
|
+
return operand;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a $ref string within expression context. Handles event#/, $reduce/acc, $map/, #/state/,
|
|
93
|
+
* and other schemes.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} ref
|
|
96
|
+
* @param {Record<string, any>} state
|
|
97
|
+
* @param {Event | null} event
|
|
98
|
+
* @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
|
|
99
|
+
* @returns {any}
|
|
100
|
+
*/
|
|
101
|
+
function resolveExprRef(ref, state, event, iterCtx) {
|
|
102
|
+
if (ref === "$reduce/acc") {
|
|
103
|
+
return iterCtx?.acc;
|
|
104
|
+
}
|
|
105
|
+
if (ref.startsWith("$reduce/")) {
|
|
106
|
+
return iterCtx?.acc;
|
|
107
|
+
}
|
|
108
|
+
if (ref.startsWith("event#/")) {
|
|
109
|
+
const path = ref.slice("event#/".length);
|
|
110
|
+
return getPath(event, path);
|
|
111
|
+
}
|
|
112
|
+
if (ref.startsWith("$map/")) {
|
|
113
|
+
const parts = ref.split("/");
|
|
114
|
+
const key = parts[1];
|
|
115
|
+
let base;
|
|
116
|
+
if (key === "item") base = iterCtx?.item ?? state.$map?.item ?? state["$map/item"];
|
|
117
|
+
else if (key === "index") base = iterCtx?.index ?? state.$map?.index ?? state["$map/index"];
|
|
118
|
+
else base = state.$map?.[key] ?? state["$map/" + key];
|
|
119
|
+
return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
|
|
120
|
+
}
|
|
121
|
+
if (ref.startsWith("#/state/")) {
|
|
122
|
+
const sub = ref.slice("#/state/".length);
|
|
123
|
+
const slash = sub.indexOf("/");
|
|
124
|
+
if (slash < 0) return state[sub];
|
|
125
|
+
return getPath(state[sub.slice(0, slash)], sub.slice(slash + 1));
|
|
126
|
+
}
|
|
127
|
+
if (ref.startsWith("parent#/")) return state[ref.slice("parent#/".length)];
|
|
128
|
+
if (ref.startsWith("window#/")) return getPath(globalThis.window, ref.slice("window#/".length));
|
|
129
|
+
if (ref.startsWith("document#/"))
|
|
130
|
+
return getPath(globalThis.document, ref.slice("document#/".length));
|
|
131
|
+
return state[ref] ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a $ref to a writable location — returns { obj, key } for assignment.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} ref
|
|
138
|
+
* @param {Record<string, any>} state
|
|
139
|
+
* @param {Event | null} event
|
|
140
|
+
* @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
|
|
141
|
+
* @returns {{ obj: any; key: string }}
|
|
142
|
+
*/
|
|
143
|
+
function resolveWritableRef(ref, state, event, iterCtx) {
|
|
144
|
+
if (ref.startsWith("$map/")) {
|
|
145
|
+
const parts = ref.split("/");
|
|
146
|
+
const key = parts[1];
|
|
147
|
+
let base;
|
|
148
|
+
if (key === "item") base = iterCtx?.item ?? state.$map?.item ?? state["$map/item"];
|
|
149
|
+
else if (key === "index") base = iterCtx?.index ?? state.$map?.index ?? state["$map/index"];
|
|
150
|
+
else base = state.$map?.[key] ?? state["$map/" + key];
|
|
151
|
+
if (parts.length > 2) {
|
|
152
|
+
const pathParts = parts.slice(2);
|
|
153
|
+
const lastKey = pathParts.pop();
|
|
154
|
+
const obj = pathParts.length > 0 ? getPath(base, pathParts.join("/")) : base;
|
|
155
|
+
return { obj, key: /** @type {string} */ (lastKey) };
|
|
156
|
+
}
|
|
157
|
+
return { obj: state.$map ?? state, key: key === "item" ? "$map/item" : "$map/index" };
|
|
158
|
+
}
|
|
159
|
+
if (ref.startsWith("#/state/")) {
|
|
160
|
+
const sub = ref.slice("#/state/".length);
|
|
161
|
+
const slash = sub.indexOf("/");
|
|
162
|
+
if (slash < 0) return { obj: state, key: sub };
|
|
163
|
+
const parts = sub.split("/");
|
|
164
|
+
const lastKey = parts.pop();
|
|
165
|
+
let obj = state;
|
|
166
|
+
for (const p of parts) obj = obj[p];
|
|
167
|
+
return { obj, key: /** @type {string} */ (lastKey) };
|
|
168
|
+
}
|
|
169
|
+
return { obj: state, key: ref };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Evaluate an expression node at runtime.
|
|
174
|
+
*
|
|
175
|
+
* @param {ExpressionNode} node
|
|
176
|
+
* @param {Record<string, any>} state
|
|
177
|
+
* @param {Event | null} event
|
|
178
|
+
* @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
|
|
179
|
+
* @returns {any}
|
|
180
|
+
*/
|
|
181
|
+
export function evaluateExpression(node, state, event, iterCtx) {
|
|
182
|
+
const { operator, target, value, initial } = node;
|
|
183
|
+
|
|
184
|
+
if (!BLESSED_OPERATORS.has(operator)) {
|
|
185
|
+
throw new Error(`$expression: unknown operator "${operator}"`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── Unary ───
|
|
189
|
+
if (UNARY_OPS.has(operator) && !("value" in node)) {
|
|
190
|
+
const operand = resolveOperand(target, state, event, iterCtx);
|
|
191
|
+
if (operator === "!") return !operand;
|
|
192
|
+
if (operator === "-") return -operand;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Binary (pure) ───
|
|
196
|
+
if (BINARY_OPS.has(operator)) {
|
|
197
|
+
const left = resolveOperand(target, state, event, iterCtx);
|
|
198
|
+
const right = resolveOperand(value, state, event, iterCtx);
|
|
199
|
+
switch (operator) {
|
|
200
|
+
case "+":
|
|
201
|
+
return left + right;
|
|
202
|
+
case "-":
|
|
203
|
+
return left - right;
|
|
204
|
+
case "*":
|
|
205
|
+
return left * right;
|
|
206
|
+
case "/":
|
|
207
|
+
return left / right;
|
|
208
|
+
case "%":
|
|
209
|
+
return left % right;
|
|
210
|
+
case "===":
|
|
211
|
+
return left === right;
|
|
212
|
+
case "!==":
|
|
213
|
+
return left !== right;
|
|
214
|
+
case "<":
|
|
215
|
+
return left < right;
|
|
216
|
+
case "<=":
|
|
217
|
+
return left <= right;
|
|
218
|
+
case ">":
|
|
219
|
+
return left > right;
|
|
220
|
+
case ">=":
|
|
221
|
+
return left >= right;
|
|
222
|
+
case "&&":
|
|
223
|
+
return left && right;
|
|
224
|
+
case "||":
|
|
225
|
+
return left || right;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Assignment ───
|
|
230
|
+
if (ASSIGNMENT_OPS.has(operator)) {
|
|
231
|
+
const rhs = resolveOperand(value, state, event, iterCtx);
|
|
232
|
+
const { obj, key } = resolveWritableRef(target.$ref, state, event, iterCtx);
|
|
233
|
+
switch (operator) {
|
|
234
|
+
case "=":
|
|
235
|
+
obj[key] = rhs;
|
|
236
|
+
break;
|
|
237
|
+
case "+=":
|
|
238
|
+
obj[key] += rhs;
|
|
239
|
+
break;
|
|
240
|
+
case "-=":
|
|
241
|
+
obj[key] -= rhs;
|
|
242
|
+
break;
|
|
243
|
+
case "*=":
|
|
244
|
+
obj[key] *= rhs;
|
|
245
|
+
break;
|
|
246
|
+
case "/=":
|
|
247
|
+
obj[key] /= rhs;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Array methods ───
|
|
254
|
+
if (ARRAY_METHOD_OPS.has(operator)) {
|
|
255
|
+
const arr = resolveOperand(target, state, event, iterCtx);
|
|
256
|
+
switch (operator) {
|
|
257
|
+
case "push":
|
|
258
|
+
return arr.push(resolveOperand(value, state, event, iterCtx));
|
|
259
|
+
case "unshift":
|
|
260
|
+
return arr.unshift(resolveOperand(value, state, event, iterCtx));
|
|
261
|
+
case "pop":
|
|
262
|
+
return arr.pop();
|
|
263
|
+
case "shift":
|
|
264
|
+
return arr.shift();
|
|
265
|
+
case "splice": {
|
|
266
|
+
const args = resolveOperand(value, state, event, iterCtx);
|
|
267
|
+
return arr.splice(...args);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── Aggregates (pure) ───
|
|
273
|
+
if (AGGREGATE_OPS.has(operator)) {
|
|
274
|
+
const arr = resolveOperand(target, state, event, iterCtx);
|
|
275
|
+
if (operator === "reduce") {
|
|
276
|
+
const seed = resolveOperand(initial, state, event, iterCtx);
|
|
277
|
+
return arr.reduce(
|
|
278
|
+
(/** @type {any} */ acc, /** @type {any} */ item, /** @type {number} */ index) => {
|
|
279
|
+
return evaluateExpression(value, state, event, { acc, item, index });
|
|
280
|
+
},
|
|
281
|
+
seed,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (operator === "map") {
|
|
285
|
+
return arr.map((/** @type {any} */ item, /** @type {number} */ index) => {
|
|
286
|
+
return evaluateExpression(value, state, event, { ...iterCtx, item, index });
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (operator === "filter") {
|
|
290
|
+
return arr.filter((/** @type {any} */ item, /** @type {number} */ index) => {
|
|
291
|
+
return evaluateExpression(value, state, event, { ...iterCtx, item, index });
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── Compiler: Expression → JS Source ────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
/** @typedef {{ statePrefix?: string; eventParam?: string }} CompileOpts */
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Compile an operand to a JS source string.
|
|
303
|
+
*
|
|
304
|
+
* @param {any} operand
|
|
305
|
+
* @param {CompileOpts} opts
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
function compileOperand(operand, opts) {
|
|
309
|
+
if (operand === null) return "null";
|
|
310
|
+
if (operand === undefined) return "undefined";
|
|
311
|
+
|
|
312
|
+
// Nested expression node
|
|
313
|
+
if (typeof operand === "object" && !Array.isArray(operand) && "operator" in operand) {
|
|
314
|
+
return compileExpression(operand, opts);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// $ref pointer
|
|
318
|
+
if (typeof operand === "object" && !Array.isArray(operand) && "$ref" in operand) {
|
|
319
|
+
return compileRef(operand.$ref, opts);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Array of operands
|
|
323
|
+
if (Array.isArray(operand)) {
|
|
324
|
+
return operand.map((o) => compileOperand(o, opts)).join(", ");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Literal
|
|
328
|
+
return JSON.stringify(operand);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Compile a $ref to its JS equivalent.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} ref
|
|
335
|
+
* @param {CompileOpts} opts
|
|
336
|
+
* @returns {string}
|
|
337
|
+
*/
|
|
338
|
+
function compileRef(ref, opts) {
|
|
339
|
+
const s = opts.statePrefix ?? "state";
|
|
340
|
+
const e = opts.eventParam ?? "event";
|
|
341
|
+
|
|
342
|
+
if (ref === "$reduce/acc") return "_acc";
|
|
343
|
+
if (ref.startsWith("$reduce/")) return "_acc";
|
|
344
|
+
|
|
345
|
+
if (ref.startsWith("event#/")) {
|
|
346
|
+
const path = ref.slice("event#/".length);
|
|
347
|
+
return `${e}.${path.replace(/\//g, ".")}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (ref.startsWith("$map/")) {
|
|
351
|
+
const parts = ref.split("/");
|
|
352
|
+
const key = parts[1];
|
|
353
|
+
if (key === "item") {
|
|
354
|
+
return parts.length > 2 ? `_item.${parts.slice(2).join(".")}` : "_item";
|
|
355
|
+
}
|
|
356
|
+
if (key === "index") return "_index";
|
|
357
|
+
return `_${key}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (ref.startsWith("#/state/")) {
|
|
361
|
+
const path = ref.slice("#/state/".length);
|
|
362
|
+
return `${s}.${path.replace(/\//g, ".")}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (ref.startsWith("parent#/")) return `${s}.${ref.slice("parent#/".length)}`;
|
|
366
|
+
if (ref.startsWith("window#/"))
|
|
367
|
+
return `window.${ref.slice("window#/".length).replace(/\//g, ".")}`;
|
|
368
|
+
if (ref.startsWith("document#/"))
|
|
369
|
+
return `document.${ref.slice("document#/".length).replace(/\//g, ".")}`;
|
|
370
|
+
|
|
371
|
+
return `${s}.${ref}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Compile a writable $ref target to its JS equivalent (for LHS of assignment).
|
|
376
|
+
*
|
|
377
|
+
* @param {any} target
|
|
378
|
+
* @param {CompileOpts} opts
|
|
379
|
+
* @returns {string}
|
|
380
|
+
*/
|
|
381
|
+
function compileTarget(target, opts) {
|
|
382
|
+
if (typeof target === "object" && "$ref" in target) {
|
|
383
|
+
return compileRef(target.$ref, opts);
|
|
384
|
+
}
|
|
385
|
+
return compileOperand(target, opts);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Compile an expression node to a JS source string. Returns a statement for mutating ops, a value
|
|
390
|
+
* expression for pure ops.
|
|
391
|
+
*
|
|
392
|
+
* @param {ExpressionNode} node
|
|
393
|
+
* @param {CompileOpts} [opts]
|
|
394
|
+
* @returns {string}
|
|
395
|
+
*/
|
|
396
|
+
export function compileExpression(node, opts = {}) {
|
|
397
|
+
const { operator, target, value, initial } = node;
|
|
398
|
+
|
|
399
|
+
// ─── Unary ───
|
|
400
|
+
if (UNARY_OPS.has(operator) && !("value" in node)) {
|
|
401
|
+
const operand = compileOperand(target, opts);
|
|
402
|
+
if (operator === "!") return `!(${operand})`;
|
|
403
|
+
if (operator === "-") return `-(${operand})`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── Binary (pure) ───
|
|
407
|
+
if (BINARY_OPS.has(operator)) {
|
|
408
|
+
const left = compileOperand(target, opts);
|
|
409
|
+
const right = compileOperand(value, opts);
|
|
410
|
+
return `(${left} ${operator} ${right})`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── Assignment ───
|
|
414
|
+
if (ASSIGNMENT_OPS.has(operator)) {
|
|
415
|
+
const lhs = compileTarget(target, opts);
|
|
416
|
+
const rhs = compileOperand(value, opts);
|
|
417
|
+
return `${lhs} ${operator} ${rhs}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Array methods ───
|
|
421
|
+
if (ARRAY_METHOD_OPS.has(operator)) {
|
|
422
|
+
const arr = compileTarget(target, opts);
|
|
423
|
+
switch (operator) {
|
|
424
|
+
case "push":
|
|
425
|
+
return `${arr}.push(${compileOperand(value, opts)})`;
|
|
426
|
+
case "unshift":
|
|
427
|
+
return `${arr}.unshift(${compileOperand(value, opts)})`;
|
|
428
|
+
case "pop":
|
|
429
|
+
return `${arr}.pop()`;
|
|
430
|
+
case "shift":
|
|
431
|
+
return `${arr}.shift()`;
|
|
432
|
+
case "splice": {
|
|
433
|
+
const args = Array.isArray(value)
|
|
434
|
+
? value.map((o) => compileOperand(o, opts)).join(", ")
|
|
435
|
+
: compileOperand(value, opts);
|
|
436
|
+
return `${arr}.splice(${args})`;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ─── Aggregates (pure) ───
|
|
442
|
+
if (AGGREGATE_OPS.has(operator)) {
|
|
443
|
+
const arr = compileOperand(target, opts);
|
|
444
|
+
const itemExpr = compileExpression(value, opts);
|
|
445
|
+
if (operator === "reduce") {
|
|
446
|
+
const seed = compileOperand(initial, opts);
|
|
447
|
+
return `${arr}.reduce((_acc, _item, _index) => ${itemExpr}, ${seed})`;
|
|
448
|
+
}
|
|
449
|
+
if (operator === "map") {
|
|
450
|
+
return `${arr}.map((_item, _index) => ${itemExpr})`;
|
|
451
|
+
}
|
|
452
|
+
if (operator === "filter") {
|
|
453
|
+
return `${arr}.filter((_item, _index) => ${itemExpr})`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return "undefined";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {any} obj
|
|
464
|
+
* @param {string} path
|
|
465
|
+
* @returns {any}
|
|
466
|
+
*/
|
|
467
|
+
function getPath(obj, path) {
|
|
468
|
+
return path.split(/[./]/).reduce((o, k) => /** @type {any} */ (o)?.[k], obj);
|
|
469
|
+
}
|
package/src/runtime.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { reactive, ref, computed, effect, isRef, onEffectCleanup } from "@vue/reactivity";
|
|
16
|
+
import { evaluateExpression, isMutating } from "./expression.js";
|
|
16
17
|
|
|
17
18
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -161,6 +162,7 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
|
|
|
161
162
|
// 4. Object
|
|
162
163
|
if (typeof def === "object") {
|
|
163
164
|
if (def.$prototype) continue; // handled in later passes
|
|
165
|
+
if ("$expression" in def) continue; // handled in pass 2.5
|
|
164
166
|
if (def.timing === "server" && def.$src && def.$export) continue; // handled in fifth pass
|
|
165
167
|
if ("default" in def) {
|
|
166
168
|
raw[key] = def.default;
|
|
@@ -181,6 +183,19 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
|
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
// Pass 2.5: $expression entries (Shape 5)
|
|
187
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
188
|
+
if (def && typeof def === "object" && !Array.isArray(def) && "$expression" in def) {
|
|
189
|
+
const node = /** @type {any} */ (def).$expression;
|
|
190
|
+
if (isMutating(node.operator)) {
|
|
191
|
+
state[key] = (/** @type {any} */ s, /** @type {any} */ event) =>
|
|
192
|
+
evaluateExpression(node, s, event);
|
|
193
|
+
} else {
|
|
194
|
+
state[key] = computed(() => evaluateExpression(node, state, null));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
184
199
|
// Third pass: $prototype: "Function" entries
|
|
185
200
|
for (const [key, def] of Object.entries(defs)) {
|
|
186
201
|
if (typeof def === "object" && def?.$prototype === "Function") {
|
|
@@ -495,6 +510,13 @@ function applyProperties(el, def, state) {
|
|
|
495
510
|
el.addEventListener(key.slice(2), (e) => fn(scope, e));
|
|
496
511
|
continue;
|
|
497
512
|
}
|
|
513
|
+
// Event handler: inline $expression
|
|
514
|
+
if (val && typeof val === "object" && "$expression" in val) {
|
|
515
|
+
const node = /** @type {any} */ (val).$expression;
|
|
516
|
+
const scope = state;
|
|
517
|
+
el.addEventListener(key.slice(2), (e) => evaluateExpression(node, scope, e));
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
498
520
|
}
|
|
499
521
|
|
|
500
522
|
bindProperty(el, key, val, state);
|
|
@@ -548,6 +570,27 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
|
|
|
548
570
|
const nested = {};
|
|
549
571
|
/** @type {Record<string, any>} */
|
|
550
572
|
const media = {};
|
|
573
|
+
/** @type {Record<string, string>} */
|
|
574
|
+
const baseDecls = {};
|
|
575
|
+
|
|
576
|
+
// Collect properties overridden by media queries so we can avoid inline styles for them
|
|
577
|
+
/** @type {Set<string>} */
|
|
578
|
+
const mediaOverriddenProps = new Set();
|
|
579
|
+
for (const [prop, val] of Object.entries(styleDef)) {
|
|
580
|
+
if (prop.startsWith("@") && val && typeof val === "object") {
|
|
581
|
+
for (const k of Object.keys(val)) {
|
|
582
|
+
if (
|
|
583
|
+
!k.startsWith(":") &&
|
|
584
|
+
!k.startsWith(".") &&
|
|
585
|
+
!k.startsWith("&") &&
|
|
586
|
+
!k.startsWith("[") &&
|
|
587
|
+
!k.startsWith("@")
|
|
588
|
+
) {
|
|
589
|
+
mediaOverriddenProps.add(k);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
551
594
|
|
|
552
595
|
for (const [prop, val] of Object.entries(styleDef)) {
|
|
553
596
|
if (prop.startsWith("@")) media[prop] = val;
|
|
@@ -564,17 +607,21 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
|
|
|
564
607
|
/** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] =
|
|
565
608
|
evaluateTemplate(val, state);
|
|
566
609
|
});
|
|
610
|
+
else if (mediaOverriddenProps.has(prop)) baseDecls[prop] = val;
|
|
567
611
|
else /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] = val;
|
|
568
612
|
}
|
|
569
613
|
|
|
570
614
|
const hasNested = Object.keys(nested).length > 0;
|
|
571
615
|
const hasMedia = Object.keys(media).length > 0;
|
|
572
|
-
|
|
616
|
+
const hasBaseDecls = Object.keys(baseDecls).length > 0;
|
|
617
|
+
if (!hasNested && !hasMedia && !hasBaseDecls) return;
|
|
573
618
|
|
|
574
619
|
const uid = `jx-${Math.random().toString(36).slice(2, 7)}`;
|
|
575
620
|
el.dataset.jx = uid;
|
|
576
621
|
|
|
577
622
|
let css = "";
|
|
623
|
+
const baseCSS = toCSSText(baseDecls);
|
|
624
|
+
if (baseCSS) css += `[data-jx="${uid}"] { ${baseCSS} }\n`;
|
|
578
625
|
|
|
579
626
|
function emitNested(/** @type {string} */ scope, /** @type {Record<string, any>} */ rules) {
|
|
580
627
|
const props = toCSSText(rules);
|
|
@@ -675,8 +722,7 @@ function renderMappedArray(def, state, options) {
|
|
|
675
722
|
applyProperties(container, def, state);
|
|
676
723
|
applyStyle(container, def.style ?? {}, state["$media"] ?? {}, state);
|
|
677
724
|
applyAttributes(container, def.attributes ?? {}, state);
|
|
678
|
-
const { items:
|
|
679
|
-
const itemsSrc = _items ?? _of;
|
|
725
|
+
const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef } = def.children;
|
|
680
726
|
|
|
681
727
|
effect(() => {
|
|
682
728
|
container.innerHTML = "";
|