miki-template 1.3.3 → 1.3.6
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/.github/release-notes/v1.3.1.md +55 -55
- package/.github/release-notes/v1.3.3.md +77 -0
- package/.github/workflows/ci.yml +38 -54
- package/.github/workflows/release.yml +106 -0
- package/AGENT.md +71 -71
- package/API_REFERENCE.md +314 -314
- package/CHANGELOG.md +173 -169
- package/CODE_OF_CONDUCT.md +14 -14
- package/CONTRIBUTING.md +27 -27
- package/README.md +342 -321
- package/ROADMAP.md +40 -40
- package/benchmarks/report.json +16 -16
- package/benchmarks/run.js +49 -49
- package/benchmarks/stress.mjs +647 -647
- package/benchmarks/templates/large.dtpl +7 -7
- package/benchmarks/templates/medium.dtpl +3 -3
- package/benchmarks/templates/small.dtpl +7 -7
- package/context/component.md +109 -109
- package/context/prd.md +131 -131
- package/context/project-structure.md +33 -33
- package/dir/base.html +22 -22
- package/dir/cmpnt.html +10 -10
- package/dir/footer.html +2 -2
- package/dir/home.html +80 -80
- package/dir/index.html +80 -0
- package/dir/navbar.html +8 -8
- package/docs/README.md +18 -18
- package/docs/advanced_usage.md +71 -71
- package/docs/api.md +119 -119
- package/docs/filters.md +708 -708
- package/docs/installation.md +106 -106
- package/docs/overview.md +57 -57
- package/docs/partialdef.md +70 -70
- package/docs/security.md +27 -27
- package/docs/tags.md +673 -673
- package/docs/usage.md +646 -646
- package/eslint.config.mjs +42 -42
- package/ex.mjs +32 -32
- package/miki-template-extension/.github/workflows/ci.yml +116 -116
- package/miki-template-extension/.vscodeignore +7 -7
- package/miki-template-extension/CHANGELOG.md +99 -99
- package/miki-template-extension/LICENSE +21 -21
- package/miki-template-extension/README.md +273 -273
- package/miki-template-extension/extension.js +1013 -1013
- package/miki-template-extension/icon.svg +10 -10
- package/miki-template-extension/package.json +280 -280
- package/miki-template-extension/snippets/miki-template.json +717 -717
- package/miki-template-extension/syntaxes/language-configuration.json +114 -114
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -355
- package/miki-template-extension/tests/grammar-tests.json +162 -162
- package/miki-template-extension/tests/run-grammar-tests.js +82 -82
- package/package.json +37 -34
- package/sample-app/package-lock.json +901 -0
- package/sample-app/package.json +9 -0
- package/sample-app/server.js +14 -0
- package/sample-app/views/index.html +1 -0
- package/scripts/build-vsix.js +129 -129
- package/scripts/build-vsix.ps1 +15 -15
- package/snippets/miki-template.json +177 -177
- package/src/asyncRender.js +20 -20
- package/src/cache.js +80 -80
- package/src/context.js +126 -126
- package/src/context_processors.js +48 -48
- package/src/esm.mjs +84 -84
- package/src/filters.js +975 -975
- package/src/i18n.js +171 -171
- package/src/index.js +974 -940
- package/src/lexer.js +114 -114
- package/src/libraries.js +371 -371
- package/src/parser.js +270 -270
- package/src/security.js +53 -53
- package/src/tags/control.js +719 -719
- package/src/tags/extra.js +154 -154
- package/src/tags/helpers.js +26 -26
- package/src/tags/i18n.js +256 -256
- package/src/tags/inheritance.js +335 -335
- package/src/tags/registry.js +18 -18
- package/src/tags/util.js +400 -400
- package/src/types.d.ts +107 -107
- package/syntaxes/language-configuration.json +26 -26
- package/syntaxes/miki-template.tmLanguage.json +146 -146
- package/tests/asyncRender.test.js +17 -17
- package/tests/base.html +6 -6
- package/tests/child.html +3 -3
- package/tests/context_processors.test.js +13 -13
- package/tests/esm.test.mjs +61 -61
- package/tests/filters.test.js +254 -254
- package/tests/include_security.test.js +9 -9
- package/tests/integration/README.md +32 -32
- package/tests/integration/features.test.cjs +1681 -1681
- package/tests/integration/features.test.mjs +1697 -1697
- package/tests/integration/templates/base.miki +6 -6
- package/tests/integration/templates/child.miki +6 -6
- package/tests/integration/templates/index.html +17 -17
- package/tests/lexer.test.js +45 -45
- package/tests/parser.test.js +57 -57
- package/tests/partial.html +1 -1
- package/tests/partialdef.test.js +79 -79
- package/tests/production_checks.js +57 -57
- package/tests/security.test.js +28 -28
- package/tests/tags.test.js +233 -233
package/src/index.js
CHANGED
|
@@ -1,940 +1,974 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Django-Style Template Engine for Node.js/Express
|
|
3
|
-
* Main entrypoint.
|
|
4
|
-
*/
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { tokenize } = require('./lexer');
|
|
8
|
-
const { Parser } = require('./parser');
|
|
9
|
-
const { Context } = require('./context');
|
|
10
|
-
|
|
11
|
-
const { registerContextProcessor, applyContextProcessors, clearContextProcessors } = require('./context_processors');
|
|
12
|
-
const { registerFilter, getFilter } = require('./filters');
|
|
13
|
-
const { SafeString, markSafe, isSafe, escapeHtml } = require('./security');
|
|
14
|
-
const { getCompiled, clearCache, getParentSource, hasParentSource } = require('./cache');
|
|
15
|
-
const { registerHelper } = require('./tags/helpers');
|
|
16
|
-
const { registerTag, getTagRegistry } = require('./tags/registry');
|
|
17
|
-
|
|
18
|
-
// Load control tags
|
|
19
|
-
const controlTags = require('./tags/control');
|
|
20
|
-
for (const [name, parserFn] of Object.entries(controlTags.parsers)) {
|
|
21
|
-
registerTag(name, parserFn);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Load inheritance tags
|
|
25
|
-
const inheritanceTags = require('./tags/inheritance');
|
|
26
|
-
for (const [name, parserFn] of Object.entries(inheritanceTags.parsers)) {
|
|
27
|
-
registerTag(name, parserFn);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Load utility tags
|
|
31
|
-
const utilTags = require('./tags/util');
|
|
32
|
-
for (const [name, parserFn] of Object.entries(utilTags.parsers)) {
|
|
33
|
-
registerTag(name, parserFn);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Load i18n tags
|
|
37
|
-
const i18nTags = require('./tags/i18n');
|
|
38
|
-
for (const [name, parserFn] of Object.entries(i18nTags.parsers)) {
|
|
39
|
-
registerTag(name, parserFn);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Load extra utility tags
|
|
43
|
-
const extraTags = require('./tags/extra');
|
|
44
|
-
for (const [name, parserFn] of Object.entries(extraTags.parsers)) {
|
|
45
|
-
registerTag(name, parserFn);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// i18n module
|
|
49
|
-
const i18n = require('./i18n');
|
|
50
|
-
|
|
51
|
-
// Plugin/filter library system
|
|
52
|
-
const libraries = require('./libraries');
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const context = new Context(processedContextObj,
|
|
261
|
-
context.
|
|
262
|
-
if (parser.blocks) {
|
|
263
|
-
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
264
|
-
context.blocks[name] = [...blockList];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
268
|
-
context.registerPartial(name, partial);
|
|
269
|
-
}
|
|
270
|
-
return
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
const context = new Context(processedContextObj,
|
|
290
|
-
context.blocks = {};
|
|
291
|
-
if (parser.blocks) {
|
|
292
|
-
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
293
|
-
context.blocks[name] = [...blockList];
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
297
|
-
context.registerPartial(name, partial);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
context.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
context.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
context
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
* );
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (typeof
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Django-Style Template Engine for Node.js/Express
|
|
3
|
+
* Main entrypoint.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { tokenize } = require('./lexer');
|
|
8
|
+
const { Parser } = require('./parser');
|
|
9
|
+
const { Context } = require('./context');
|
|
10
|
+
|
|
11
|
+
const { registerContextProcessor, applyContextProcessors, clearContextProcessors } = require('./context_processors');
|
|
12
|
+
const { registerFilter, getFilter } = require('./filters');
|
|
13
|
+
const { SafeString, markSafe, isSafe, escapeHtml } = require('./security');
|
|
14
|
+
const { getCompiled, clearCache, getParentSource, hasParentSource } = require('./cache');
|
|
15
|
+
const { registerHelper } = require('./tags/helpers');
|
|
16
|
+
const { registerTag, getTagRegistry } = require('./tags/registry');
|
|
17
|
+
|
|
18
|
+
// Load control tags
|
|
19
|
+
const controlTags = require('./tags/control');
|
|
20
|
+
for (const [name, parserFn] of Object.entries(controlTags.parsers)) {
|
|
21
|
+
registerTag(name, parserFn);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Load inheritance tags
|
|
25
|
+
const inheritanceTags = require('./tags/inheritance');
|
|
26
|
+
for (const [name, parserFn] of Object.entries(inheritanceTags.parsers)) {
|
|
27
|
+
registerTag(name, parserFn);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Load utility tags
|
|
31
|
+
const utilTags = require('./tags/util');
|
|
32
|
+
for (const [name, parserFn] of Object.entries(utilTags.parsers)) {
|
|
33
|
+
registerTag(name, parserFn);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Load i18n tags
|
|
37
|
+
const i18nTags = require('./tags/i18n');
|
|
38
|
+
for (const [name, parserFn] of Object.entries(i18nTags.parsers)) {
|
|
39
|
+
registerTag(name, parserFn);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Load extra utility tags
|
|
43
|
+
const extraTags = require('./tags/extra');
|
|
44
|
+
for (const [name, parserFn] of Object.entries(extraTags.parsers)) {
|
|
45
|
+
registerTag(name, parserFn);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// i18n module
|
|
49
|
+
const i18n = require('./i18n');
|
|
50
|
+
|
|
51
|
+
// Plugin/filter library system
|
|
52
|
+
const libraries = require('./libraries');
|
|
53
|
+
|
|
54
|
+
// Normalize views entries: if a views entry points to a file,
|
|
55
|
+
// return its directory so lookups work regardless of whether the
|
|
56
|
+
// caller passed a file path or a directory.
|
|
57
|
+
function normalizeViews(views) {
|
|
58
|
+
if (!views) return ['.'];
|
|
59
|
+
const arr = Array.isArray(views) ? views.slice() : [views];
|
|
60
|
+
return arr.map(v => {
|
|
61
|
+
try {
|
|
62
|
+
const st = fs.statSync(v);
|
|
63
|
+
if (st.isFile()) return path.dirname(v);
|
|
64
|
+
return v;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// If the path doesn't exist or can't be stat'd, return as-is.
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Inject the registration functions so libraries can activate without
|
|
73
|
+
// triggering a circular require. This must happen BEFORE the
|
|
74
|
+
// auto-activation loop below.
|
|
75
|
+
libraries.setRegistrationFunctions({
|
|
76
|
+
registerTag,
|
|
77
|
+
registerFilter,
|
|
78
|
+
registerHelper
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Auto-activate the built-in libraries so their filters/tags/helpers
|
|
82
|
+
// are available out of the box. Users can still opt out by calling
|
|
83
|
+
// `unregisterLibrary` or by re-registering without activating.
|
|
84
|
+
for (const libName of libraries.getLibraryNames()) {
|
|
85
|
+
libraries.activateLibrary(libName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render an AST recursively to resolve inheritance chain.
|
|
90
|
+
* Async-aware: awaits Promises from any node.
|
|
91
|
+
*/
|
|
92
|
+
function readParentSource(parentName, viewsDirs) {
|
|
93
|
+
for (const dir of viewsDirs) {
|
|
94
|
+
const key = dir + '\0' + parentName;
|
|
95
|
+
if (hasParentSource(key)) {
|
|
96
|
+
return getParentSource(key, () => null);
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const fullPath = path.resolve(dir, parentName);
|
|
100
|
+
const relative = path.relative(path.resolve(dir), fullPath);
|
|
101
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
102
|
+
throw new Error(`Extends tag attempted path traversal outside allowed views: '${parentName}'`);
|
|
103
|
+
}
|
|
104
|
+
const fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
105
|
+
// Populate the LRU cache and return the value
|
|
106
|
+
return getParentSource(key, () => fileContent);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e.message && e.message.startsWith('Extends tag attempted path traversal')) {
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Template not found: '${parentName}' in directories ${JSON.stringify(viewsDirs)}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function renderASTAsync(nodes, context) {
|
|
117
|
+
context.parentTemplate = null;
|
|
118
|
+
const parts = [];
|
|
119
|
+
for (const node of nodes) {
|
|
120
|
+
const result = node.render(context);
|
|
121
|
+
parts.push(result instanceof Promise ? await result : result);
|
|
122
|
+
}
|
|
123
|
+
let output = parts.join('');
|
|
124
|
+
|
|
125
|
+
if (context.parentTemplate) {
|
|
126
|
+
const parentName = context.parentTemplate;
|
|
127
|
+
context.parentTemplate = null;
|
|
128
|
+
|
|
129
|
+
let viewsDirs = ['.'];
|
|
130
|
+
if (context.options && context.options.settings && context.options.settings.views) {
|
|
131
|
+
viewsDirs = normalizeViews(context.options.settings.views);
|
|
132
|
+
} else if (context.options && context.options.views) {
|
|
133
|
+
viewsDirs = normalizeViews(context.options.views);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fileContent = readParentSource(parentName, viewsDirs);
|
|
137
|
+
|
|
138
|
+
const parentTokens = tokenize(fileContent);
|
|
139
|
+
const parentParser = new Parser(parentTokens, getTagRegistry());
|
|
140
|
+
const parentNodes = parentParser.parse();
|
|
141
|
+
|
|
142
|
+
if (parentParser.blocks) {
|
|
143
|
+
for (const [name, blockList] of Object.entries(parentParser.blocks)) {
|
|
144
|
+
if (!context.blocks[name]) {
|
|
145
|
+
context.blocks[name] = [];
|
|
146
|
+
}
|
|
147
|
+
for (const blockNode of blockList) {
|
|
148
|
+
if (!context.blocks[name].includes(blockNode)) {
|
|
149
|
+
context.blocks[name].push(blockNode);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return await renderASTAsync(parentNodes, context);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Synchronous renderAST - throws if any node returns a Promise.
|
|
163
|
+
*/
|
|
164
|
+
function renderAST(nodes, context) {
|
|
165
|
+
context.parentTemplate = null;
|
|
166
|
+
const output = nodes.map(node => {
|
|
167
|
+
const result = node.render(context);
|
|
168
|
+
if (result instanceof Promise) {
|
|
169
|
+
throw new Error('Async node encountered during sync render. Use asyncRender() instead.');
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}).join('');
|
|
173
|
+
|
|
174
|
+
if (context.parentTemplate) {
|
|
175
|
+
const parentName = context.parentTemplate;
|
|
176
|
+
context.parentTemplate = null;
|
|
177
|
+
|
|
178
|
+
let viewsDirs = ['.'];
|
|
179
|
+
if (context.options && context.options.settings && context.options.settings.views) {
|
|
180
|
+
viewsDirs = normalizeViews(context.options.settings.views);
|
|
181
|
+
} else if (context.options && context.options.views) {
|
|
182
|
+
viewsDirs = normalizeViews(context.options.views);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const fileContent = readParentSource(parentName, viewsDirs);
|
|
186
|
+
|
|
187
|
+
const parentTokens = tokenize(fileContent);
|
|
188
|
+
const parentParser = new Parser(parentTokens, getTagRegistry());
|
|
189
|
+
const parentNodes = parentParser.parse();
|
|
190
|
+
|
|
191
|
+
if (parentParser.blocks) {
|
|
192
|
+
for (const [name, blockList] of Object.entries(parentParser.blocks)) {
|
|
193
|
+
if (!context.blocks[name]) {
|
|
194
|
+
context.blocks[name] = [];
|
|
195
|
+
}
|
|
196
|
+
for (const blockNode of blockList) {
|
|
197
|
+
if (!context.blocks[name].includes(blockNode)) {
|
|
198
|
+
context.blocks[name].push(blockNode);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return renderAST(parentNodes, context);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return output;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Compiles a template string into a renderable object.
|
|
212
|
+
*/
|
|
213
|
+
function compile(templateStr, options = {}) {
|
|
214
|
+
return getCompiled(templateStr, options, (tmpl, opts) => {
|
|
215
|
+
const tokens = tokenize(tmpl);
|
|
216
|
+
const parser = new Parser(tokens, getTagRegistry());
|
|
217
|
+
const nodes = parser.parse();
|
|
218
|
+
|
|
219
|
+
// Collect partial definitions at compile time
|
|
220
|
+
const partialDefs = {};
|
|
221
|
+
function collectPartials(nodeList) {
|
|
222
|
+
for (const node of nodeList) {
|
|
223
|
+
if (node.constructor.name === 'PartialDefNode') {
|
|
224
|
+
partialDefs[node.name] = node;
|
|
225
|
+
}
|
|
226
|
+
if (node.body) {
|
|
227
|
+
collectPartials(node.body);
|
|
228
|
+
}
|
|
229
|
+
if (node.elifBranches) {
|
|
230
|
+
for (const branch of node.elifBranches) {
|
|
231
|
+
collectPartials(branch.body);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (node.elseBody) {
|
|
235
|
+
collectPartials(node.elseBody);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
collectPartials(nodes);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
render: (contextObj = {}) => {
|
|
243
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
244
|
+
const context = new Context(processedContextObj, opts);
|
|
245
|
+
context.reset();
|
|
246
|
+
if (parser.blocks) {
|
|
247
|
+
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
248
|
+
context.blocks[name] = [...blockList];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Register partial definitions from compile-time
|
|
252
|
+
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
253
|
+
context.registerPartial(name, partial);
|
|
254
|
+
}
|
|
255
|
+
return renderAST(nodes, context);
|
|
256
|
+
},
|
|
257
|
+
renderWith: (contextObj = {}, callOptions = {}) => {
|
|
258
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
259
|
+
const mergedOpts = { ...opts, ...callOptions };
|
|
260
|
+
const context = new Context(processedContextObj, mergedOpts);
|
|
261
|
+
context.reset();
|
|
262
|
+
if (parser.blocks) {
|
|
263
|
+
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
264
|
+
context.blocks[name] = [...blockList];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
268
|
+
context.registerPartial(name, partial);
|
|
269
|
+
}
|
|
270
|
+
return renderAST(nodes, context);
|
|
271
|
+
},
|
|
272
|
+
asyncRender: async (contextObj = {}) => {
|
|
273
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
274
|
+
const context = new Context(processedContextObj, opts);
|
|
275
|
+
context.blocks = {};
|
|
276
|
+
if (parser.blocks) {
|
|
277
|
+
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
278
|
+
context.blocks[name] = [...blockList];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
282
|
+
context.registerPartial(name, partial);
|
|
283
|
+
}
|
|
284
|
+
return await renderASTAsync(nodes, context);
|
|
285
|
+
},
|
|
286
|
+
asyncRenderWith: async (contextObj = {}, callOptions = {}) => {
|
|
287
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
288
|
+
const mergedOpts = { ...opts, ...callOptions };
|
|
289
|
+
const context = new Context(processedContextObj, mergedOpts);
|
|
290
|
+
context.blocks = {};
|
|
291
|
+
if (parser.blocks) {
|
|
292
|
+
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
293
|
+
context.blocks[name] = [...blockList];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
297
|
+
context.registerPartial(name, partial);
|
|
298
|
+
}
|
|
299
|
+
return await renderASTAsync(nodes, context);
|
|
300
|
+
},
|
|
301
|
+
renderBlock: (blockName, contextObj = {}) => {
|
|
302
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
303
|
+
const context = new Context(processedContextObj, opts);
|
|
304
|
+
context.blocks = {};
|
|
305
|
+
if (parser.blocks) {
|
|
306
|
+
for (const [name, blockList] of Object.entries(parser.blocks)) {
|
|
307
|
+
context.blocks[name] = [...blockList];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
311
|
+
context.registerPartial(name, partial);
|
|
312
|
+
}
|
|
313
|
+
renderAST(nodes, context);
|
|
314
|
+
const blockStack = context.blocks[blockName];
|
|
315
|
+
if (!blockStack || blockStack.length === 0) {
|
|
316
|
+
throw new Error(`Block '${blockName}' not found in template`);
|
|
317
|
+
}
|
|
318
|
+
if (!context.blockRenderIndices) {
|
|
319
|
+
context.blockRenderIndices = {};
|
|
320
|
+
}
|
|
321
|
+
context.blockRenderIndices[blockName] = 0;
|
|
322
|
+
let superVal = '';
|
|
323
|
+
if (blockStack.length > 1) {
|
|
324
|
+
context.blockRenderIndices[blockName] = 1;
|
|
325
|
+
superVal = blockStack[1].render(context);
|
|
326
|
+
}
|
|
327
|
+
context.push({ block: { super: superVal } });
|
|
328
|
+
context.blockRenderIndices[blockName] = 0;
|
|
329
|
+
const result = blockStack[0].body.map(n => n.render(context)).join('');
|
|
330
|
+
context.pop();
|
|
331
|
+
context.blockRenderIndices[blockName] = -1;
|
|
332
|
+
return result;
|
|
333
|
+
},
|
|
334
|
+
renderPartial: (partialName, contextObj = {}) => {
|
|
335
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
336
|
+
const context = new Context(processedContextObj, opts);
|
|
337
|
+
for (const [name, partial] of Object.entries(partialDefs)) {
|
|
338
|
+
context.registerPartial(name, partial);
|
|
339
|
+
}
|
|
340
|
+
const partial = context.getPartial(partialName);
|
|
341
|
+
if (!partial) {
|
|
342
|
+
throw new Error(`Partial '${partialName}' not found`);
|
|
343
|
+
}
|
|
344
|
+
return partial.body.map(n => n.render(context)).join('');
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Convenience rendering function.
|
|
352
|
+
*
|
|
353
|
+
* If `templateStr` looks like a file path with a `#partialName` suffix
|
|
354
|
+
* (e.g. `"home.html#card"`) AND `options.views` is set, the file is
|
|
355
|
+
* loaded from the views dir(s) and only the named partial is rendered.
|
|
356
|
+
* This is the engine's idiomatic way to serve HTMX partial responses.
|
|
357
|
+
*
|
|
358
|
+
* If the string contains template syntax (`{{` or `{%`), it is always
|
|
359
|
+
* treated as a template string (not a file path), so existing call
|
|
360
|
+
* sites that pass template source are unaffected.
|
|
361
|
+
*
|
|
362
|
+
* Without a `#` suffix, the whole template is rendered (existing
|
|
363
|
+
* behavior).
|
|
364
|
+
*/
|
|
365
|
+
function render(templateStr, contextObj = {}, options = {}) {
|
|
366
|
+
// Only treat the input as a file path when:
|
|
367
|
+
// 1. It has a `#` and a partial name
|
|
368
|
+
// 2. options.views is configured
|
|
369
|
+
// 3. The string does NOT contain template syntax
|
|
370
|
+
const hashIdx = templateStr.indexOf('#');
|
|
371
|
+
const hasTemplateSyntax = /\{[{%]/.test(templateStr);
|
|
372
|
+
if (hashIdx >= 0 && !hasTemplateSyntax && options && options.views) {
|
|
373
|
+
const fileName = templateStr.slice(0, hashIdx);
|
|
374
|
+
const partialName = templateStr.slice(hashIdx + 1);
|
|
375
|
+
return renderPartialFromFile(fileName, partialName, contextObj, options);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const compiled = compile(templateStr, options);
|
|
379
|
+
if (options && Object.keys(options).length > 0) {
|
|
380
|
+
return compiled.renderWith(contextObj, options);
|
|
381
|
+
}
|
|
382
|
+
return compiled.render(contextObj);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Load a template file from the configured views dir(s), register its
|
|
387
|
+
* partials, and render only the named partial. This is the
|
|
388
|
+
* implementation behind `render("file.html#partial", ...)`.
|
|
389
|
+
*/
|
|
390
|
+
function renderPartialFromFile(fileName, partialName, contextObj, options) {
|
|
391
|
+
const { Context } = require('./context');
|
|
392
|
+
const { tokenize } = require('./lexer');
|
|
393
|
+
const { Parser } = require('./parser');
|
|
394
|
+
const { getTagRegistry } = require('./tags/registry');
|
|
395
|
+
const { applyContextProcessors } = require('./context_processors');
|
|
396
|
+
|
|
397
|
+
let viewsDirs = ['.'];
|
|
398
|
+
if (options.settings && options.settings.views) {
|
|
399
|
+
viewsDirs = normalizeViews(options.settings.views);
|
|
400
|
+
} else if (options.views) {
|
|
401
|
+
viewsDirs = normalizeViews(options.views);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let fileContent = null;
|
|
405
|
+
let loaded = false;
|
|
406
|
+
// Try the literal name first, then with each supported extension appended.
|
|
407
|
+
const candidates = [];
|
|
408
|
+
if (/\.[a-z0-9]+$/i.test(fileName)) {
|
|
409
|
+
candidates.push(fileName);
|
|
410
|
+
} else {
|
|
411
|
+
candidates.push(fileName + '.html', fileName + '.miki');
|
|
412
|
+
}
|
|
413
|
+
for (const dir of viewsDirs) {
|
|
414
|
+
for (const candidate of candidates) {
|
|
415
|
+
try {
|
|
416
|
+
const fullPath = path.resolve(dir, candidate);
|
|
417
|
+
const relative = path.relative(path.resolve(dir), fullPath);
|
|
418
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
422
|
+
loaded = true;
|
|
423
|
+
break;
|
|
424
|
+
} catch {
|
|
425
|
+
// try next candidate
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (loaded) break;
|
|
429
|
+
}
|
|
430
|
+
if (!loaded) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Template not found: '${fileName}' in directories ${JSON.stringify(viewsDirs)}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const tokens = tokenize(fileContent);
|
|
437
|
+
const parser = new Parser(tokens, getTagRegistry());
|
|
438
|
+
const nodes = parser.parse();
|
|
439
|
+
|
|
440
|
+
// Build a context to register the partials. We share the partial
|
|
441
|
+
// registry with a fresh context for the actual render.
|
|
442
|
+
const tempContext = new Context({}, options);
|
|
443
|
+
for (const node of nodes) {
|
|
444
|
+
node.render(tempContext);
|
|
445
|
+
}
|
|
446
|
+
const partial = tempContext.getPartial(partialName);
|
|
447
|
+
if (!partial) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Partial '${partialName}' not found in template '${fileName}'`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
454
|
+
const context = new Context(processedContextObj, options);
|
|
455
|
+
context.partialDefs = tempContext.partialDefs;
|
|
456
|
+
return partial.body.map(n => n.render(context)).join('');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Async rendering function – returns a Promise.
|
|
461
|
+
*/
|
|
462
|
+
function asyncRender(templateStr, contextObj = {}, options = {}) {
|
|
463
|
+
const hashIdx = templateStr.indexOf('#');
|
|
464
|
+
const hasTemplateSyntax = /\{[{%]/.test(templateStr);
|
|
465
|
+
if (hashIdx >= 0 && !hasTemplateSyntax && options && options.views) {
|
|
466
|
+
const fileName = templateStr.slice(0, hashIdx);
|
|
467
|
+
const partialName = templateStr.slice(hashIdx + 1);
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
try {
|
|
470
|
+
resolve(renderPartialFromFile(fileName, partialName, contextObj, options));
|
|
471
|
+
} catch (e) {
|
|
472
|
+
reject(e);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const compiled = compile(templateStr, options);
|
|
477
|
+
if (options && Object.keys(options).length > 0) {
|
|
478
|
+
return compiled.asyncRenderWith(contextObj, options);
|
|
479
|
+
}
|
|
480
|
+
return compiled.asyncRender(contextObj);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Express adapter engine (synchronous callback form).
|
|
485
|
+
* Strips Express framework keys from the context so they don't leak
|
|
486
|
+
* into the template scope.
|
|
487
|
+
*
|
|
488
|
+
* If `filePath` contains a `#partialName` suffix, only the named
|
|
489
|
+
* partial is rendered. This enables HTMX-style partial responses:
|
|
490
|
+
*
|
|
491
|
+
* res.render('home#card', { ... });
|
|
492
|
+
*/
|
|
493
|
+
function __express(filePath, options, callback) {
|
|
494
|
+
if (typeof callback !== 'function') {
|
|
495
|
+
return __expressAsync(filePath, options);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Detect "file#partial" form
|
|
499
|
+
const hashIdx = filePath.lastIndexOf('#');
|
|
500
|
+
if (hashIdx > 0) {
|
|
501
|
+
const realFilePath = filePath.slice(0, hashIdx);
|
|
502
|
+
const partialName = filePath.slice(hashIdx + 1);
|
|
503
|
+
try {
|
|
504
|
+
const fileContent = fs.readFileSync(realFilePath, 'utf8');
|
|
505
|
+
const renderOptions = {
|
|
506
|
+
views: path.dirname(realFilePath),
|
|
507
|
+
...(options || {})
|
|
508
|
+
};
|
|
509
|
+
const ctx = stripExpressContext(options);
|
|
510
|
+
const result = renderPartialFromSource(fileContent, partialName, ctx, renderOptions, realFilePath);
|
|
511
|
+
return callback(null, result);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
return callback(err);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
519
|
+
const renderOptions = {
|
|
520
|
+
views: options && options.settings ? options.settings.views : path.dirname(filePath),
|
|
521
|
+
...(options || {})
|
|
522
|
+
};
|
|
523
|
+
// Strip Express framework keys from the context
|
|
524
|
+
const ctx = stripExpressContext(options);
|
|
525
|
+
const result = render(fileContent, ctx, renderOptions);
|
|
526
|
+
return callback(null, result);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
return callback(err);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Compile a template source string and render only the named partial
|
|
534
|
+
* from it. Used by __express when a view name carries a `#partial`
|
|
535
|
+
* suffix.
|
|
536
|
+
*
|
|
537
|
+
* The challenge: when a template uses `{% extends 'parent' %}`,
|
|
538
|
+
* the partials are typically defined inside `{% block ... %}` tags.
|
|
539
|
+
* The top-level nodes are [ExtendsNode, BlockNode, ...], not the
|
|
540
|
+
* blocks themselves. We need to register all partials regardless of
|
|
541
|
+
* whether their enclosing for/if blocks have data to iterate.
|
|
542
|
+
*/
|
|
543
|
+
function renderPartialFromSource(fileContent, partialName, contextObj, options, filePath) {
|
|
544
|
+
const { Context } = require('./context');
|
|
545
|
+
const { tokenize } = require('./lexer');
|
|
546
|
+
const { Parser } = require('./parser');
|
|
547
|
+
const { getTagRegistry } = require('./tags/registry');
|
|
548
|
+
const { applyContextProcessors } = require('./context_processors');
|
|
549
|
+
|
|
550
|
+
const tokens = tokenize(fileContent);
|
|
551
|
+
const parser = new Parser(tokens, getTagRegistry());
|
|
552
|
+
const nodes = parser.parse();
|
|
553
|
+
|
|
554
|
+
// Build a context with the caller's data so for-loops and other
|
|
555
|
+
// constructs iterate properly when collecting partials.
|
|
556
|
+
const processedContextObj = applyContextProcessors({ ...contextObj });
|
|
557
|
+
const tempContext = new Context(processedContextObj, options);
|
|
558
|
+
|
|
559
|
+
// Walk the ENTIRE AST and register every PartialDefNode we find,
|
|
560
|
+
// regardless of whether its enclosing for/if has data. This ensures
|
|
561
|
+
// partials are always available by name even when the caller
|
|
562
|
+
// didn't provide the data the template would need to render them
|
|
563
|
+
// in context.
|
|
564
|
+
function registerAllPartials(nodeList) {
|
|
565
|
+
for (const node of nodeList) {
|
|
566
|
+
if (node.constructor.name === 'PartialDefNode') {
|
|
567
|
+
tempContext.registerPartial(node.name, node);
|
|
568
|
+
}
|
|
569
|
+
if (node.body) registerAllPartials(node.body);
|
|
570
|
+
if (node.elifBranches) for (const b of node.elifBranches) registerAllPartials(b.body);
|
|
571
|
+
if (node.elseBody) registerAllPartials(node.elseBody);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
registerAllPartials(nodes);
|
|
575
|
+
|
|
576
|
+
// Also try rendering top-level nodes so partials defined via
|
|
577
|
+
// {% load %} or other dynamic mechanisms get a chance to register.
|
|
578
|
+
// Errors are swallowed — we only care about partial registration.
|
|
579
|
+
for (const node of nodes) {
|
|
580
|
+
try {
|
|
581
|
+
node.render(tempContext);
|
|
582
|
+
} catch {
|
|
583
|
+
// Ignore
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// If the template extends a parent, also collect partials from
|
|
588
|
+
// the parent. This handles the common case where partials are
|
|
589
|
+
// defined inside blocks that are part of an extended template.
|
|
590
|
+
if (tempContext.parentTemplate) {
|
|
591
|
+
try {
|
|
592
|
+
const parentName = tempContext.parentTemplate;
|
|
593
|
+
let viewsDirs = ['.'];
|
|
594
|
+
if (options && options.settings && options.settings.views) {
|
|
595
|
+
viewsDirs = normalizeViews(options.settings.views);
|
|
596
|
+
} else if (options && options.views) {
|
|
597
|
+
viewsDirs = normalizeViews(options.views);
|
|
598
|
+
}
|
|
599
|
+
for (const dir of viewsDirs) {
|
|
600
|
+
const parentPath = require('path').resolve(dir, parentName);
|
|
601
|
+
const relative = require('path').relative(require('path').resolve(dir), parentPath);
|
|
602
|
+
if (relative.startsWith('..') || require('path').isAbsolute(relative)) continue;
|
|
603
|
+
if (!require('fs').existsSync(parentPath)) continue;
|
|
604
|
+
const parentContent = require('fs').readFileSync(parentPath, 'utf8');
|
|
605
|
+
const pTokens = tokenize(parentContent);
|
|
606
|
+
const pParser = new Parser(pTokens, getTagRegistry());
|
|
607
|
+
const pNodes = pParser.parse();
|
|
608
|
+
registerAllPartials(pNodes);
|
|
609
|
+
for (const pNode of pNodes) {
|
|
610
|
+
try { pNode.render(tempContext); } catch {}
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
// Ignore parent resolution errors
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const partial = tempContext.getPartial(partialName);
|
|
620
|
+
if (!partial) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Partial '${partialName}' not found in template '${filePath || 'inline'}'`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Build a fresh context for the actual render using the caller's
|
|
627
|
+
// data. Share the partial registry from the temp context.
|
|
628
|
+
const context = new Context(processedContextObj, options);
|
|
629
|
+
context.partialDefs = tempContext.partialDefs;
|
|
630
|
+
return partial.body.map(n => n.render(context)).join('');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Strip Express-specific framework keys from a context object.
|
|
635
|
+
* Internal keys (those starting with `_`), `settings`, and `cache` are removed.
|
|
636
|
+
*/
|
|
637
|
+
function stripExpressContext(options) {
|
|
638
|
+
if (!options) return {};
|
|
639
|
+
const ctx = {};
|
|
640
|
+
for (const [k, v] of Object.entries(options)) {
|
|
641
|
+
if (!k.startsWith('_') && k !== 'settings' && k !== 'cache') {
|
|
642
|
+
ctx[k] = v;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return ctx;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Async view engine for Express 5+. Returns a Promise that resolves
|
|
650
|
+
* to the rendered HTML. Use this when your templates have async helpers.
|
|
651
|
+
*
|
|
652
|
+
* app.engine('html', miki.__expressAsync);
|
|
653
|
+
*/
|
|
654
|
+
function __expressAsync(filePath, options) {
|
|
655
|
+
return new Promise((resolve, reject) => {
|
|
656
|
+
// Detect "file#partial" form
|
|
657
|
+
const hashIdx = filePath.lastIndexOf('#');
|
|
658
|
+
if (hashIdx > 0) {
|
|
659
|
+
const realFilePath = filePath.slice(0, hashIdx);
|
|
660
|
+
const partialName = filePath.slice(hashIdx + 1);
|
|
661
|
+
try {
|
|
662
|
+
const fileContent = fs.readFileSync(realFilePath, 'utf8');
|
|
663
|
+
const renderOptions = {
|
|
664
|
+
views: path.dirname(realFilePath),
|
|
665
|
+
...(options || {})
|
|
666
|
+
};
|
|
667
|
+
const ctx = stripExpressContext(options);
|
|
668
|
+
resolve(renderPartialFromSource(fileContent, partialName, ctx, renderOptions, realFilePath));
|
|
669
|
+
} catch (err) {
|
|
670
|
+
reject(err);
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
677
|
+
const renderOptions = {
|
|
678
|
+
views: options && options.settings ? options.settings.views : path.dirname(filePath),
|
|
679
|
+
...(options || {})
|
|
680
|
+
};
|
|
681
|
+
const ctx = stripExpressContext(options);
|
|
682
|
+
// Use asyncRender so async helpers are awaited
|
|
683
|
+
asyncRender(fileContent, ctx, renderOptions)
|
|
684
|
+
.then(resolve)
|
|
685
|
+
.catch(reject);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
reject(err);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Express integration helper. Returns a function suitable for
|
|
694
|
+
* `app.engine(ext, fn)` that makes `res.render('view#partial', ...)`
|
|
695
|
+
* just work without any extra middleware.
|
|
696
|
+
*
|
|
697
|
+
* Usage:
|
|
698
|
+
*
|
|
699
|
+
* const miki = require('miki-template');
|
|
700
|
+
* const app = express();
|
|
701
|
+
* app.engine('html', miki.express());
|
|
702
|
+
* app.set('view engine', 'html');
|
|
703
|
+
* app.set('views', './views');
|
|
704
|
+
*
|
|
705
|
+
* // Full page:
|
|
706
|
+
* app.get('/', (req, res) => res.render('home', { user }));
|
|
707
|
+
*
|
|
708
|
+
* // HTMX partial — just one named partial from a template:
|
|
709
|
+
* app.get('/partials/:name', (req, res) =>
|
|
710
|
+
* res.render(`home#${req.params.name}`, { user })
|
|
711
|
+
* );
|
|
712
|
+
*
|
|
713
|
+
* The wrapper detects a `#partialName` suffix in the view name BEFORE
|
|
714
|
+
* Express's view lookup runs, so it never tries to open a file like
|
|
715
|
+
* `home#card.html`. It resolves the real file, calls the partial
|
|
716
|
+
* renderer, and sends the result.
|
|
717
|
+
*/
|
|
718
|
+
function express(options = {}) {
|
|
719
|
+
const baseEngine = options.async ? __expressAsync : __express;
|
|
720
|
+
return function mikiViewEngine(filePath, engineOptions, callback) {
|
|
721
|
+
// Detect the partial selector in the view name. Express passes
|
|
722
|
+
// the resolved file path here — if the user wrote
|
|
723
|
+
// `res.render('home#card', ...)`, Express will have already
|
|
724
|
+
// tried (and failed) to resolve `home#card.html`. To support
|
|
725
|
+
// partials, we need to intercept BEFORE Express resolves the
|
|
726
|
+
// view. We do that by hooking `res.render` when this engine is
|
|
727
|
+
// installed.
|
|
728
|
+
// For the direct path (when called from `res.renderPartial` or
|
|
729
|
+
// from our own `res.render` shim), we honor the `#partial`
|
|
730
|
+
// suffix here.
|
|
731
|
+
if (typeof callback !== 'function') {
|
|
732
|
+
return Promise.reject(
|
|
733
|
+
new Error('miki.express() engine must be called via res.render() with a callback')
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return baseEngine(filePath, engineOptions, callback);
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* One-shot Express setup. Wires `app.engine('html', ...)` and
|
|
742
|
+
* installs a `res.render` shim so that `res.render('view#partial')`
|
|
743
|
+
* works without any extra middleware. This is the recommended way
|
|
744
|
+
* to integrate miki-template with Express.
|
|
745
|
+
*
|
|
746
|
+
* Usage:
|
|
747
|
+
*
|
|
748
|
+
* const miki = require('miki-template');
|
|
749
|
+
* const app = express();
|
|
750
|
+
* miki.setupExpress(app, { extension: 'html', views: './views' });
|
|
751
|
+
*
|
|
752
|
+
* app.get('/partials/:name', (req, res) =>
|
|
753
|
+
* res.render(`home#${req.params.name}`, { user: req.user })
|
|
754
|
+
* );
|
|
755
|
+
*/
|
|
756
|
+
function setupExpress(app, opts = {}) {
|
|
757
|
+
const ext = (opts.extension || 'html').replace(/^\.+/, '');
|
|
758
|
+
const async = !!opts.async;
|
|
759
|
+
|
|
760
|
+
// Set view engine if not already set
|
|
761
|
+
if (!app.get('view engine')) {
|
|
762
|
+
app.set('view engine', ext);
|
|
763
|
+
}
|
|
764
|
+
// If opts.views is provided, always set it (so users can set
|
|
765
|
+
// views via setupExpress without an extra app.set call).
|
|
766
|
+
if (opts.views) {
|
|
767
|
+
app.set('views', opts.views);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Install the raw engine so Express can use it
|
|
771
|
+
app.engine(ext, async ? __expressAsync : __express);
|
|
772
|
+
|
|
773
|
+
// Capture config in a closure so patchedRender can use it even
|
|
774
|
+
// when called before the request handler runs.
|
|
775
|
+
const configExt = ext;
|
|
776
|
+
const configViews = opts.views;
|
|
777
|
+
|
|
778
|
+
// Capture the original res.render so we can dispatch on #partial
|
|
779
|
+
const originalRender = app.response.render;
|
|
780
|
+
app.response.render = function patchedRender(view, locals, callback) {
|
|
781
|
+
// Normalize arguments: (view, callback) or (view, locals, callback)
|
|
782
|
+
let cb = callback;
|
|
783
|
+
let opts = locals;
|
|
784
|
+
if (typeof locals === 'function') {
|
|
785
|
+
cb = locals;
|
|
786
|
+
opts = {};
|
|
787
|
+
}
|
|
788
|
+
opts = opts || {};
|
|
789
|
+
// Inject settings so the engine can find the views dir
|
|
790
|
+
if (!opts.settings) {
|
|
791
|
+
opts.settings = this.req && this.req.app ? this.req.app.settings : {};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// If the view name has a `#partial` suffix, handle it ourselves
|
|
795
|
+
// and never delegate to Express's view lookup.
|
|
796
|
+
if (typeof view === 'string' && view.includes('#')) {
|
|
797
|
+
const hashIdx = view.lastIndexOf('#');
|
|
798
|
+
const fileName = view.slice(0, hashIdx);
|
|
799
|
+
const partialName = view.slice(hashIdx + 1);
|
|
800
|
+
|
|
801
|
+
// Resolve the real file path. Prefer opts.views (set by
|
|
802
|
+
// setupExpress), then app.get('views'), then the default.
|
|
803
|
+
const extname = require('path').extname(fileName);
|
|
804
|
+
const candidates = extname
|
|
805
|
+
? [fileName]
|
|
806
|
+
: [fileName + '.' + configExt, fileName + '.miki'];
|
|
807
|
+
let viewsDir = configViews
|
|
808
|
+
|| (this.req && this.req.app ? this.req.app.get('views') : null)
|
|
809
|
+
|| process.cwd() + '/views';
|
|
810
|
+
// Normalize arrays or accidental file paths to directories
|
|
811
|
+
if (Array.isArray(viewsDir)) {
|
|
812
|
+
const arr = normalizeViews(viewsDir);
|
|
813
|
+
viewsDir = arr.length > 0 ? arr[0] : viewsDir[0];
|
|
814
|
+
} else {
|
|
815
|
+
try {
|
|
816
|
+
const st = fs.statSync(viewsDir);
|
|
817
|
+
if (st.isFile()) viewsDir = path.dirname(viewsDir);
|
|
818
|
+
} catch (e) {
|
|
819
|
+
// ignore
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
let filePath = null;
|
|
823
|
+
for (const cand of candidates) {
|
|
824
|
+
const p = require('path').resolve(viewsDir, cand);
|
|
825
|
+
if (require('fs').existsSync(p)) {
|
|
826
|
+
filePath = p;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (!filePath) {
|
|
831
|
+
const err = new Error(
|
|
832
|
+
`Failed to lookup view "${view}" in views directory "${viewsDir}"`
|
|
833
|
+
);
|
|
834
|
+
if (typeof cb === 'function') return cb(err);
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const fileContent = require('fs').readFileSync(filePath, 'utf8');
|
|
839
|
+
try {
|
|
840
|
+
const html = renderPartialFromSource(
|
|
841
|
+
fileContent,
|
|
842
|
+
partialName,
|
|
843
|
+
stripExpressContext(opts),
|
|
844
|
+
Object.assign({ views: viewsDir }, opts),
|
|
845
|
+
fileName
|
|
846
|
+
);
|
|
847
|
+
if (typeof cb === 'function') {
|
|
848
|
+
return cb(null, html);
|
|
849
|
+
}
|
|
850
|
+
this.send(html);
|
|
851
|
+
return;
|
|
852
|
+
} catch (e) {
|
|
853
|
+
if (typeof cb === 'function') return cb(e);
|
|
854
|
+
throw e;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// No partial selector: behave exactly like the original res.render
|
|
859
|
+
if (cb) {
|
|
860
|
+
return originalRender.call(this, view, opts, cb);
|
|
861
|
+
}
|
|
862
|
+
return originalRender.call(this, view, opts);
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Express middleware helper. Adds a `res.renderPartial(view, locals)`
|
|
868
|
+
* method that renders only the named partial (after a `#`) from a
|
|
869
|
+
* view file. The view name follows the same syntax as `render()`:
|
|
870
|
+
*
|
|
871
|
+
* app.use(miki.expressPartialRenderer());
|
|
872
|
+
* app.get('/card', (req, res) => res.renderPartial('home#card', { user }));
|
|
873
|
+
*/
|
|
874
|
+
function expressPartialRenderer() {
|
|
875
|
+
return function (req, res, next) {
|
|
876
|
+
res.renderPartial = function (view, locals = {}) {
|
|
877
|
+
// Compose an Express-shaped options object so the engine can
|
|
878
|
+
// find the view file. We mirror what res.render provides.
|
|
879
|
+
const opts = Object.assign({}, res.locals, locals, {
|
|
880
|
+
settings: req.app.settings
|
|
881
|
+
});
|
|
882
|
+
const hashIdx = view.lastIndexOf('#');
|
|
883
|
+
if (hashIdx < 0) {
|
|
884
|
+
// No partial selector: just delegate to res.render
|
|
885
|
+
return res.render(view, locals);
|
|
886
|
+
}
|
|
887
|
+
const fileName = view.slice(0, hashIdx);
|
|
888
|
+
const partialName = view.slice(hashIdx + 1);
|
|
889
|
+
// Find the actual file the way Express would
|
|
890
|
+
const ext = require('path').extname(fileName);
|
|
891
|
+
const candidates = ext
|
|
892
|
+
? [fileName]
|
|
893
|
+
: [fileName + '.html', fileName + '.miki'];
|
|
894
|
+
let viewsDir = req.app.get('views');
|
|
895
|
+
// Normalize viewsDir to a directory if needed
|
|
896
|
+
if (Array.isArray(viewsDir)) {
|
|
897
|
+
const arr = normalizeViews(viewsDir);
|
|
898
|
+
viewsDir = arr.length > 0 ? arr[0] : viewsDir[0];
|
|
899
|
+
} else {
|
|
900
|
+
try {
|
|
901
|
+
const st = fs.statSync(viewsDir);
|
|
902
|
+
if (st.isFile()) viewsDir = path.dirname(viewsDir);
|
|
903
|
+
} catch (e) {}
|
|
904
|
+
}
|
|
905
|
+
let filePath = null;
|
|
906
|
+
for (const cand of candidates) {
|
|
907
|
+
try {
|
|
908
|
+
filePath = require('path').resolve(viewsDir, cand);
|
|
909
|
+
if (require('fs').existsSync(filePath)) break;
|
|
910
|
+
filePath = null;
|
|
911
|
+
} catch { filePath = null; }
|
|
912
|
+
}
|
|
913
|
+
if (!filePath) {
|
|
914
|
+
return res.status(404).send(
|
|
915
|
+
`Template not found: '${fileName}' in '${viewsDir}'`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
const html = renderPartialFromSource(
|
|
920
|
+
require('fs').readFileSync(filePath, 'utf8'),
|
|
921
|
+
partialName,
|
|
922
|
+
stripExpressContext(opts),
|
|
923
|
+
{ views: viewsDir, ...opts },
|
|
924
|
+
fileName
|
|
925
|
+
);
|
|
926
|
+
res.send(html);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
res.status(500).send(err.message);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
next();
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
module.exports = {
|
|
936
|
+
compile,
|
|
937
|
+
render,
|
|
938
|
+
asyncRender,
|
|
939
|
+
renderPartialFromFile,
|
|
940
|
+
renderPartialFromSource,
|
|
941
|
+
__express,
|
|
942
|
+
__expressAsync,
|
|
943
|
+
express,
|
|
944
|
+
setupExpress,
|
|
945
|
+
expressPartialRenderer,
|
|
946
|
+
stripExpressContext,
|
|
947
|
+
clearCache,
|
|
948
|
+
registerTag,
|
|
949
|
+
registerFilter,
|
|
950
|
+
getFilter,
|
|
951
|
+
registerHelper,
|
|
952
|
+
registerContextProcessor,
|
|
953
|
+
clearContextProcessors,
|
|
954
|
+
SafeString,
|
|
955
|
+
markSafe,
|
|
956
|
+
isSafe,
|
|
957
|
+
escapeHtml,
|
|
958
|
+
// i18n
|
|
959
|
+
registerTranslation: i18n.registerTranslation,
|
|
960
|
+
unregisterTranslation: i18n.unregisterTranslation,
|
|
961
|
+
setLanguage: i18n.setLanguage,
|
|
962
|
+
getLanguage: i18n.getLanguage,
|
|
963
|
+
setFallbackLanguage: i18n.setFallbackLanguage,
|
|
964
|
+
getFallbackLanguage: i18n.getFallbackLanguage,
|
|
965
|
+
getAvailableLanguages: i18n.getAvailableLanguages,
|
|
966
|
+
// Plugin/filter libraries
|
|
967
|
+
registerLibrary: libraries.registerLibrary,
|
|
968
|
+
unregisterLibrary: libraries.unregisterLibrary,
|
|
969
|
+
getLibrary: libraries.getLibrary,
|
|
970
|
+
getLibraryNames: libraries.getLibraryNames,
|
|
971
|
+
hasLibrary: libraries.hasLibrary,
|
|
972
|
+
registerLibraryFromPath: libraries.registerLibraryFromPath,
|
|
973
|
+
activateLibrary: libraries.activateLibrary
|
|
974
|
+
};
|