miki-template 2.2.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.github/workflows/docs.yml +3 -1
  2. package/.github/workflows/release.yml +0 -5
  3. package/benchmarks/ejs-results.json +6 -6
  4. package/benchmarks/ejs.js +5 -3
  5. package/benchmarks/handlebars-results.json +6 -6
  6. package/benchmarks/handlebars.js +5 -8
  7. package/benchmarks/miki-results.json +6 -6
  8. package/benchmarks/miki.js +6 -3
  9. package/benchmarks/pug-results.json +6 -6
  10. package/benchmarks/pug.js +5 -3
  11. package/docs/api/async-render.md +88 -3
  12. package/docs/api/cache.md +90 -3
  13. package/docs/api/compile.md +131 -3
  14. package/docs/api/context-processors.md +80 -3
  15. package/docs/api/filters.md +223 -3
  16. package/docs/api/finder.md +97 -3
  17. package/docs/api/helpers.md +56 -3
  18. package/docs/api/i18n.md +160 -3
  19. package/docs/api/index.md +82 -28
  20. package/docs/api/libraries.md +210 -3
  21. package/docs/api/render-partial.md +84 -3
  22. package/docs/api/render.md +95 -3
  23. package/docs/api/security.md +148 -3
  24. package/docs/api/setup-express.md +78 -2
  25. package/docs/api/tags.md +138 -4
  26. package/docs/filter.md +0 -0
  27. package/docs/guide/advanced-usage.md +403 -6
  28. package/docs/guide/async-rendering.md +312 -4
  29. package/docs/guide/context-processors.md +261 -4
  30. package/docs/guide/custom-filters.md +315 -4
  31. package/docs/guide/custom-tags.md +275 -4
  32. package/docs/guide/filters.md +675 -3
  33. package/docs/guide/getting-started.md +109 -7
  34. package/docs/guide/installation.md +99 -4
  35. package/docs/guide/partial-templates.md +371 -4
  36. package/docs/guide/quick-start.md +228 -6
  37. package/docs/guide/security.md +348 -3
  38. package/docs/guide/tags.md +789 -6
  39. package/docs/guide/template-discovery.md +174 -4
  40. package/docs/guide/template-inheritance.md +277 -4
  41. package/docs/index.md +24 -42
  42. package/docs/integrations/elysia.md +4 -2
  43. package/docs/integrations/express.md +219 -219
  44. package/docs/integrations/fastify.md +4 -2
  45. package/docs/integrations/hono.md +4 -2
  46. package/docs/integrations/index.md +68 -68
  47. package/docs/integrations/koa.md +4 -2
  48. package/docs/integrations/nestjs.md +4 -2
  49. package/docs/integrations/tsed.md +4 -2
  50. package/docs/performance.md +45 -8
  51. package/ex.mjs +1 -1
  52. package/mkdocs.yml +0 -22
  53. package/overrides/main.html +1 -1
  54. package/package.json +1 -1
  55. package/requirements-docs.txt +2 -1
  56. package/src/codegen.js +905 -0
  57. package/src/context.js +42 -30
  58. package/src/filters.js +16 -0
  59. package/src/index.js +66 -61
  60. package/src/tags/control.js +15 -12
  61. package/src/utils.js +60 -0
  62. package/tests/filters.test.js +9 -0
  63. package/docs/javascripts/extra.js +0 -174
  64. package/docs/stylesheets/extra.css +0 -819
  65. 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
- - [Built-in Tags Reference](../api/tags)
270
- - [Custom Filters](./custom-filters)
271
- - [Guide: Tags](./tags)
535
+
536
+
537
+ - [Built-in Tags Reference](../api/tags.md)
538
+
539
+ - [Custom Filters](./custom-filters.md)
540
+
541
+ - [Guide: Tags](./tags.md)
542
+