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,642 +1,1314 @@
1
1
  # Filters
2
2
 
3
+
4
+
3
5
  Filters transform variable output using the pipe (`|`) syntax. You can chain multiple filters left-to-right, and many accept arguments after a colon (`:`).
4
6
 
7
+
8
+
5
9
  ## Table of Contents
6
10
 
11
+
12
+
7
13
  - [Basic Usage](#basic-usage)
14
+
8
15
  - [Filter Chaining](#filter-chaining)
16
+
9
17
  - [Filter Arguments](#filter-arguments)
18
+
10
19
  - [Text Filters](#text-filters)
20
+
11
21
  - [HTML / Security Filters](#html--security-filters)
22
+
12
23
  - [List / Array Filters](#list--array-filters)
24
+
13
25
  - [Default Value Filters](#default-value-filters)
26
+
14
27
  - [Date and Time Filters](#date-and-time-filters)
28
+
15
29
  - [Numeric / Math Filters](#numeric--math-filters)
30
+
16
31
  - [Data Formatting Filters](#data-formatting-filters)
32
+
17
33
  - [Encoding Filters](#encoding-filters)
34
+
18
35
  - [Time-Ago Filters](#time-ago-filters)
36
+
19
37
  - [Built-in Library Filters](#built-in-library-filters)
38
+
20
39
  - [Writing Custom Filters](#writing-custom-filters)
21
40
 
41
+
42
+
22
43
  ---
23
44
 
45
+
46
+
24
47
  ## Basic Usage
25
48
 
49
+
50
+
26
51
  ```html
52
+
27
53
  {{ name|upper }}
54
+
28
55
  {{ price|floatformat:2 }}
56
+
29
57
  {{ body|truncatewords:30|escape }}
58
+
30
59
  ```
31
60
 
61
+
62
+
32
63
  Each `|` applies a filter to the value on its left. Filters are evaluated left-to-right: the output of one filter becomes the input of the next.
33
64
 
65
+
66
+
34
67
  ## Filter Chaining
35
68
 
69
+
70
+
36
71
  Filters apply left-to-right. The output of each filter becomes the input of the next:
37
72
 
73
+
74
+
38
75
  ```html
76
+
39
77
  {{ name|lower|capfirst }}
78
+
40
79
  <!-- "Miki" → "miki" → "Miki" -->
41
80
 
81
+
82
+
42
83
  {{ bio|striptags|truncatewords:20 }}
84
+
43
85
  <!-- Strip HTML tags, then truncate to 20 words -->
44
86
 
87
+
88
+
45
89
  {{ text|escape|truncatechars:50|upper }}
90
+
46
91
  ```
47
92
 
93
+
94
+
48
95
  You can chain any number of filters:
49
96
 
97
+
98
+
50
99
  ```html
100
+
51
101
  {{ price|mult:1.2|add:2|floatformat:2|currency:"$" }}
102
+
52
103
  <!-- price = 10 → 12.0 → 14.0 → "14.00" → "$14.00" -->
104
+
53
105
  ```
54
106
 
107
+
108
+
55
109
  ## Filter Arguments
56
110
 
111
+
112
+
57
113
  Filters accept the following argument types:
58
114
 
115
+
116
+
59
117
  | Syntax | Type | Example |
118
+
60
119
  |--------|------|---------|
120
+
61
121
  | Unquoted | Variable lookup | `value|filter:count` |
122
+
62
123
  | Double-quoted | String literal | `value|filter:"hello"` |
124
+
63
125
  | Single-quoted | String literal | `value|filter:'world'` |
126
+
64
127
  | Number | Integer/float literal | `value|truncatewords:10` |
128
+
65
129
  | Boolean | `true`/`false` | `value|yesno:"yes,no"` |
66
130
 
131
+
132
+
67
133
  **Real-world example — conditional greeting with fallback:**
68
134
 
135
+
136
+
69
137
  ```html
138
+
70
139
  <h1>{{ user.name|default:"Guest"|capfirst }}</h1>
140
+
71
141
  <span class="badge {% if user.is_premium|yesno:"yes,no" %}premium{% else %}free{% endif %}">
142
+
72
143
  {{ user.plan|default:"Free" }}
144
+
73
145
  </span>
146
+
74
147
  ```
75
148
 
149
+
150
+
76
151
  ---
77
152
 
153
+
154
+
78
155
  ## Text Filters
79
156
 
157
+
158
+
80
159
  | Filter | Description | Example |
160
+
81
161
  |--------|-------------|---------|
162
+
82
163
  | `upper` | Uppercase | `{{ "hello"|upper }}` |
164
+
83
165
  | `lower` | Lowercase | `{{ "HELLO"|lower }}` |
166
+
84
167
  | `title` | Title case | `{{ "miki coder"|title }}` |
168
+
85
169
  | `capfirst` | Capitalize first letter | `{{ "hello"|capfirst }}` |
170
+
86
171
  | `truncatewords:N` | Truncate to N words, appends ` ...` | `{{ body|truncatewords:30 }}` |
172
+
87
173
  | `truncatechars:N` | Truncate to N chars, appends `...` | `{{ title|truncatechars:50 }}` |
174
+
88
175
  | `truncatechars_html:N` | HTML-aware truncation to N chars, preserves tags | `{{ html|truncatechars_html:100 }}` |
176
+
89
177
  | `wordcount` | Count words | `{{ body|wordcount }}` |
178
+
90
179
  | `linebreaks` | Convert double newlines to `<p>` and single to `<br>` | `{{ text|linebreaks }}` |
180
+
91
181
  | `linebreaksbr` | Convert all newlines to `<br>` | `{{ text|linebreaksbr }}` |
182
+
92
183
  | `striptags` | Remove HTML tags | `{{ html|striptags }}` |
184
+ | `repeat` | Repeat a string N times | `{{ "ha"|repeat:3 }}` |
185
+ | `range` | Generate a range of integers | `{{ 5|range }}` |
186
+
187
+
93
188
  | `slugify` | URL-friendly slug | `{{ title|slugify }}` |
189
+
94
190
  | `length_is:N` | Test if length equals N | `{{ items|length_is:0 }}` |
95
191
 
192
+
193
+
96
194
  ### truncatechars_html
97
195
 
196
+
197
+
98
198
  Like `truncatechars` but respects HTML tags — tags are preserved in full and only visible text counts toward the limit:
99
199
 
200
+
201
+
100
202
  ```html
203
+
101
204
  {{ "<p>Hello world</p>"|truncatechars_html:10 }}
205
+
102
206
  <!-- → "<p>Hello worl...</p>" -->
207
+
103
208
  ```
104
209
 
210
+
211
+
105
212
  **Real-world blog excerpt:**
106
213
 
214
+
215
+
107
216
  ```html
217
+
108
218
  <article>
219
+
109
220
  {{ post.body|truncatechars_html:200 }}
221
+
110
222
  </article>
223
+
111
224
  ```
112
225
 
226
+
227
+
113
228
  ### linebreaks
114
229
 
230
+
231
+
115
232
  Converts newlines into paragraphs (`<p>`) and standalone line breaks into `<br>`:
116
233
 
234
+
235
+
117
236
  ```html
237
+
118
238
  {{ "Line one\n\nLine two\nLine three"|linebreaks }}
239
+
119
240
  <!-- Output: <p>Line one</p><p>Line two<br>Line three</p> -->
241
+
120
242
  ```
121
243
 
244
+
245
+
122
246
  ### slugify
123
247
 
248
+
249
+
124
250
  Converts text to a URL-safe slug (lowercase, hyphens, no special characters):
125
251
 
252
+
253
+
126
254
  ```html
255
+
127
256
  <a href="/posts/{{ post.title|slugify }}">{{ post.title }}</a>
257
+
128
258
  <!-- title: "Hello World: A New Beginning!" → href="/posts/hello-world-a-new-beginning" -->
259
+
129
260
  ```
130
261
 
262
+
263
+
131
264
  ### wordcount
132
265
 
266
+
267
+
133
268
  Use with `pluralize` for dynamic labels:
134
269
 
270
+
271
+
135
272
  ```html
273
+
136
274
  <p>{{ post.body|wordcount }} {{ post.body|wordcount|pluralize:"word,words" }} read</p>
275
+
137
276
  ```
138
277
 
278
+
279
+
139
280
  ---
140
281
 
282
+
283
+
141
284
  ## HTML / Security Filters
142
285
 
286
+
287
+
143
288
  | Filter | Description |
289
+
144
290
  |--------|-------------|
291
+
145
292
  | `safe` | Mark string as safe (no escaping) |
293
+
146
294
  | `escape` | Force HTML escaping, even on SafeString |
147
295
 
296
+
297
+
148
298
  **Real-world CMS rendering:**
149
299
 
300
+
301
+
150
302
  ```html
303
+
151
304
  <!-- Body is trusted HTML from the CMS -->
305
+
152
306
  <div class="content">{{ post.body_html|safe }}</div>
153
307
 
308
+
309
+
154
310
  <!-- User comments are always escaped -->
311
+
155
312
  <div class="comment">{{ comment.text|escape }}</div>
313
+
156
314
  ```
157
315
 
316
+
317
+
158
318
  ---
159
319
 
320
+
321
+
160
322
  ## List / Array Filters
161
323
 
324
+
325
+
162
326
  | Filter | Description | Example |
327
+
163
328
  |--------|-------------|---------|
329
+
164
330
  | `length` | Length of list/string | `{{ items|length }}` |
331
+
165
332
  | `join:","` | Join with separator | `{{ tags|join:", " }}` |
333
+
166
334
  | `slice:"start:end"` | Slice list | `{{ items|slice:"0:5" }}` |
335
+
167
336
  | `dictsort:"key"` | Sort dict by key | `{{ dict|dictsort:"name" }}` |
337
+
168
338
  | `dictsortreversed:"key"` | Reverse sort dict | `{{ dict|dictsortreversed:"name" }}` |
339
+
169
340
  | `sort` | Sort array | `{{ items|sort }}` |
341
+
170
342
  | `unique` | Deduplicate array | `{{ tags|unique }}` |
343
+
171
344
  | `random` | Random item from array | `{{ items|random }}` |
345
+
172
346
  | `reverse` | Reverse array or string | `{{ items|reverse }}` |
347
+
173
348
  | `split:","` | Split string into array | `{{ csv|split:"," }}` |
349
+
174
350
  | `replace:"old,new"` | Replace substring | `{{ text|replace:"foo,bar" }}` |
175
351
 
352
+
353
+
176
354
  ### slice
177
355
 
356
+
357
+
178
358
  Slices an array or string like Python (`[start:end]`). Supports negative indices:
179
359
 
360
+
361
+
180
362
  ```html
363
+
181
364
  <!-- First 3 items -->
365
+
182
366
  {{ items|slice:"0:3" }}
183
367
 
368
+
369
+
184
370
  <!-- From index 2 onwards -->
371
+
185
372
  {{ items|slice:"2:" }}
186
373
 
374
+
375
+
187
376
  <!-- Last 2 items -->
377
+
188
378
  {{ items|slice:"-2:" }}
379
+
189
380
  ```
190
381
 
382
+
383
+
191
384
  ### join
192
385
 
386
+
387
+
193
388
  **Real-world tag cloud:**
194
389
 
390
+
391
+
195
392
  ```html
393
+
196
394
  <div class="tag-cloud">
395
+
197
396
  {% for tag in post.tags|join:", " %}
397
+
198
398
  <span class="tag">{{ tag }}</span>
399
+
199
400
  {% endfor %}
401
+
200
402
  </div>
403
+
201
404
  <!-- But better as: -->
405
+
202
406
  <span class="tag-cloud">{{ post.tags|join:", " }}</span>
407
+
203
408
  ```
204
409
 
410
+
411
+
205
412
  ### sort + unique
206
413
 
414
+
415
+
207
416
  **Real-world: deduplicated, sorted navigation:**
208
417
 
418
+
419
+
209
420
  ```html
421
+
210
422
  <ul>
423
+
211
424
  {% for category in categories|sort %}
425
+
212
426
  <li>{{ category|capfirst }}</li>
427
+
213
428
  {% endfor %}
429
+
214
430
  </ul>
431
+
215
432
  ```
216
433
 
434
+
435
+
217
436
  ### split
218
437
 
438
+
439
+
219
440
  **Real-world: parsing comma-separated metadata:**
220
441
 
442
+
443
+
221
444
  ```html
445
+
222
446
  <!-- post.keywords = "nature,landscape,autumn" -->
447
+
223
448
  <div class="meta">
449
+
224
450
  {% for kw in post.keywords|split:"," %}
451
+
225
452
  <a href="/tag/{{ kw|slugify }}">{{ kw|capfirst }}</a>
453
+
226
454
  {% endfor %}
455
+
227
456
  </div>
457
+
228
458
  ```
229
459
 
460
+
461
+
230
462
  ---
231
463
 
464
+
465
+
232
466
  ## Default Value Filters
233
467
 
468
+
469
+
234
470
  | Filter | Description |
471
+
235
472
  |--------|-------------|
473
+
236
474
  | `default:"fallback"` | Use fallback for empty string, null, or undefined |
475
+
237
476
  | `default_if_none:"fallback"` | Use fallback only for null/undefined |
477
+
238
478
  | `firstof:v1 v2 v3` | Return first truthy value |
239
479
 
480
+
481
+
240
482
  ### default vs default_if_none
241
483
 
484
+
485
+
242
486
  - `default` uses the fallback for falsy values: `null`, `undefined`, `""`, `0`, `false`
487
+
243
488
  - `default_if_none` only uses the fallback for `null` and `undefined`
244
489
 
490
+
491
+
245
492
  ```html
493
+
246
494
  {{ ""|default:"empty" }} → "empty"
495
+
247
496
  {{ ""|default_if_none:"N/A" }} → "" (empty string is not none)
497
+
248
498
  {{ 0|default:"zero" }} → "zero"
499
+
249
500
  {{ 0|default_if_none:"N/A" }} → 0 (zero is not none)
501
+
250
502
  ```
251
503
 
504
+
505
+
252
506
  **Real-world user profile:**
253
507
 
508
+
509
+
254
510
  ```html
511
+
255
512
  <!-- Show "No bio yet" only when bio is truly empty -->
513
+
256
514
  <p class="bio">{{ user.bio|default:"No bio yet." }}</p>
257
515
 
516
+
517
+
258
518
  <!-- Distinguish between "never set" and "explicitly empty" -->
519
+
259
520
  {% if user.display_name %}
521
+
260
522
  <h2>{{ user.display_name|default_if_none:"Anonymous" }}</h2>
523
+
261
524
  {% endif %}
525
+
262
526
  ```
263
527
 
528
+
529
+
264
530
  ---
265
531
 
532
+
533
+
266
534
  ## Date and Time Filters
267
535
 
536
+
537
+
268
538
  | Filter | Description | Example |
539
+
269
540
  |--------|-------------|---------|
541
+
270
542
  | `date:"Y-m-d"` | Format date (Django-style tokens) | `{{ d|date:"Y-m-d" }}` |
543
+
271
544
  | `time:"H:i"` | Format time | `{{ d|time:"H:i" }}` |
545
+
272
546
  | `date_format:"yyyy-MM-dd"` | Format date (date-fns tokens) | `{{ d|date_format:"yyyy-MM-dd" }}` |
547
+
273
548
  | `strftime:"PPpp"` | Format date (date-fns tokens) | `{{ now|strftime:"PPpp" }}` |
549
+
274
550
  | `timesince` | Time since date | `{{ created|timesince }}` |
551
+
275
552
  | `timeuntil` | Time until date | `{{ start|timeuntil }}` |
553
+
276
554
  | `ago` | Time since date, human-readable | `{{ created|ago }}` |
555
+
277
556
  | `until` | Time until date, human-readable | `{{ start|until }}` |
557
+
278
558
  | `time_diff:other_date` | Difference between two dates | `{{ start|time_diff:end }}` |
279
559
 
560
+
561
+
280
562
  ### Date format tokens (`date` and `time` filters)
281
563
 
564
+
565
+
282
566
  The `date` filter uses Django-style format tokens with longest-first matching:
283
567
 
568
+
569
+
284
570
  | Token | Output |
571
+
285
572
  |-------|--------|
573
+
286
574
  | `Y` | 4-digit year |
575
+
287
576
  | `y` | 2-digit year |
577
+
288
578
  | `m` | Month number (no pad) |
579
+
289
580
  | `n` | Month number (no pad) |
581
+
290
582
  | `d` | Day number (no pad) |
583
+
291
584
  | `j` | Day number (no pad) |
585
+
292
586
  | `H` | 24-hour hour (no pad) |
587
+
293
588
  | `G` | 24-hour hour (no pad) |
589
+
294
590
  | `i` | Minutes |
591
+
295
592
  | `s` | Seconds |
593
+
296
594
  | `F` | Long month name |
595
+
297
596
  | `D` | Short day name |
597
+
298
598
  | `M` | Long month name |
299
599
 
600
+
601
+
300
602
  ### date_format and strftime (date-fns tokens)
301
603
 
604
+
605
+
302
606
  For ISO and date-fns patterns:
303
607
 
608
+
609
+
304
610
  ```html
611
+
305
612
  {{ post.published_at|date_format:"yyyy-MM-dd" }} → "2026-08-31"
613
+
306
614
  {{ post.published_at|strftime:"PPpp" }} → "Aug 31, 2026 at 10:30 PM"
615
+
307
616
  {{ event.date|strftime:"EEEE, MMMM do yyyy, h:mm a" }} → "Sunday, August 31st 2026, 10:30 PM"
617
+
308
618
  ```
309
619
 
620
+
621
+
310
622
  ### timesince / timeuntil
311
623
 
624
+
625
+
312
626
  Returns a human-readable time difference:
313
627
 
628
+
629
+
314
630
  ```html
631
+
315
632
  {{ post.created|timesince }} → "2 hours"
633
+
316
634
  {{ post.created|timesince:other_date }} → "3 days" (relative to other_date)
635
+
317
636
  {{ event.date|timeuntil }} → "5 days"
637
+
318
638
  ```
319
639
 
640
+
641
+
320
642
  ### ago / until
321
643
 
644
+
645
+
322
646
  More human-readable relative time strings:
323
647
 
648
+
649
+
324
650
  ```html
651
+
325
652
  <!-- "2 days ago", "just now", "3 months ago" -->
653
+
326
654
  <span class="timestamp">{{ comment.created|ago }}</span>
327
655
 
656
+
657
+
328
658
  <!-- "2 days", "3 weeks", "1 month" -->
659
+
329
660
  <time datetime="{{ event.date|date:'c' }}">{{ event.date|until }}</time>
661
+
330
662
  ```
331
663
 
664
+
665
+
332
666
  **Real-world blog post metadata:**
333
667
 
668
+
669
+
334
670
  ```html
671
+
335
672
  <article class="post">
673
+
336
674
  <header>
675
+
337
676
  <h1>{{ post.title }}</h1>
677
+
338
678
  <time class="posted-at">
679
+
339
680
  Published {{ post.published_at|date:"F j, Y" }} ({{ post.published_at|ago }})
681
+
340
682
  </time>
683
+
341
684
  </header>
685
+
342
686
  <div class="content">
687
+
343
688
  {{ post.body|linebreaks }}
689
+
344
690
  </div>
691
+
345
692
  </article>
693
+
346
694
  ```
347
695
 
696
+
697
+
348
698
  ---
349
699
 
700
+
701
+
350
702
  ## Numeric / Math Filters
351
703
 
704
+
705
+
352
706
  | Filter | Description | Example |
707
+
353
708
  |--------|-------------|---------|
709
+
354
710
  | `add:N` | Add number | `{{ count\|add:1 }}` |
711
+
355
712
  | `sub:N` | Subtract number | `{{ total\|sub:tax }}` |
713
+
356
714
  | `mult:N` | Multiply number | `{{ price\|mult:1.2 }}` |
715
+
357
716
  | `divisibleby:N` | Test divisibility | `{{ i\|divisibleby:2 }}` |
717
+
358
718
  | `mod:N` | Modulo | `{{ i\|mod:3 }}` |
719
+
359
720
  | `floatformat:N` | Format float | `{{ price\|floatformat:2 }}` |
721
+
360
722
  | `square` | Square a number | `{{ n\|square }}` |
723
+
361
724
  | `sqrt` | Square root | `{{ n\|sqrt }}` |
725
+
362
726
  | `abs` | Absolute value | `{{ delta\|abs }}` |
727
+
363
728
  | `round:N` | Round to N decimals | `{{ price\|round:2 }}` |
729
+
364
730
  | `floor` | Floor | `{{ ratio\|floor }}` |
731
+
365
732
  | `ceil` | Ceiling | `{{ ratio\|ceil }}` |
733
+
366
734
  | `min:N` | Minimum of value and arg | `{{ temp\|min:0 }}` |
735
+
367
736
  | `max:N` | Maximum of value and arg | `{{ temp\|max:100 }}` |
737
+
368
738
  | `sum` | Sum array | `{{ numbers\|sum }}` |
739
+
369
740
  | `average` | Average array | `{{ scores\|average }}` |
370
741
 
742
+
743
+
371
744
  ### floatformat behavior
372
745
 
746
+
747
+
373
748
  | arg | behavior |
749
+
374
750
  |-----|----------|
751
+
375
752
  | (none) | 1 decimal (`3.4`) |
753
+
376
754
  | `0` | 0 decimals (`3`) |
755
+
377
756
  | `1` | 1 decimal (`3.4`) |
757
+
378
758
  | `2` | 2 decimals (`3.40`) |
759
+
379
760
  | `-1` | all decimals, trimmed |
380
761
 
762
+
763
+
381
764
  ### Real-world pricing example
382
765
 
766
+
767
+
383
768
  ```html
769
+
384
770
  <td class="price">
771
+
385
772
  ${{ item.price|mult:item.qty|floatformat:2 }}
773
+
386
774
  </td>
775
+
387
776
  <!-- If not on sale, apply discount -->
777
+
388
778
  {% if not item.on_sale %}
779
+
389
780
  <td>{{ item.base_price|mult:0.9|floatformat:2 }}</td>
781
+
390
782
  {% endif %}
783
+
391
784
  ```
392
785
 
786
+
787
+
393
788
  ---
394
789
 
790
+
791
+
395
792
  ## Data Formatting Filters
396
793
 
794
+
795
+
397
796
  | Filter | Description | Example |
797
+
398
798
  |--------|-------------|---------|
799
+
399
800
  | `currency:"$"` | Format as currency | `{{ price|currency:"$" }}` |
801
+
400
802
  | `phone_number` | Format as phone number | `{{ raw|phone_number }}` |
803
+
401
804
  | `email` | Format as mailto link | `{{ address|email }}` |
805
+
402
806
  | `url` | Format as URL | `{{ domain|url }}` |
807
+
403
808
  | `mask:"*"` | Mask string, show last 4 chars | `{{ card|mask }}` |
809
+
404
810
  | `whatsapp_link:"msg"` | Generate WhatsApp link | `{{ phone|whatsapp_link }}` |
811
+
405
812
  | `credit_card` | Format as credit card | `{{ raw|credit_card }}` |
813
+
406
814
  | `ssn` | Format as SSN | `{{ raw|ssn }}` |
815
+
407
816
  | `ip_address` | Format as IP address | `{{ raw|ip_address }}` |
817
+
408
818
  | `uuid` | Generate UUID (no input needed) | `{{ x|uuid }}` |
819
+
409
820
  | `filesizeformat` | Human-readable file size | `{{ bytes|filesizeformat }}` |
821
+
410
822
  | `yesno:"yes,no,maybe"` | Convert bool to string | `{{ active|yesno:"Active,Inactive" }}` |
823
+
411
824
  | `pluralize:"s"` | Pluralize based on count | `{{ count|pluralize }}` |
825
+
412
826
  | `urlencode` | URL encode | `{{ text|urlencode }}` |
827
+
413
828
  | `escapeuri` | URI encode | `{{ text|escapeuri }}` |
829
+
414
830
  | `stringformat:"%s"` | sprintf-style formatting | `{{ name|stringformat:"%s" }}` |
831
+
415
832
  | `cut:"text"` | Remove substring | `{{ text|cut:"foo" }}` |
833
+
416
834
  | `addslashes` | Escape quotes | `{{ text|addslashes }}` |
835
+
417
836
  | `removetags:"p,div"` | Remove specific tags | `{{ html|removetags:"p,div" }}` |
837
+
418
838
  | `trans` | Translate via i18n | `{{ "hello"|trans }}` |
839
+
419
840
  | `regroup:"attr"` | Group list by attribute | `{{ items|regroup:"category" }}` |
841
+
420
842
  | `json` | JSON-encode value | `{{ obj|json }}` |
843
+
421
844
  | `urlize` | Convert URLs to links | `{{ text|urlize }}` |
422
845
 
846
+
847
+
423
848
  ### phone_number
424
849
 
850
+
851
+
425
852
  Formats a 10-digit US phone number as `(123) 456-7890`. Handles 11-digit numbers with leading `1` as `+1 (123) 456-7890`:
426
853
 
854
+
855
+
427
856
  ```html
857
+
428
858
  {{ "1234567890"|phone_number }} → "(123) 456-7890"
859
+
429
860
  {{ "11234567890"|phone_number }} → "+1 (123) 456-7890"
861
+
430
862
  ```
431
863
 
864
+
865
+
432
866
  ### credit_card
433
867
 
868
+
869
+
434
870
  Formats a credit card number with dashes every 4 digits:
435
871
 
872
+
873
+
436
874
  ```html
875
+
437
876
  {{ "4111111111111111"|credit_card }} → "4111-1111-1111-1111"
877
+
438
878
  ```
439
879
 
880
+
881
+
440
882
  ### ssn
441
883
 
884
+
885
+
442
886
  Formats a 9-digit Social Security Number:
443
887
 
888
+
889
+
444
890
  ```html
891
+
445
892
  {{ "123456789"|ssn }} → "123-45-6789"
893
+
446
894
  ```
447
895
 
896
+
897
+
448
898
  ### mask
449
899
 
900
+
901
+
450
902
  Masks all but the last 4 characters. Default mask character is `*`:
451
903
 
904
+
905
+
452
906
  ```html
907
+
453
908
  <!-- Mask a phone number or credit card in display -->
909
+
454
910
  <span class="masked">{{ user.phone|mask }}</span>
455
911
 
912
+
913
+
456
914
  <!-- Custom mask character -->
915
+
457
916
  {{ "1234567890"|mask:"#" }} → "######7890"
917
+
458
918
  ```
459
919
 
920
+
921
+
460
922
  ### url (with safe filter)
461
923
 
924
+
925
+
462
926
  ```html
927
+
463
928
  <a href="{{ post.share_url|url|safe }}">{{ post.share_url|url }}</a>
929
+
464
930
  ```
465
931
 
932
+
933
+
466
934
  ### email
467
935
 
936
+
937
+
468
938
  ```html
939
+
469
940
  <a href="{{ user.email|email }}">{{ user.email }}</a>
941
+
470
942
  <!-- → <a href="mailto:user@example.com">user@example.com</a> -->
943
+
471
944
  ```
472
945
 
946
+
947
+
473
948
  ### whatsapp_link
474
949
 
950
+
951
+
475
952
  Generates a WhatsApp link with an optional pre-filled message:
476
953
 
954
+
955
+
477
956
  ```html
957
+
478
958
  <a href="{{ phone|whatsapp_link:"Hello! I'd like to know more." }}" target="_blank">
959
+
479
960
  Chat on WhatsApp
961
+
480
962
  </a>
963
+
481
964
  ```
482
965
 
966
+
967
+
483
968
  ### json (safe for `<script>` blocks)
484
969
 
970
+
971
+
485
972
  Safely serializes data to JSON for client-side consumption:
486
973
 
974
+
975
+
487
976
  ```html
977
+
488
978
  <script>
979
+
489
980
  const initialState = {{ page_state|json|safe }};
981
+
490
982
  </script>
983
+
491
984
  ```
492
985
 
986
+
987
+
493
988
  > Note: `json` output is marked safe automatically. Use `|safe` in the template only to signal intent — the engine handles it correctly either way.
494
989
 
990
+
991
+
495
992
  ### stringformat
496
993
 
994
+
995
+
497
996
  ```html
997
+
498
998
  {{ 3.14159|stringformat:".2f" }} → "3.14"
999
+
499
1000
  {{ count|stringformat:"04d" }} → "0042"
1001
+
500
1002
  {{ name|stringformat:"%s" }} → "Alice"
1003
+
501
1004
  ```
502
1005
 
1006
+
1007
+
503
1008
  ### cut
504
1009
 
1010
+
1011
+
505
1012
  Removes all occurrences of a substring:
506
1013
 
1014
+
1015
+
507
1016
  ```html
1017
+
508
1018
  <!-- Sanitize a URL by removing unwanted query params -->
1019
+
509
1020
  {{ request_uri|cut:"?debug=1" }}
1021
+
510
1022
  ```
511
1023
 
1024
+
1025
+
512
1026
  ### pluralize
513
1027
 
1028
+
1029
+
514
1030
  **Real-world item count:**
515
1031
 
1032
+
1033
+
516
1034
  ```html
1035
+
517
1036
  <p>{{ cart.items|length }} item{{ cart.items|length|pluralize }} in your cart</p>
1037
+
518
1038
  <!-- "1 item in your cart" / "3 items in your cart" -->
519
1039
 
1040
+
1041
+
520
1042
  <!-- Custom suffixes -->
1043
+
521
1044
  <p>{{ count|pluralize:"y,ies" }} comment{{ count|pluralize:"y,ies" }}</p>
1045
+
522
1046
  ```
523
1047
 
1048
+
1049
+
524
1050
  ### yesno
525
1051
 
1052
+
1053
+
526
1054
  **Real-world status badge:**
527
1055
 
1056
+
1057
+
528
1058
  ```html
1059
+
529
1060
  <span class="status {{ user.is_active|yesno:"active,inactive" }}">
1061
+
530
1062
  {{ user.is_active|yesno:"Active,Inactive" }}
1063
+
531
1064
  </span>
1065
+
532
1066
  ```
533
1067
 
1068
+
1069
+
534
1070
  ---
535
1071
 
1072
+
1073
+
536
1074
  ## Encoding Filters
537
1075
 
1076
+
1077
+
538
1078
  | Filter | Description | Example |
1079
+
539
1080
  |--------|-------------|---------|
1081
+
540
1082
  | `base64_encode` | Base64 encode | `{{ text|base64_encode }}` |
1083
+
541
1084
  | `base64_decode` | Base64 decode | `{{ text|base64_decode }}` |
542
1085
 
1086
+
1087
+
543
1088
  ### Real-world: embedding a CSRF token in a header
544
1089
 
1090
+
1091
+
545
1092
  ```html
1093
+
546
1094
  <meta name="csrf-token" content="{{ csrf_token|base64_encode }}">
1095
+
547
1096
  ```
548
1097
 
1098
+
1099
+
549
1100
  ---
550
1101
 
1102
+
1103
+
551
1104
  ## Time-Ago Filters
552
1105
 
1106
+
1107
+
553
1108
  | Filter | Description | Example |
1109
+
554
1110
  |--------|-------------|---------|
1111
+
555
1112
  | `time_diff:other` | Human-readable time difference | `{{ start|time_diff:end }}` |
1113
+
556
1114
  | `ago` | Time since date | `{{ created|ago }}` |
1115
+
557
1116
  | `until` | Time until date | `{{ start|until }}` |
558
1117
 
1118
+
1119
+
559
1120
  ### ago — human-readable "time since"
560
1121
 
1122
+
1123
+
561
1124
  ```html
1125
+
562
1126
  <!-- "just now", "5 minutes ago", "2 hours ago", "3 days ago", "1 year ago" -->
1127
+
563
1128
  <span class="time-ago">{{ post.created_at|ago }}</span>
1129
+
564
1130
  ```
565
1131
 
1132
+
1133
+
566
1134
  ### until — human-readable "time remaining"
567
1135
 
1136
+
1137
+
568
1138
  ```html
1139
+
569
1140
  <!-- "2 days", "3 weeks", "1 month" -->
1141
+
570
1142
  <span class="countdown">{{ auction.ends_at|until }}</span>
1143
+
571
1144
  ```
572
1145
 
1146
+
1147
+
573
1148
  ---
574
1149
 
1150
+
1151
+
575
1152
  ## Built-in Library Filters
576
1153
 
1154
+
1155
+
577
1156
  ### humanize
578
1157
 
1158
+
1159
+
579
1160
  Available after `{% load humanize %}`:
580
1161
 
1162
+
1163
+
581
1164
  | Filter | Description | Example |
1165
+
582
1166
  |--------|-------------|---------|
1167
+
583
1168
  | `intcomma` | Add comma separators | `{{ views|intcomma }}` → `1,234` |
1169
+
584
1170
  | `intword` | Convert to human word | `{{ 1000000|intword }}` → `1.0 million` |
1171
+
585
1172
  | `apnumber` | Convert 0-19 to words | `{{ 3|apnumber }}` → `three` |
1173
+
586
1174
  | `ordinal` | Add ordinal suffix | `{{ 1|ordinal }}` → `1st` |
1175
+
587
1176
  | `naturalday` | Convert date to relative day | `{{ date|naturalday }}` → `today` |
588
1177
 
1178
+
1179
+
589
1180
  Usage:
590
1181
 
1182
+
1183
+
591
1184
  ```html
1185
+
592
1186
  {% load humanize %}
1187
+
593
1188
  {{ post.view_count|intcomma }}
1189
+
594
1190
  {{ comment_count|ordinal }}
1191
+
595
1192
  ```
596
1193
 
1194
+
1195
+
597
1196
  ### cache
598
1197
 
1198
+
1199
+
599
1200
  The `cache` library provides a `{% cache %}` tag, not a filter. See [Tags: cache](#cache-tag) for details.
600
1201
 
1202
+
1203
+
601
1204
  ### lorem
602
1205
 
1206
+
1207
+
603
1208
  The `lorem` library provides a `{% lorem %}` tag and a `lorem` filter. These are auto-activated (no `{% load %}` needed).
604
1209
 
1210
+
1211
+
605
1212
  ```html
1213
+
606
1214
  <!-- Generate placeholder text -->
1215
+
607
1216
  {{ 5|lorem }}
1217
+
608
1218
  ```
609
1219
 
1220
+
1221
+
610
1222
  ---
611
1223
 
1224
+
1225
+
612
1226
  ## Writing Custom Filters
613
1227
 
1228
+
1229
+
614
1230
  You can register your own filters. See the [Custom Filters guide](custom-filters.md) for details.
615
1231
 
1232
+
1233
+
616
1234
  === "CommonJS"
617
1235
 
1236
+
1237
+
618
1238
  ```javascript
1239
+
619
1240
  const { registerFilter } = require('miki-template');
620
1241
 
1242
+
1243
+
621
1244
  registerFilter('reverse', (val) => {
1245
+
622
1246
  return String(val).split('').reverse().join('');
1247
+
623
1248
  });
1249
+
624
1250
  ```
625
1251
 
1252
+
1253
+
626
1254
  === "ES Modules"
627
1255
 
1256
+
1257
+
628
1258
  ```javascript
1259
+
629
1260
  import { registerFilter } from 'miki-template';
630
1261
 
1262
+
1263
+
631
1264
  registerFilter('reverse', (val) => {
1265
+
632
1266
  return String(val).split('').reverse().join('');
1267
+
633
1268
  });
1269
+
634
1270
  ```
635
1271
 
1272
+
1273
+
636
1274
  ---
637
1275
 
1276
+ # Filter Documentation
1277
+
1278
+ ## `repeat`
1279
+
1280
+ **Signature:** `repeat(value, count)`
1281
+
1282
+ - **Purpose:** Returns a new string consisting of `value` repeated `count` times.
1283
+ - **Parameters:**
1284
+ - `value` – Any value that can be converted to a string (null/undefined become empty string).
1285
+ - `count` – Number of repetitions, parsed as integer; non‑positive yields empty string.
1286
+ - **Implementation:** Uses native `String.prototype.repeat` for optimal V8 performance.
1287
+ - **Example:** `{{ "ab"|repeat:3 }} => "ababab"`
1288
+
1289
+ ## `range`
1290
+
1291
+ **Signature:** `range(end, start?)`
1292
+
1293
+ - **Purpose:** Generates an array of integers from `start` (inclusive) to `end` (exclusive) with step 1.
1294
+ - **Parameters:**
1295
+ - `end` – Upper bound (exclusive), parsed as integer.
1296
+ - `start` – Optional lower bound, defaults to `0`.
1297
+ - **Implementation:** Simple for‑loop building a JavaScript array.
1298
+ - **Example:** `{% for i in 5|range %}{{ i }}{% endfor %}` outputs `0 1 2 3 4`.
1299
+
1300
+ These filters are covered by unit tests in `tests/filters.test.js` and are documented here for developers and template authors.
1301
+
1302
+
1303
+
1304
+
638
1305
  ## Next Steps
639
1306
 
640
- - [Custom Filters](./custom-filters)
641
- - [Tags](./tags)
642
- - [API Reference: Filters](../api/filters)
1307
+
1308
+
1309
+ - [Custom Filters](./custom-filters.md)
1310
+
1311
+ - [Tags](./tags.md)
1312
+
1313
+ - [API Reference: Filters](../api/filters.md)
1314
+