miki-template 2.2.2 → 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 (66) hide show
  1. package/.github/workflows/docs.yml +3 -1
  2. package/.github/workflows/release.yml +1 -0
  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/.github/workflows/npm-publish-github-packages.yml +0 -36
  64. package/docs/javascripts/extra.js +0 -174
  65. package/docs/stylesheets/extra.css +0 -819
  66. package/overrides/partials/footer.html +0 -9
@@ -1,311 +1,622 @@
1
- # Custom Filters
1
+ # Custom Filters
2
+
3
+
2
4
 
3
5
  Add your own filters to transform values in templates. miki-template's filter API mirrors Django's — filters are simply functions that receive a value and optional argument, and return the transformed value.
4
6
 
7
+
8
+
5
9
  ## Table of Contents
6
10
 
11
+
12
+
7
13
  - [Register a Simple Filter](#register-a-simple-filter)
14
+
8
15
  - [Filters with Arguments](#filters-with-arguments)
16
+
9
17
  - [Multiple Arguments](#multiple-arguments)
18
+
10
19
  - [Context-Aware Filters](#context-aware-filters)
20
+
11
21
  - [SafeString Filters](#safestring-filters)
22
+
12
23
  - [Async Filters](#async-filters)
24
+
13
25
  - [Filter Registration Best Practices](#filter-registration-best-practices)
26
+
14
27
  - [Chaining Custom Filters](#chaining-custom-filters)
15
28
 
29
+
30
+
16
31
  ---
17
32
 
33
+
34
+
18
35
  ## Register a Simple Filter
19
36
 
37
+
38
+
20
39
  === "CommonJS"
21
40
 
41
+
42
+
22
43
  ```javascript
44
+
23
45
  const { registerFilter } = require('miki-template');
24
46
 
47
+
48
+
25
49
  registerFilter('reverse', (val) => {
50
+
26
51
  return String(val).split('').reverse().join('');
52
+
27
53
  });
54
+
28
55
  ```
29
56
 
57
+
58
+
30
59
  === "ES Modules"
31
60
 
61
+
62
+
32
63
  ```javascript
64
+
33
65
  import { registerFilter } from 'miki-template';
34
66
 
67
+
68
+
35
69
  registerFilter('reverse', (val) => {
70
+
36
71
  return String(val).split('').reverse().join('');
72
+
37
73
  });
74
+
38
75
  ```
39
76
 
77
+
78
+
40
79
  Use it in templates:
41
80
 
81
+
82
+
42
83
  ```html
84
+
43
85
  {{ name|reverse }}
86
+
44
87
  ```
45
88
 
89
+
90
+
46
91
  ## Filters with Arguments
47
92
 
93
+
94
+
48
95
  Filters can accept arguments after a colon:
49
96
 
97
+
98
+
50
99
  === "CommonJS"
51
100
 
101
+
102
+
52
103
  ```javascript
104
+
53
105
  const { registerFilter } = require('miki-template');
54
106
 
107
+
108
+
55
109
  registerFilter('multiply', (val, factor) => {
110
+
56
111
  return Number(val) * Number(factor);
112
+
57
113
  });
114
+
58
115
  ```
59
116
 
117
+
118
+
60
119
  === "ES Modules"
61
120
 
121
+
122
+
62
123
  ```javascript
124
+
63
125
  import { registerFilter } from 'miki-template';
64
126
 
127
+
128
+
65
129
  registerFilter('multiply', (val, factor) => {
130
+
66
131
  return Number(val) * Number(factor);
132
+
67
133
  });
134
+
68
135
  ```
69
136
 
137
+
138
+
70
139
  Usage:
71
140
 
141
+
142
+
72
143
  ```html
144
+
73
145
  {{ price|multiply:1.2 }}
146
+
74
147
  ```
75
148
 
149
+
150
+
76
151
  ## Multiple Arguments
77
152
 
153
+
154
+
78
155
  Pass multiple arguments separated by commas:
79
156
 
157
+
158
+
80
159
  === "CommonJS"
81
160
 
161
+
162
+
82
163
  ```javascript
164
+
83
165
  const { registerFilter } = require('miki-template');
84
166
 
167
+
168
+
85
169
  registerFilter('format', (val, prefix, suffix) => {
170
+
86
171
  return `${prefix}${val}${suffix}`;
172
+
87
173
  });
174
+
88
175
  ```
89
176
 
177
+
178
+
90
179
  === "ES Modules"
91
180
 
181
+
182
+
92
183
  ```javascript
184
+
93
185
  import { registerFilter } from 'miki-template';
94
186
 
187
+
188
+
95
189
  registerFilter('format', (val, prefix, suffix) => {
190
+
96
191
  return `${prefix}${val}${suffix}`;
192
+
97
193
  });
194
+
98
195
  ```
99
196
 
197
+
198
+
100
199
  Usage:
101
200
 
201
+
202
+
102
203
  ```html
204
+
103
205
  {{ name|format:"<b>","</b>" }}
206
+
104
207
  <!-- → "<b>Alice</b>" -->
208
+
105
209
  ```
106
210
 
211
+
212
+
107
213
  ### Real-World Example: Dynamic Currency Filter
108
214
 
215
+
216
+
109
217
  === "CommonJS"
110
218
 
219
+
220
+
111
221
  ```javascript
222
+
112
223
  const { registerFilter, markSafe } = require('miki-template');
113
224
 
225
+
226
+
114
227
  registerFilter('currency_dynamic', (val, code, locale = 'en-US') => {
228
+
115
229
  const num = Number(val);
230
+
116
231
  if (isNaN(num)) return '';
232
+
117
233
  return new Intl.NumberFormat(locale, {
234
+
118
235
  style: 'currency',
236
+
119
237
  currency: code
238
+
120
239
  }).format(num);
240
+
121
241
  });
242
+
122
243
  ```
123
244
 
245
+
246
+
124
247
  === "ES Modules"
125
248
 
249
+
250
+
126
251
  ```javascript
252
+
127
253
  import { registerFilter } from 'miki-template';
128
254
 
255
+
256
+
129
257
  registerFilter('currency_dynamic', (val, code, locale = 'en-US') => {
258
+
130
259
  const num = Number(val);
260
+
131
261
  if (isNaN(num)) return '';
262
+
132
263
  return new Intl.NumberFormat(locale, {
264
+
133
265
  style: 'currency',
266
+
134
267
  currency: code
268
+
135
269
  }).format(num);
270
+
136
271
  });
272
+
137
273
  ```
138
274
 
275
+
276
+
139
277
  Template usage:
140
278
 
279
+
280
+
141
281
  ```html
282
+
142
283
  <!-- €1,234.56 -->
284
+
143
285
  {{ 1234.5|currency_dynamic:"EUR", "de-DE" }}
144
286
 
287
+
288
+
145
289
  <!-- $1,234.56 -->
290
+
146
291
  {{ 1234.5|currency_dynamic:"USD" }}
292
+
147
293
  ```
148
294
 
295
+
296
+
149
297
  ## Context-Aware Filters
150
298
 
299
+
300
+
151
301
  Filters receive the rendering `context` as the third argument, enabling context-aware transformations:
152
302
 
303
+
304
+
153
305
  === "CommonJS"
154
306
 
307
+
308
+
155
309
  ```javascript
310
+
156
311
  const { registerFilter } = require('miki-template');
157
312
 
313
+
314
+
158
315
  registerFilter('currency', (val, symbol, ctx) => {
316
+
159
317
  const num = Number(val);
318
+
160
319
  if (isNaN(num)) return '';
320
+
161
321
  const sym = symbol || ctx.currencySymbol || '$';
322
+
162
323
  return sym + num.toFixed(2);
324
+
163
325
  });
326
+
164
327
  ```
165
328
 
329
+
330
+
166
331
  === "ES Modules"
167
332
 
333
+
334
+
168
335
  ```javascript
336
+
169
337
  import { registerFilter } from 'miki-template';
170
338
 
339
+
340
+
171
341
  registerFilter('currency', (val, symbol, ctx) => {
342
+
172
343
  const num = Number(val);
344
+
173
345
  if (isNaN(num)) return '';
346
+
174
347
  const sym = symbol || ctx.currencySymbol || '$';
348
+
175
349
  return sym + num.toFixed(2);
350
+
176
351
  });
352
+
177
353
  ```
178
354
 
355
+
356
+
179
357
  Usage:
180
358
 
359
+
360
+
181
361
  ```html
362
+
182
363
  {{ price|currency:"€" }}
364
+
183
365
  <!-- The filter can also read ctx.currencySymbol for a default -->
366
+
184
367
  ```
185
368
 
369
+
370
+
186
371
  **Real-world locale-aware formatter:**
187
372
 
373
+
374
+
188
375
  ```javascript
376
+
189
377
  registerFilter('datetime', (val, format, ctx) => {
378
+
190
379
  const locale = ctx.locale || 'en-US';
380
+
191
381
  const d = new Date(val);
382
+
192
383
  return new Intl.DateTimeFormat(locale, {
384
+
193
385
  dateStyle: format === 'short' ? 'short' : 'full',
386
+
194
387
  timeStyle: format === 'short' ? 'short' : undefined
388
+
195
389
  }).format(d);
390
+
196
391
  });
392
+
197
393
  ```
198
394
 
395
+
396
+
199
397
  ```html
398
+
200
399
  {{ post.created_at|datetime:"full" }}
400
+
201
401
  ```
202
402
 
403
+
404
+
203
405
  ## SafeString Filters
204
406
 
407
+
408
+
205
409
  Filters can return `SafeString` to prevent escaping — useful when generating HTML:
206
410
 
411
+
412
+
207
413
  === "CommonJS"
208
414
 
415
+
416
+
209
417
  ```javascript
418
+
210
419
  const { registerFilter, markSafe } = require('miki-template');
211
420
 
421
+
422
+
212
423
  registerFilter('badge', (val) => {
424
+
213
425
  const color = val === 'active' ? 'green' : 'gray';
426
+
214
427
  return markSafe(`<span class="badge badge-${color}">${val}</span>`);
428
+
215
429
  });
430
+
216
431
  ```
217
432
 
433
+
434
+
218
435
  === "ES Modules"
219
436
 
437
+
438
+
220
439
  ```javascript
440
+
221
441
  import { registerFilter, markSafe } from 'miki-template';
222
442
 
443
+
444
+
223
445
  registerFilter('badge', (val) => {
446
+
224
447
  const color = val === 'active' ? 'green' : 'gray';
448
+
225
449
  return markSafe(`<span class="badge badge-${color}">${val}</span>`);
450
+
226
451
  });
452
+
227
453
  ```
228
454
 
455
+
456
+
229
457
  Usage:
230
458
 
459
+
460
+
231
461
  ```html
462
+
232
463
  {{ user.status|badge }}
464
+
233
465
  ```
234
466
 
467
+
468
+
235
469
  ## Async Filters
236
470
 
471
+
472
+
237
473
  Filters can be async by returning a Promise. Use `asyncRender()` to render templates with async filters:
238
474
 
475
+
476
+
239
477
  === "CommonJS"
240
478
 
479
+
480
+
241
481
  ```javascript
482
+
242
483
  const { registerFilter } = require('miki-template');
243
484
 
485
+
486
+
244
487
  registerFilter('fetch_user', async (val) => {
488
+
245
489
  const res = await fetch(`https://api.example.com/users/${val}`);
490
+
246
491
  const data = await res.json();
492
+
247
493
  return data.display_name;
494
+
248
495
  });
496
+
249
497
  ```
250
498
 
499
+
500
+
251
501
  === "ES Modules"
252
502
 
503
+
504
+
253
505
  ```javascript
506
+
254
507
  import { registerFilter } from 'miki-template';
255
508
 
509
+
510
+
256
511
  registerFilter('fetch_user', async (val) => {
512
+
257
513
  const res = await fetch(`https://api.example.com/users/${val}`);
514
+
258
515
  const data = await res.json();
516
+
259
517
  return data.display_name;
518
+
260
519
  });
520
+
261
521
  ```
262
522
 
523
+
524
+
263
525
  Usage:
264
526
 
527
+
528
+
265
529
  === "CommonJS (asyncRender)"
266
530
 
531
+
532
+
267
533
  ```javascript
534
+
268
535
  const { asyncRender } = require('miki-template');
269
536
 
537
+
538
+
270
539
  const html = await asyncRender('Author: {{ user.id|fetch_user }}', { user: { id: 42 } });
540
+
271
541
  ```
272
542
 
543
+
544
+
273
545
  === "ES Modules"
274
546
 
547
+
548
+
275
549
  ```javascript
550
+
276
551
  import { asyncRender } from 'miki-template';
277
552
 
553
+
554
+
278
555
  const html = await asyncRender('Author: {{ user.id|fetch_user }}', { user: { id: 42 } });
556
+
279
557
  ```
280
558
 
559
+
560
+
281
561
  > **Note:** Async filters only work with `asyncRender()` or `compiled.asyncRender()`. Using them with `render()` or `compiled.render()` will throw.
282
562
 
563
+
564
+
283
565
  ## Filter Registration Best Practices
284
566
 
567
+
568
+
285
569
  1. **Handle null/undefined gracefully** — Return empty string or a fallback value.
570
+
286
571
  2. **Return strings** — Filters should generally return string representations for template output.
572
+
287
573
  3. **Don't mutate the input** — Treat values as immutable.
574
+
288
575
  4. **Use `markSafe()` for HTML output** — Prevent auto-escaping when returning HTML.
576
+
289
577
  5. **Validate arguments** — Coerce numeric arguments with `Number()` and handle `NaN`.
290
578
 
579
+
580
+
291
581
  ## Chaining Custom Filters
292
582
 
583
+
584
+
293
585
  Custom filters chain the same way as built-in filters:
294
586
 
587
+
588
+
295
589
  ```html
590
+
296
591
  {{ text|trim|highlight:"important"|safe }}
592
+
297
593
  ```
298
594
 
595
+
596
+
299
597
  ```javascript
598
+
300
599
  registerFilter('trim', (val) => String(val || '').trim());
600
+
301
601
  registerFilter('highlight', (val, term) => {
602
+
302
603
  const re = new RegExp(`(${term})`, 'gi');
604
+
303
605
  return markSafe(String(val).replace(re, '<mark>$1</mark>'));
606
+
304
607
  });
608
+
305
609
  ```
306
610
 
611
+
612
+
307
613
  ## Next Steps
308
614
 
309
- - [Custom Tags](./custom-tags)
310
- - [Advanced Usage](./advanced-usage)
311
- - [API Reference: Filters](../api/filters)
615
+
616
+
617
+ - [Custom Tags](./custom-tags.md)
618
+
619
+ - [Advanced Usage](./advanced-usage.md)
620
+
621
+ - [API Reference: Filters](../api/filters.md)
622
+