@erickxavier/no-js 1.8.2 → 1.9.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/README.md +2 -2
- package/dist/cjs/no.js +9 -6
- package/dist/cjs/no.js.map +3 -3
- package/dist/esm/no.js +9 -6
- package/dist/esm/no.js.map +3 -3
- package/dist/iife/no.js +9 -6
- package/dist/iife/no.js.map +3 -3
- package/package.json +2 -3
- package/src/directives/loops.js +18 -0
- package/src/evaluate.js +1039 -93
- package/src/globals.js +0 -1
- package/src/i18n.js +4 -4
- package/src/index.js +5 -1
package/src/evaluate.js
CHANGED
|
@@ -2,89 +2,1018 @@
|
|
|
2
2
|
// EXPRESSION EVALUATOR
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
-
import { _stores, _routerInstance, _filters, _warn,
|
|
5
|
+
import { _stores, _routerInstance, _filters, _warn, _notifyStoreWatchers } from "./globals.js";
|
|
6
6
|
import { _i18n } from "./i18n.js";
|
|
7
7
|
import { _collectKeys } from "./context.js";
|
|
8
8
|
|
|
9
9
|
const _exprCache = new Map();
|
|
10
|
+
const _stmtCache = new Map();
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
// Handles dot-notation paths, basic comparisons, boolean operators, negation, and literals.
|
|
13
|
-
function _cspSafeEval(expr, keys, vals) {
|
|
14
|
-
const scope = {};
|
|
15
|
-
for (let i = 0; i < keys.length; i++) scope[keys[i]] = vals[i];
|
|
12
|
+
// ── Tokenizer ──────────────────────────────────────────────────────────
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const _KEYWORDS = new Set(["true", "false", "null", "undefined", "typeof", "in", "instanceof"]);
|
|
15
|
+
const _FORBIDDEN = new Set(["__proto__", "constructor", "prototype"]);
|
|
16
|
+
|
|
17
|
+
// Multi-char operators/punctuation, sorted longest-first for greedy matching
|
|
18
|
+
const _MULTI = ["===", "!==", "...", "??", "?.", "==", "!=", ">=", "<=", "&&", "||", "+=", "-=", "*=", "/=", "%=", "++", "--", "=>"];
|
|
19
|
+
const _SINGLE_OPS = new Set(["+", "-", "*", "/", "%", ">", "<", "!", "=", "|"]);
|
|
20
|
+
const _SINGLE_PUNC = new Set(["(", ")", "[", "]", "{", "}", ".", ",", ":", ";", "?"]);
|
|
21
|
+
|
|
22
|
+
function _tokenize(expr) {
|
|
23
|
+
if (typeof expr !== "string") return [];
|
|
24
|
+
const tokens = [];
|
|
25
|
+
const len = expr.length;
|
|
26
|
+
let pos = 0;
|
|
27
|
+
|
|
28
|
+
while (pos < len) {
|
|
29
|
+
const ch = expr[pos];
|
|
30
|
+
|
|
31
|
+
// Skip whitespace
|
|
32
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { pos++; continue; }
|
|
33
|
+
|
|
34
|
+
// String literals (single or double quoted)
|
|
35
|
+
if (ch === "'" || ch === '"') {
|
|
36
|
+
const start = pos;
|
|
37
|
+
const quote = ch;
|
|
38
|
+
pos++;
|
|
39
|
+
let value = "";
|
|
40
|
+
while (pos < len && expr[pos] !== quote) {
|
|
41
|
+
if (expr[pos] === "\\") {
|
|
42
|
+
pos++;
|
|
43
|
+
if (pos < len) {
|
|
44
|
+
const esc = expr[pos];
|
|
45
|
+
if (esc === "n") value += "\n";
|
|
46
|
+
else if (esc === "t") value += "\t";
|
|
47
|
+
else if (esc === "r") value += "\r";
|
|
48
|
+
else value += esc;
|
|
49
|
+
pos++;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
value += expr[pos++];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (pos < len) pos++; // skip closing quote
|
|
56
|
+
tokens.push({ type: "String", value, pos: start });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Template literals
|
|
61
|
+
if (ch === "`") {
|
|
62
|
+
const start = pos;
|
|
63
|
+
pos++;
|
|
64
|
+
const parts = [];
|
|
65
|
+
const exprs = [];
|
|
66
|
+
let seg = "";
|
|
67
|
+
while (pos < len && expr[pos] !== "`") {
|
|
68
|
+
if (expr[pos] === "\\" && pos + 1 < len) {
|
|
69
|
+
const esc = expr[pos + 1];
|
|
70
|
+
if (esc === "n") seg += "\n";
|
|
71
|
+
else if (esc === "t") seg += "\t";
|
|
72
|
+
else if (esc === "r") seg += "\r";
|
|
73
|
+
else seg += esc;
|
|
74
|
+
pos += 2;
|
|
75
|
+
} else if (expr[pos] === "$" && pos + 1 < len && expr[pos + 1] === "{") {
|
|
76
|
+
parts.push(seg);
|
|
77
|
+
seg = "";
|
|
78
|
+
pos += 2; // skip ${
|
|
79
|
+
// Collect expression text respecting nested braces
|
|
80
|
+
let depth = 1;
|
|
81
|
+
let inner = "";
|
|
82
|
+
while (pos < len && depth > 0) {
|
|
83
|
+
if (expr[pos] === "{") depth++;
|
|
84
|
+
else if (expr[pos] === "}") { depth--; if (depth === 0) break; }
|
|
85
|
+
else if (expr[pos] === "'" || expr[pos] === '"' || expr[pos] === "`") {
|
|
86
|
+
// skip string inside interpolation
|
|
87
|
+
const q = expr[pos];
|
|
88
|
+
inner += q; pos++;
|
|
89
|
+
while (pos < len && expr[pos] !== q) {
|
|
90
|
+
if (expr[pos] === "\\") { inner += expr[pos++]; if (pos < len) inner += expr[pos++]; }
|
|
91
|
+
else inner += expr[pos++];
|
|
92
|
+
}
|
|
93
|
+
if (pos < len) { inner += expr[pos]; pos++; }
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
inner += expr[pos++];
|
|
97
|
+
}
|
|
98
|
+
if (pos < len) pos++; // skip closing }
|
|
99
|
+
exprs.push(_tokenize(inner));
|
|
100
|
+
} else {
|
|
101
|
+
seg += expr[pos++];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (pos < len) pos++; // skip closing `
|
|
105
|
+
parts.push(seg);
|
|
106
|
+
tokens.push({ type: "Template", parts, exprs, pos: start });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Numbers: starts with digit, or '.' followed by digit
|
|
111
|
+
if ((ch >= "0" && ch <= "9") || (ch === "." && pos + 1 < len && expr[pos + 1] >= "0" && expr[pos + 1] <= "9")) {
|
|
112
|
+
const start = pos;
|
|
113
|
+
let num = "";
|
|
114
|
+
while (pos < len && ((expr[pos] >= "0" && expr[pos] <= "9") || expr[pos] === ".")) {
|
|
115
|
+
num += expr[pos++];
|
|
116
|
+
}
|
|
117
|
+
tokens.push({ type: "Number", value: num, pos: start });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Identifiers / Keywords
|
|
122
|
+
if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || ch === "_" || ch === "$") {
|
|
123
|
+
const start = pos;
|
|
124
|
+
let id = "";
|
|
125
|
+
while (pos < len) {
|
|
126
|
+
const c = expr[pos];
|
|
127
|
+
if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c === "_" || c === "$") {
|
|
128
|
+
id += c; pos++;
|
|
129
|
+
} else break;
|
|
130
|
+
}
|
|
131
|
+
if (_FORBIDDEN.has(id)) tokens.push({ type: "Forbidden", value: id, pos: start });
|
|
132
|
+
else if (_KEYWORDS.has(id)) tokens.push({ type: "Keyword", value: id, pos: start });
|
|
133
|
+
else tokens.push({ type: "Ident", value: id, pos: start });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Multi-char operators / punctuation (longest first)
|
|
138
|
+
let matched = false;
|
|
139
|
+
for (let m = 0; m < _MULTI.length; m++) {
|
|
140
|
+
const op = _MULTI[m];
|
|
141
|
+
if (expr.startsWith(op, pos)) {
|
|
142
|
+
const isPunc = op === "..." || op === "?.";
|
|
143
|
+
tokens.push({ type: isPunc ? "Punc" : "Op", value: op, pos });
|
|
144
|
+
pos += op.length;
|
|
145
|
+
matched = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (matched) continue;
|
|
150
|
+
|
|
151
|
+
// Single-char operators
|
|
152
|
+
if (_SINGLE_OPS.has(ch)) {
|
|
153
|
+
tokens.push({ type: "Op", value: ch, pos });
|
|
154
|
+
pos++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Single-char punctuation
|
|
159
|
+
if (_SINGLE_PUNC.has(ch)) {
|
|
160
|
+
tokens.push({ type: "Punc", value: ch, pos });
|
|
161
|
+
pos++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Unrecognized character — skip
|
|
166
|
+
pos++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return tokens;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Recursive-descent expression parser ────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function _parseExpr(tokens) {
|
|
175
|
+
if (!tokens || tokens.length === 0) return { type: "Literal", value: undefined };
|
|
176
|
+
|
|
177
|
+
let pos = 0;
|
|
178
|
+
|
|
179
|
+
function peek() { return tokens[pos]; }
|
|
180
|
+
function next() { return tokens[pos++]; }
|
|
181
|
+
|
|
182
|
+
function match(type, value) {
|
|
183
|
+
const t = tokens[pos];
|
|
184
|
+
if (!t) return false;
|
|
185
|
+
if (value !== undefined) return t.type === type && t.value === value;
|
|
186
|
+
return t.type === type;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function expect(type, value) {
|
|
190
|
+
const t = tokens[pos];
|
|
191
|
+
if (t && t.type === type && (value === undefined || t.value === value)) {
|
|
192
|
+
pos++;
|
|
193
|
+
return t;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Grammar rules (lowest → highest precedence) ───
|
|
199
|
+
|
|
200
|
+
function parseExpression() {
|
|
201
|
+
return parseTernary();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseTernary() {
|
|
205
|
+
let node = parseNullishOr();
|
|
206
|
+
if (match("Punc", "?")) {
|
|
207
|
+
next(); // consume ?
|
|
208
|
+
const consequent = parseTernary();
|
|
209
|
+
expect("Punc", ":");
|
|
210
|
+
const alternate = parseTernary();
|
|
211
|
+
node = { type: "ConditionalExpr", test: node, consequent, alternate };
|
|
212
|
+
}
|
|
213
|
+
return node;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseNullishOr() {
|
|
217
|
+
let node = parseLogicalOr();
|
|
218
|
+
if (match("Op", "??")) {
|
|
219
|
+
next();
|
|
220
|
+
const right = parseNullishOr();
|
|
221
|
+
node = { type: "BinaryExpr", op: "??", left: node, right };
|
|
222
|
+
}
|
|
223
|
+
return node;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseLogicalOr() {
|
|
227
|
+
let node = parseLogicalAnd();
|
|
228
|
+
while (match("Op", "||")) {
|
|
229
|
+
next();
|
|
230
|
+
const right = parseLogicalAnd();
|
|
231
|
+
node = { type: "BinaryExpr", op: "||", left: node, right };
|
|
232
|
+
}
|
|
233
|
+
return node;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseLogicalAnd() {
|
|
237
|
+
let node = parseBitwiseOr();
|
|
238
|
+
while (match("Op", "&&")) {
|
|
239
|
+
next();
|
|
240
|
+
const right = parseBitwiseOr();
|
|
241
|
+
node = { type: "BinaryExpr", op: "&&", left: node, right };
|
|
242
|
+
}
|
|
243
|
+
return node;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseBitwiseOr() {
|
|
247
|
+
let node = parseComparison();
|
|
248
|
+
while (peek() && peek().type === "Op" && peek().value === "|" && (!tokens[pos + 1] || tokens[pos + 1].value !== "|")) {
|
|
249
|
+
next();
|
|
250
|
+
const right = parseComparison();
|
|
251
|
+
node = { type: "BinaryExpr", op: "|", left: node, right };
|
|
252
|
+
}
|
|
253
|
+
return node;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseComparison() {
|
|
257
|
+
let node = parseAddition();
|
|
258
|
+
const t = peek();
|
|
259
|
+
if (!t) return node;
|
|
260
|
+
const compOps = ["===", "!==", "==", "!=", ">=", "<=", ">", "<"];
|
|
261
|
+
if ((t.type === "Op" && compOps.indexOf(t.value) !== -1) ||
|
|
262
|
+
(t.type === "Keyword" && (t.value === "in" || t.value === "instanceof"))) {
|
|
263
|
+
const op = next().value;
|
|
264
|
+
const right = parseAddition();
|
|
265
|
+
node = { type: "BinaryExpr", op, left: node, right };
|
|
266
|
+
}
|
|
267
|
+
return node;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function parseAddition() {
|
|
271
|
+
let node = parseMultiplication();
|
|
272
|
+
while (peek() && peek().type === "Op" && (peek().value === "+" || peek().value === "-")) {
|
|
273
|
+
const op = next().value;
|
|
274
|
+
const right = parseMultiplication();
|
|
275
|
+
node = { type: "BinaryExpr", op, left: node, right };
|
|
276
|
+
}
|
|
277
|
+
return node;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseMultiplication() {
|
|
281
|
+
let node = parseUnary();
|
|
282
|
+
while (peek() && peek().type === "Op" && (peek().value === "*" || peek().value === "/" || peek().value === "%")) {
|
|
283
|
+
const op = next().value;
|
|
284
|
+
const right = parseUnary();
|
|
285
|
+
node = { type: "BinaryExpr", op, left: node, right };
|
|
286
|
+
}
|
|
287
|
+
return node;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parseUnary() {
|
|
291
|
+
const t = peek();
|
|
292
|
+
if (!t) return { type: "Literal", value: undefined };
|
|
293
|
+
// typeof
|
|
294
|
+
if (t.type === "Keyword" && t.value === "typeof") {
|
|
295
|
+
next();
|
|
296
|
+
return { type: "UnaryExpr", op: "typeof", argument: parseUnary() };
|
|
297
|
+
}
|
|
298
|
+
// ! or unary - or unary +
|
|
299
|
+
if (t.type === "Op" && (t.value === "!" || t.value === "-" || t.value === "+")) {
|
|
300
|
+
next();
|
|
301
|
+
return { type: "UnaryExpr", op: t.value, argument: parseUnary() };
|
|
302
|
+
}
|
|
303
|
+
// Prefix ++ / --
|
|
304
|
+
if (t.type === "Op" && (t.value === "++" || t.value === "--")) {
|
|
305
|
+
next();
|
|
306
|
+
return { type: "UnaryExpr", op: t.value, argument: parseUnary(), prefix: true };
|
|
307
|
+
}
|
|
308
|
+
return parsePostfix();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parsePostfix() {
|
|
312
|
+
let node = parseCallMember();
|
|
313
|
+
const t = peek();
|
|
314
|
+
if (t && t.type === "Op" && (t.value === "++" || t.value === "--")) {
|
|
315
|
+
next();
|
|
316
|
+
node = { type: "PostfixExpr", op: t.value, argument: node };
|
|
317
|
+
}
|
|
318
|
+
return node;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseCallMember() {
|
|
322
|
+
let node = parsePrimary();
|
|
323
|
+
|
|
324
|
+
while (true) {
|
|
325
|
+
const t = peek();
|
|
326
|
+
if (!t) break;
|
|
327
|
+
|
|
328
|
+
// Dot access: obj.prop
|
|
329
|
+
if (t.type === "Punc" && t.value === ".") {
|
|
330
|
+
next();
|
|
331
|
+
const prop = peek();
|
|
332
|
+
if (prop && (prop.type === "Ident" || prop.type === "Keyword")) {
|
|
333
|
+
next();
|
|
334
|
+
node = { type: "MemberExpr", object: node, property: { type: "Identifier", name: prop.value }, computed: false };
|
|
335
|
+
} else if (prop && prop.type === "Forbidden") {
|
|
336
|
+
next();
|
|
337
|
+
node = { type: "Forbidden" };
|
|
338
|
+
} else {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Optional chaining: obj?.prop or obj?.(args)
|
|
345
|
+
if (t.type === "Punc" && t.value === "?.") {
|
|
346
|
+
next();
|
|
347
|
+
const nt = peek();
|
|
348
|
+
// Optional call: obj?.(args)
|
|
349
|
+
if (nt && nt.type === "Punc" && nt.value === "(") {
|
|
350
|
+
next(); // consume (
|
|
351
|
+
const args = parseArgsList();
|
|
352
|
+
expect("Punc", ")");
|
|
353
|
+
node = { type: "OptionalCallExpr", callee: node, args };
|
|
354
|
+
}
|
|
355
|
+
// Optional member: obj?.prop
|
|
356
|
+
else if (nt && (nt.type === "Ident" || nt.type === "Keyword")) {
|
|
357
|
+
next();
|
|
358
|
+
node = { type: "OptionalMemberExpr", object: node, property: { type: "Identifier", name: nt.value }, computed: false };
|
|
359
|
+
}
|
|
360
|
+
// Optional bracket: obj?.[expr]
|
|
361
|
+
else if (nt && nt.type === "Punc" && nt.value === "[") {
|
|
362
|
+
next(); // consume [
|
|
363
|
+
const prop = parseExpression();
|
|
364
|
+
expect("Punc", "]");
|
|
365
|
+
node = { type: "OptionalMemberExpr", object: node, property: prop, computed: true };
|
|
366
|
+
} else {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Bracket access: obj[expr]
|
|
373
|
+
if (t.type === "Punc" && t.value === "[") {
|
|
374
|
+
next();
|
|
375
|
+
const prop = parseExpression();
|
|
376
|
+
expect("Punc", "]");
|
|
377
|
+
node = { type: "MemberExpr", object: node, property: prop, computed: true };
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Function call: fn(args)
|
|
382
|
+
if (t.type === "Punc" && t.value === "(") {
|
|
383
|
+
next();
|
|
384
|
+
const args = parseArgsList();
|
|
385
|
+
expect("Punc", ")");
|
|
386
|
+
node = { type: "CallExpr", callee: node, args };
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return node;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function parseArgsList() {
|
|
397
|
+
const args = [];
|
|
398
|
+
if (match("Punc", ")")) return args;
|
|
399
|
+
args.push(parseSpreadOrExpr());
|
|
400
|
+
while (match("Punc", ",")) {
|
|
401
|
+
next();
|
|
402
|
+
if (match("Punc", ")")) break; // trailing comma
|
|
403
|
+
args.push(parseSpreadOrExpr());
|
|
404
|
+
}
|
|
405
|
+
return args;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function parseSpreadOrExpr() {
|
|
409
|
+
if (match("Punc", "...")) {
|
|
410
|
+
next();
|
|
411
|
+
return { type: "SpreadElement", argument: parseExpression() };
|
|
412
|
+
}
|
|
413
|
+
return parseExpression();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ─── Arrow function detection helpers ───
|
|
417
|
+
|
|
418
|
+
function isArrowParams() {
|
|
419
|
+
// Lookahead from current pos (after consuming "(") to see if this is (id, id, ...) =>
|
|
420
|
+
const saved = pos;
|
|
421
|
+
// Empty params: () =>
|
|
422
|
+
if (match("Punc", ")")) {
|
|
423
|
+
const after = tokens[pos + 1];
|
|
424
|
+
if (after && after.type === "Op" && after.value === "=>") {
|
|
425
|
+
pos = saved;
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
pos = saved;
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
// Check for ident list followed by ) =>
|
|
432
|
+
while (pos < tokens.length) {
|
|
433
|
+
const t = peek();
|
|
434
|
+
if (!t) break;
|
|
435
|
+
if (t.type === "Ident") {
|
|
436
|
+
next();
|
|
437
|
+
if (match("Punc", ",")) {
|
|
438
|
+
next();
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (match("Punc", ")")) {
|
|
442
|
+
const after = tokens[pos + 1];
|
|
443
|
+
if (after && after.type === "Op" && after.value === "=>") {
|
|
444
|
+
pos = saved;
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
pos = saved;
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
pos = saved;
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
// Spread param: (...rest) =>
|
|
454
|
+
if (t.type === "Punc" && t.value === "...") {
|
|
455
|
+
next();
|
|
456
|
+
if (match("Ident")) { next(); }
|
|
457
|
+
if (match("Punc", ")")) {
|
|
458
|
+
const after = tokens[pos + 1];
|
|
459
|
+
if (after && after.type === "Op" && after.value === "=>") {
|
|
460
|
+
pos = saved;
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
pos = saved;
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
pos = saved;
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
pos = saved;
|
|
471
|
+
return false;
|
|
19
472
|
}
|
|
20
473
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
474
|
+
function parseArrowParams() {
|
|
475
|
+
// Parse comma-separated identifiers until ")"
|
|
476
|
+
const params = [];
|
|
477
|
+
if (match("Punc", ")")) return params;
|
|
478
|
+
if (match("Punc", "...")) {
|
|
479
|
+
next();
|
|
480
|
+
if (match("Ident")) params.push("..." + next().value);
|
|
481
|
+
} else if (match("Ident")) {
|
|
482
|
+
params.push(next().value);
|
|
483
|
+
}
|
|
484
|
+
while (match("Punc", ",")) {
|
|
485
|
+
next();
|
|
486
|
+
if (match("Punc", ")")) break;
|
|
487
|
+
if (match("Punc", "...")) {
|
|
488
|
+
next();
|
|
489
|
+
if (match("Ident")) params.push("..." + next().value);
|
|
490
|
+
} else if (match("Ident")) {
|
|
491
|
+
params.push(next().value);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return params;
|
|
31
495
|
}
|
|
32
496
|
|
|
33
|
-
|
|
497
|
+
// ─── Primary ───
|
|
498
|
+
|
|
499
|
+
function parsePrimary() {
|
|
500
|
+
const t = peek();
|
|
501
|
+
if (!t) return { type: "Literal", value: undefined };
|
|
502
|
+
|
|
503
|
+
// Forbidden token
|
|
504
|
+
if (t.type === "Forbidden") {
|
|
505
|
+
next();
|
|
506
|
+
return { type: "Forbidden" };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Number literal
|
|
510
|
+
if (t.type === "Number") {
|
|
511
|
+
next();
|
|
512
|
+
return { type: "Literal", value: Number(t.value) };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// String literal
|
|
516
|
+
if (t.type === "String") {
|
|
517
|
+
next();
|
|
518
|
+
return { type: "Literal", value: t.value };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Template literal
|
|
522
|
+
if (t.type === "Template") {
|
|
523
|
+
next();
|
|
524
|
+
return {
|
|
525
|
+
type: "TemplateLiteral",
|
|
526
|
+
parts: t.parts,
|
|
527
|
+
expressions: t.exprs.map(function(exprTokens) { return _parseExpr(exprTokens); })
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Keywords: true, false, null, undefined
|
|
532
|
+
if (t.type === "Keyword") {
|
|
533
|
+
if (t.value === "true") { next(); return { type: "Literal", value: true }; }
|
|
534
|
+
if (t.value === "false") { next(); return { type: "Literal", value: false }; }
|
|
535
|
+
if (t.value === "null") { next(); return { type: "Literal", value: null }; }
|
|
536
|
+
if (t.value === "undefined") { next(); return { type: "Literal", value: undefined }; }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Array literal: [...]
|
|
540
|
+
if (t.type === "Punc" && t.value === "[") {
|
|
541
|
+
next();
|
|
542
|
+
const elements = [];
|
|
543
|
+
while (!match("Punc", "]") && pos < tokens.length) {
|
|
544
|
+
elements.push(parseSpreadOrExpr());
|
|
545
|
+
if (match("Punc", ",")) next();
|
|
546
|
+
}
|
|
547
|
+
expect("Punc", "]");
|
|
548
|
+
return { type: "ArrayExpr", elements };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Object literal: { ... }
|
|
552
|
+
if (t.type === "Punc" && t.value === "{") {
|
|
553
|
+
return parseObjectLiteral();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Parenthesized expression or arrow function with parens
|
|
557
|
+
if (t.type === "Punc" && t.value === "(") {
|
|
558
|
+
next(); // consume (
|
|
559
|
+
|
|
560
|
+
// Check for arrow function: (params) =>
|
|
561
|
+
if (isArrowParams()) {
|
|
562
|
+
const params = parseArrowParams();
|
|
563
|
+
expect("Punc", ")");
|
|
564
|
+
expect("Op", "=>");
|
|
565
|
+
const body = parseExpression();
|
|
566
|
+
return { type: "ArrowFunction", params, body };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Regular grouping
|
|
570
|
+
const expr = parseExpression();
|
|
571
|
+
expect("Punc", ")");
|
|
572
|
+
return expr;
|
|
573
|
+
}
|
|
34
574
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
575
|
+
// Identifier (possibly arrow: x => ...)
|
|
576
|
+
if (t.type === "Ident") {
|
|
577
|
+
next();
|
|
578
|
+
// Single-param arrow function: x => expr
|
|
579
|
+
if (match("Op", "=>")) {
|
|
580
|
+
next(); // consume =>
|
|
581
|
+
const body = parseExpression();
|
|
582
|
+
return { type: "ArrowFunction", params: [t.value], body };
|
|
583
|
+
}
|
|
584
|
+
return { type: "Identifier", name: t.value };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Assignment operators
|
|
588
|
+
if (t.type === "Op" && (t.value === "=" || t.value === "+=" || t.value === "-=" || t.value === "*=" || t.value === "/=" || t.value === "%=")) {
|
|
589
|
+
// Should not appear as primary; skip
|
|
590
|
+
next();
|
|
591
|
+
return { type: "Literal", value: undefined };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Spread in unexpected position (e.g. top level)
|
|
595
|
+
if (t.type === "Punc" && t.value === "...") {
|
|
596
|
+
next();
|
|
597
|
+
return { type: "SpreadElement", argument: parseExpression() };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Fallback: skip unrecognized token
|
|
601
|
+
next();
|
|
602
|
+
return { type: "Literal", value: undefined };
|
|
42
603
|
}
|
|
43
604
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
605
|
+
function parseObjectLiteral() {
|
|
606
|
+
next(); // consume {
|
|
607
|
+
const properties = [];
|
|
608
|
+
while (!match("Punc", "}") && pos < tokens.length) {
|
|
609
|
+
// Spread property: ...expr
|
|
610
|
+
if (match("Punc", "...")) {
|
|
611
|
+
next();
|
|
612
|
+
properties.push({ key: null, value: parseExpression(), computed: false, spread: true });
|
|
613
|
+
if (match("Punc", ",")) next();
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Computed property: [expr]: value
|
|
618
|
+
if (match("Punc", "[")) {
|
|
619
|
+
next();
|
|
620
|
+
const keyExpr = parseExpression();
|
|
621
|
+
expect("Punc", "]");
|
|
622
|
+
expect("Punc", ":");
|
|
623
|
+
const val = parseExpression();
|
|
624
|
+
properties.push({ key: keyExpr, value: val, computed: true, spread: false });
|
|
625
|
+
if (match("Punc", ",")) next();
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// String key: 'key': value
|
|
630
|
+
if (match("String")) {
|
|
631
|
+
const keyToken = next();
|
|
632
|
+
if (match("Punc", ":")) {
|
|
633
|
+
next();
|
|
634
|
+
const val = parseExpression();
|
|
635
|
+
properties.push({ key: keyToken.value, value: val, computed: false, spread: false });
|
|
636
|
+
}
|
|
637
|
+
if (match("Punc", ",")) next();
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Identifier key (shorthand or key: value)
|
|
642
|
+
if (match("Ident") || match("Keyword")) {
|
|
643
|
+
const keyToken = next();
|
|
644
|
+
if (match("Punc", ":")) {
|
|
645
|
+
next();
|
|
646
|
+
const val = parseExpression();
|
|
647
|
+
properties.push({ key: keyToken.value, value: val, computed: false, spread: false });
|
|
648
|
+
} else {
|
|
649
|
+
// Shorthand: { key } → { key: key }
|
|
650
|
+
properties.push({
|
|
651
|
+
key: keyToken.value,
|
|
652
|
+
value: { type: "Identifier", name: keyToken.value },
|
|
653
|
+
computed: false,
|
|
654
|
+
spread: false
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (match("Punc", ",")) next();
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Number key
|
|
662
|
+
if (match("Number")) {
|
|
663
|
+
const keyToken = next();
|
|
664
|
+
if (match("Punc", ":")) {
|
|
665
|
+
next();
|
|
666
|
+
const val = parseExpression();
|
|
667
|
+
properties.push({ key: keyToken.value, value: val, computed: false, spread: false });
|
|
668
|
+
}
|
|
669
|
+
if (match("Punc", ",")) next();
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Unrecognized — skip
|
|
674
|
+
next();
|
|
50
675
|
}
|
|
51
|
-
|
|
676
|
+
expect("Punc", "}");
|
|
677
|
+
return { type: "ObjectExpr", properties };
|
|
52
678
|
}
|
|
53
679
|
|
|
54
|
-
// Handle
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for
|
|
59
|
-
|
|
60
|
-
|
|
680
|
+
// ─── Handle top-level assignment ───
|
|
681
|
+
|
|
682
|
+
function parseTopLevel() {
|
|
683
|
+
const expr = parseExpression();
|
|
684
|
+
// Check for assignment at top level: ident = expr, ident += expr, etc.
|
|
685
|
+
const t = peek();
|
|
686
|
+
if (t && t.type === "Op" && (t.value === "=" || t.value === "+=" || t.value === "-=" || t.value === "*=" || t.value === "/=" || t.value === "%=")) {
|
|
687
|
+
const op = next().value;
|
|
688
|
+
const right = parseExpression();
|
|
689
|
+
return { type: "AssignExpr", op, left: expr, right };
|
|
61
690
|
}
|
|
62
|
-
return
|
|
691
|
+
return expr;
|
|
63
692
|
}
|
|
64
693
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
694
|
+
const ast = parseTopLevel();
|
|
695
|
+
return ast;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// AST tree-walking evaluator
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
const _FORBIDDEN_PROPS = { __proto__: 1, constructor: 1, prototype: 1 };
|
|
702
|
+
|
|
703
|
+
/* Safe subset of JS globals available in expressions (no eval/Function/process) */
|
|
704
|
+
const _SAFE_GLOBALS = {
|
|
705
|
+
Array, Object, String, Number, Boolean, Math, Date, RegExp, Map, Set,
|
|
706
|
+
JSON, parseInt, parseFloat, isNaN, isFinite, Infinity, NaN, undefined,
|
|
707
|
+
Error, Symbol, console,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const _DENY_GLOBALS = { eval: 1, Function: 1, process: 1, require: 1, importScripts: 1 };
|
|
711
|
+
|
|
712
|
+
function _evalNode(node, scope) {
|
|
713
|
+
try {
|
|
714
|
+
if (!node) return undefined;
|
|
715
|
+
|
|
716
|
+
switch (node.type) {
|
|
717
|
+
|
|
718
|
+
case 'Literal':
|
|
719
|
+
return node.value;
|
|
720
|
+
|
|
721
|
+
case 'Identifier':
|
|
722
|
+
if (node.name in scope) return scope[node.name];
|
|
723
|
+
if (node.name in _SAFE_GLOBALS) return _SAFE_GLOBALS[node.name];
|
|
724
|
+
// Allow access to browser globals (window, document, etc.) for backward compat
|
|
725
|
+
if (typeof globalThis !== 'undefined' && node.name in globalThis && !_DENY_GLOBALS[node.name]) return globalThis[node.name];
|
|
726
|
+
return undefined;
|
|
727
|
+
|
|
728
|
+
case 'Forbidden':
|
|
729
|
+
return undefined;
|
|
730
|
+
|
|
731
|
+
case 'BinaryExpr': {
|
|
732
|
+
// Short-circuit operators evaluate lazily
|
|
733
|
+
if (node.op === '&&') {
|
|
734
|
+
const l = _evalNode(node.left, scope);
|
|
735
|
+
return l ? _evalNode(node.right, scope) : l;
|
|
736
|
+
}
|
|
737
|
+
if (node.op === '||') {
|
|
738
|
+
const l = _evalNode(node.left, scope);
|
|
739
|
+
return l ? l : _evalNode(node.right, scope);
|
|
740
|
+
}
|
|
741
|
+
if (node.op === '??') {
|
|
742
|
+
const l = _evalNode(node.left, scope);
|
|
743
|
+
return (l === null || l === undefined) ? _evalNode(node.right, scope) : l;
|
|
744
|
+
}
|
|
745
|
+
const left = _evalNode(node.left, scope);
|
|
746
|
+
const right = _evalNode(node.right, scope);
|
|
747
|
+
switch (node.op) {
|
|
748
|
+
case '+': return left + right;
|
|
749
|
+
case '-': return left - right;
|
|
750
|
+
case '*': return left * right;
|
|
751
|
+
case '/': return left / right;
|
|
752
|
+
case '%': return left % right;
|
|
753
|
+
case '**': return left ** right;
|
|
754
|
+
case '===': return left === right;
|
|
755
|
+
case '!==': return left !== right;
|
|
756
|
+
case '==': return left == right;
|
|
757
|
+
case '!=': return left != right;
|
|
758
|
+
case '>': return left > right;
|
|
759
|
+
case '<': return left < right;
|
|
760
|
+
case '>=': return left >= right;
|
|
761
|
+
case '<=': return left <= right;
|
|
762
|
+
case 'in': return (right && typeof right === 'object') ? (left in right) : undefined;
|
|
763
|
+
case 'instanceof': return left instanceof right;
|
|
764
|
+
case '&': return left & right;
|
|
765
|
+
case '|': return left | right;
|
|
766
|
+
case '^': return left ^ right;
|
|
767
|
+
case '<<': return left << right;
|
|
768
|
+
case '>>': return left >> right;
|
|
769
|
+
case '>>>': return left >>> right;
|
|
770
|
+
default: return undefined;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
case 'UnaryExpr': {
|
|
775
|
+
if (node.op === 'typeof') {
|
|
776
|
+
// Special: if identifier not in scope, return "undefined" string
|
|
777
|
+
if (node.argument && node.argument.type === 'Identifier' && !(node.argument.name in scope)) {
|
|
778
|
+
return 'undefined';
|
|
779
|
+
}
|
|
780
|
+
return typeof _evalNode(node.argument, scope);
|
|
781
|
+
}
|
|
782
|
+
// Prefix ++ / --
|
|
783
|
+
if (node.op === '++' || node.op === '--') {
|
|
784
|
+
const oldVal = _evalNode(node.argument, scope);
|
|
785
|
+
const newVal = node.op === '++' ? oldVal + 1 : oldVal - 1;
|
|
786
|
+
return node.prefix ? newVal : oldVal;
|
|
787
|
+
}
|
|
788
|
+
const arg = _evalNode(node.argument, scope);
|
|
789
|
+
switch (node.op) {
|
|
790
|
+
case '!': return !arg;
|
|
791
|
+
case '-': return -arg;
|
|
792
|
+
case '+': return +arg;
|
|
793
|
+
case '~': return ~arg;
|
|
794
|
+
case 'void': return undefined;
|
|
795
|
+
default: return undefined;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
case 'ConditionalExpr': {
|
|
800
|
+
return _evalNode(node.test, scope)
|
|
801
|
+
? _evalNode(node.consequent, scope)
|
|
802
|
+
: _evalNode(node.alternate, scope);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
case 'MemberExpr':
|
|
806
|
+
case 'OptionalMemberExpr': {
|
|
807
|
+
const obj = _evalNode(node.object, scope);
|
|
808
|
+
if (obj == null) return undefined;
|
|
809
|
+
const prop = node.computed
|
|
810
|
+
? _evalNode(node.property, scope)
|
|
811
|
+
: node.property.name || node.property.value;
|
|
812
|
+
if (_FORBIDDEN_PROPS[prop]) return undefined;
|
|
813
|
+
return obj[prop];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
case 'CallExpr':
|
|
817
|
+
case 'OptionalCallExpr': {
|
|
818
|
+
// Evaluate args (handle spread)
|
|
819
|
+
const evalArgs = (args) => {
|
|
820
|
+
const result = [];
|
|
821
|
+
for (let i = 0; i < args.length; i++) {
|
|
822
|
+
if (args[i].type === 'SpreadElement') {
|
|
823
|
+
const spread = _evalNode(args[i].argument, scope);
|
|
824
|
+
if (spread && typeof spread[Symbol.iterator] === 'function') {
|
|
825
|
+
result.push(...spread);
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
result.push(_evalNode(args[i], scope));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
if (node.callee.type === 'MemberExpr' || node.callee.type === 'OptionalMemberExpr') {
|
|
835
|
+
const thisObj = _evalNode(node.callee.object, scope);
|
|
836
|
+
if (thisObj == null) {
|
|
837
|
+
if (node.type === 'OptionalCallExpr' || node.callee.type === 'OptionalMemberExpr') return undefined;
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
const prop = node.callee.computed
|
|
841
|
+
? _evalNode(node.callee.property, scope)
|
|
842
|
+
: node.callee.property.name;
|
|
843
|
+
if (_FORBIDDEN_PROPS[prop]) return undefined;
|
|
844
|
+
const fn = thisObj[prop];
|
|
845
|
+
if (typeof fn !== 'function') return undefined;
|
|
846
|
+
return fn.apply(thisObj, evalArgs(node.args));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const fn = _evalNode(node.callee, scope);
|
|
850
|
+
if (fn == null && node.type === 'OptionalCallExpr') return undefined;
|
|
851
|
+
if (typeof fn !== 'function') return undefined;
|
|
852
|
+
return fn.apply(undefined, evalArgs(node.args));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
case 'ArrayExpr': {
|
|
856
|
+
const arr = [];
|
|
857
|
+
for (let i = 0; i < node.elements.length; i++) {
|
|
858
|
+
const el = node.elements[i];
|
|
859
|
+
if (el.type === 'SpreadElement') {
|
|
860
|
+
const spread = _evalNode(el.argument, scope);
|
|
861
|
+
if (spread && typeof spread[Symbol.iterator] === 'function') {
|
|
862
|
+
arr.push(...spread);
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
arr.push(_evalNode(el, scope));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return arr;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
case 'ObjectExpr': {
|
|
872
|
+
const obj = {};
|
|
873
|
+
for (let i = 0; i < node.properties.length; i++) {
|
|
874
|
+
const prop = node.properties[i];
|
|
875
|
+
if (prop.spread) {
|
|
876
|
+
Object.assign(obj, _evalNode(prop.value, scope));
|
|
877
|
+
} else {
|
|
878
|
+
const key = prop.computed ? _evalNode(prop.key, scope) : prop.key;
|
|
879
|
+
if (_FORBIDDEN_PROPS[key]) continue;
|
|
880
|
+
obj[key] = _evalNode(prop.value, scope);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return obj;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
case 'SpreadElement':
|
|
887
|
+
return _evalNode(node.argument, scope);
|
|
888
|
+
|
|
889
|
+
case 'ArrowFunction':
|
|
890
|
+
return function (...callArgs) {
|
|
891
|
+
const childScope = Object.create(scope);
|
|
892
|
+
for (let i = 0; i < node.params.length; i++) {
|
|
893
|
+
const p = node.params[i];
|
|
894
|
+
if (typeof p === 'string' && p.startsWith('...')) {
|
|
895
|
+
childScope[p.slice(3)] = callArgs.slice(i);
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
childScope[p] = callArgs[i];
|
|
899
|
+
}
|
|
900
|
+
return _evalNode(node.body, childScope);
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
case 'TemplateLiteral': {
|
|
904
|
+
let result = node.parts[0];
|
|
905
|
+
for (let i = 0; i < node.expressions.length; i++) {
|
|
906
|
+
result += String(_evalNode(node.expressions[i], scope));
|
|
907
|
+
result += node.parts[i + 1];
|
|
908
|
+
}
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
case 'PostfixExpr': {
|
|
913
|
+
// In expression context, return the current value (no mutation)
|
|
914
|
+
return _evalNode(node.argument, scope);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
case 'AssignExpr': {
|
|
918
|
+
// In expression context, evaluate and return the RHS
|
|
919
|
+
return _evalNode(node.right, scope);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
default:
|
|
923
|
+
return undefined;
|
|
79
924
|
}
|
|
925
|
+
} catch (_e) {
|
|
926
|
+
return undefined;
|
|
80
927
|
}
|
|
928
|
+
}
|
|
81
929
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
// Statement parser & executor (for on:*, watch, etc.)
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
|
|
934
|
+
// Parse semicolon-separated statements into an array of AST nodes
|
|
935
|
+
function _parseStatements(expr) {
|
|
936
|
+
if (_stmtCache.has(expr)) return _stmtCache.get(expr);
|
|
937
|
+
const tokens = _tokenize(expr);
|
|
938
|
+
const stmts = [];
|
|
939
|
+
let start = 0;
|
|
940
|
+
for (let i = 0; i <= tokens.length; i++) {
|
|
941
|
+
if (i === tokens.length || (tokens[i].type === "Punc" && tokens[i].value === ";")) {
|
|
942
|
+
const chunk = tokens.slice(start, i);
|
|
943
|
+
if (chunk.length > 0) stmts.push(_parseExpr(chunk));
|
|
944
|
+
start = i + 1;
|
|
945
|
+
}
|
|
85
946
|
}
|
|
947
|
+
_stmtCache.set(expr, stmts);
|
|
948
|
+
return stmts;
|
|
949
|
+
}
|
|
86
950
|
|
|
87
|
-
|
|
951
|
+
// Assign a value to an AST target node (Identifier or MemberExpr)
|
|
952
|
+
function _assignToTarget(target, value, scope) {
|
|
953
|
+
if (target.type === "Identifier") {
|
|
954
|
+
scope[target.name] = value;
|
|
955
|
+
} else if (target.type === "MemberExpr" || target.type === "OptionalMemberExpr") {
|
|
956
|
+
const obj = _evalNode(target.object, scope);
|
|
957
|
+
if (obj == null) return;
|
|
958
|
+
const prop = target.computed
|
|
959
|
+
? _evalNode(target.property, scope)
|
|
960
|
+
: target.property.name || target.property.value;
|
|
961
|
+
if (_FORBIDDEN_PROPS[prop]) return;
|
|
962
|
+
obj[prop] = value;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Execute a single statement node with mutation support
|
|
967
|
+
function _execStmtNode(node, scope) {
|
|
968
|
+
if (!node) return undefined;
|
|
969
|
+
switch (node.type) {
|
|
970
|
+
case "AssignExpr": {
|
|
971
|
+
const rhs = _evalNode(node.right, scope);
|
|
972
|
+
let value;
|
|
973
|
+
if (node.op === "=") {
|
|
974
|
+
value = rhs;
|
|
975
|
+
} else {
|
|
976
|
+
const lhs = _evalNode(node.left, scope);
|
|
977
|
+
switch (node.op) {
|
|
978
|
+
case "+=": value = lhs + rhs; break;
|
|
979
|
+
case "-=": value = lhs - rhs; break;
|
|
980
|
+
case "*=": value = lhs * rhs; break;
|
|
981
|
+
case "/=": value = lhs / rhs; break;
|
|
982
|
+
case "%=": value = lhs % rhs; break;
|
|
983
|
+
default: value = rhs;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
_assignToTarget(node.left, value, scope);
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
989
|
+
case "PostfixExpr": {
|
|
990
|
+
const oldVal = _evalNode(node.argument, scope);
|
|
991
|
+
const newVal = node.op === "++" ? oldVal + 1 : oldVal - 1;
|
|
992
|
+
_assignToTarget(node.argument, newVal, scope);
|
|
993
|
+
return oldVal;
|
|
994
|
+
}
|
|
995
|
+
case "UnaryExpr": {
|
|
996
|
+
if (node.op === "++" || node.op === "--") {
|
|
997
|
+
const oldVal = _evalNode(node.argument, scope);
|
|
998
|
+
const newVal = node.op === "++" ? oldVal + 1 : oldVal - 1;
|
|
999
|
+
_assignToTarget(node.argument, newVal, scope);
|
|
1000
|
+
return newVal;
|
|
1001
|
+
}
|
|
1002
|
+
return _evalNode(node, scope);
|
|
1003
|
+
}
|
|
1004
|
+
default: {
|
|
1005
|
+
// In statement context, throw for undefined function calls
|
|
1006
|
+
// so error-boundary directives can catch the error
|
|
1007
|
+
if (node.type === "CallExpr" && node.callee.type === "Identifier") {
|
|
1008
|
+
const name = node.callee.name;
|
|
1009
|
+
if (!(name in scope) && !(name in _SAFE_GLOBALS) &&
|
|
1010
|
+
(typeof globalThis === "undefined" || !(name in globalThis))) {
|
|
1011
|
+
throw new ReferenceError(name + " is not defined");
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return _evalNode(node, scope);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
88
1017
|
}
|
|
89
1018
|
|
|
90
1019
|
// Parse pipe syntax: "expr | filter1 | filter2:arg"
|
|
@@ -211,22 +1140,20 @@ export function evaluate(expr, ctx) {
|
|
|
211
1140
|
}
|
|
212
1141
|
}
|
|
213
1142
|
|
|
214
|
-
|
|
215
|
-
const
|
|
1143
|
+
// Build scope object from keys/vals
|
|
1144
|
+
const scope = {};
|
|
1145
|
+
for (let i = 0; i < keys.length; i++) scope[keys[i]] = vals[keys[i]];
|
|
216
1146
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
let fn = _exprCache.get(cacheKey);
|
|
223
|
-
if (!fn) {
|
|
224
|
-
fn = new Function(...keyArr, `return (${mainExpr})`);
|
|
225
|
-
_exprCache.set(cacheKey, fn);
|
|
226
|
-
}
|
|
227
|
-
result = fn(...valArr);
|
|
1147
|
+
// Parse expression into AST (cached)
|
|
1148
|
+
let ast = _exprCache.get(mainExpr);
|
|
1149
|
+
if (!ast) {
|
|
1150
|
+
ast = _parseExpr(_tokenize(mainExpr));
|
|
1151
|
+
_exprCache.set(mainExpr, ast);
|
|
228
1152
|
}
|
|
229
1153
|
|
|
1154
|
+
// Evaluate AST against scope
|
|
1155
|
+
let result = _evalNode(ast, scope);
|
|
1156
|
+
|
|
230
1157
|
// Apply filters
|
|
231
1158
|
for (let i = 1; i < pipes.length; i++) {
|
|
232
1159
|
result = _applyFilter(result, pipes[i]);
|
|
@@ -234,6 +1161,7 @@ export function evaluate(expr, ctx) {
|
|
|
234
1161
|
|
|
235
1162
|
return result;
|
|
236
1163
|
} catch (e) {
|
|
1164
|
+
_warn("Expression error:", expr, e.message);
|
|
237
1165
|
return undefined;
|
|
238
1166
|
}
|
|
239
1167
|
}
|
|
@@ -258,35 +1186,53 @@ export function _execStatement(expr, ctx, extraVars = {}) {
|
|
|
258
1186
|
}
|
|
259
1187
|
}
|
|
260
1188
|
|
|
261
|
-
|
|
262
|
-
const
|
|
1189
|
+
// Build scope
|
|
1190
|
+
const scope = {};
|
|
1191
|
+
for (let i = 0; i < keys.length; i++) scope[keys[i]] = vals[keys[i]];
|
|
263
1192
|
|
|
264
|
-
//
|
|
265
|
-
// For each key in any ancestor context, find the owning context at runtime
|
|
266
|
-
// and call $set on it — so mutations inside `each` loops correctly
|
|
267
|
-
// propagate back to parent state (e.g. cart updated from a loop's on:click).
|
|
268
|
-
// Only write back values that actually changed locally, to avoid
|
|
269
|
-
// overwriting proxy mutations made by called functions.
|
|
1193
|
+
// Snapshot context chain values for write-back comparison
|
|
270
1194
|
const chainKeys = new Set();
|
|
271
1195
|
let _wCtx = ctx;
|
|
272
1196
|
while (_wCtx && _wCtx.__isProxy) {
|
|
273
1197
|
for (const k of Object.keys(_wCtx.__raw)) chainKeys.add(k);
|
|
274
1198
|
_wCtx = _wCtx.$parent;
|
|
275
1199
|
}
|
|
276
|
-
const
|
|
1200
|
+
const originals = {};
|
|
277
1201
|
for (const k of chainKeys) {
|
|
278
|
-
if (!k.startsWith("$") && k in
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1202
|
+
if (!k.startsWith("$") && k in scope) originals[k] = scope[k];
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Parse and execute statements
|
|
1206
|
+
const stmts = _parseStatements(expr);
|
|
1207
|
+
for (let i = 0; i < stmts.length; i++) _execStmtNode(stmts[i], scope);
|
|
1208
|
+
|
|
1209
|
+
// Write back changed values to owning context
|
|
1210
|
+
for (const k of chainKeys) {
|
|
1211
|
+
if (k.startsWith("$")) continue;
|
|
1212
|
+
if (!(k in scope)) continue;
|
|
1213
|
+
const newVal = scope[k];
|
|
1214
|
+
const oldVal = originals[k];
|
|
1215
|
+
if (newVal !== oldVal) {
|
|
1216
|
+
let c = ctx;
|
|
1217
|
+
while (c && c.__isProxy) {
|
|
1218
|
+
if (k in c.__raw) { c.$set(k, newVal); break; }
|
|
1219
|
+
c = c.$parent;
|
|
1220
|
+
}
|
|
1221
|
+
} else if (typeof newVal === "object" && newVal !== null) {
|
|
1222
|
+
let c = ctx;
|
|
1223
|
+
while (c && c.__isProxy) {
|
|
1224
|
+
if (k in c.__raw) { c.$notify(); break; }
|
|
1225
|
+
c = c.$parent;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Write back new variables created during execution
|
|
1231
|
+
for (const k in scope) {
|
|
1232
|
+
if (k.startsWith("$") || chainKeys.has(k)) continue;
|
|
1233
|
+
if (k in vals) continue;
|
|
1234
|
+
ctx.$set(k, scope[k]);
|
|
1235
|
+
}
|
|
290
1236
|
|
|
291
1237
|
// Notify global store watchers when expression touches $store
|
|
292
1238
|
if (typeof expr === "string" && expr.includes("$store")) {
|