zero-query 0.5.2 → 0.7.5

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.
Files changed (58) hide show
  1. package/README.md +12 -10
  2. package/cli/commands/build.js +7 -5
  3. package/cli/commands/bundle.js +286 -8
  4. package/cli/commands/dev/index.js +82 -0
  5. package/cli/commands/dev/logger.js +70 -0
  6. package/cli/commands/dev/overlay.js +366 -0
  7. package/cli/commands/dev/server.js +158 -0
  8. package/cli/commands/dev/validator.js +94 -0
  9. package/cli/commands/dev/watcher.js +147 -0
  10. package/cli/scaffold/favicon.ico +0 -0
  11. package/cli/scaffold/index.html +1 -0
  12. package/cli/scaffold/scripts/app.js +15 -22
  13. package/cli/scaffold/scripts/components/about.js +14 -2
  14. package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
  15. package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
  16. package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
  17. package/cli/scaffold/scripts/components/counter.js +30 -10
  18. package/cli/scaffold/scripts/components/home.js +3 -3
  19. package/cli/scaffold/scripts/components/todos.js +6 -5
  20. package/cli/scaffold/styles/styles.css +1 -0
  21. package/cli/utils.js +111 -6
  22. package/dist/zquery.dist.zip +0 -0
  23. package/dist/zquery.js +2005 -216
  24. package/dist/zquery.min.js +3 -13
  25. package/index.d.ts +149 -1080
  26. package/index.js +18 -7
  27. package/package.json +9 -3
  28. package/src/component.js +186 -45
  29. package/src/core.js +327 -35
  30. package/src/diff.js +280 -0
  31. package/src/errors.js +155 -0
  32. package/src/expression.js +806 -0
  33. package/src/http.js +18 -10
  34. package/src/reactive.js +29 -4
  35. package/src/router.js +59 -6
  36. package/src/ssr.js +224 -0
  37. package/src/store.js +24 -8
  38. package/tests/component.test.js +304 -0
  39. package/tests/core.test.js +726 -0
  40. package/tests/diff.test.js +194 -0
  41. package/tests/errors.test.js +162 -0
  42. package/tests/expression.test.js +334 -0
  43. package/tests/http.test.js +181 -0
  44. package/tests/reactive.test.js +191 -0
  45. package/tests/router.test.js +332 -0
  46. package/tests/store.test.js +253 -0
  47. package/tests/utils.test.js +353 -0
  48. package/types/collection.d.ts +368 -0
  49. package/types/component.d.ts +210 -0
  50. package/types/errors.d.ts +103 -0
  51. package/types/http.d.ts +81 -0
  52. package/types/misc.d.ts +166 -0
  53. package/types/reactive.d.ts +76 -0
  54. package/types/router.d.ts +132 -0
  55. package/types/ssr.d.ts +49 -0
  56. package/types/store.d.ts +107 -0
  57. package/types/utils.d.ts +142 -0
  58. /package/cli/commands/{dev.js → dev.old.js} +0 -0
@@ -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
+ }