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.
- package/.github/workflows/docs.yml +3 -1
- package/.github/workflows/release.yml +0 -5
- package/README.md +17 -5
- package/benchmarks/ejs-results.json +6 -6
- package/benchmarks/ejs.js +5 -3
- package/benchmarks/handlebars-results.json +6 -6
- package/benchmarks/handlebars.js +5 -8
- package/benchmarks/miki-results.json +6 -6
- package/benchmarks/miki.js +6 -3
- package/benchmarks/pug-results.json +6 -6
- package/benchmarks/pug.js +5 -3
- package/docs/api/async-render.md +88 -3
- package/docs/api/cache.md +90 -3
- package/docs/api/compile.md +131 -3
- package/docs/api/context-processors.md +80 -3
- package/docs/api/filters.md +223 -3
- package/docs/api/finder.md +97 -3
- package/docs/api/helpers.md +56 -3
- package/docs/api/i18n.md +160 -3
- package/docs/api/index.md +82 -28
- package/docs/api/libraries.md +210 -3
- package/docs/api/render-partial.md +84 -3
- package/docs/api/render.md +95 -3
- package/docs/api/security.md +148 -3
- package/docs/api/setup-express.md +78 -2
- package/docs/api/tags.md +138 -4
- package/docs/filter.md +0 -0
- package/docs/guide/advanced-usage.md +403 -6
- package/docs/guide/async-rendering.md +312 -4
- package/docs/guide/context-processors.md +261 -4
- package/docs/guide/custom-filters.md +315 -4
- package/docs/guide/custom-tags.md +275 -4
- package/docs/guide/filters.md +675 -3
- package/docs/guide/getting-started.md +109 -7
- package/docs/guide/installation.md +99 -4
- package/docs/guide/partial-templates.md +371 -4
- package/docs/guide/quick-start.md +228 -6
- package/docs/guide/security.md +348 -3
- package/docs/guide/tags.md +789 -6
- package/docs/guide/template-discovery.md +174 -4
- package/docs/guide/template-inheritance.md +277 -4
- package/docs/index.md +24 -42
- package/docs/integrations/elysia.md +4 -2
- package/docs/integrations/express.md +219 -219
- package/docs/integrations/fastify.md +4 -2
- package/docs/integrations/hono.md +4 -2
- package/docs/integrations/index.md +68 -68
- package/docs/integrations/koa.md +4 -2
- package/docs/integrations/nestjs.md +4 -2
- package/docs/integrations/tsed.md +4 -2
- package/docs/performance.md +45 -8
- package/ex.mjs +1 -1
- package/mkdocs.yml +0 -22
- package/overrides/main.html +1 -1
- package/package.json +1 -1
- package/requirements-docs.txt +2 -1
- package/src/codegen.js +905 -0
- package/src/context.js +42 -30
- package/src/filters.js +16 -0
- package/src/index.js +66 -61
- package/src/tags/control.js +15 -12
- package/src/utils.js +60 -0
- package/tests/filters.test.js +9 -0
- package/docs/javascripts/extra.js +0 -174
- package/docs/stylesheets/extra.css +0 -819
- package/overrides/partials/footer.html +0 -9
|
@@ -1,271 +1,542 @@
|
|
|
1
|
-
# Custom Tags
|
|
1
|
+
# Custom Tags
|
|
2
|
+
|
|
3
|
+
|
|
2
4
|
|
|
3
5
|
Create your own template tags by registering a parser function. miki-template's tag API mirrors Django's — a tag is a parser that returns a Node object with a `render(context)` method.
|
|
4
6
|
|
|
7
|
+
|
|
8
|
+
|
|
5
9
|
## Table of Contents
|
|
6
10
|
|
|
11
|
+
|
|
12
|
+
|
|
7
13
|
- [Register a Simple Tag](#register-a-simple-tag)
|
|
14
|
+
|
|
8
15
|
- [Async Custom Tags](#async-custom-tags)
|
|
16
|
+
|
|
9
17
|
- [Parsing Complex Tags](#parsing-complex-tags)
|
|
18
|
+
|
|
10
19
|
- [Accessing the Parser](#accessing-the-parser)
|
|
20
|
+
|
|
11
21
|
- [Tag Registration Best Practices](#tag-registration-best-practices)
|
|
12
22
|
|
|
23
|
+
|
|
24
|
+
|
|
13
25
|
---
|
|
14
26
|
|
|
27
|
+
|
|
28
|
+
|
|
15
29
|
## Register a Simple Tag
|
|
16
30
|
|
|
31
|
+
|
|
32
|
+
|
|
17
33
|
=== "CommonJS"
|
|
18
34
|
|
|
35
|
+
|
|
36
|
+
|
|
19
37
|
```javascript
|
|
38
|
+
|
|
20
39
|
const { registerTag } = require('miki-template');
|
|
21
40
|
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
registerTag('hello', (tagContent, parser) => {
|
|
44
|
+
|
|
23
45
|
return {
|
|
46
|
+
|
|
24
47
|
render: (context) => 'Hello World!'
|
|
48
|
+
|
|
25
49
|
};
|
|
50
|
+
|
|
26
51
|
});
|
|
52
|
+
|
|
27
53
|
```
|
|
28
54
|
|
|
55
|
+
|
|
56
|
+
|
|
29
57
|
=== "ES Modules"
|
|
30
58
|
|
|
59
|
+
|
|
60
|
+
|
|
31
61
|
```javascript
|
|
62
|
+
|
|
32
63
|
import { registerTag } from 'miki-template';
|
|
33
64
|
|
|
65
|
+
|
|
66
|
+
|
|
34
67
|
registerTag('hello', (tagContent, parser) => {
|
|
68
|
+
|
|
35
69
|
return {
|
|
70
|
+
|
|
36
71
|
render: (context) => 'Hello World!'
|
|
72
|
+
|
|
37
73
|
};
|
|
74
|
+
|
|
38
75
|
});
|
|
76
|
+
|
|
39
77
|
```
|
|
40
78
|
|
|
79
|
+
|
|
80
|
+
|
|
41
81
|
Usage in templates:
|
|
42
82
|
|
|
83
|
+
|
|
84
|
+
|
|
43
85
|
```html
|
|
86
|
+
|
|
44
87
|
{% hello %}
|
|
88
|
+
|
|
45
89
|
```
|
|
46
90
|
|
|
91
|
+
|
|
92
|
+
|
|
47
93
|
### Passing Arguments
|
|
48
94
|
|
|
95
|
+
|
|
96
|
+
|
|
49
97
|
```javascript
|
|
98
|
+
|
|
50
99
|
registerTag('greet', (tagContent, parser) => {
|
|
100
|
+
|
|
51
101
|
// tagContent is the full text after the tag name: "user.name"
|
|
102
|
+
|
|
52
103
|
const varName = tagContent.trim();
|
|
104
|
+
|
|
53
105
|
return {
|
|
106
|
+
|
|
54
107
|
render: (context) => {
|
|
108
|
+
|
|
55
109
|
const value = context.get(varName);
|
|
110
|
+
|
|
56
111
|
return `Hello, ${value}!`;
|
|
112
|
+
|
|
57
113
|
}
|
|
114
|
+
|
|
58
115
|
};
|
|
116
|
+
|
|
59
117
|
});
|
|
118
|
+
|
|
60
119
|
```
|
|
61
120
|
|
|
121
|
+
|
|
122
|
+
|
|
62
123
|
```html
|
|
124
|
+
|
|
63
125
|
{% greet user.name %}
|
|
126
|
+
|
|
64
127
|
```
|
|
65
128
|
|
|
129
|
+
|
|
130
|
+
|
|
66
131
|
## Returning a Node Class
|
|
67
132
|
|
|
133
|
+
|
|
134
|
+
|
|
68
135
|
For more complex tags, return a Node class instance:
|
|
69
136
|
|
|
137
|
+
|
|
138
|
+
|
|
70
139
|
=== "CommonJS"
|
|
71
140
|
|
|
141
|
+
|
|
142
|
+
|
|
72
143
|
```javascript
|
|
144
|
+
|
|
73
145
|
const { registerTag } = require('miki-template');
|
|
74
146
|
|
|
147
|
+
|
|
148
|
+
|
|
75
149
|
class GreetNode {
|
|
150
|
+
|
|
76
151
|
constructor(varName) {
|
|
152
|
+
|
|
77
153
|
this.varName = varName;
|
|
154
|
+
|
|
78
155
|
}
|
|
156
|
+
|
|
79
157
|
render(context) {
|
|
158
|
+
|
|
80
159
|
const value = context.get(this.varName);
|
|
160
|
+
|
|
81
161
|
return `Hello, ${value || 'Guest'}!`;
|
|
162
|
+
|
|
82
163
|
}
|
|
164
|
+
|
|
83
165
|
}
|
|
84
166
|
|
|
167
|
+
|
|
168
|
+
|
|
85
169
|
registerTag('greet', (tagContent, parser) => {
|
|
170
|
+
|
|
86
171
|
const varName = tagContent.trim();
|
|
172
|
+
|
|
87
173
|
return new GreetNode(varName);
|
|
174
|
+
|
|
88
175
|
});
|
|
176
|
+
|
|
89
177
|
```
|
|
90
178
|
|
|
179
|
+
|
|
180
|
+
|
|
91
181
|
=== "ES Modules"
|
|
92
182
|
|
|
183
|
+
|
|
184
|
+
|
|
93
185
|
```javascript
|
|
186
|
+
|
|
94
187
|
import { registerTag } from 'miki-template';
|
|
95
188
|
|
|
189
|
+
|
|
190
|
+
|
|
96
191
|
class GreetNode {
|
|
192
|
+
|
|
97
193
|
constructor(varName) {
|
|
194
|
+
|
|
98
195
|
this.varName = varName;
|
|
196
|
+
|
|
99
197
|
}
|
|
198
|
+
|
|
100
199
|
render(context) {
|
|
200
|
+
|
|
101
201
|
const value = context.get(this.varName);
|
|
202
|
+
|
|
102
203
|
return `Hello, ${value || 'Guest'}!`;
|
|
204
|
+
|
|
103
205
|
}
|
|
206
|
+
|
|
104
207
|
}
|
|
105
208
|
|
|
209
|
+
|
|
210
|
+
|
|
106
211
|
registerTag('greet', (tagContent, parser) => {
|
|
212
|
+
|
|
107
213
|
const varName = tagContent.trim();
|
|
214
|
+
|
|
108
215
|
return new GreetNode(varName);
|
|
216
|
+
|
|
109
217
|
});
|
|
218
|
+
|
|
110
219
|
```
|
|
111
220
|
|
|
221
|
+
|
|
222
|
+
|
|
112
223
|
## Async Custom Tags
|
|
113
224
|
|
|
225
|
+
|
|
226
|
+
|
|
114
227
|
If your `render()` method returns a Promise, the template must be rendered with `asyncRender()`:
|
|
115
228
|
|
|
229
|
+
|
|
230
|
+
|
|
116
231
|
=== "CommonJS"
|
|
117
232
|
|
|
233
|
+
|
|
234
|
+
|
|
118
235
|
```javascript
|
|
236
|
+
|
|
119
237
|
const { registerTag, asyncRender } = require('miki-template');
|
|
120
238
|
|
|
239
|
+
|
|
240
|
+
|
|
121
241
|
registerTag('fetch_greeting', (tagContent, parser) => {
|
|
242
|
+
|
|
122
243
|
const urlVar = tagContent.trim();
|
|
244
|
+
|
|
123
245
|
return {
|
|
246
|
+
|
|
124
247
|
async render(context) {
|
|
248
|
+
|
|
125
249
|
const url = context.get(urlVar);
|
|
250
|
+
|
|
126
251
|
const res = await fetch(url);
|
|
252
|
+
|
|
127
253
|
const data = await res.json();
|
|
254
|
+
|
|
128
255
|
return data.message;
|
|
256
|
+
|
|
129
257
|
}
|
|
258
|
+
|
|
130
259
|
};
|
|
260
|
+
|
|
131
261
|
});
|
|
132
262
|
|
|
263
|
+
|
|
264
|
+
|
|
133
265
|
// Must use asyncRender
|
|
266
|
+
|
|
134
267
|
const html = await asyncRender('{% fetch_greeting api_url %}', { api_url: 'https://...' });
|
|
268
|
+
|
|
135
269
|
```
|
|
136
270
|
|
|
271
|
+
|
|
272
|
+
|
|
137
273
|
=== "ES Modules"
|
|
138
274
|
|
|
275
|
+
|
|
276
|
+
|
|
139
277
|
```javascript
|
|
278
|
+
|
|
140
279
|
import { registerTag, asyncRender } from 'miki-template';
|
|
141
280
|
|
|
281
|
+
|
|
282
|
+
|
|
142
283
|
registerTag('fetch_greeting', (tagContent, parser) => {
|
|
284
|
+
|
|
143
285
|
const urlVar = tagContent.trim();
|
|
286
|
+
|
|
144
287
|
return {
|
|
288
|
+
|
|
145
289
|
async render(context) {
|
|
290
|
+
|
|
146
291
|
const url = context.get(urlVar);
|
|
292
|
+
|
|
147
293
|
const res = await fetch(url);
|
|
294
|
+
|
|
148
295
|
const data = await res.json();
|
|
296
|
+
|
|
149
297
|
return data.message;
|
|
298
|
+
|
|
150
299
|
}
|
|
300
|
+
|
|
151
301
|
};
|
|
302
|
+
|
|
152
303
|
});
|
|
153
304
|
|
|
305
|
+
|
|
306
|
+
|
|
154
307
|
const html = await asyncRender('{% fetch_greeting api_url %}', { api_url: 'https://...' });
|
|
308
|
+
|
|
155
309
|
```
|
|
156
310
|
|
|
311
|
+
|
|
312
|
+
|
|
157
313
|
## Parsing Complex Tags
|
|
158
314
|
|
|
315
|
+
|
|
316
|
+
|
|
159
317
|
Use the `parser` object to consume tokens and build multi-part tags:
|
|
160
318
|
|
|
319
|
+
|
|
320
|
+
|
|
161
321
|
=== "CommonJS"
|
|
162
322
|
|
|
323
|
+
|
|
324
|
+
|
|
163
325
|
```javascript
|
|
326
|
+
|
|
164
327
|
const { registerTag } = require('miki-template');
|
|
165
328
|
|
|
329
|
+
|
|
330
|
+
|
|
166
331
|
registerTag('panel', (tagContent, parser) => {
|
|
332
|
+
|
|
167
333
|
const classes = tagContent.trim() || '';
|
|
334
|
+
|
|
168
335
|
const nodelist = parser.parse(['endpanel']);
|
|
336
|
+
|
|
169
337
|
parser.skipTag(); // consume endpanel
|
|
170
338
|
|
|
339
|
+
|
|
340
|
+
|
|
171
341
|
return {
|
|
342
|
+
|
|
172
343
|
render: (context) => {
|
|
344
|
+
|
|
173
345
|
const body = nodelist.map(n => n.render(context)).join('');
|
|
346
|
+
|
|
174
347
|
return `<div class="panel ${classes}">${body}</div>`;
|
|
348
|
+
|
|
175
349
|
}
|
|
350
|
+
|
|
176
351
|
};
|
|
352
|
+
|
|
177
353
|
});
|
|
354
|
+
|
|
178
355
|
```
|
|
179
356
|
|
|
357
|
+
|
|
358
|
+
|
|
180
359
|
=== "ES Modules"
|
|
181
360
|
|
|
361
|
+
|
|
362
|
+
|
|
182
363
|
```javascript
|
|
364
|
+
|
|
183
365
|
import { registerTag } from 'miki-template';
|
|
184
366
|
|
|
367
|
+
|
|
368
|
+
|
|
185
369
|
registerTag('panel', (tagContent, parser) => {
|
|
370
|
+
|
|
186
371
|
const classes = tagContent.trim() || '';
|
|
372
|
+
|
|
187
373
|
const nodelist = parser.parse(['endpanel']);
|
|
374
|
+
|
|
188
375
|
parser.skipTag();
|
|
189
376
|
|
|
377
|
+
|
|
378
|
+
|
|
190
379
|
return {
|
|
380
|
+
|
|
191
381
|
render: (context) => {
|
|
382
|
+
|
|
192
383
|
const body = nodelist.map(n => n.render(context)).join('');
|
|
384
|
+
|
|
193
385
|
return `<div class="panel ${classes}">${body}</div>`;
|
|
386
|
+
|
|
194
387
|
}
|
|
388
|
+
|
|
195
389
|
};
|
|
390
|
+
|
|
196
391
|
});
|
|
392
|
+
|
|
197
393
|
```
|
|
198
394
|
|
|
395
|
+
|
|
396
|
+
|
|
199
397
|
Usage with nested content:
|
|
200
398
|
|
|
399
|
+
|
|
400
|
+
|
|
201
401
|
```html
|
|
402
|
+
|
|
202
403
|
{% panel "card" %}
|
|
404
|
+
|
|
203
405
|
<h2>{{ title }}</h2>
|
|
406
|
+
|
|
204
407
|
<p>{{ description }}</p>
|
|
408
|
+
|
|
205
409
|
{% endpanel %}
|
|
410
|
+
|
|
206
411
|
```
|
|
207
412
|
|
|
413
|
+
|
|
414
|
+
|
|
208
415
|
### Real-World Example: Cache Tag
|
|
209
416
|
|
|
417
|
+
|
|
418
|
+
|
|
210
419
|
=== "CommonJS"
|
|
211
420
|
|
|
421
|
+
|
|
422
|
+
|
|
212
423
|
```javascript
|
|
424
|
+
|
|
213
425
|
const { registerTag } = require('miki-template');
|
|
214
426
|
|
|
427
|
+
|
|
428
|
+
|
|
215
429
|
registerTag('cache_block', (tagContent, parser) => {
|
|
430
|
+
|
|
216
431
|
const [key, ...rest] = tagContent.trim().split(/\s+/);
|
|
432
|
+
|
|
217
433
|
const nodelist = parser.parse(['endcache_block']);
|
|
434
|
+
|
|
218
435
|
parser.skipTag();
|
|
219
436
|
|
|
437
|
+
|
|
438
|
+
|
|
220
439
|
return {
|
|
440
|
+
|
|
221
441
|
render: (context) => {
|
|
442
|
+
|
|
222
443
|
const cacheKey = key;
|
|
444
|
+
|
|
223
445
|
const cache = context.get('cache') || global.__cache__;
|
|
446
|
+
|
|
224
447
|
if (!cache) return nodelist.map(n => n.render(context)).join('');
|
|
448
|
+
|
|
225
449
|
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
450
|
+
|
|
226
451
|
const output = nodelist.map(n => n.render(context)).join('');
|
|
452
|
+
|
|
227
453
|
cache.set(cacheKey, output, rest[0] || 300);
|
|
454
|
+
|
|
228
455
|
return output;
|
|
456
|
+
|
|
229
457
|
}
|
|
458
|
+
|
|
230
459
|
};
|
|
460
|
+
|
|
231
461
|
});
|
|
462
|
+
|
|
232
463
|
```
|
|
233
464
|
|
|
465
|
+
|
|
466
|
+
|
|
234
467
|
=== "ES Modules"
|
|
235
468
|
|
|
469
|
+
|
|
470
|
+
|
|
236
471
|
```javascript
|
|
472
|
+
|
|
237
473
|
import { registerTag } from 'miki-template';
|
|
238
474
|
|
|
475
|
+
|
|
476
|
+
|
|
239
477
|
registerTag('cache_block', (tagContent, parser) => {
|
|
478
|
+
|
|
240
479
|
const [key, ...rest] = tagContent.trim().split(/\s+/);
|
|
480
|
+
|
|
241
481
|
const nodelist = parser.parse(['endcache_block']);
|
|
482
|
+
|
|
242
483
|
parser.skipTag();
|
|
243
484
|
|
|
485
|
+
|
|
486
|
+
|
|
244
487
|
return {
|
|
488
|
+
|
|
245
489
|
render: (context) => {
|
|
490
|
+
|
|
246
491
|
const cacheKey = key;
|
|
492
|
+
|
|
247
493
|
const cache = context.get('cache') || global.__cache__;
|
|
494
|
+
|
|
248
495
|
if (!cache) return nodelist.map(n => n.render(context)).join('');
|
|
496
|
+
|
|
249
497
|
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
498
|
+
|
|
250
499
|
const output = nodelist.map(n => n.render(context)).join('');
|
|
500
|
+
|
|
251
501
|
cache.set(cacheKey, output, rest[0] || 300);
|
|
502
|
+
|
|
252
503
|
return output;
|
|
504
|
+
|
|
253
505
|
}
|
|
506
|
+
|
|
254
507
|
};
|
|
508
|
+
|
|
255
509
|
});
|
|
510
|
+
|
|
256
511
|
```
|
|
257
512
|
|
|
513
|
+
|
|
514
|
+
|
|
258
515
|
## Tag Registration Best Practices
|
|
259
516
|
|
|
517
|
+
|
|
518
|
+
|
|
260
519
|
1. **Return objects with `render(context)`** — the render signature must accept a context object.
|
|
520
|
+
|
|
261
521
|
2. **Use `parser.parse([...terminators])`** for tags with bodies — this lets the parser consume nested content correctly.
|
|
522
|
+
|
|
262
523
|
3. **Always call `parser.skipTag()`** after `parser.parse` to consume the end tag.
|
|
524
|
+
|
|
263
525
|
4. **Handle whitespace** — `tagContent.trim()` for single-argument tags.
|
|
526
|
+
|
|
264
527
|
5. **Async tags need asyncRender** — return a Promise from `render()` and use `asyncRender()` to render.
|
|
528
|
+
|
|
265
529
|
6. **Access context values** — use `context.get('key')` or `context.resolve('expr')`.
|
|
266
530
|
|
|
531
|
+
|
|
532
|
+
|
|
267
533
|
## Next Steps
|
|
268
534
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
- [
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
- [Built-in Tags Reference](../api/tags.md)
|
|
538
|
+
|
|
539
|
+
- [Custom Filters](./custom-filters.md)
|
|
540
|
+
|
|
541
|
+
- [Guide: Tags](./tags.md)
|
|
542
|
+
|