zero-query 0.4.9 → 0.6.3
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 +16 -10
- package/cli/commands/build.js +4 -2
- package/cli/commands/bundle.js +113 -10
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +317 -0
- package/cli/commands/dev/server.js +129 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +114 -0
- package/cli/commands/{dev.js → dev.old.js} +8 -4
- package/cli/help.js +18 -6
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.html +5 -4
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1550 -97
- package/dist/zquery.min.js +11 -8
- package/index.d.ts +253 -14
- package/index.js +25 -8
- package/package.json +8 -2
- package/src/component.js +175 -44
- package/src/core.js +25 -18
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +11 -5
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zQuery Expression Parser — CSP-safe expression evaluator
|
|
3
|
+
*
|
|
4
|
+
* Replaces `new Function()` / `eval()` with a hand-written parser that
|
|
5
|
+
* evaluates expressions safely without violating Content Security Policy.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Property access: user.name, items[0], items[i]
|
|
9
|
+
* - Method calls: items.length, str.toUpperCase()
|
|
10
|
+
* - Arithmetic: a + b, count * 2, i % 2
|
|
11
|
+
* - Comparison: a === b, count > 0, x != null
|
|
12
|
+
* - Logical: a && b, a || b, !a
|
|
13
|
+
* - Ternary: a ? b : c
|
|
14
|
+
* - Typeof: typeof x
|
|
15
|
+
* - Unary: -a, +a, !a
|
|
16
|
+
* - Literals: 42, 'hello', "world", true, false, null, undefined
|
|
17
|
+
* - Template literals: `Hello ${name}`
|
|
18
|
+
* - Array literals: [1, 2, 3]
|
|
19
|
+
* - Object literals: { foo: 'bar', baz: 1 }
|
|
20
|
+
* - Grouping: (a + b) * c
|
|
21
|
+
* - Nullish coalescing: a ?? b
|
|
22
|
+
* - Optional chaining: a?.b, a?.[b], a?.()
|
|
23
|
+
* - Arrow functions: x => x.id, (a, b) => a + b
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Token types
|
|
27
|
+
const T = {
|
|
28
|
+
NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Operator precedence (higher = binds tighter)
|
|
32
|
+
const PREC = {
|
|
33
|
+
'??': 2,
|
|
34
|
+
'||': 3,
|
|
35
|
+
'&&': 4,
|
|
36
|
+
'==': 8, '!=': 8, '===': 8, '!==': 8,
|
|
37
|
+
'<': 9, '>': 9, '<=': 9, '>=': 9, 'instanceof': 9, 'in': 9,
|
|
38
|
+
'+': 11, '-': 11,
|
|
39
|
+
'*': 12, '/': 12, '%': 12,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const KEYWORDS = new Set([
|
|
43
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
44
|
+
'new', 'void'
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Tokenizer
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
function tokenize(expr) {
|
|
51
|
+
const tokens = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
const len = expr.length;
|
|
54
|
+
|
|
55
|
+
while (i < len) {
|
|
56
|
+
const ch = expr[i];
|
|
57
|
+
|
|
58
|
+
// Whitespace
|
|
59
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; }
|
|
60
|
+
|
|
61
|
+
// Numbers
|
|
62
|
+
if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < len && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {
|
|
63
|
+
let num = '';
|
|
64
|
+
if (ch === '0' && i + 1 < len && (expr[i + 1] === 'x' || expr[i + 1] === 'X')) {
|
|
65
|
+
num = '0x'; i += 2;
|
|
66
|
+
while (i < len && /[0-9a-fA-F]/.test(expr[i])) num += expr[i++];
|
|
67
|
+
} else {
|
|
68
|
+
while (i < len && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) num += expr[i++];
|
|
69
|
+
if (i < len && (expr[i] === 'e' || expr[i] === 'E')) {
|
|
70
|
+
num += expr[i++];
|
|
71
|
+
if (i < len && (expr[i] === '+' || expr[i] === '-')) num += expr[i++];
|
|
72
|
+
while (i < len && expr[i] >= '0' && expr[i] <= '9') num += expr[i++];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
tokens.push({ t: T.NUM, v: Number(num) });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Strings
|
|
80
|
+
if (ch === "'" || ch === '"') {
|
|
81
|
+
const quote = ch;
|
|
82
|
+
let str = '';
|
|
83
|
+
i++;
|
|
84
|
+
while (i < len && expr[i] !== quote) {
|
|
85
|
+
if (expr[i] === '\\' && i + 1 < len) {
|
|
86
|
+
const esc = expr[++i];
|
|
87
|
+
if (esc === 'n') str += '\n';
|
|
88
|
+
else if (esc === 't') str += '\t';
|
|
89
|
+
else if (esc === 'r') str += '\r';
|
|
90
|
+
else if (esc === '\\') str += '\\';
|
|
91
|
+
else if (esc === quote) str += quote;
|
|
92
|
+
else str += esc;
|
|
93
|
+
} else {
|
|
94
|
+
str += expr[i];
|
|
95
|
+
}
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
i++; // closing quote
|
|
99
|
+
tokens.push({ t: T.STR, v: str });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Template literals
|
|
104
|
+
if (ch === '`') {
|
|
105
|
+
const parts = []; // alternating: string, expr, string, expr, ...
|
|
106
|
+
let str = '';
|
|
107
|
+
i++;
|
|
108
|
+
while (i < len && expr[i] !== '`') {
|
|
109
|
+
if (expr[i] === '$' && i + 1 < len && expr[i + 1] === '{') {
|
|
110
|
+
parts.push(str);
|
|
111
|
+
str = '';
|
|
112
|
+
i += 2;
|
|
113
|
+
let depth = 1;
|
|
114
|
+
let inner = '';
|
|
115
|
+
while (i < len && depth > 0) {
|
|
116
|
+
if (expr[i] === '{') depth++;
|
|
117
|
+
else if (expr[i] === '}') { depth--; if (depth === 0) break; }
|
|
118
|
+
inner += expr[i++];
|
|
119
|
+
}
|
|
120
|
+
i++; // closing }
|
|
121
|
+
parts.push({ expr: inner });
|
|
122
|
+
} else {
|
|
123
|
+
if (expr[i] === '\\' && i + 1 < len) { str += expr[++i]; }
|
|
124
|
+
else str += expr[i];
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
i++; // closing backtick
|
|
129
|
+
parts.push(str);
|
|
130
|
+
tokens.push({ t: T.TMPL, v: parts });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Identifiers & keywords
|
|
135
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
136
|
+
let ident = '';
|
|
137
|
+
while (i < len && /[\w$]/.test(expr[i])) ident += expr[i++];
|
|
138
|
+
tokens.push({ t: T.IDENT, v: ident });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Multi-char operators
|
|
143
|
+
const two = expr.slice(i, i + 3);
|
|
144
|
+
if (two === '===' || two === '!==' || two === '?.') {
|
|
145
|
+
if (two === '?.') {
|
|
146
|
+
tokens.push({ t: T.OP, v: '?.' });
|
|
147
|
+
i += 2;
|
|
148
|
+
} else {
|
|
149
|
+
tokens.push({ t: T.OP, v: two });
|
|
150
|
+
i += 3;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const pair = expr.slice(i, i + 2);
|
|
155
|
+
if (pair === '==' || pair === '!=' || pair === '<=' || pair === '>=' ||
|
|
156
|
+
pair === '&&' || pair === '||' || pair === '??' || pair === '?.' ||
|
|
157
|
+
pair === '=>') {
|
|
158
|
+
tokens.push({ t: T.OP, v: pair });
|
|
159
|
+
i += 2;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Single char operators and punctuation
|
|
164
|
+
if ('+-*/%'.includes(ch)) {
|
|
165
|
+
tokens.push({ t: T.OP, v: ch });
|
|
166
|
+
i++; continue;
|
|
167
|
+
}
|
|
168
|
+
if ('<>=!'.includes(ch)) {
|
|
169
|
+
tokens.push({ t: T.OP, v: ch });
|
|
170
|
+
i++; continue;
|
|
171
|
+
}
|
|
172
|
+
if ('()[]{},.?:'.includes(ch)) {
|
|
173
|
+
tokens.push({ t: T.PUNC, v: ch });
|
|
174
|
+
i++; continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Unknown — skip
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
tokens.push({ t: T.EOF, v: null });
|
|
182
|
+
return tokens;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Parser — Pratt (precedence climbing)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
class Parser {
|
|
189
|
+
constructor(tokens, scope) {
|
|
190
|
+
this.tokens = tokens;
|
|
191
|
+
this.pos = 0;
|
|
192
|
+
this.scope = scope;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
peek() { return this.tokens[this.pos]; }
|
|
196
|
+
next() { return this.tokens[this.pos++]; }
|
|
197
|
+
|
|
198
|
+
expect(type, val) {
|
|
199
|
+
const t = this.next();
|
|
200
|
+
if (t.t !== type || (val !== undefined && t.v !== val)) {
|
|
201
|
+
throw new Error(`Expected ${val || type} but got ${t.v}`);
|
|
202
|
+
}
|
|
203
|
+
return t;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
match(type, val) {
|
|
207
|
+
const t = this.peek();
|
|
208
|
+
if (t.t === type && (val === undefined || t.v === val)) {
|
|
209
|
+
return this.next();
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Main entry
|
|
215
|
+
parse() {
|
|
216
|
+
const result = this.parseExpression(0);
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Precedence climbing
|
|
221
|
+
parseExpression(minPrec) {
|
|
222
|
+
let left = this.parseUnary();
|
|
223
|
+
|
|
224
|
+
while (true) {
|
|
225
|
+
const tok = this.peek();
|
|
226
|
+
|
|
227
|
+
// Ternary
|
|
228
|
+
if (tok.t === T.PUNC && tok.v === '?') {
|
|
229
|
+
// Distinguish ternary ? from optional chaining ?.
|
|
230
|
+
if (this.tokens[this.pos + 1]?.v !== '.') {
|
|
231
|
+
if (1 <= minPrec) break; // ternary has very low precedence
|
|
232
|
+
this.next(); // consume ?
|
|
233
|
+
const truthy = this.parseExpression(0);
|
|
234
|
+
this.expect(T.PUNC, ':');
|
|
235
|
+
const falsy = this.parseExpression(1);
|
|
236
|
+
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Binary operators
|
|
242
|
+
if (tok.t === T.OP && tok.v in PREC) {
|
|
243
|
+
const prec = PREC[tok.v];
|
|
244
|
+
if (prec <= minPrec) break;
|
|
245
|
+
this.next();
|
|
246
|
+
const right = this.parseExpression(prec);
|
|
247
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// instanceof and in as binary operators
|
|
252
|
+
if (tok.t === T.IDENT && (tok.v === 'instanceof' || tok.v === 'in') && PREC[tok.v] > minPrec) {
|
|
253
|
+
const prec = PREC[tok.v];
|
|
254
|
+
this.next();
|
|
255
|
+
const right = this.parseExpression(prec);
|
|
256
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return left;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
parseUnary() {
|
|
267
|
+
const tok = this.peek();
|
|
268
|
+
|
|
269
|
+
// typeof
|
|
270
|
+
if (tok.t === T.IDENT && tok.v === 'typeof') {
|
|
271
|
+
this.next();
|
|
272
|
+
const arg = this.parseUnary();
|
|
273
|
+
return { type: 'typeof', arg };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// void
|
|
277
|
+
if (tok.t === T.IDENT && tok.v === 'void') {
|
|
278
|
+
this.next();
|
|
279
|
+
this.parseUnary(); // evaluate but discard
|
|
280
|
+
return { type: 'literal', value: undefined };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// !expr
|
|
284
|
+
if (tok.t === T.OP && tok.v === '!') {
|
|
285
|
+
this.next();
|
|
286
|
+
const arg = this.parseUnary();
|
|
287
|
+
return { type: 'not', arg };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// -expr, +expr
|
|
291
|
+
if (tok.t === T.OP && (tok.v === '-' || tok.v === '+')) {
|
|
292
|
+
this.next();
|
|
293
|
+
const arg = this.parseUnary();
|
|
294
|
+
return { type: 'unary', op: tok.v, arg };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return this.parsePostfix();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
parsePostfix() {
|
|
301
|
+
let left = this.parsePrimary();
|
|
302
|
+
|
|
303
|
+
while (true) {
|
|
304
|
+
const tok = this.peek();
|
|
305
|
+
|
|
306
|
+
// Property access: a.b
|
|
307
|
+
if (tok.t === T.PUNC && tok.v === '.') {
|
|
308
|
+
this.next();
|
|
309
|
+
const prop = this.next();
|
|
310
|
+
left = { type: 'member', obj: left, prop: prop.v, computed: false };
|
|
311
|
+
// Check for method call: a.b()
|
|
312
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
313
|
+
left = this._parseCall(left);
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Optional chaining: a?.b, a?.[b], a?.()
|
|
319
|
+
if (tok.t === T.OP && tok.v === '?.') {
|
|
320
|
+
this.next();
|
|
321
|
+
const next = this.peek();
|
|
322
|
+
if (next.t === T.PUNC && next.v === '[') {
|
|
323
|
+
// a?.[expr]
|
|
324
|
+
this.next();
|
|
325
|
+
const prop = this.parseExpression(0);
|
|
326
|
+
this.expect(T.PUNC, ']');
|
|
327
|
+
left = { type: 'optional_member', obj: left, prop, computed: true };
|
|
328
|
+
} else if (next.t === T.PUNC && next.v === '(') {
|
|
329
|
+
// a?.()
|
|
330
|
+
left = { type: 'optional_call', callee: left, args: this._parseArgs() };
|
|
331
|
+
} else {
|
|
332
|
+
// a?.b
|
|
333
|
+
const prop = this.next();
|
|
334
|
+
left = { type: 'optional_member', obj: left, prop: prop.v, computed: false };
|
|
335
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
336
|
+
left = this._parseCall(left);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Computed access: a[b]
|
|
343
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
344
|
+
this.next();
|
|
345
|
+
const prop = this.parseExpression(0);
|
|
346
|
+
this.expect(T.PUNC, ']');
|
|
347
|
+
left = { type: 'member', obj: left, prop, computed: true };
|
|
348
|
+
// Check for method call: a[b]()
|
|
349
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
350
|
+
left = this._parseCall(left);
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Function call: fn()
|
|
356
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
357
|
+
left = this._parseCall(left);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return left;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_parseCall(callee) {
|
|
368
|
+
const args = this._parseArgs();
|
|
369
|
+
return { type: 'call', callee, args };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_parseArgs() {
|
|
373
|
+
this.expect(T.PUNC, '(');
|
|
374
|
+
const args = [];
|
|
375
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
376
|
+
args.push(this.parseExpression(0));
|
|
377
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
378
|
+
}
|
|
379
|
+
this.expect(T.PUNC, ')');
|
|
380
|
+
return args;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
parsePrimary() {
|
|
384
|
+
const tok = this.peek();
|
|
385
|
+
|
|
386
|
+
// Number literal
|
|
387
|
+
if (tok.t === T.NUM) {
|
|
388
|
+
this.next();
|
|
389
|
+
return { type: 'literal', value: tok.v };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// String literal
|
|
393
|
+
if (tok.t === T.STR) {
|
|
394
|
+
this.next();
|
|
395
|
+
return { type: 'literal', value: tok.v };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Template literal
|
|
399
|
+
if (tok.t === T.TMPL) {
|
|
400
|
+
this.next();
|
|
401
|
+
return { type: 'template', parts: tok.v };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Arrow function with parens: () =>, (a) =>, (a, b) =>
|
|
405
|
+
// or regular grouping: (expr)
|
|
406
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
407
|
+
const savedPos = this.pos;
|
|
408
|
+
this.next(); // consume (
|
|
409
|
+
const params = [];
|
|
410
|
+
let couldBeArrow = true;
|
|
411
|
+
|
|
412
|
+
if (this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
413
|
+
// () => ... — no params
|
|
414
|
+
} else {
|
|
415
|
+
while (couldBeArrow) {
|
|
416
|
+
const p = this.peek();
|
|
417
|
+
if (p.t === T.IDENT && !KEYWORDS.has(p.v)) {
|
|
418
|
+
params.push(this.next().v);
|
|
419
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') {
|
|
420
|
+
this.next();
|
|
421
|
+
} else {
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
couldBeArrow = false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (couldBeArrow && this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
431
|
+
this.next(); // consume )
|
|
432
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
433
|
+
this.next(); // consume =>
|
|
434
|
+
const body = this.parseExpression(0);
|
|
435
|
+
return { type: 'arrow', params, body };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Not an arrow — restore and parse as grouping
|
|
440
|
+
this.pos = savedPos;
|
|
441
|
+
this.next(); // consume (
|
|
442
|
+
const expr = this.parseExpression(0);
|
|
443
|
+
this.expect(T.PUNC, ')');
|
|
444
|
+
return expr;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Array literal
|
|
448
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
449
|
+
this.next();
|
|
450
|
+
const elements = [];
|
|
451
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
452
|
+
elements.push(this.parseExpression(0));
|
|
453
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
454
|
+
}
|
|
455
|
+
this.expect(T.PUNC, ']');
|
|
456
|
+
return { type: 'array', elements };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Object literal
|
|
460
|
+
if (tok.t === T.PUNC && tok.v === '{') {
|
|
461
|
+
this.next();
|
|
462
|
+
const properties = [];
|
|
463
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
464
|
+
const keyTok = this.next();
|
|
465
|
+
let key;
|
|
466
|
+
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
467
|
+
else if (keyTok.t === T.NUM) key = String(keyTok.v);
|
|
468
|
+
else throw new Error('Invalid object key: ' + keyTok.v);
|
|
469
|
+
|
|
470
|
+
// Shorthand property: { foo } means { foo: foo }
|
|
471
|
+
if (this.peek().t === T.PUNC && (this.peek().v === ',' || this.peek().v === '}')) {
|
|
472
|
+
properties.push({ key, value: { type: 'ident', name: key } });
|
|
473
|
+
} else {
|
|
474
|
+
this.expect(T.PUNC, ':');
|
|
475
|
+
properties.push({ key, value: this.parseExpression(0) });
|
|
476
|
+
}
|
|
477
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
478
|
+
}
|
|
479
|
+
this.expect(T.PUNC, '}');
|
|
480
|
+
return { type: 'object', properties };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Identifiers & keywords
|
|
484
|
+
if (tok.t === T.IDENT) {
|
|
485
|
+
this.next();
|
|
486
|
+
|
|
487
|
+
// Keywords
|
|
488
|
+
if (tok.v === 'true') return { type: 'literal', value: true };
|
|
489
|
+
if (tok.v === 'false') return { type: 'literal', value: false };
|
|
490
|
+
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
491
|
+
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
492
|
+
|
|
493
|
+
// new keyword
|
|
494
|
+
if (tok.v === 'new') {
|
|
495
|
+
const classExpr = this.parsePostfix();
|
|
496
|
+
let args = [];
|
|
497
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
498
|
+
args = this._parseArgs();
|
|
499
|
+
}
|
|
500
|
+
return { type: 'new', callee: classExpr, args };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Arrow function: x => expr
|
|
504
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
505
|
+
this.next(); // consume =>
|
|
506
|
+
const body = this.parseExpression(0);
|
|
507
|
+
return { type: 'arrow', params: [tok.v], body };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { type: 'ident', name: tok.v };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Fallback — return undefined for unparseable
|
|
514
|
+
this.next();
|
|
515
|
+
return { type: 'literal', value: undefined };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Evaluator — walks the AST, resolves against scope
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
/** Safe property access whitelist for built-in prototypes */
|
|
524
|
+
const SAFE_ARRAY_METHODS = new Set([
|
|
525
|
+
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
526
|
+
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
527
|
+
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
528
|
+
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
const SAFE_STRING_METHODS = new Set([
|
|
532
|
+
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
533
|
+
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
534
|
+
'toUpperCase', 'split', 'replace', 'replaceAll', 'match', 'search',
|
|
535
|
+
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'at',
|
|
536
|
+
'toString', 'valueOf',
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
const SAFE_NUMBER_METHODS = new Set([
|
|
540
|
+
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
const SAFE_OBJECT_METHODS = new Set([
|
|
544
|
+
'hasOwnProperty', 'toString', 'valueOf',
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
const SAFE_MATH_PROPS = new Set([
|
|
548
|
+
'PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2',
|
|
549
|
+
'abs', 'ceil', 'floor', 'round', 'trunc', 'max', 'min', 'pow',
|
|
550
|
+
'sqrt', 'sign', 'random', 'log', 'log2', 'log10',
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
const SAFE_JSON_PROPS = new Set(['parse', 'stringify']);
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Check if property access is safe
|
|
557
|
+
*/
|
|
558
|
+
function _isSafeAccess(obj, prop) {
|
|
559
|
+
// Never allow access to dangerous properties
|
|
560
|
+
const BLOCKED = new Set([
|
|
561
|
+
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
562
|
+
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
563
|
+
]);
|
|
564
|
+
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
565
|
+
|
|
566
|
+
// Always allow plain object/function property access and array index access
|
|
567
|
+
if (obj !== null && obj !== undefined && (typeof obj === 'object' || typeof obj === 'function')) return true;
|
|
568
|
+
if (typeof obj === 'string') return SAFE_STRING_METHODS.has(prop);
|
|
569
|
+
if (typeof obj === 'number') return SAFE_NUMBER_METHODS.has(prop);
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function evaluate(node, scope) {
|
|
574
|
+
if (!node) return undefined;
|
|
575
|
+
|
|
576
|
+
switch (node.type) {
|
|
577
|
+
case 'literal':
|
|
578
|
+
return node.value;
|
|
579
|
+
|
|
580
|
+
case 'ident': {
|
|
581
|
+
const name = node.name;
|
|
582
|
+
// Check scope layers in order
|
|
583
|
+
for (const layer of scope) {
|
|
584
|
+
if (layer && typeof layer === 'object' && name in layer) {
|
|
585
|
+
return layer[name];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Built-in globals (safe ones only)
|
|
589
|
+
if (name === 'Math') return Math;
|
|
590
|
+
if (name === 'JSON') return JSON;
|
|
591
|
+
if (name === 'Date') return Date;
|
|
592
|
+
if (name === 'Array') return Array;
|
|
593
|
+
if (name === 'Object') return Object;
|
|
594
|
+
if (name === 'String') return String;
|
|
595
|
+
if (name === 'Number') return Number;
|
|
596
|
+
if (name === 'Boolean') return Boolean;
|
|
597
|
+
if (name === 'parseInt') return parseInt;
|
|
598
|
+
if (name === 'parseFloat') return parseFloat;
|
|
599
|
+
if (name === 'isNaN') return isNaN;
|
|
600
|
+
if (name === 'isFinite') return isFinite;
|
|
601
|
+
if (name === 'Infinity') return Infinity;
|
|
602
|
+
if (name === 'NaN') return NaN;
|
|
603
|
+
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
604
|
+
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
605
|
+
if (name === 'console') return console;
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
case 'template': {
|
|
610
|
+
// Template literal with interpolation
|
|
611
|
+
let result = '';
|
|
612
|
+
for (const part of node.parts) {
|
|
613
|
+
if (typeof part === 'string') {
|
|
614
|
+
result += part;
|
|
615
|
+
} else if (part && part.expr) {
|
|
616
|
+
result += String(safeEval(part.expr, scope) ?? '');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
case 'member': {
|
|
623
|
+
const obj = evaluate(node.obj, scope);
|
|
624
|
+
if (obj == null) return undefined;
|
|
625
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
626
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
627
|
+
return obj[prop];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
case 'optional_member': {
|
|
631
|
+
const obj = evaluate(node.obj, scope);
|
|
632
|
+
if (obj == null) return undefined;
|
|
633
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
634
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
635
|
+
return obj[prop];
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
case 'call': {
|
|
639
|
+
const result = _resolveCall(node, scope, false);
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
case 'optional_call': {
|
|
644
|
+
const callee = evaluate(node.callee, scope);
|
|
645
|
+
if (callee == null) return undefined;
|
|
646
|
+
if (typeof callee !== 'function') return undefined;
|
|
647
|
+
const args = node.args.map(a => evaluate(a, scope));
|
|
648
|
+
return callee(...args);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
case 'new': {
|
|
652
|
+
const Ctor = evaluate(node.callee, scope);
|
|
653
|
+
if (typeof Ctor !== 'function') return undefined;
|
|
654
|
+
// Only allow safe constructors
|
|
655
|
+
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
656
|
+
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
657
|
+
const args = node.args.map(a => evaluate(a, scope));
|
|
658
|
+
return new Ctor(...args);
|
|
659
|
+
}
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
case 'binary':
|
|
664
|
+
return _evalBinary(node, scope);
|
|
665
|
+
|
|
666
|
+
case 'unary': {
|
|
667
|
+
const val = evaluate(node.arg, scope);
|
|
668
|
+
return node.op === '-' ? -val : +val;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
case 'not':
|
|
672
|
+
return !evaluate(node.arg, scope);
|
|
673
|
+
|
|
674
|
+
case 'typeof': {
|
|
675
|
+
try {
|
|
676
|
+
return typeof evaluate(node.arg, scope);
|
|
677
|
+
} catch {
|
|
678
|
+
return 'undefined';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
case 'ternary': {
|
|
683
|
+
const cond = evaluate(node.cond, scope);
|
|
684
|
+
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
case 'array':
|
|
688
|
+
return node.elements.map(e => evaluate(e, scope));
|
|
689
|
+
|
|
690
|
+
case 'object': {
|
|
691
|
+
const obj = {};
|
|
692
|
+
for (const { key, value } of node.properties) {
|
|
693
|
+
obj[key] = evaluate(value, scope);
|
|
694
|
+
}
|
|
695
|
+
return obj;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
case 'arrow': {
|
|
699
|
+
const paramNames = node.params;
|
|
700
|
+
const bodyNode = node.body;
|
|
701
|
+
const closedScope = scope;
|
|
702
|
+
return function(...args) {
|
|
703
|
+
const arrowScope = {};
|
|
704
|
+
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
705
|
+
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
default:
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Resolve and execute a function call safely.
|
|
716
|
+
*/
|
|
717
|
+
function _resolveCall(node, scope) {
|
|
718
|
+
const callee = node.callee;
|
|
719
|
+
const args = node.args.map(a => evaluate(a, scope));
|
|
720
|
+
|
|
721
|
+
// Method call: obj.method() — bind `this` to obj
|
|
722
|
+
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
723
|
+
const obj = evaluate(callee.obj, scope);
|
|
724
|
+
if (obj == null) return undefined;
|
|
725
|
+
const prop = callee.computed ? evaluate(callee.prop, scope) : callee.prop;
|
|
726
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
727
|
+
const fn = obj[prop];
|
|
728
|
+
if (typeof fn !== 'function') return undefined;
|
|
729
|
+
return fn.apply(obj, args);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Direct call: fn(args)
|
|
733
|
+
const fn = evaluate(callee, scope);
|
|
734
|
+
if (typeof fn !== 'function') return undefined;
|
|
735
|
+
return fn(...args);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Evaluate binary expression.
|
|
740
|
+
*/
|
|
741
|
+
function _evalBinary(node, scope) {
|
|
742
|
+
// Short-circuit for logical ops
|
|
743
|
+
if (node.op === '&&') {
|
|
744
|
+
const left = evaluate(node.left, scope);
|
|
745
|
+
return left ? evaluate(node.right, scope) : left;
|
|
746
|
+
}
|
|
747
|
+
if (node.op === '||') {
|
|
748
|
+
const left = evaluate(node.left, scope);
|
|
749
|
+
return left ? left : evaluate(node.right, scope);
|
|
750
|
+
}
|
|
751
|
+
if (node.op === '??') {
|
|
752
|
+
const left = evaluate(node.left, scope);
|
|
753
|
+
return left != null ? left : evaluate(node.right, scope);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const left = evaluate(node.left, scope);
|
|
757
|
+
const right = evaluate(node.right, scope);
|
|
758
|
+
|
|
759
|
+
switch (node.op) {
|
|
760
|
+
case '+': return left + right;
|
|
761
|
+
case '-': return left - right;
|
|
762
|
+
case '*': return left * right;
|
|
763
|
+
case '/': return left / 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
|
+
case '>': return left > right;
|
|
771
|
+
case '<=': return left <= right;
|
|
772
|
+
case '>=': return left >= right;
|
|
773
|
+
case 'instanceof': return left instanceof right;
|
|
774
|
+
case 'in': return left in right;
|
|
775
|
+
default: return undefined;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
// Public API
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Safely evaluate a JS expression string against scope layers.
|
|
786
|
+
*
|
|
787
|
+
* @param {string} expr — expression string
|
|
788
|
+
* @param {object[]} scope — array of scope objects, checked in order
|
|
789
|
+
* Typical: [loopVars, state, { props, refs, $ }]
|
|
790
|
+
* @returns {*} — evaluation result, or undefined on error
|
|
791
|
+
*/
|
|
792
|
+
export function safeEval(expr, scope) {
|
|
793
|
+
try {
|
|
794
|
+
const trimmed = expr.trim();
|
|
795
|
+
if (!trimmed) return undefined;
|
|
796
|
+
const tokens = tokenize(trimmed);
|
|
797
|
+
const parser = new Parser(tokens, scope);
|
|
798
|
+
const ast = parser.parse();
|
|
799
|
+
return evaluate(ast, scope);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
802
|
+
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
803
|
+
}
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
}
|