@jxsuite/runtime 0.22.0 → 0.23.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 +286 -12
- package/dist/runtime.js.map +5 -4
- package/package.json +3 -2
- package/src/expression.js +469 -0
- package/src/runtime.js +63 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
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
|
|
|
@@ -43,6 +44,10 @@ export async function Jx(source, target = document.body, options) {
|
|
|
43
44
|
injectHead(doc.$head, base);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
if (doc.$media) {
|
|
48
|
+
_rootMedia = doc.$media;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
const state = await buildScope(doc, {}, base);
|
|
47
52
|
target.appendChild(renderNode(doc, state, options));
|
|
48
53
|
if (typeof state.onMount === "function") state.onMount(state);
|
|
@@ -157,6 +162,7 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
|
|
|
157
162
|
// 4. Object
|
|
158
163
|
if (typeof def === "object") {
|
|
159
164
|
if (def.$prototype) continue; // handled in later passes
|
|
165
|
+
if ("$expression" in def) continue; // handled in pass 2.5
|
|
160
166
|
if (def.timing === "server" && def.$src && def.$export) continue; // handled in fifth pass
|
|
161
167
|
if ("default" in def) {
|
|
162
168
|
raw[key] = def.default;
|
|
@@ -177,6 +183,19 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
|
|
|
177
183
|
}
|
|
178
184
|
}
|
|
179
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
|
+
|
|
180
199
|
// Third pass: $prototype: "Function" entries
|
|
181
200
|
for (const [key, def] of Object.entries(defs)) {
|
|
182
201
|
if (typeof def === "object" && def?.$prototype === "Function") {
|
|
@@ -209,6 +228,8 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
|
|
|
209
228
|
|
|
210
229
|
if (doc.$media) {
|
|
211
230
|
state["$media"] = doc.$media;
|
|
231
|
+
} else if (!state["$media"] && Object.keys(_rootMedia).length > 0) {
|
|
232
|
+
state["$media"] = _rootMedia;
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
return state;
|
|
@@ -489,6 +510,13 @@ function applyProperties(el, def, state) {
|
|
|
489
510
|
el.addEventListener(key.slice(2), (e) => fn(scope, e));
|
|
490
511
|
continue;
|
|
491
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
|
+
}
|
|
492
520
|
}
|
|
493
521
|
|
|
494
522
|
bindProperty(el, key, val, state);
|
|
@@ -542,6 +570,27 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
|
|
|
542
570
|
const nested = {};
|
|
543
571
|
/** @type {Record<string, any>} */
|
|
544
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
|
+
}
|
|
545
594
|
|
|
546
595
|
for (const [prop, val] of Object.entries(styleDef)) {
|
|
547
596
|
if (prop.startsWith("@")) media[prop] = val;
|
|
@@ -558,17 +607,21 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
|
|
|
558
607
|
/** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] =
|
|
559
608
|
evaluateTemplate(val, state);
|
|
560
609
|
});
|
|
610
|
+
else if (mediaOverriddenProps.has(prop)) baseDecls[prop] = val;
|
|
561
611
|
else /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] = val;
|
|
562
612
|
}
|
|
563
613
|
|
|
564
614
|
const hasNested = Object.keys(nested).length > 0;
|
|
565
615
|
const hasMedia = Object.keys(media).length > 0;
|
|
566
|
-
|
|
616
|
+
const hasBaseDecls = Object.keys(baseDecls).length > 0;
|
|
617
|
+
if (!hasNested && !hasMedia && !hasBaseDecls) return;
|
|
567
618
|
|
|
568
619
|
const uid = `jx-${Math.random().toString(36).slice(2, 7)}`;
|
|
569
620
|
el.dataset.jx = uid;
|
|
570
621
|
|
|
571
622
|
let css = "";
|
|
623
|
+
const baseCSS = toCSSText(baseDecls);
|
|
624
|
+
if (baseCSS) css += `[data-jx="${uid}"] { ${baseCSS} }\n`;
|
|
572
625
|
|
|
573
626
|
function emitNested(/** @type {string} */ scope, /** @type {Record<string, any>} */ rules) {
|
|
574
627
|
const props = toCSSText(rules);
|
|
@@ -600,11 +653,13 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
|
|
|
600
653
|
|
|
601
654
|
for (const [key, rules] of Object.entries(media)) {
|
|
602
655
|
if (key === "@--") continue; // base canvas width, not a real media query
|
|
603
|
-
const
|
|
604
|
-
?
|
|
605
|
-
: key.
|
|
656
|
+
const atRule = key.startsWith("@--")
|
|
657
|
+
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
658
|
+
: key.startsWith("@(")
|
|
659
|
+
? `@media ${key.slice(1)}`
|
|
660
|
+
: key;
|
|
606
661
|
const scope = `[data-jx="${uid}"]`;
|
|
607
|
-
css +=
|
|
662
|
+
css += `${atRule} { ${scope} { ${toCSSText(rules)} } }\n`;
|
|
608
663
|
|
|
609
664
|
function emitMediaNested(
|
|
610
665
|
/** @type {string} */ parentSel,
|
|
@@ -621,7 +676,7 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
|
|
|
621
676
|
? `${parentSel}${sel}`
|
|
622
677
|
: `${parentSel} ${sel}`;
|
|
623
678
|
const props = toCSSText(sub);
|
|
624
|
-
if (props) css +=
|
|
679
|
+
if (props) css += `${atRule} { ${resolved} { ${props} } }\n`;
|
|
625
680
|
emitMediaNested(resolved, sub);
|
|
626
681
|
}
|
|
627
682
|
}
|
|
@@ -667,8 +722,7 @@ function renderMappedArray(def, state, options) {
|
|
|
667
722
|
applyProperties(container, def, state);
|
|
668
723
|
applyStyle(container, def.style ?? {}, state["$media"] ?? {}, state);
|
|
669
724
|
applyAttributes(container, def.attributes ?? {}, state);
|
|
670
|
-
const { items:
|
|
671
|
-
const itemsSrc = _items ?? _of;
|
|
725
|
+
const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef } = def.children;
|
|
672
726
|
|
|
673
727
|
effect(() => {
|
|
674
728
|
container.innerHTML = "";
|
|
@@ -1562,6 +1616,7 @@ export function toCSSText(rules) {
|
|
|
1562
1616
|
|
|
1563
1617
|
// ─── Custom Element Registration ──────────────────────────────────────────────
|
|
1564
1618
|
|
|
1619
|
+
let _rootMedia = {};
|
|
1565
1620
|
const _elementDefs = new Map();
|
|
1566
1621
|
|
|
1567
1622
|
/**
|