miki-template 2.2.3 → 2.3.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.
Files changed (66) hide show
  1. package/.github/workflows/docs.yml +3 -1
  2. package/.github/workflows/release.yml +0 -5
  3. package/README.md +17 -5
  4. package/benchmarks/ejs-results.json +6 -6
  5. package/benchmarks/ejs.js +5 -3
  6. package/benchmarks/handlebars-results.json +6 -6
  7. package/benchmarks/handlebars.js +5 -8
  8. package/benchmarks/miki-results.json +6 -6
  9. package/benchmarks/miki.js +6 -3
  10. package/benchmarks/pug-results.json +6 -6
  11. package/benchmarks/pug.js +5 -3
  12. package/docs/api/async-render.md +88 -3
  13. package/docs/api/cache.md +90 -3
  14. package/docs/api/compile.md +131 -3
  15. package/docs/api/context-processors.md +80 -3
  16. package/docs/api/filters.md +223 -3
  17. package/docs/api/finder.md +97 -3
  18. package/docs/api/helpers.md +56 -3
  19. package/docs/api/i18n.md +160 -3
  20. package/docs/api/index.md +82 -28
  21. package/docs/api/libraries.md +210 -3
  22. package/docs/api/render-partial.md +84 -3
  23. package/docs/api/render.md +95 -3
  24. package/docs/api/security.md +148 -3
  25. package/docs/api/setup-express.md +78 -2
  26. package/docs/api/tags.md +138 -4
  27. package/docs/filter.md +0 -0
  28. package/docs/guide/advanced-usage.md +403 -6
  29. package/docs/guide/async-rendering.md +312 -4
  30. package/docs/guide/context-processors.md +261 -4
  31. package/docs/guide/custom-filters.md +315 -4
  32. package/docs/guide/custom-tags.md +275 -4
  33. package/docs/guide/filters.md +675 -3
  34. package/docs/guide/getting-started.md +109 -7
  35. package/docs/guide/installation.md +99 -4
  36. package/docs/guide/partial-templates.md +371 -4
  37. package/docs/guide/quick-start.md +228 -6
  38. package/docs/guide/security.md +348 -3
  39. package/docs/guide/tags.md +789 -6
  40. package/docs/guide/template-discovery.md +174 -4
  41. package/docs/guide/template-inheritance.md +277 -4
  42. package/docs/index.md +24 -42
  43. package/docs/integrations/elysia.md +4 -2
  44. package/docs/integrations/express.md +219 -219
  45. package/docs/integrations/fastify.md +4 -2
  46. package/docs/integrations/hono.md +4 -2
  47. package/docs/integrations/index.md +68 -68
  48. package/docs/integrations/koa.md +4 -2
  49. package/docs/integrations/nestjs.md +4 -2
  50. package/docs/integrations/tsed.md +4 -2
  51. package/docs/performance.md +45 -8
  52. package/ex.mjs +1 -1
  53. package/mkdocs.yml +0 -22
  54. package/overrides/main.html +1 -1
  55. package/package.json +1 -1
  56. package/requirements-docs.txt +2 -1
  57. package/src/codegen.js +905 -0
  58. package/src/context.js +42 -30
  59. package/src/filters.js +16 -0
  60. package/src/index.js +66 -61
  61. package/src/tags/control.js +15 -12
  62. package/src/utils.js +60 -0
  63. package/tests/filters.test.js +9 -0
  64. package/docs/javascripts/extra.js +0 -174
  65. package/docs/stylesheets/extra.css +0 -819
  66. package/overrides/partials/footer.html +0 -9
package/src/codegen.js ADDED
@@ -0,0 +1,905 @@
1
+ /**
2
+ * High-performance codegen — generates native JavaScript compilation functions.
3
+ *
4
+ * Strategy: Translate 100% of template AST nodes and condition expressions
5
+ * directly into native JS code executed by V8 at full machine speed.
6
+ */
7
+ const filtersModule = require('./filters');
8
+ const { SafeString } = require('./security');
9
+ const { tokenizeExpr } = require('./tags/control');
10
+
11
+ function js(v) {
12
+ if (v === null) return 'null';
13
+ if (v === undefined) return 'void 0';
14
+ if (typeof v === 'string') return JSON.stringify(v);
15
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
16
+ if (Array.isArray(v)) return '[' + v.map(js).join(',') + ']';
17
+ if (typeof v === 'object') return JSON.stringify(v);
18
+ return JSON.stringify(String(v));
19
+ }
20
+
21
+ function pad(s, level) {
22
+ return ' '.repeat(level) + s;
23
+ }
24
+
25
+ function compileExprToJs(conditionStr, loopVarMap = {}) {
26
+ if (!conditionStr || !conditionStr.trim()) return 'false';
27
+ const tokens = tokenizeExpr(conditionStr);
28
+ if (tokens.length === 0) return 'false';
29
+
30
+ const ops = [];
31
+ const vals = []; function compileValue(t) {
32
+ if (!t) return '""';
33
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith('\'') && t.endsWith('\''))) {
34
+ return js(t.slice(1, -1));
35
+ }
36
+ if (t === 'true' || t === 'True') return 'true';
37
+ if (t === 'false' || t === 'False') return 'false';
38
+ if (t === 'none' || t === 'None' || t === 'null') return 'null';
39
+ if (/^-?\d+(\.\d+)?$/.test(t)) return String(Number(t));
40
+ if (loopVarMap && loopVarMap[t]) return loopVarMap[t];
41
+ return `_get(_ctx, ${js(t)})`;
42
+ }
43
+
44
+ function applyBinaryOp(op) {
45
+ const b = vals.pop();
46
+ const a = vals.pop();
47
+ switch (op) {
48
+ case '==': vals.push(`(${a} == ${b})`); break;
49
+ case '!=': vals.push(`(${a} != ${b})`); break;
50
+ case '<': vals.push(`(${a} < ${b})`); break;
51
+ case '<=': vals.push(`(${a} <= ${b})`); break;
52
+ case '>': vals.push(`(${a} > ${b})`); break;
53
+ case '>=': vals.push(`(${a} >= ${b})`); break;
54
+ case '%': vals.push(`(${a} % ${b})`); break;
55
+ case '*': vals.push(`(${a} * ${b})`); break;
56
+ case '/': vals.push(`(${a} / ${b})`); break;
57
+ case '+': vals.push(`(${a} + ${b})`); break;
58
+ case '-': vals.push(`(${a} - ${b})`); break;
59
+ case 'and': vals.push(`(${a} && ${b})`); break;
60
+ case 'or': vals.push(`(${a} || ${b})`); break;
61
+ case 'in': vals.push(`_in(${a}, ${b})`); break;
62
+ case 'not in': vals.push(`(!_in(${a}, ${b}))`); break;
63
+ default: vals.push('false');
64
+ }
65
+ }
66
+
67
+ function applyUnaryNot() {
68
+ const a = vals.pop();
69
+ vals.push(`(!${a})`);
70
+ }
71
+
72
+ const prec = {
73
+ 'or': 1,
74
+ 'and': 2,
75
+ '==': 4,
76
+ '!=': 4,
77
+ '<': 5,
78
+ '<=': 5,
79
+ '>': 5,
80
+ '>=': 5,
81
+ '+': 6,
82
+ '-': 6,
83
+ '%': 7,
84
+ '*': 7,
85
+ '/': 7,
86
+ 'in': 8,
87
+ 'not in': 8
88
+ };
89
+
90
+ for (let i = 0; i < tokens.length; i++) {
91
+ const t = tokens[i];
92
+ if (t === 'not') {
93
+ ops.push('not');
94
+ } else if (['and', 'or', '==', '!=', '<', '<=', '>', '>=', '%', '*', '/', '+', '-', 'in', 'not in'].includes(t)) {
95
+ while (ops.length > 0 && ops[ops.length - 1] !== 'not' && prec[ops[ops.length - 1]] >= prec[t]) {
96
+ applyBinaryOp(ops.pop());
97
+ }
98
+ ops.push(t);
99
+ } else {
100
+ vals.push(compileValue(t));
101
+ }
102
+ }
103
+
104
+ while (ops.length > 0) {
105
+ const op = ops.pop();
106
+ if (op === 'not') applyUnaryNot();
107
+ else applyBinaryOp(op);
108
+ }
109
+
110
+ return vals.length > 0 ? vals[0] : 'false';
111
+ }
112
+
113
+ function hasForloopRef(nodes) {
114
+ if (!nodes) return false;
115
+ for (let i = 0; i < nodes.length; i++) {
116
+ const n = nodes[i];
117
+ if (!n) continue;
118
+ const name = n.constructor ? n.constructor.name : '';
119
+ if (name === 'VariableNode' && n.varPath && n.varPath.startsWith('forloop')) return true;
120
+ if (name === 'IfNode') {
121
+ if (n.conditionStr && n.conditionStr.includes('forloop')) return true;
122
+ }
123
+ if (n.body && hasForloopRef(n.body)) return true;
124
+ if (n.elseBody && hasForloopRef(n.elseBody)) return true;
125
+ if (n.emptyBody && hasForloopRef(n.emptyBody)) return true;
126
+ if (n.elifBranches) {
127
+ for (let j = 0; j < n.elifBranches.length; j++) {
128
+ if (n.elifBranches[j].conditionStr && n.elifBranches[j].conditionStr.includes('forloop')) return true;
129
+ if (hasForloopRef(n.elifBranches[j].body)) return true;
130
+ }
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+
136
+ function genNodes(nodes, stmts, level, buf = 'out', loopVarMap = {}) {
137
+ for (let i = 0; i < nodes.length; i++) genNode(nodes[i], stmts, level, buf, loopVarMap);
138
+ }
139
+
140
+ function genNode(node, stmts, level, buf = 'out', loopVarMap = {}) {
141
+ if (!node) return;
142
+ const name = node.constructor.name;
143
+ switch (name) {
144
+ case 'TextNode':
145
+ if (node.content) stmts.push(pad(`${buf} += ${js(node.content)};`, level));
146
+ return;
147
+
148
+ case 'VariableNode': {
149
+ let expr;
150
+ if (node.isLiteral) {
151
+ expr = js(node.literalValue);
152
+ } else if (loopVarMap && loopVarMap[node.varPath]) {
153
+ expr = loopVarMap[node.varPath];
154
+ } else {
155
+ expr = `_get(_ctx, ${js(node.varPath)})`;
156
+ }
157
+ for (let i = 0; i < node.filters.length; i++) {
158
+ const f = node.filters[i];
159
+ let arg = 'void 0';
160
+ if (f.arg) {
161
+ if (f.arg.type === 'literal') arg = js(f.arg.value);
162
+ else if (loopVarMap && loopVarMap[f.arg.value]) arg = loopVarMap[f.arg.value];
163
+ else arg = `_get(_ctx, ${js(f.arg.value)})`;
164
+ }
165
+ if (f.name === 'upper' && (arg === 'void 0' || !arg)) {
166
+ expr = `(typeof ${expr} === 'string' ? ${expr}.toUpperCase() : String(${expr} == null ? '' : ${expr}).toUpperCase())`;
167
+ } else if (f.name === 'lower' && (arg === 'void 0' || !arg)) {
168
+ expr = `(typeof ${expr} === 'string' ? ${expr}.toLowerCase() : String(${expr} == null ? '' : ${expr}).toLowerCase())`;
169
+ } else if (f.name === 'length' && (arg === 'void 0' || !arg)) {
170
+ expr = `(${expr} ? (${expr}.length !== undefined ? ${expr}.length : (typeof ${expr} === 'object' ? Object.keys(${expr}).length : 0)) : 0)`;
171
+ } else if (f.name === 'repeat' && arg !== 'void 0') {
172
+ expr = `String(${expr} == null ? '' : ${expr}).repeat(${arg})`;
173
+ } else {
174
+ const fnRef = f._fnRef ? `_f_${f.name}` : '_missingFilter';
175
+ if (f._fnRef) {
176
+ expr = `${fnRef}(${expr}, ${arg}, _ctx)`;
177
+ } else {
178
+ expr = `_missingFilter('${f.name}', ${expr}, ${arg}, _ctx)`;
179
+ }
180
+ }
181
+ }
182
+ const vName = `_v${node._id}`;
183
+ if (node.isLiteral && node.filters.length === 0) {
184
+ stmts.push(pad(`${buf} += ((${expr}) == null ? '' : String(${expr}));`, level));
185
+ return;
186
+ }
187
+ stmts.push(pad(`const ${vName} = ${expr};`, level));
188
+ stmts.push(pad(`if (typeof ${vName} === 'number') ${buf} += ${vName}; else if (typeof ${vName} === 'string') ${buf} += (a ? _escapeStr(${vName}) : ${vName}); else if (${vName} != null) ${buf} += (typeof ${vName} === 'function' ? ${vName}() : (a && !(${vName} instanceof _SafeString) ? _escape(${vName}) : ${vName}));`, level));
189
+ return;
190
+ }
191
+
192
+ case 'IfNode': {
193
+ const condJs = compileExprToJs(node.conditionStr, loopVarMap);
194
+ stmts.push(pad(`if (${condJs}) {`, level));
195
+ genNodes(node.body, stmts, level + 1, buf, loopVarMap);
196
+ stmts.push(pad('}', level));
197
+ for (let i = 0; i < node.elifBranches.length; i++) {
198
+ const b = node.elifBranches[i];
199
+ const elifJs = compileExprToJs(b.conditionStr, loopVarMap);
200
+ stmts.push(pad(`else if (${elifJs}) {`, level));
201
+ genNodes(b.body, stmts, level + 1, buf, loopVarMap);
202
+ stmts.push(pad('}', level));
203
+ }
204
+ if (node.elseBody) {
205
+ stmts.push(pad('else {', level));
206
+ genNodes(node.elseBody, stmts, level + 1, buf, loopVarMap);
207
+ stmts.push(pad('}', level));
208
+ }
209
+ return;
210
+ }
211
+
212
+ case 'ForNode': {
213
+ const s = '_' + level;
214
+ stmts.push(pad('{', level));
215
+ const iterExpr = (loopVarMap && loopVarMap[node.iterablePath]) ? loopVarMap[node.iterablePath] : `_get(_ctx, ${js(node.iterablePath)})`;
216
+ stmts.push(pad(`let _raw${s} = ${iterExpr};`, level + 1));
217
+ for (let i = 0; i < node.filters.length; i++) {
218
+ const f = node.filters[i];
219
+ let arg = 'void 0';
220
+ if (f.arg) {
221
+ if (f.arg.type === 'literal') arg = js(f.arg.value);
222
+ else arg = (loopVarMap && loopVarMap[f.arg.value]) ? loopVarMap[f.arg.value] : `_get(_ctx, ${js(f.arg.value)})`;
223
+ }
224
+ const fnRef = f._fnRef ? `_f_${f.name}` : '_missingFilter';
225
+ if (f._fnRef) {
226
+ stmts.push(pad(`_raw${s} = ${fnRef}(_raw${s}, ${arg}, _ctx);`, level + 1));
227
+ } else {
228
+ stmts.push(pad(`_raw${s} = _missingFilter('${f.name}', _raw${s}, ${arg}, _ctx);`, level + 1));
229
+ }
230
+ }
231
+
232
+ const needForloop = hasForloopRef(node.body);
233
+ const loopVars = node.loopVars;
234
+ const newLoopVarMap = Object.assign({}, loopVarMap);
235
+
236
+ stmts.push(pad(`if (Array.isArray(_raw${s})) {`, level + 1));
237
+ stmts.push(pad(`const _len${s} = _raw${s}.length;`, level + 2));
238
+ stmts.push(pad(`if (_len${s} === 0) {`, level + 2));
239
+ if (node.emptyBody) genNodes(node.emptyBody, stmts, level + 3, buf, loopVarMap);
240
+ stmts.push(pad('} else {', level + 2));
241
+
242
+ if (needForloop) stmts.push(pad(`const _parentLoop${s} = _get(_ctx, 'forloop');`, level + 3));
243
+ for (let i = 0; i < loopVars.length; i++) {
244
+ const v = loopVars[i];
245
+ stmts.push(pad(`const _saved_${v}${s} = _ctx._local[${js(v)}];`, level + 3));
246
+ }
247
+ if (needForloop) stmts.push(pad(`const _saved_forloop${s} = _ctx._local.forloop;`, level + 3));
248
+
249
+ stmts.push(pad(`for (let _i${s} = 0; _i${s} < _len${s}; _i${s}++) {`, level + 3));
250
+ stmts.push(pad(`const _item${s} = _raw${s}[_i${s}];`, level + 4));
251
+ if (loopVars.length === 1) {
252
+ stmts.push(pad(`_ctx._local[${js(loopVars[0])}] = _item${s};`, level + 4));
253
+ newLoopVarMap[loopVars[0]] = `_item${s}`;
254
+ } else if (loopVars.length === 2) {
255
+ stmts.push(pad(`_ctx._local[${js(loopVars[0])}] = Array.isArray(_item${s}) ? _item${s}[0] : null;`, level + 4));
256
+ stmts.push(pad(`_ctx._local[${js(loopVars[1])}] = Array.isArray(_item${s}) ? _item${s}[1] : _item${s};`, level + 4));
257
+ newLoopVarMap[loopVars[0]] = `(Array.isArray(_item${s}) ? _item${s}[0] : null)`;
258
+ newLoopVarMap[loopVars[1]] = `(Array.isArray(_item${s}) ? _item${s}[1] : _item${s})`;
259
+ }
260
+
261
+ if (needForloop) {
262
+ stmts.push(pad(`_ctx._local.forloop = { counter: _i${s}+1, counter0: _i${s}, revcounter: _len${s}-_i${s}, revcounter0: _len${s}-_i${s}-1, first: _i${s}===0, last: _i${s}===_len${s}-1, parentloop: _parentLoop${s} && typeof _parentLoop${s} === 'object' ? _parentLoop${s} : null };`, level + 4));
263
+ }
264
+
265
+ genNodes(node.body, stmts, level + 4, buf, newLoopVarMap);
266
+
267
+ stmts.push(pad('}', level + 3));
268
+
269
+ for (let i = 0; i < loopVars.length; i++) {
270
+ const v = loopVars[i];
271
+ stmts.push(pad(`_ctx._local[${js(v)}] = _saved_${v}${s};`, level + 3));
272
+ }
273
+ if (needForloop) stmts.push(pad(`_ctx._local.forloop = _saved_forloop${s};`, level + 3));
274
+
275
+ stmts.push(pad('}', level + 2));
276
+
277
+ stmts.push(pad('} else {', level + 1));
278
+ stmts.push(pad(`const _items${s} = _normalizeFor(_raw${s});`, level + 2));
279
+ stmts.push(pad(`if (_items${s}.length === 0) {`, level + 2));
280
+ if (node.emptyBody) genNodes(node.emptyBody, stmts, level + 3, buf, loopVarMap);
281
+ stmts.push(pad('} else {', level + 2));
282
+ stmts.push(pad(`const _len${s} = _items${s}.length;`, level + 3));
283
+ if (needForloop) stmts.push(pad(`const _parentLoop${s} = _get(_ctx, 'forloop');`, level + 3));
284
+ for (let i = 0; i < loopVars.length; i++) {
285
+ const v = loopVars[i];
286
+ stmts.push(pad(`const _saved_${v}${s} = _ctx._local[${js(v)}];`, level + 3));
287
+ }
288
+ if (needForloop) stmts.push(pad(`const _saved_forloop${s} = _ctx._local.forloop;`, level + 3));
289
+
290
+ stmts.push(pad(`for (let _i${s} = 0; _i${s} < _len${s}; _i${s}++) {`, level + 3));
291
+ stmts.push(pad(`const _kv${s} = _items${s}[_i${s}];`, level + 4));
292
+ for (let i = 0; i < loopVars.length; i++) {
293
+ const v = loopVars[i];
294
+ const valExpr = loopVars.length === 1 ? `_kv${s}[1]` : (i === 0 ? `_kv${s}[0]` : (i === 1 ? `_kv${s}[1]` : `[_kv${s}[0], _kv${s}[1]]`));
295
+ stmts.push(pad(`_ctx._local[${js(v)}] = ${valExpr};`, level + 4));
296
+ newLoopVarMap[v] = valExpr;
297
+ }
298
+ if (needForloop) {
299
+ stmts.push(pad(`_ctx._local.forloop = { counter: _i${s}+1, counter0: _i${s}, revcounter: _len${s}-_i${s}, revcounter0: _len${s}-_i${s}-1, first: _i${s}===0, last: _i${s}===_len${s}-1, parentloop: _parentLoop${s} && typeof _parentLoop${s} === 'object' ? _parentLoop${s} : null };`, level + 4));
300
+ }
301
+ genNodes(node.body, stmts, level + 4, buf, newLoopVarMap);
302
+
303
+ stmts.push(pad('}', level + 3));
304
+
305
+ for (let i = 0; i < loopVars.length; i++) {
306
+ const v = loopVars[i];
307
+ stmts.push(pad(`_ctx._local[${js(v)}] = _saved_${v}${s};`, level + 3));
308
+ }
309
+ if (needForloop) stmts.push(pad(`_ctx._local.forloop = _saved_forloop${s};`, level + 3));
310
+
311
+ stmts.push(pad('}', level + 2));
312
+ stmts.push(pad('}', level + 1));
313
+ stmts.push(pad('}', level));
314
+ return;
315
+ }
316
+
317
+ case 'WithNode': {
318
+ stmts.push(pad('{', level));
319
+ stmts.push(pad('const _scope = {};', level + 1));
320
+ const savedKeys = [];
321
+ for (let i = 0; i < node.mappings.length; i++) {
322
+ const m = node.mappings[i];
323
+ stmts.push(pad(`_scope[${js(m.name)}] = _resolveVal(${js(m.valPath)}, _ctx);`, level + 1));
324
+ savedKeys.push(m.name);
325
+ }
326
+ if (node.aliasName && node.mappings.length > 0) {
327
+ const last = node.mappings[node.mappings.length - 1];
328
+ stmts.push(pad(`_scope[${js(node.aliasName)}] = _scope[${js(last.name)}];`, level + 1));
329
+ savedKeys.push(node.aliasName);
330
+ }
331
+ for (let i = 0; i < savedKeys.length; i++) {
332
+ stmts.push(pad(`const _saved_${savedKeys[i]} = _ctx._local[${js(savedKeys[i])}];`, level + 1));
333
+ }
334
+ for (let i = 0; i < savedKeys.length; i++) {
335
+ stmts.push(pad(`_ctx._local[${js(savedKeys[i])}] = _scope[${js(savedKeys[i])}];`, level + 1));
336
+ }
337
+ genNodes(node.body, stmts, level + 1, buf, loopVarMap);
338
+ for (let i = 0; i < savedKeys.length; i++) {
339
+ stmts.push(pad(`_ctx._local[${js(savedKeys[i])}] = _saved_${savedKeys[i]};`, level + 1));
340
+ }
341
+ stmts.push(pad('}', level));
342
+ return;
343
+ }
344
+
345
+ case 'CommentNode':
346
+ return;
347
+
348
+ case 'AutoescapeNode': {
349
+ stmts.push(pad('{', level));
350
+ stmts.push(pad(`const _ae = a; a = ${node.setting === 'on' ? 'true' : 'false'};`, level + 1));
351
+ genNodes(node.body, stmts, level + 1, buf, loopVarMap);
352
+ stmts.push(pad('a = _ae;', level + 1));
353
+ stmts.push(pad('}', level));
354
+ return;
355
+ }
356
+
357
+ case 'CycleNode':
358
+ stmts.push(pad(`${buf} += _cycle(_ctx, ${js(node.args)}, ${js(node.asName)});`, level));
359
+ return;
360
+
361
+ case 'FirstofNode':
362
+ stmts.push(pad(`${buf} += _firstof(_ctx, ${js(node.args)});`, level));
363
+ return;
364
+
365
+ case 'PartialDefNode': {
366
+ stmts.push(pad(`_registerPartial(_ctx, ${js(node.name)}, _partials[${node._partialId}]);`, level));
367
+ if (node.inline) {
368
+ stmts.push(pad('{', level));
369
+ genNodes(node.body, stmts, level + 1, buf, loopVarMap);
370
+ stmts.push(pad('}', level));
371
+ }
372
+ return;
373
+ }
374
+
375
+ case 'PartialNode': {
376
+ stmts.push(pad(`${buf} += _partial(_ctx, ${js(node.name)}, ${js(node.extraMappings)});`, level));
377
+ return;
378
+ }
379
+
380
+ case 'StaticNode':
381
+ stmts.push(pad(`${buf} += _static(_ctx, ${js(node.pathExpr)});`, level));
382
+ return;
383
+
384
+ case 'UrlNode':
385
+ stmts.push(pad(`${buf} += _url(_ctx, ${js(node.routeNameExpr)}, ${js(node.positionalArgs)}, ${js(node.kwargs)});`, level));
386
+ return;
387
+
388
+ case 'RegroupNode':
389
+ stmts.push(pad(`${buf} += _regroup(_ctx, ${js(node.listPath)}, ${js(node.attr)}, ${js(node.targetName)});`, level));
390
+ return;
391
+
392
+ case 'SpacelessNode': {
393
+ stmts.push(pad('{', level));
394
+ stmts.push(pad('let _spaceBuf = \'\';', level + 1));
395
+ genNodes(node.body, stmts, level + 1, '_spaceBuf', loopVarMap);
396
+ stmts.push(pad(`${buf} += _spaceBuf.replace(/>\\s+</g, '><'); }`, level));
397
+ return;
398
+ }
399
+
400
+ case 'CsrfTokenNode':
401
+ stmts.push(pad(`${buf} += _csrf(_ctx);`, level));
402
+ return;
403
+
404
+ case 'CspNonceAttrNode':
405
+ stmts.push(pad(`${buf} += _csp_nonce(_ctx);`, level));
406
+ return;
407
+
408
+ case 'LoadNode':
409
+ stmts.push(pad(`_loadLibs(_ctx, ${js(node.libraries)});`, level));
410
+ return;
411
+
412
+ case 'TemplatetagNode': {
413
+ const map = {
414
+ 'openblock': '{%',
415
+ 'closeblock': '%}',
416
+ 'openvariable': '{{',
417
+ 'closevariable': '}}',
418
+ 'openbrace': '{',
419
+ 'closebrace': '}',
420
+ 'opencomment': '{#',
421
+ 'closecomment': '#}'
422
+ };
423
+ const tokenStr = map[node.token] || node.token;
424
+ stmts.push(pad(`${buf} += ${js(tokenStr)};`, level));
425
+ return;
426
+ }
427
+
428
+ case 'WidthRatioNode': {
429
+ const out = String(Math.max(0, Math.min(Math.floor((node.value / node.maxValue) * node.maxWidth), node.maxWidth)));
430
+ stmts.push(pad(`${buf} += ${js(out)};`, level));
431
+ return;
432
+ }
433
+
434
+ case 'DebugNode':
435
+ stmts.push(pad(`${buf} += _debug(_ctx);`, level));
436
+ return;
437
+
438
+ case 'NowNode':
439
+ stmts.push(pad(`${buf} += _now(_ctx, ${js(node.formatExpr)});`, level));
440
+ return;
441
+
442
+ case 'SetNode': {
443
+ const valExpr = node.valueExpr !== undefined ? `_resolveVal(${js(node.valueExpr)}, _ctx)` : '\'\'';
444
+ stmts.push(pad(`_ctx._local[${js(node.nameExpr)}] = ${valExpr};`, level));
445
+ if (node.body && node.body.length) genNodes(node.body, stmts, level, buf, loopVarMap);
446
+ return;
447
+ }
448
+
449
+ case 'IfChangedNode': {
450
+ stmts.push(pad('{', level));
451
+ stmts.push(pad('if (!_ctx.ifChangedState) _ctx.ifChangedState = new Map();', level + 1));
452
+ stmts.push(pad(`const _cur = _resolveVal(${js(node.conditionStr)}, _ctx);`, level + 1));
453
+ stmts.push(pad(`const _last = _ctx.ifChangedState.get(${js(node.conditionStr)});`, level + 1));
454
+ stmts.push(pad('if (_last === undefined || _cur !== _last) {', level + 1));
455
+ stmts.push(pad(`_ctx.ifChangedState.set(${js(node.conditionStr)}, _cur);`, level + 2));
456
+ stmts.push(pad('let _subBuf = \'\';', level + 2));
457
+ genNodes(node.body, stmts, level + 3, '_subBuf', loopVarMap);
458
+ stmts.push(pad(`${buf} += _subBuf; }`, level + 2));
459
+ if (node.elseBody) {
460
+ stmts.push(pad('else {', level + 1));
461
+ stmts.push(pad('let _subBuf2 = \'\';', level + 2));
462
+ genNodes(node.elseBody, stmts, level + 3, '_subBuf2', loopVarMap);
463
+ stmts.push(pad(`${buf} += _subBuf2; }`, level + 2));
464
+ }
465
+ stmts.push(pad('}', level + 1));
466
+ return;
467
+ }
468
+
469
+ case 'TransNode':
470
+ stmts.push(pad(`${buf} += _trans(_ctx, ${js(node.key)}, ${js(node.args)});`, level));
471
+ return;
472
+
473
+ case 'BlockTransNode':
474
+ stmts.push(pad(`${buf} += _blocktrans(_ctx, ${js(node.textParts)}, ${js(node.withMappings)}, ${js(node.pluralMappings)});`, level));
475
+ return;
476
+
477
+ case 'LanguageNode':
478
+ stmts.push(pad(`${buf} += _language(_ctx, ${js(node.lang)}, _languages[${node._langId}]);`, level));
479
+ return;
480
+
481
+ case 'HelperNode': {
482
+ stmts.push(pad(`${buf} += _astNodes[${node._id}].render(_ctx);`, level));
483
+ return;
484
+ }
485
+
486
+ case 'PluralMappingNode':
487
+ return;
488
+
489
+ case 'ExtendsNode':
490
+ stmts.push(pad(`${buf} += _extends(_ctx, ${js(node.parentTemplateExpr)});`, level));
491
+ return;
492
+
493
+ case 'BlockNode':
494
+ stmts.push(pad(`${buf} += _block(_ctx, ${js(node.name)});`, level));
495
+ return;
496
+
497
+ case 'IncludeNode':
498
+ stmts.push(pad(`${buf} += _include(_ctx, ${js(node.templateNameExpr)}, ${js(node.extraMappings)}, ${js(node.partialName)});`, level));
499
+ return;
500
+
501
+ default:
502
+ stmts.push(pad(`${buf} += _fallback(_ctx, _astNodes[${node._id}]);`, level));
503
+ }
504
+ }
505
+
506
+ function preResolveFilters(nodes) {
507
+ function walk(list) {
508
+ if (!list) return;
509
+ for (let i = 0; i < list.length; i++) {
510
+ const n = list[i];
511
+ if (n.filters) {
512
+ for (let j = 0; j < n.filters.length; j++) {
513
+ const fn = filtersModule.getFilter(n.filters[j].name);
514
+ if (fn) {
515
+ n.filters[j]._fnRef = `_f_${n.filters[j].name}`;
516
+ n.filters[j]._fn = fn;
517
+ }
518
+ }
519
+ }
520
+ if (n.body) walk(n.body);
521
+ if (n.elseBody) walk(n.elseBody);
522
+ if (n.elifBranches) for (let j = 0; j < n.elifBranches.length; j++) walk(n.elifBranches[j].body);
523
+ }
524
+ }
525
+ walk(nodes);
526
+ }
527
+
528
+ function collectFilterNames(nodes) {
529
+ const names = new Set();
530
+ function walk(list) {
531
+ if (!list) return;
532
+ for (let i = 0; i < list.length; i++) {
533
+ const n = list[i];
534
+ if (n.filters) {
535
+ for (let j = 0; j < n.filters.length; j++) {
536
+ if (n.filters[j]._fnRef) names.add(n.filters[j].name);
537
+ }
538
+ }
539
+ if (n.body) walk(n.body);
540
+ if (n.elseBody) walk(n.elseBody);
541
+ if (n.elifBranches) for (let j = 0; j < n.elifBranches.length; j++) walk(n.elifBranches[j].body);
542
+ }
543
+ }
544
+ walk(nodes);
545
+ return Array.from(names);
546
+ }
547
+
548
+ function canCodegen(_nodes) {
549
+ return true;
550
+ }
551
+
552
+ function tagNodes(nodes) {
553
+ let nextId = 0;
554
+ function walk(list) {
555
+ if (!list) return;
556
+ for (let i = 0; i < list.length; i++) {
557
+ list[i]._id = nextId++;
558
+ if (list[i].body) walk(list[i].body);
559
+ if (list[i].elseBody) walk(list[i].elseBody);
560
+ if (list[i].elifBranches) for (let j = 0; j < list[i].elifBranches.length; j++) walk(list[i].elifBranches[j].body);
561
+ }
562
+ }
563
+ walk(nodes);
564
+ }
565
+
566
+ function tagPartialNodes(nodes) {
567
+ let nextId = 0;
568
+ function walk(list) {
569
+ if (!list) return;
570
+ for (let i = 0; i < list.length; i++) {
571
+ if (list[i].constructor.name === 'PartialDefNode') {
572
+ list[i]._partialId = nextId++;
573
+ }
574
+ if (list[i].body) walk(list[i].body);
575
+ if (list[i].elseBody) walk(list[i].elseBody);
576
+ if (list[i].elifBranches) for (let j = 0; j < list[i].elifBranches.length; j++) walk(list[i].elifBranches[j].body);
577
+ }
578
+ }
579
+ walk(nodes);
580
+ }
581
+
582
+ function tagLanguageNodes(nodes) {
583
+ let nextId = 0;
584
+ function walk(list) {
585
+ if (!list) return;
586
+ for (let i = 0; i < list.length; i++) {
587
+ if (list[i].constructor.name === 'LanguageNode') {
588
+ list[i]._langId = nextId++;
589
+ }
590
+ if (list[i].body) walk(list[i].body);
591
+ if (list[i].elseBody) walk(list[i].elseBody);
592
+ if (list[i].elifBranches) for (let j = 0; j < list[i].elifBranches.length; j++) walk(list[i].elifBranches[j].body);
593
+ }
594
+ }
595
+ walk(nodes);
596
+ }
597
+
598
+ function flattenNodes(nodes) {
599
+ const out = [];
600
+ function walk(list) {
601
+ if (!list) return;
602
+ for (let i = 0; i < list.length; i++) {
603
+ out.push(list[i]);
604
+ if (list[i].body) walk(list[i].body);
605
+ if (list[i].elseBody) walk(list[i].elseBody);
606
+ if (list[i].elifBranches) for (let j = 0; j < list[i].elifBranches.length; j++) walk(list[i].elifBranches[j].body);
607
+ }
608
+ }
609
+ walk(nodes);
610
+ return out;
611
+ }
612
+
613
+ function buildCode(nodes) {
614
+ preResolveFilters(nodes);
615
+ tagNodes(nodes);
616
+ tagPartialNodes(nodes);
617
+ tagLanguageNodes(nodes);
618
+
619
+ const filterNames = collectFilterNames(nodes);
620
+ const filterDecls = filterNames.map(n => `const _f_${n} = _f['${n}'];`).join('\n');
621
+
622
+ const stmts = [];
623
+ stmts.push('\'use strict\';');
624
+ stmts.push('let out = \'\';');
625
+ stmts.push('let a = _ctx.autoescape;');
626
+ if (filterDecls) stmts.push(filterDecls);
627
+ genNodes(nodes, stmts, 1, 'out', {});
628
+ stmts.push('return out;');
629
+
630
+ const body = stmts.join('\n');
631
+
632
+ const args = [
633
+ '_ctx', '_get', '_escape', '_escapeStr', '_f', '_cycle', '_firstof',
634
+ '_partial', '_include', '_static', '_url', '_regroup', '_csrf',
635
+ '_csp_nonce', '_loadLibs', '_debug', '_now', '_set', '_ifchanged',
636
+ '_resolveVal', '_helperCall', '_missingFilter', '_normalizeFor',
637
+ '_trans', '_blocktrans', '_registerPartial', '_extends', '_block',
638
+ '_language', '_fallback', '_SafeString', '_astNodes', '_partials', '_languages', '_in'
639
+ ];
640
+
641
+ const src = `"use strict"; return function(${args.join(',')}) { ${body} }`;
642
+
643
+ // eslint-disable-next-line no-new-func
644
+ return new Function(src)();
645
+ }
646
+
647
+ const ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' };
648
+ const HAS_ESCAPE_RE = /[&<>"']/;
649
+ const ESCAPE_RE = /[&<>"']/g;
650
+
651
+ function fastEscape(v) {
652
+ if (v == null) return '';
653
+ if (v instanceof SafeString) return v.toString();
654
+ const str = typeof v === 'string' ? v : String(v);
655
+ if (!HAS_ESCAPE_RE.test(str)) return str;
656
+ return str.replace(ESCAPE_RE, c => ESCAPE_MAP[c]);
657
+ }
658
+
659
+ function fastEscapeString(s) {
660
+ return HAS_ESCAPE_RE.test(s) ? s.replace(ESCAPE_RE, c => ESCAPE_MAP[c]) : s;
661
+ }
662
+
663
+ function inHelper(a, b) {
664
+ if (Array.isArray(b) || typeof b === 'string') return b.includes(a);
665
+ if (b && typeof b === 'object') return a in b;
666
+ return false;
667
+ }
668
+
669
+ function resolveVal(token, context) {
670
+ if (token === undefined || token === null) return '';
671
+ if (typeof token !== 'string') return token;
672
+ if (token === '') return '';
673
+ if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
674
+ return token.slice(1, -1);
675
+ }
676
+ if (token === 'true' || token === 'True') return true;
677
+ if (token === 'false' || token === 'False') return false;
678
+ if (token === 'none' || token === 'None' || token === 'null') return null;
679
+ if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
680
+ return context.get(token);
681
+ }
682
+
683
+ function getVal(ctx, path) {
684
+ let v = ctx._local[path];
685
+ if (v === undefined) v = ctx.get(path);
686
+ return typeof v === 'function' ? v.call(null) : v;
687
+ }
688
+
689
+ function normalizeFor(raw) {
690
+ if (Array.isArray(raw)) {
691
+ const out = new Array(raw.length);
692
+ for (let i = 0; i < raw.length; i++) out[i] = [null, raw[i]];
693
+ return out;
694
+ }
695
+ if (raw && typeof raw === 'object' && !(raw instanceof Date)) {
696
+ const out = [];
697
+ for (const k of Object.keys(raw)) out.push([k, raw[k]]);
698
+ return out;
699
+ }
700
+ return [];
701
+ }
702
+
703
+ function cycleHelper(ctx, args, asName) {
704
+ const key = args.join(',');
705
+ let idx = ctx.cycleStates.get(key) || 0;
706
+ const token = args[idx % args.length];
707
+ const val = resolveVal(token, ctx);
708
+ ctx.cycleStates.set(key, idx + 1);
709
+ if (asName) {
710
+ ctx._local[asName] = val;
711
+ return '';
712
+ }
713
+ return String(val);
714
+ }
715
+
716
+ function firstofHelper(ctx, args) {
717
+ for (let i = 0; i < args.length; i++) {
718
+ const v = resolveVal(args[i], ctx);
719
+ if (v && v !== '' && v !== null && v !== undefined) return String(v);
720
+ }
721
+ return '';
722
+ }
723
+
724
+ function partialHelper(ctx, name, extra) {
725
+ const partial = ctx.getPartial(name);
726
+ if (!partial) throw new Error(`Partial '${name}' not found`);
727
+ if (extra && extra.length) {
728
+ const scope = {};
729
+ for (let i = 0; i < extra.length; i++) scope[extra[i].name] = resolveVal(extra[i].valPath, ctx);
730
+ ctx.push(scope);
731
+ let out = '';
732
+ for (let i = 0; i < partial.body.length; i++) {
733
+ const r = partial.body[i].render(ctx);
734
+ out += r instanceof Promise ? '' : (r || '');
735
+ }
736
+ ctx.pop();
737
+ return out;
738
+ }
739
+ let out = '';
740
+ for (let i = 0; i < partial.body.length; i++) {
741
+ const r = partial.body[i].render(ctx);
742
+ out += r instanceof Promise ? '' : (r || '');
743
+ }
744
+ return out;
745
+ }
746
+
747
+ function includeHelper(ctx, name, extra, partial) {
748
+ const { IncludeNode } = require('./tags/inheritance');
749
+ return new IncludeNode(name, extra, partial).render(ctx);
750
+ }
751
+
752
+ function staticHelper(ctx, path) {
753
+ const { StaticNode } = require('./tags/util');
754
+ return new StaticNode(path).render(ctx);
755
+ }
756
+
757
+ function urlHelper(ctx, route, pos, kw) {
758
+ const { UrlNode } = require('./tags/util');
759
+ return new UrlNode(route, pos, kw).render(ctx);
760
+ }
761
+
762
+ function regroupHelper(ctx, list, attr, target) {
763
+ const { RegroupNode } = require('./tags/util');
764
+ return new RegroupNode(list, attr, target).render(ctx);
765
+ }
766
+
767
+ function csrfHelper(ctx) {
768
+ const { CsrfTokenNode } = require('./tags/util');
769
+ return new CsrfTokenNode().render(ctx);
770
+ }
771
+
772
+ function cspNonceHelper(ctx) {
773
+ const { CspNonceAttrNode } = require('./tags/util');
774
+ return new CspNonceAttrNode().render(ctx);
775
+ }
776
+
777
+ function loadLibsHelper(ctx, libs) {
778
+ const { LoadNode } = require('./tags/util');
779
+ return new LoadNode(libs).render(ctx);
780
+ }
781
+
782
+ function debugHelper(ctx) {
783
+ const { DebugNode } = require('./tags/util');
784
+ return new DebugNode().render(ctx);
785
+ }
786
+
787
+ function nowHelper(ctx, fmt) {
788
+ const { NowNode } = require('./tags/extra');
789
+ return new NowNode(fmt).render(ctx);
790
+ }
791
+
792
+ function transHelper(ctx, key, args) {
793
+ const i18n = require('./i18n');
794
+ const params = {};
795
+ if (args) for (const [k, v] of Object.entries(args)) params[k] = ctx.get(v);
796
+ let k = key;
797
+ if (!k.startsWith('"') && !k.startsWith('\'') && !k.includes(' ')) {
798
+ const resolved = ctx.get(k);
799
+ if (typeof resolved === 'string' && resolved.length > 0) k = resolved;
800
+ } else {
801
+ k = k.slice(1, -1);
802
+ }
803
+ return i18n.lookup(k, params);
804
+ }
805
+
806
+ function blocktransHelper(ctx, parts, withMap, pluralMap) {
807
+ const { BlockTransNode } = require('./tags/i18n');
808
+ return new BlockTransNode(parts, withMap, pluralMap, []).render(ctx);
809
+ }
810
+
811
+ function languageHelper(ctx, lang, node) { return node.render(ctx); }
812
+ function extendsHelper(ctx, expr) {
813
+ const { ExtendsNode } = require('./tags/inheritance');
814
+ return new ExtendsNode(expr).render(ctx);
815
+ }
816
+ function blockHelper(ctx, name) {
817
+ const { BlockNode } = require('./tags/inheritance');
818
+ return new BlockNode(name, []).render(ctx);
819
+ }
820
+ function helperCall(ctx, name, inner) {
821
+ const fn = ctx._helpers && ctx._helpers.get(name);
822
+ if (!fn) throw new Error(`Helper '${name}' not found`);
823
+ return fn(inner, ctx);
824
+ }
825
+ function registerPartialHelper(ctx, name, node) { ctx.registerPartial(name, node); }
826
+ function fallbackHelper(ctx, node) { if (!node) return ''; const r = node.render(ctx); return r instanceof Promise ? '' : (r || ''); }
827
+ function missingFilter(name) { throw new Error(`Unknown filter: '${name}'`); }
828
+
829
+ function generateCode(nodes) {
830
+ if (!canCodegen(nodes)) return null;
831
+ const fn = buildCode(nodes);
832
+ const partialsList = [];
833
+ const languagesList = [];
834
+ function collect(list) {
835
+ if (!list) return;
836
+ for (let i = 0; i < list.length; i++) {
837
+ const n = list[i];
838
+ if (n.constructor.name === 'PartialDefNode') partialsList.push(n);
839
+ if (n.constructor.name === 'LanguageNode') languagesList.push(n);
840
+ if (n.body) collect(n.body);
841
+ if (n.elseBody) collect(n.elseBody);
842
+ if (n.elifBranches) for (let j = 0; j < n.elifBranches.length; j++) collect(n.elifBranches[j].body);
843
+ }
844
+ }
845
+ collect(nodes);
846
+ const flat = flattenNodes(nodes);
847
+
848
+ const filterNames = collectFilterNames(nodes);
849
+ const filterMap = {};
850
+ for (const name of filterNames) {
851
+ const fn = filtersModule.getFilter(name);
852
+ if (fn) filterMap[name] = fn;
853
+ }
854
+
855
+ return function renderGenerated(ctx) {
856
+ return fn(
857
+ ctx,
858
+ getVal,
859
+ fastEscape,
860
+ fastEscapeString,
861
+ filterMap,
862
+ cycleHelper,
863
+ firstofHelper,
864
+ partialHelper,
865
+ includeHelper,
866
+ staticHelper,
867
+ urlHelper,
868
+ regroupHelper,
869
+ csrfHelper,
870
+ cspNonceHelper,
871
+ loadLibsHelper,
872
+ debugHelper,
873
+ nowHelper,
874
+ null,
875
+ null,
876
+ resolveVal,
877
+ helperCall,
878
+ missingFilter,
879
+ normalizeFor,
880
+ transHelper,
881
+ blocktransHelper,
882
+ registerPartialHelper,
883
+ extendsHelper,
884
+ blockHelper,
885
+ languageHelper,
886
+ fallbackHelper,
887
+ SafeString,
888
+ flat,
889
+ partialsList,
890
+ languagesList,
891
+ inHelper
892
+ );
893
+ };
894
+ }
895
+
896
+ module.exports = {
897
+ generateCode,
898
+ canCodegen,
899
+ buildCode,
900
+ tagNodes,
901
+ flattenNodes,
902
+ preResolveFilters,
903
+ collectFilterNames,
904
+ genNodes
905
+ };