@shaferllc/keel 0.36.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +14 -2
  2. package/dist/core/application.d.ts +58 -2
  3. package/dist/core/application.js +99 -3
  4. package/dist/core/auth.d.ts +21 -2
  5. package/dist/core/auth.js +38 -3
  6. package/dist/core/authorization.d.ts +52 -0
  7. package/dist/core/authorization.js +97 -0
  8. package/dist/core/broadcasting.d.ts +49 -0
  9. package/dist/core/broadcasting.js +84 -0
  10. package/dist/core/broker.d.ts +398 -0
  11. package/dist/core/broker.js +602 -0
  12. package/dist/core/crypto.d.ts +51 -1
  13. package/dist/core/crypto.js +96 -3
  14. package/dist/core/database.d.ts +26 -1
  15. package/dist/core/database.js +65 -2
  16. package/dist/core/decorators.d.ts +39 -0
  17. package/dist/core/decorators.js +72 -0
  18. package/dist/core/exceptions.d.ts +77 -6
  19. package/dist/core/exceptions.js +168 -10
  20. package/dist/core/helpers.d.ts +6 -0
  21. package/dist/core/helpers.js +12 -0
  22. package/dist/core/http/kernel.js +14 -2
  23. package/dist/core/http/router.d.ts +14 -0
  24. package/dist/core/http/router.js +30 -1
  25. package/dist/core/index.d.ts +31 -8
  26. package/dist/core/index.js +17 -5
  27. package/dist/core/logger.d.ts +5 -0
  28. package/dist/core/logger.js +24 -2
  29. package/dist/core/migrations.js +3 -3
  30. package/dist/core/model.d.ts +19 -1
  31. package/dist/core/model.js +72 -4
  32. package/dist/core/provider.d.ts +13 -4
  33. package/dist/core/provider.js +12 -2
  34. package/dist/core/redis.d.ts +78 -0
  35. package/dist/core/redis.js +176 -0
  36. package/dist/core/request-logger.d.ts +26 -0
  37. package/dist/core/request-logger.js +48 -0
  38. package/dist/core/request.d.ts +17 -1
  39. package/dist/core/request.js +27 -1
  40. package/dist/core/scheduler.d.ts +60 -0
  41. package/dist/core/scheduler.js +166 -0
  42. package/dist/core/session.js +17 -2
  43. package/dist/core/storage.d.ts +57 -0
  44. package/dist/core/storage.js +98 -0
  45. package/dist/core/template.d.ts +50 -0
  46. package/dist/core/template.js +753 -0
  47. package/dist/core/testing.d.ts +54 -0
  48. package/dist/core/testing.js +141 -0
  49. package/dist/core/transformer.d.ts +89 -0
  50. package/dist/core/transformer.js +152 -0
  51. package/dist/core/validation.d.ts +20 -0
  52. package/dist/core/validation.js +52 -1
  53. package/dist/core/vite.d.ts +117 -0
  54. package/dist/core/vite.js +258 -0
  55. package/dist/vite/index.d.ts +40 -0
  56. package/dist/vite/index.js +146 -0
  57. package/package.json +16 -1
@@ -0,0 +1,753 @@
1
+ /**
2
+ * A string templating engine in the spirit of AdonisJS Edge — `{{ }}`
3
+ * interpolation and `@`-prefixed tags for logic, includes, layouts, and
4
+ * components.
5
+ *
6
+ * const t = new TemplateEngine();
7
+ * t.register("hello", "Hello, {{ name }}!");
8
+ * await t.render("hello", { name: "Ada" }); // "Hello, Ada!"
9
+ *
10
+ * Unlike engines that compile templates to a function via `eval`/`new
11
+ * Function`, this one *interprets* them against a small, safe expression
12
+ * evaluator — no dynamic code generation — so it runs unchanged on Node and on
13
+ * Cloudflare Workers (where `eval` is forbidden). The expression language is a
14
+ * practical subset of JS: literals, property/index access, method and helper
15
+ * calls, the usual operators, ternaries, arrays/objects, and `|` filters.
16
+ */
17
+ /* ------------------------------- escaping --------------------------------- */
18
+ const ESCAPE = {
19
+ "&": "&",
20
+ "<": "&lt;",
21
+ ">": "&gt;",
22
+ '"': "&quot;",
23
+ "'": "&#39;",
24
+ };
25
+ /** HTML-escape a value for safe interpolation. */
26
+ export function escapeHtml(value) {
27
+ if (value == null)
28
+ return "";
29
+ return String(value).replace(/[&<>"']/g, (c) => ESCAPE[c]);
30
+ }
31
+ const PUNCT = new Set([".", ",", "(", ")", "[", "]", "{", "}", ":", "|", "?"]);
32
+ function lexExpr(src) {
33
+ const toks = [];
34
+ let i = 0;
35
+ const two = ["==", "!=", "<=", ">=", "&&", "||", "??"];
36
+ const three = ["===", "!=="];
37
+ while (i < src.length) {
38
+ const c = src[i];
39
+ if (c === " " || c === "\t" || c === "\n" || c === "\r") {
40
+ i++;
41
+ continue;
42
+ }
43
+ if (c === '"' || c === "'") {
44
+ let s = "";
45
+ i++;
46
+ while (i < src.length && src[i] !== c) {
47
+ if (src[i] === "\\") {
48
+ i++;
49
+ s += src[i] ?? "";
50
+ }
51
+ else
52
+ s += src[i];
53
+ i++;
54
+ }
55
+ i++;
56
+ toks.push({ t: "str", v: s });
57
+ continue;
58
+ }
59
+ if (c >= "0" && c <= "9") {
60
+ let n = "";
61
+ while (i < src.length && /[0-9.]/.test(src[i]))
62
+ n += src[i++];
63
+ toks.push({ t: "num", v: Number(n) });
64
+ continue;
65
+ }
66
+ if (/[A-Za-z_$]/.test(c)) {
67
+ let id = "";
68
+ while (i < src.length && /[A-Za-z0-9_$]/.test(src[i]))
69
+ id += src[i++];
70
+ if (id === "true" || id === "false")
71
+ toks.push({ t: "bool", v: id });
72
+ else if (id === "null" || id === "undefined")
73
+ toks.push({ t: "nullish", v: id });
74
+ else
75
+ toks.push({ t: "id", v: id });
76
+ continue;
77
+ }
78
+ const t3 = src.slice(i, i + 3);
79
+ if (three.includes(t3)) {
80
+ toks.push({ t: t3 });
81
+ i += 3;
82
+ continue;
83
+ }
84
+ const t2 = src.slice(i, i + 2);
85
+ if (two.includes(t2)) {
86
+ toks.push({ t: t2 });
87
+ i += 2;
88
+ continue;
89
+ }
90
+ if (PUNCT.has(c) || "+-*/%<>!=".includes(c)) {
91
+ toks.push({ t: c });
92
+ i++;
93
+ continue;
94
+ }
95
+ throw new Error(`template: unexpected character '${c}' in expression: ${src}`);
96
+ }
97
+ toks.push({ t: "eof" });
98
+ return toks;
99
+ }
100
+ const BIN_PREC = {
101
+ "||": 1, "??": 1,
102
+ "&&": 2,
103
+ "==": 3, "!=": 3, "===": 3, "!==": 3,
104
+ "<": 4, "<=": 4, ">": 4, ">=": 4,
105
+ "+": 5, "-": 5,
106
+ "*": 6, "/": 6, "%": 6,
107
+ };
108
+ class ExprParser {
109
+ toks;
110
+ p = 0;
111
+ constructor(toks) {
112
+ this.toks = toks;
113
+ }
114
+ peek() {
115
+ return this.toks[this.p];
116
+ }
117
+ next() {
118
+ return this.toks[this.p++];
119
+ }
120
+ expect(t) {
121
+ const tok = this.next();
122
+ if (tok.t !== t)
123
+ throw new Error(`template: expected '${t}', got '${tok.t}'`);
124
+ return tok;
125
+ }
126
+ parse() {
127
+ const node = this.ternary();
128
+ if (this.peek().t !== "eof")
129
+ throw new Error(`template: trailing tokens in expression`);
130
+ return node;
131
+ }
132
+ ternary() {
133
+ let node = this.pipe();
134
+ if (this.peek().t === "?") {
135
+ this.next();
136
+ const cons = this.ternary();
137
+ this.expect(":");
138
+ const alt = this.ternary();
139
+ node = { k: "cond", test: node, cons, alt };
140
+ }
141
+ return node;
142
+ }
143
+ pipe() {
144
+ let node = this.binary(0);
145
+ while (this.peek().t === "|") {
146
+ this.next();
147
+ const name = String(this.expect("id").v);
148
+ const args = [];
149
+ if (this.peek().t === "(") {
150
+ this.next();
151
+ while (this.peek().t !== ")") {
152
+ args.push(this.ternary());
153
+ if (this.peek().t === ",")
154
+ this.next();
155
+ }
156
+ this.expect(")");
157
+ }
158
+ node = { k: "pipe", expr: node, name, args };
159
+ }
160
+ return node;
161
+ }
162
+ binary(minPrec) {
163
+ let left = this.unary();
164
+ for (;;) {
165
+ const op = this.peek().t;
166
+ const prec = BIN_PREC[op];
167
+ if (prec === undefined || prec < minPrec)
168
+ break;
169
+ this.next();
170
+ const right = this.binary(prec + 1);
171
+ left = { k: "bin", op, l: left, r: right };
172
+ }
173
+ return left;
174
+ }
175
+ unary() {
176
+ const t = this.peek().t;
177
+ if (t === "!" || t === "-") {
178
+ this.next();
179
+ return { k: "unary", op: t, arg: this.unary() };
180
+ }
181
+ return this.postfix();
182
+ }
183
+ postfix() {
184
+ let node = this.primary();
185
+ for (;;) {
186
+ const t = this.peek().t;
187
+ if (t === ".") {
188
+ this.next();
189
+ const prop = String(this.expect("id").v);
190
+ node = { k: "member", obj: node, prop };
191
+ }
192
+ else if (t === "[") {
193
+ this.next();
194
+ const idx = this.ternary();
195
+ this.expect("]");
196
+ node = { k: "index", obj: node, idx };
197
+ }
198
+ else if (t === "(") {
199
+ this.next();
200
+ const args = [];
201
+ while (this.peek().t !== ")") {
202
+ args.push(this.ternary());
203
+ if (this.peek().t === ",")
204
+ this.next();
205
+ }
206
+ this.expect(")");
207
+ node = { k: "call", callee: node, args };
208
+ }
209
+ else
210
+ break;
211
+ }
212
+ return node;
213
+ }
214
+ primary() {
215
+ const tok = this.next();
216
+ switch (tok.t) {
217
+ case "num":
218
+ return { k: "lit", v: tok.v };
219
+ case "str":
220
+ return { k: "lit", v: tok.v };
221
+ case "bool":
222
+ return { k: "lit", v: tok.v === "true" };
223
+ case "nullish":
224
+ return { k: "lit", v: tok.v === "null" ? null : undefined };
225
+ case "id":
226
+ return { k: "id", name: String(tok.v) };
227
+ case "(": {
228
+ const node = this.ternary();
229
+ this.expect(")");
230
+ return node;
231
+ }
232
+ case "[": {
233
+ const items = [];
234
+ while (this.peek().t !== "]") {
235
+ items.push(this.ternary());
236
+ if (this.peek().t === ",")
237
+ this.next();
238
+ }
239
+ this.expect("]");
240
+ return { k: "arr", items };
241
+ }
242
+ case "{": {
243
+ const entries = [];
244
+ while (this.peek().t !== "}") {
245
+ const keyTok = this.next();
246
+ const key = keyTok.t === "str" ? String(keyTok.v) : String(keyTok.v);
247
+ this.expect(":");
248
+ entries.push([key, this.ternary()]);
249
+ if (this.peek().t === ",")
250
+ this.next();
251
+ }
252
+ this.expect("}");
253
+ return { k: "obj", entries };
254
+ }
255
+ default:
256
+ throw new Error(`template: unexpected token '${tok.t}' in expression`);
257
+ }
258
+ }
259
+ }
260
+ const exprCache = new Map();
261
+ function compileExpr(src) {
262
+ let node = exprCache.get(src);
263
+ if (!node) {
264
+ node = new ExprParser(lexExpr(src)).parse();
265
+ exprCache.set(src, node);
266
+ }
267
+ return node;
268
+ }
269
+ /* -------------------------- expression evaluator -------------------------- */
270
+ const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);
271
+ function member(obj, prop) {
272
+ if (obj == null)
273
+ return undefined;
274
+ if (BLOCKED.has(prop))
275
+ throw new Error(`template: access to '${prop}' is not allowed`);
276
+ return obj[prop];
277
+ }
278
+ function evalNode(node, scope, filters) {
279
+ switch (node.k) {
280
+ case "lit":
281
+ return node.v;
282
+ case "id":
283
+ return scope[node.name];
284
+ case "member":
285
+ return member(evalNode(node.obj, scope, filters), node.prop);
286
+ case "index":
287
+ return member(evalNode(node.obj, scope, filters), String(evalNode(node.idx, scope, filters)));
288
+ case "arr":
289
+ return node.items.map((n) => evalNode(n, scope, filters));
290
+ case "obj": {
291
+ const out = {};
292
+ for (const [k, v] of node.entries)
293
+ out[k] = evalNode(v, scope, filters);
294
+ return out;
295
+ }
296
+ case "unary": {
297
+ const v = evalNode(node.arg, scope, filters);
298
+ return node.op === "!" ? !v : -v;
299
+ }
300
+ case "cond":
301
+ return evalNode(node.test, scope, filters)
302
+ ? evalNode(node.cons, scope, filters)
303
+ : evalNode(node.alt, scope, filters);
304
+ case "bin":
305
+ return evalBin(node, scope, filters);
306
+ case "call": {
307
+ const args = node.args.map((a) => evalNode(a, scope, filters));
308
+ if (node.callee.k === "member") {
309
+ const obj = evalNode(node.callee.obj, scope, filters);
310
+ const fn = member(obj, node.callee.prop);
311
+ if (typeof fn !== "function") {
312
+ throw new Error(`template: '${node.callee.prop}' is not a function`);
313
+ }
314
+ return fn.apply(obj, args);
315
+ }
316
+ const fn = evalNode(node.callee, scope, filters);
317
+ if (typeof fn !== "function")
318
+ throw new Error(`template: value is not callable`);
319
+ return fn(...args);
320
+ }
321
+ case "pipe": {
322
+ const filter = filters.get(node.name);
323
+ if (!filter)
324
+ throw new Error(`template: unknown filter '${node.name}'`);
325
+ const value = evalNode(node.expr, scope, filters);
326
+ const args = node.args.map((a) => evalNode(a, scope, filters));
327
+ return filter(value, ...args);
328
+ }
329
+ }
330
+ }
331
+ function evalBin(node, scope, filters) {
332
+ const { op } = node;
333
+ // Short-circuit logical operators.
334
+ if (op === "&&")
335
+ return evalNode(node.l, scope, filters) && evalNode(node.r, scope, filters);
336
+ if (op === "||")
337
+ return evalNode(node.l, scope, filters) || evalNode(node.r, scope, filters);
338
+ if (op === "??")
339
+ return evalNode(node.l, scope, filters) ?? evalNode(node.r, scope, filters);
340
+ const l = evalNode(node.l, scope, filters);
341
+ const r = evalNode(node.r, scope, filters);
342
+ switch (op) {
343
+ case "+": return l + r;
344
+ case "-": return l - r;
345
+ case "*": return l * r;
346
+ case "/": return l / r;
347
+ case "%": return l % r;
348
+ case "==": return l == r;
349
+ case "!=": return l != r;
350
+ case "===": return l === r;
351
+ case "!==": return l !== r;
352
+ case "<": return l < r;
353
+ case "<=": return l <= r;
354
+ case ">": return l > r;
355
+ case ">=": return l >= r;
356
+ default: throw new Error(`template: unknown operator '${op}'`);
357
+ }
358
+ }
359
+ function lexTemplate(src) {
360
+ const pieces = [];
361
+ let i = 0;
362
+ let text = "";
363
+ const flush = () => {
364
+ if (text)
365
+ pieces.push({ p: "text", v: text });
366
+ text = "";
367
+ };
368
+ while (i < src.length) {
369
+ // Comments {{-- ... --}}
370
+ if (src.startsWith("{{--", i)) {
371
+ const end = src.indexOf("--}}", i + 4);
372
+ i = end === -1 ? src.length : end + 4;
373
+ continue;
374
+ }
375
+ // Raw {{{ ... }}}
376
+ if (src.startsWith("{{{", i)) {
377
+ const end = src.indexOf("}}}", i + 3);
378
+ if (end === -1)
379
+ throw new Error("template: unclosed {{{");
380
+ flush();
381
+ pieces.push({ p: "out", expr: src.slice(i + 3, end).trim(), raw: true });
382
+ i = end + 3;
383
+ continue;
384
+ }
385
+ // Escaped {{ ... }}
386
+ if (src.startsWith("{{", i)) {
387
+ const end = src.indexOf("}}", i + 2);
388
+ if (end === -1)
389
+ throw new Error("template: unclosed {{");
390
+ flush();
391
+ pieces.push({ p: "out", expr: src.slice(i + 2, end).trim(), raw: false });
392
+ i = end + 2;
393
+ continue;
394
+ }
395
+ // Tags @name(args) or @name at line level. Require @ not preceded by a word char.
396
+ if (src[i] === "@" && /[a-zA-Z]/.test(src[i + 1] ?? "")) {
397
+ let j = i + 1;
398
+ let name = "";
399
+ while (j < src.length && /[a-zA-Z]/.test(src[j]))
400
+ name += src[j++];
401
+ let arg = "";
402
+ if (src[j] === "(") {
403
+ // balanced-paren capture
404
+ let depth = 0;
405
+ const start = j;
406
+ for (; j < src.length; j++) {
407
+ if (src[j] === "(")
408
+ depth++;
409
+ else if (src[j] === ")") {
410
+ depth--;
411
+ if (depth === 0) {
412
+ j++;
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ arg = src.slice(start + 1, j - 1);
418
+ }
419
+ flush();
420
+ pieces.push({ p: "tag", name, arg });
421
+ i = j;
422
+ continue;
423
+ }
424
+ text += src[i++];
425
+ }
426
+ flush();
427
+ return pieces;
428
+ }
429
+ const BLOCK_END = new Set(["end", "endif", "endeach", "endfor", "endcomponent", "endsection"]);
430
+ function parseTemplate(src) {
431
+ const pieces = lexTemplate(src);
432
+ let pos = 0;
433
+ let layout;
434
+ function unquote(s) {
435
+ const t = s.trim();
436
+ return t.replace(/^['"]|['"]$/g, "");
437
+ }
438
+ // Parse a run of nodes until a terminator tag (in `stops`) is seen. The
439
+ // terminator is CONSUMED; its name and arg are returned so block parsers can
440
+ // branch (e.g. @elseif's condition, @slot's name). Returns stop=null at EOF.
441
+ function parseNodes(stops) {
442
+ const nodes = [];
443
+ while (pos < pieces.length) {
444
+ const pc = pieces[pos];
445
+ if (pc.p === "text") {
446
+ nodes.push({ k: "text", v: pc.v });
447
+ pos++;
448
+ }
449
+ else if (pc.p === "out") {
450
+ nodes.push({ k: "out", expr: pc.expr, raw: pc.raw });
451
+ pos++;
452
+ }
453
+ else {
454
+ // tag
455
+ if (stops.has(pc.name)) {
456
+ pos++;
457
+ return { nodes, stop: pc.name, arg: pc.arg };
458
+ }
459
+ pos++;
460
+ parseTag(pc, nodes);
461
+ }
462
+ }
463
+ return { nodes, stop: null, arg: "" };
464
+ }
465
+ function parseTag(pc, nodes) {
466
+ switch (pc.name) {
467
+ case "layout":
468
+ layout = unquote(pc.arg);
469
+ break;
470
+ case "if": {
471
+ const branches = [];
472
+ let alt;
473
+ let cond = pc.arg;
474
+ for (;;) {
475
+ const { nodes: body, stop, arg } = parseNodes(new Set(["elseif", "else", "end", "endif"]));
476
+ branches.push({ cond, body });
477
+ if (stop === "elseif") {
478
+ cond = arg;
479
+ continue;
480
+ }
481
+ if (stop === "else") {
482
+ alt = parseNodes(new Set(["end", "endif"])).nodes;
483
+ }
484
+ break;
485
+ }
486
+ nodes.push({ k: "if", branches, alt });
487
+ break;
488
+ }
489
+ case "each":
490
+ case "for": {
491
+ // @each(item in list) or @each(item, index in list)
492
+ const m = /^\s*([\w$]+)\s*(?:,\s*([\w$]+)\s*)?\s+in\s+([\s\S]+)$/.exec(pc.arg);
493
+ if (!m)
494
+ throw new Error(`template: malformed @each(${pc.arg})`);
495
+ const { nodes: body } = parseNodes(new Set(["end", "endeach", "endfor"]));
496
+ nodes.push({ k: "each", item: m[1], index: m[2], list: m[3].trim(), body });
497
+ break;
498
+ }
499
+ case "include":
500
+ nodes.push({ k: "include", name: unquote(splitArgs(pc.arg)[0] ?? "") });
501
+ break;
502
+ case "includeIf": {
503
+ const parts = splitArgs(pc.arg);
504
+ nodes.push({ k: "include", name: unquote(parts[1] ?? ""), cond: parts[0] });
505
+ break;
506
+ }
507
+ case "set": {
508
+ const parts = splitArgs(pc.arg);
509
+ nodes.push({ k: "set", name: unquote(parts[0] ?? ""), expr: parts[1] ?? "null" });
510
+ break;
511
+ }
512
+ case "section": {
513
+ const { nodes: body } = parseNodes(new Set(["end", "endsection"]));
514
+ nodes.push({ k: "section", name: unquote(pc.arg), body });
515
+ break;
516
+ }
517
+ case "yield": {
518
+ // `@yield('name') fallback @end` — the body is default content.
519
+ const { nodes: fallback } = parseNodes(new Set(["end"]));
520
+ nodes.push({ k: "yield", name: unquote(splitArgs(pc.arg)[0] ?? ""), fallback });
521
+ break;
522
+ }
523
+ case "component": {
524
+ const parts = splitArgs(pc.arg);
525
+ const name = unquote(parts[0] ?? "");
526
+ const props = parts.slice(1).join(",") || "{}";
527
+ const slots = {};
528
+ const body = [];
529
+ for (;;) {
530
+ const { nodes: run, stop, arg } = parseNodes(new Set(["slot", "end", "endcomponent"]));
531
+ body.push(...run);
532
+ if (stop === "slot") {
533
+ const slotName = unquote(splitArgs(arg)[0] ?? "");
534
+ slots[slotName] = parseNodes(new Set(["end", "endslot"])).nodes;
535
+ continue;
536
+ }
537
+ break;
538
+ }
539
+ nodes.push({ k: "component", name, props, slots, body });
540
+ break;
541
+ }
542
+ case "dump":
543
+ nodes.push({ k: "dump", expr: pc.arg });
544
+ break;
545
+ default:
546
+ if (BLOCK_END.has(pc.name))
547
+ throw new Error(`template: unexpected @${pc.name}`);
548
+ throw new Error(`template: unknown tag @${pc.name}`);
549
+ }
550
+ }
551
+ const { nodes } = parseNodes(new Set());
552
+ return { layout, nodes };
553
+ }
554
+ // Split "a, b, c" respecting quotes, parens, brackets, braces.
555
+ function splitArgs(src) {
556
+ const parts = [];
557
+ let depth = 0;
558
+ let cur = "";
559
+ let quote = "";
560
+ for (let i = 0; i < src.length; i++) {
561
+ const c = src[i];
562
+ if (quote) {
563
+ cur += c;
564
+ if (c === quote)
565
+ quote = "";
566
+ continue;
567
+ }
568
+ if (c === '"' || c === "'") {
569
+ quote = c;
570
+ cur += c;
571
+ continue;
572
+ }
573
+ if ("([{".includes(c))
574
+ depth++;
575
+ if (")]}".includes(c))
576
+ depth--;
577
+ if (c === "," && depth === 0) {
578
+ parts.push(cur.trim());
579
+ cur = "";
580
+ continue;
581
+ }
582
+ cur += c;
583
+ }
584
+ if (cur.trim())
585
+ parts.push(cur.trim());
586
+ return parts;
587
+ }
588
+ export class TemplateEngine {
589
+ templates = new Map();
590
+ globals = {};
591
+ filters = new Map();
592
+ constructor() {
593
+ // A few sensible default filters.
594
+ this.filter("upper", (v) => String(v ?? "").toUpperCase());
595
+ this.filter("lower", (v) => String(v ?? "").toLowerCase());
596
+ this.filter("capitalize", (v) => {
597
+ const s = String(v ?? "");
598
+ return s.charAt(0).toUpperCase() + s.slice(1);
599
+ });
600
+ this.filter("json", (v) => JSON.stringify(v));
601
+ this.filter("length", (v) => v?.length ?? 0);
602
+ }
603
+ /** Register a template by name from its source string. */
604
+ register(name, source) {
605
+ this.templates.set(name, parseTemplate(source));
606
+ return this;
607
+ }
608
+ /** Register many templates at once (e.g. a Node loader reads files and passes them here). */
609
+ registerAll(sources) {
610
+ for (const [name, source] of Object.entries(sources))
611
+ this.register(name, source);
612
+ return this;
613
+ }
614
+ /** True if a template is registered. */
615
+ has(name) {
616
+ return this.templates.has(name);
617
+ }
618
+ /** Expose a value/function to every template as a global. */
619
+ global(name, value) {
620
+ this.globals[name] = value;
621
+ return this;
622
+ }
623
+ /** Register a `{{ value | name }}` filter. */
624
+ filter(name, fn) {
625
+ this.filters.set(name, fn);
626
+ return this;
627
+ }
628
+ /** Render a registered template with the given state. */
629
+ async render(name, state = {}) {
630
+ const tpl = this.templates.get(name);
631
+ if (!tpl)
632
+ throw new Error(`template: no template named '${name}'`);
633
+ const scope = { ...this.globals, ...state };
634
+ const ctx = { sections: {}, slots: {} };
635
+ const body = await this.renderNodes(tpl.nodes, scope, ctx);
636
+ if (tpl.layout) {
637
+ const layoutTpl = this.templates.get(tpl.layout);
638
+ if (!layoutTpl)
639
+ throw new Error(`template: no layout named '${tpl.layout}'`);
640
+ // Child's sections (collected during its render) are available to @yield.
641
+ return this.renderNodes(layoutTpl.nodes, scope, { sections: ctx.sections, slots: {} });
642
+ }
643
+ return body;
644
+ }
645
+ ev(expr, scope) {
646
+ return evalNode(compileExpr(expr), scope, this.filters);
647
+ }
648
+ async renderNodes(nodes, scope, ctx) {
649
+ let out = "";
650
+ for (const node of nodes) {
651
+ out += await this.renderNode(node, scope, ctx);
652
+ }
653
+ return out;
654
+ }
655
+ async renderNode(node, scope, ctx) {
656
+ switch (node.k) {
657
+ case "text":
658
+ return node.v;
659
+ case "out": {
660
+ const v = this.ev(node.expr, scope);
661
+ if (v == null)
662
+ return "";
663
+ return node.raw ? String(v) : escapeHtml(v);
664
+ }
665
+ case "if": {
666
+ for (const b of node.branches) {
667
+ if (this.ev(b.cond, scope))
668
+ return this.renderNodes(b.body, scope, ctx);
669
+ }
670
+ return node.alt ? this.renderNodes(node.alt, scope, ctx) : "";
671
+ }
672
+ case "each": {
673
+ const list = this.ev(node.list, scope);
674
+ if (list == null)
675
+ return "";
676
+ const items = Array.isArray(list) ? list : Object.values(list);
677
+ let out = "";
678
+ for (let idx = 0; idx < items.length; idx++) {
679
+ const child = { ...scope };
680
+ child[node.item] = items[idx];
681
+ if (node.index)
682
+ child[node.index] = idx;
683
+ child["$loop"] = {
684
+ index: idx,
685
+ iteration: idx + 1,
686
+ first: idx === 0,
687
+ last: idx === items.length - 1,
688
+ count: items.length,
689
+ even: idx % 2 === 1,
690
+ odd: idx % 2 === 0,
691
+ };
692
+ out += await this.renderNodes(node.body, child, ctx);
693
+ }
694
+ return out;
695
+ }
696
+ case "set": {
697
+ scope[node.name] = this.ev(node.expr, scope);
698
+ return "";
699
+ }
700
+ case "include": {
701
+ if (node.cond && !this.ev(node.cond, scope))
702
+ return "";
703
+ const inc = this.templates.get(node.name);
704
+ if (!inc)
705
+ throw new Error(`template: no template named '${node.name}' (include)`);
706
+ return this.renderNodes(inc.nodes, scope, ctx);
707
+ }
708
+ case "section": {
709
+ ctx.sections[node.name] = await this.renderNodes(node.body, scope, ctx);
710
+ return "";
711
+ }
712
+ case "yield": {
713
+ const provided = ctx.sections[node.name];
714
+ if (provided !== undefined)
715
+ return provided;
716
+ return this.renderNodes(node.fallback, scope, ctx);
717
+ }
718
+ case "component": {
719
+ const comp = this.templates.get(node.name);
720
+ if (!comp)
721
+ throw new Error(`template: no component named '${node.name}'`);
722
+ const props = this.ev(node.props, scope) ?? {};
723
+ // Render slots against the *caller's* scope.
724
+ const slots = {};
725
+ slots["main"] = await this.renderNodes(node.body, scope, ctx);
726
+ for (const [sname, sbody] of Object.entries(node.slots)) {
727
+ slots[sname] = await this.renderNodes(sbody, scope, ctx);
728
+ }
729
+ const compScope = { ...this.globals, ...props, slots };
730
+ return this.renderNodes(comp.nodes, compScope, { sections: {}, slots });
731
+ }
732
+ case "dump": {
733
+ const v = this.ev(node.expr, scope);
734
+ return `<pre>${escapeHtml(JSON.stringify(v, null, 2))}</pre>`;
735
+ }
736
+ }
737
+ }
738
+ }
739
+ /* -------------------------------- global ---------------------------------- */
740
+ let engine = new TemplateEngine();
741
+ /** The default template engine (register templates/globals/filters on it). */
742
+ export function templates() {
743
+ return engine;
744
+ }
745
+ /** Replace the default template engine. */
746
+ export function setTemplateEngine(e) {
747
+ engine = e;
748
+ return engine;
749
+ }
750
+ /** Render a registered template on the default engine. */
751
+ export function render(name, state) {
752
+ return engine.render(name, state);
753
+ }