miki-template 2.2.3 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docs.yml +3 -1
- package/.github/workflows/release.yml +0 -5
- package/README.md +17 -5
- package/benchmarks/ejs-results.json +6 -6
- package/benchmarks/ejs.js +5 -3
- package/benchmarks/handlebars-results.json +6 -6
- package/benchmarks/handlebars.js +5 -8
- package/benchmarks/miki-results.json +6 -6
- package/benchmarks/miki.js +6 -3
- package/benchmarks/pug-results.json +6 -6
- package/benchmarks/pug.js +5 -3
- package/docs/api/async-render.md +88 -3
- package/docs/api/cache.md +90 -3
- package/docs/api/compile.md +131 -3
- package/docs/api/context-processors.md +80 -3
- package/docs/api/filters.md +223 -3
- package/docs/api/finder.md +97 -3
- package/docs/api/helpers.md +56 -3
- package/docs/api/i18n.md +160 -3
- package/docs/api/index.md +82 -28
- package/docs/api/libraries.md +210 -3
- package/docs/api/render-partial.md +84 -3
- package/docs/api/render.md +95 -3
- package/docs/api/security.md +148 -3
- package/docs/api/setup-express.md +78 -2
- package/docs/api/tags.md +138 -4
- package/docs/filter.md +0 -0
- package/docs/guide/advanced-usage.md +403 -6
- package/docs/guide/async-rendering.md +312 -4
- package/docs/guide/context-processors.md +261 -4
- package/docs/guide/custom-filters.md +315 -4
- package/docs/guide/custom-tags.md +275 -4
- package/docs/guide/filters.md +675 -3
- package/docs/guide/getting-started.md +109 -7
- package/docs/guide/installation.md +99 -4
- package/docs/guide/partial-templates.md +371 -4
- package/docs/guide/quick-start.md +228 -6
- package/docs/guide/security.md +348 -3
- package/docs/guide/tags.md +789 -6
- package/docs/guide/template-discovery.md +174 -4
- package/docs/guide/template-inheritance.md +277 -4
- package/docs/index.md +24 -42
- package/docs/integrations/elysia.md +4 -2
- package/docs/integrations/express.md +219 -219
- package/docs/integrations/fastify.md +4 -2
- package/docs/integrations/hono.md +4 -2
- package/docs/integrations/index.md +68 -68
- package/docs/integrations/koa.md +4 -2
- package/docs/integrations/nestjs.md +4 -2
- package/docs/integrations/tsed.md +4 -2
- package/docs/performance.md +45 -8
- package/ex.mjs +1 -1
- package/mkdocs.yml +0 -22
- package/overrides/main.html +1 -1
- package/package.json +1 -1
- package/requirements-docs.txt +2 -1
- package/src/codegen.js +905 -0
- package/src/context.js +42 -30
- package/src/filters.js +16 -0
- package/src/index.js +66 -61
- package/src/tags/control.js +15 -12
- package/src/utils.js +60 -0
- package/tests/filters.test.js +9 -0
- package/docs/javascripts/extra.js +0 -174
- package/docs/stylesheets/extra.css +0 -819
- package/overrides/partials/footer.html +0 -9
package/docs/guide/filters.md
CHANGED
|
@@ -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
|
-
|
|
641
|
-
|
|
642
|
-
- [
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
- [Custom Filters](./custom-filters.md)
|
|
1310
|
+
|
|
1311
|
+
- [Tags](./tags.md)
|
|
1312
|
+
|
|
1313
|
+
- [API Reference: Filters](../api/filters.md)
|
|
1314
|
+
|