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.
- package/README.md +17 -5
- package/package.json +45 -1
- package/.eslintrc.json +0 -16
- package/.github/release-notes/v1.3.1.md +0 -55
- package/.github/release-notes/v1.3.3.md +0 -77
- package/.github/release-notes/v1.3.7.md +0 -20
- package/.github/workflows/ci.yml +0 -42
- package/.github/workflows/docs.yml +0 -107
- package/.github/workflows/release.yml +0 -107
- package/AGENT.md +0 -71
- package/API_REFERENCE.md +0 -341
- package/CHANGELOG.md +0 -173
- package/CODE_OF_CONDUCT.md +0 -14
- package/CONTRIBUTING.md +0 -27
- package/ROADMAP.md +0 -40
- package/assets/banner.png +0 -0
- package/assets/logo.png +0 -0
- package/benchmarks/ejs-results.json +0 -17
- package/benchmarks/ejs.js +0 -38
- package/benchmarks/handlebars-results.json +0 -17
- package/benchmarks/handlebars.js +0 -45
- package/benchmarks/miki-results.json +0 -17
- package/benchmarks/miki.js +0 -39
- package/benchmarks/pug-results.json +0 -17
- package/benchmarks/pug.js +0 -38
- package/benchmarks/report.json +0 -17
- package/benchmarks/run.js +0 -81
- package/benchmarks/stress.mjs +0 -647
- package/benchmarks/templates/large.dtpl +0 -7
- package/benchmarks/templates/medium.dtpl +0 -3
- package/benchmarks/templates/small.dtpl +0 -7
- package/context/component.md +0 -109
- package/context/prd.md +0 -131
- package/context/project-structure.md +0 -33
- package/dir/base.html +0 -23
- package/dir/cmpnt.html +0 -11
- package/dir/footer.html +0 -3
- package/dir/home.html +0 -80
- package/dir/index.html +0 -80
- package/dir/navbar.html +0 -9
- package/docs/api/async-render.md +0 -170
- package/docs/api/cache.md +0 -174
- package/docs/api/compile.md +0 -256
- package/docs/api/context-processors.md +0 -154
- package/docs/api/filters.md +0 -437
- package/docs/api/finder.md +0 -188
- package/docs/api/helpers.md +0 -106
- package/docs/api/i18n.md +0 -314
- package/docs/api/index.md +0 -108
- package/docs/api/libraries.md +0 -414
- package/docs/api/render-partial.md +0 -162
- package/docs/api/render.md +0 -184
- package/docs/api/security.md +0 -290
- package/docs/api/setup-express.md +0 -152
- package/docs/api/tags.md +0 -268
- package/docs/assets/banner.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/filter.md +0 -0
- package/docs/guide/advanced-usage.md +0 -794
- package/docs/guide/async-rendering.md +0 -616
- package/docs/guide/context-processors.md +0 -514
- package/docs/guide/custom-filters.md +0 -622
- package/docs/guide/custom-tags.md +0 -542
- package/docs/guide/filters.md +0 -1314
- package/docs/guide/getting-started.md +0 -204
- package/docs/guide/installation.md +0 -190
- package/docs/guide/partial-templates.md +0 -734
- package/docs/guide/quick-start.md +0 -444
- package/docs/guide/security.md +0 -690
- package/docs/guide/tags.md +0 -1566
- package/docs/guide/template-discovery.md +0 -340
- package/docs/guide/template-inheritance.md +0 -546
- package/docs/guide/what-is-miki-template.md +0 -28
- package/docs/guide/why-miki-template.md +0 -75
- package/docs/index.md +0 -86
- package/docs/integrations/elysia.md +0 -80
- package/docs/integrations/express.md +0 -219
- package/docs/integrations/fastify.md +0 -79
- package/docs/integrations/hono.md +0 -80
- package/docs/integrations/index.md +0 -68
- package/docs/integrations/koa.md +0 -90
- package/docs/integrations/nestjs.md +0 -80
- package/docs/integrations/tsed.md +0 -83
- package/docs/performance.md +0 -74
- package/eslint.config.mjs +0 -42
- package/ex.mjs +0 -36
- package/live-test/integrations/elysia-example.js +0 -16
- package/live-test/integrations/express-example.js +0 -24
- package/live-test/integrations/fastify-example.js +0 -20
- package/live-test/integrations/hono-example.js +0 -16
- package/live-test/integrations/koa-example.js +0 -30
- package/live-test/integrations/nestjs-example.js +0 -25
- package/live-test/integrations/smoke-test.js +0 -166
- package/live-test/integrations/tsed-example.js +0 -23
- package/live-test/package-lock.json +0 -1150
- package/live-test/package.json +0 -13
- package/live-test/packages/product/templates/product/detail.html +0 -7
- package/live-test/server.js +0 -38
- package/live-test/templates/app_templates/detail.html +0 -6
- package/live-test/views/base.html +0 -8
- package/live-test/views/child.html +0 -7
- package/live-test/views/home.html +0 -17
- package/live-test/views/index.html +0 -1
- package/miki-template-extension/.github/workflows/ci.yml +0 -116
- package/miki-template-extension/.vscodeignore +0 -7
- package/miki-template-extension/CHANGELOG.md +0 -99
- package/miki-template-extension/LICENSE +0 -21
- package/miki-template-extension/README.md +0 -273
- package/miki-template-extension/extension.js +0 -1013
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +0 -10
- package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
- package/miki-template-extension/package.json +0 -280
- package/miki-template-extension/snippets/miki-template.json +0 -717
- package/miki-template-extension/syntaxes/language-configuration.json +0 -114
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +0 -355
- package/miki-template-extension/tests/grammar-tests.json +0 -162
- package/miki-template-extension/tests/run-grammar-tests.js +0 -82
- package/mkdocs.yml +0 -195
- package/overrides/main.html +0 -26
- package/requirements-docs.txt +0 -2
- package/sample-app/package-lock.json +0 -901
- package/sample-app/package.json +0 -9
- package/sample-app/server.js +0 -14
- package/sample-app/views/index.html +0 -1
- package/scripts/build-vsix.js +0 -129
- package/scripts/build-vsix.ps1 +0 -15
- package/snippets/miki-template.json +0 -177
- package/src/asyncRender.js +0 -21
- package/src/types.d.ts +0 -107
- package/syntaxes/language-configuration.json +0 -26
- package/syntaxes/miki-template.tmLanguage.json +0 -146
- package/tests/asyncRender.test.js +0 -17
- package/tests/base.html +0 -6
- package/tests/child.html +0 -3
- package/tests/context_processors.test.js +0 -13
- package/tests/esm.test.mjs +0 -61
- package/tests/filters.test.js +0 -263
- package/tests/finder-appdirs.test.js +0 -19
- package/tests/finder.test.js +0 -17
- package/tests/fixtures/views/nested/index.html +0 -1
- package/tests/fixtures/views/partial.html +0 -1
- package/tests/fixtures/views/sub/deepfile.html +0 -1
- package/tests/fixtures/views-appdirs/product/site/detail.html +0 -1
- package/tests/include_security.test.js +0 -9
- package/tests/integration/README.md +0 -32
- package/tests/integration/features.test.cjs +0 -1681
- package/tests/integration/features.test.mjs +0 -1697
- package/tests/integration/finder.esm.test.mjs +0 -13
- package/tests/integration/partial-render.test.cjs +0 -13
- package/tests/integration/templates/base.miki +0 -6
- package/tests/integration/templates/child.miki +0 -6
- package/tests/integration/templates/index.html +0 -17
- package/tests/lexer.test.js +0 -45
- package/tests/parser.test.js +0 -57
- package/tests/partial.html +0 -1
- package/tests/partialdef.test.js +0 -79
- package/tests/production_checks.js +0 -57
- package/tests/security.test.js +0 -28
- package/tests/tags.test.js +0 -233
package/docs/guide/filters.md
DELETED
|
@@ -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
|
-
|