miki-template 2.3.0 → 2.3.2

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