miki-template 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +54 -0
- package/AGENT.md +71 -0
- package/API_REFERENCE.md +314 -0
- package/CHANGELOG.md +97 -0
- package/CODE_OF_CONDUCT.md +14 -0
- package/CONTRIBUTING.md +27 -0
- package/README.md +304 -0
- package/ROADMAP.md +40 -0
- package/benchmarks/report.json +17 -0
- package/benchmarks/run.js +49 -0
- package/benchmarks/templates/large.dtpl +7 -0
- package/benchmarks/templates/medium.dtpl +3 -0
- package/benchmarks/templates/small.dtpl +7 -0
- package/context/component.md +109 -0
- package/context/prd.md +131 -0
- package/context/project-structure.md +33 -0
- package/docs/README.md +18 -0
- package/docs/advanced_usage.md +71 -0
- package/docs/api.md +102 -0
- package/docs/filters.md +540 -0
- package/docs/installation.md +106 -0
- package/docs/overview.md +57 -0
- package/docs/partialdef.md +41 -0
- package/docs/security.md +27 -0
- package/docs/tags.md +610 -0
- package/docs/usage.md +599 -0
- package/eslint.config.mjs +34 -0
- package/miki-template-1.2.0.vsix +0 -0
- package/miki-template-extension/LICENSE +21 -0
- package/miki-template-extension/README.md +82 -0
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +10 -0
- package/miki-template-extension/package.json +46 -0
- package/miki-template-extension/snippets/miki-template.json +177 -0
- package/miki-template-extension/syntaxes/language-configuration.json +26 -0
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +146 -0
- package/package.json +31 -0
- package/snippets/miki-template.json +177 -0
- package/src/asyncRender.js +21 -0
- package/src/cache.js +41 -0
- package/src/context.js +122 -0
- package/src/context_processors.js +41 -0
- package/src/esm.mjs +72 -0
- package/src/filters.js +527 -0
- package/src/i18n.js +171 -0
- package/src/index.js +454 -0
- package/src/lexer.js +92 -0
- package/src/libraries.js +240 -0
- package/src/parser.js +250 -0
- package/src/security.js +51 -0
- package/src/tags/control.js +591 -0
- package/src/tags/helpers.js +27 -0
- package/src/tags/i18n.js +230 -0
- package/src/tags/inheritance.js +216 -0
- package/src/tags/registry.js +18 -0
- package/src/tags/util.js +322 -0
- package/src/types.d.ts +107 -0
- package/syntaxes/language-configuration.json +26 -0
- package/syntaxes/miki-template.tmLanguage.json +146 -0
- package/tests/asyncRender.test.js +17 -0
- package/tests/base.html +6 -0
- package/tests/child.html +3 -0
- package/tests/context_processors.test.js +13 -0
- package/tests/esm.test.mjs +26 -0
- package/tests/filters.test.js +99 -0
- package/tests/include_security.test.js +9 -0
- package/tests/lexer.test.js +45 -0
- package/tests/parser.test.js +55 -0
- package/tests/partial.html +1 -0
- package/tests/partialdef.test.js +40 -0
- package/tests/production_checks.js +57 -0
- package/tests/security.test.js +28 -0
- package/tests/tags.test.js +203 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Partial Definition (`partialdef`)
|
|
2
|
+
|
|
3
|
+
`partialdef` is the cornerstone feature that brings Django‑style **named template fragments** to Node.js. It allows you to define a reusable block once and render it multiple times, optionally **inline** for immediate output.
|
|
4
|
+
|
|
5
|
+
## Syntax
|
|
6
|
+
```html
|
|
7
|
+
{% partialdef name [inline] %}
|
|
8
|
+
...template code...
|
|
9
|
+
{% endpartialdef %}
|
|
10
|
+
```
|
|
11
|
+
- `name` – identifier used with `{% partial name %}`.
|
|
12
|
+
- Optional `inline` – if present, the block is rendered **where it is defined**; no separate `{% partial %}` call is required.
|
|
13
|
+
|
|
14
|
+
## Rendering a Partial
|
|
15
|
+
```html
|
|
16
|
+
{% partial greeting %}
|
|
17
|
+
```
|
|
18
|
+
The engine looks up the definition in the current rendering **Context** (`context.partialDefs`) and injects the rendered output.
|
|
19
|
+
|
|
20
|
+
## API Usage
|
|
21
|
+
```js
|
|
22
|
+
const tpl = `{% partialdef api %}API {{ data }}{% endpartialdef %}`;
|
|
23
|
+
const compiled = compile(tpl);
|
|
24
|
+
const out = compiled.renderPartial('api', { data: 123 }); // "API 123"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
- **Full tag parity** – conditionals (`if`), loops (`for`), variable scoping (`with`) work inside a `partialdef`.
|
|
29
|
+
- **Nested partials** – you can define a partial inside another; inner definitions are registered first and can be used by the outer.
|
|
30
|
+
- **Scope isolation** – each rendering of a partial receives its own scope, mirroring Django’s behavior.
|
|
31
|
+
- **Inline rendering** – render inline without an extra `{% partial %}` tag (`{% partialdef foo inline %}…{% endpartialdef %}`).
|
|
32
|
+
- **Performance** – partials are compiled once per template; subsequent renders reuse the compiled AST.
|
|
33
|
+
|
|
34
|
+
## Common Pitfalls
|
|
35
|
+
| Issue | Symptom | Fix |
|
|
36
|
+
|-------|---------|-----|
|
|
37
|
+
| Missing partial name | `{% partial %}` renders nothing | Ensure the name matches a defined `partialdef`. |
|
|
38
|
+
| Variable not found | Appears empty | Variables are resolved in the current context; use `{% with %}` inside the partial if you need a local alias. |
|
|
39
|
+
| Inline vs non‑inline confusion | Duplicate output | Use `inline` only when you want immediate rendering. |
|
|
40
|
+
|
|
41
|
+
> Implementation lives in `src/tags/control.js` (class `PartialDefNode` and `PartialNode`).
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
`miki-template` is built with **secure defaults**. All variables are **auto‑escaped** unless explicitly marked safe.
|
|
4
|
+
|
|
5
|
+
## Auto‑escaping
|
|
6
|
+
- Every string output goes through `escapeHtml` before being concatenated.
|
|
7
|
+
- Use the `|safe` filter or `markSafe(value)` to bypass escaping when you trust the data.
|
|
8
|
+
|
|
9
|
+
## CSRF Protection
|
|
10
|
+
- The `{% csrf_token %}` tag renders a hidden `<input>` containing the `csrf_token` value from the rendering context.
|
|
11
|
+
- Example:
|
|
12
|
+
```html
|
|
13
|
+
<form method="post">{% csrf_token %} ... </form>
|
|
14
|
+
```
|
|
15
|
+
- It is a thin wrapper; you must generate and store `csrf_token` in your Express middleware.
|
|
16
|
+
|
|
17
|
+
## CSP Nonce
|
|
18
|
+
- `{% csp_nonce_attr %}` injects `nonce="{{ csp_nonce }}"` when `csp_nonce` is present in the context.
|
|
19
|
+
- Useful for inline scripts when you have a CSP policy with `script-src 'nonce-...';`.
|
|
20
|
+
|
|
21
|
+
## SafeString Wrapper
|
|
22
|
+
- Filters returning `SafeString` bypass auto‑escaping. The wrapper is applied automatically by the `safe` filter.
|
|
23
|
+
|
|
24
|
+
## No `eval`
|
|
25
|
+
- Template expressions are parsed into an AST and evaluated using a sandboxed evaluator that **never calls `eval` or `new Function`**.
|
|
26
|
+
|
|
27
|
+
> Security‑related code lives in `src/security.js` and the tag implementations in `src/tags/control.js`.
|
package/docs/tags.md
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# Tags Reference
|
|
2
|
+
|
|
3
|
+
This document provides a detailed reference for every built-in block tag in **miki-template**, grouped by function.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Control Flow Tags
|
|
8
|
+
|
|
9
|
+
### `{% if %} / {% elif %} / {% else %} / {% endif %}`
|
|
10
|
+
|
|
11
|
+
Conditionally renders content based on an expression.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
{% if user.is_authenticated %}
|
|
15
|
+
<p>Hello, {{ user.name }}!</p>
|
|
16
|
+
{% elif user.is_guest %}
|
|
17
|
+
<p>Welcome, guest!</p>
|
|
18
|
+
{% else %}
|
|
19
|
+
<p>Please log in.</p>
|
|
20
|
+
{% endif %}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Supported operators:**
|
|
24
|
+
|
|
25
|
+
| Operator | Meaning |
|
|
26
|
+
|----------|---------|
|
|
27
|
+
| `==` | Equal |
|
|
28
|
+
| `!=` | Not equal |
|
|
29
|
+
| `<` | Less than |
|
|
30
|
+
| `<=` | Less than or equal |
|
|
31
|
+
| `>` | Greater than |
|
|
32
|
+
| `>=` | Greater than or equal |
|
|
33
|
+
| `in` | Membership (item in list) |
|
|
34
|
+
| `not in` | Non-membership |
|
|
35
|
+
| `and` | Logical AND |
|
|
36
|
+
| `or` | Logical OR |
|
|
37
|
+
| `not` | Logical NOT |
|
|
38
|
+
|
|
39
|
+
**Operator precedence** (highest to lowest): comparison → `and` → `or`
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
{% if user.age >= 18 and user.is_verified %}
|
|
43
|
+
<p>Eligible to vote.</p>
|
|
44
|
+
{% endif %}
|
|
45
|
+
|
|
46
|
+
{% if item not in cart %}
|
|
47
|
+
<button>Add to cart</button>
|
|
48
|
+
{% endif %}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### `{% for %} / {% empty %} / {% endfor %}`
|
|
54
|
+
|
|
55
|
+
Iterates over arrays or objects.
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<ul>
|
|
59
|
+
{% for user in users %}
|
|
60
|
+
<li>{{ user.name }}</li>
|
|
61
|
+
{% empty %}
|
|
62
|
+
<li>No users found.</li>
|
|
63
|
+
{% endfor %}
|
|
64
|
+
</ul>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Object iteration** — unpacks key and value:
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
{% for key, value in config %}
|
|
71
|
+
<dt>{{ key }}</dt>
|
|
72
|
+
<dd>{{ value }}</dd>
|
|
73
|
+
{% endfor %}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Tuple unpacking** — unpacks index and item:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
{% for item, index in items %}
|
|
80
|
+
{{ forloop.counter }}. {{ item }}
|
|
81
|
+
{% endfor %}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Loop metadata** — `forloop` object is available inside the loop:
|
|
85
|
+
|
|
86
|
+
| Property | Type | Description |
|
|
87
|
+
|----------|------|-------------|
|
|
88
|
+
| `forloop.counter` | integer | Current iteration (1-indexed) |
|
|
89
|
+
| `forloop.counter0` | integer | Current iteration (0-indexed) |
|
|
90
|
+
| `forloop.revcounter` | integer | Iterations remaining (counting down from 1) |
|
|
91
|
+
| `forloop.revcounter0` | integer | Iterations remaining (counting down from 0) |
|
|
92
|
+
| `forloop.first` | boolean | True on first iteration |
|
|
93
|
+
| `forloop.last` | boolean | True on last iteration |
|
|
94
|
+
| `forloop.parentloop` | object | Reference to parent loop's `forloop` |
|
|
95
|
+
|
|
96
|
+
**Nested loops example:**
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
{% for category in categories %}
|
|
100
|
+
<h2>{{ category.name }}</h2>
|
|
101
|
+
{% for product in category.products %}
|
|
102
|
+
{# forloop.counter = position in category #}
|
|
103
|
+
{# forloop.parentloop.counter = position in categories #}
|
|
104
|
+
<p>{{ forloop.parentloop.counter }}.{{ forloop.counter }}: {{ product }}</p>
|
|
105
|
+
{% endfor %}
|
|
106
|
+
{% endfor %}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `{% with %} / {% endwith %}`
|
|
112
|
+
|
|
113
|
+
Creates scoped aliases for variables or expressions.
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
{% with user.profile as profile %}
|
|
117
|
+
<img src="{{ profile.avatar }}">
|
|
118
|
+
<a href="{{ profile.url }}">{{ profile.display_name }}</a>
|
|
119
|
+
{% endwith %}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Multiple assignments** (Django-style):
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
{% with a=1 b=items.0.name c="static" %}
|
|
126
|
+
{{ a }} | {{ b }} | {{ c }}
|
|
127
|
+
{% endwith %}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
You can also unpack tuple-like values:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
{% with key, value in item %}
|
|
134
|
+
<li>{{ key }}: {{ value }}</li>
|
|
135
|
+
{% endwith %}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `{% cycle %}`
|
|
141
|
+
|
|
142
|
+
Outputs one of its arguments for each iteration of a loop.
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
{% for row in rows %}
|
|
146
|
+
<tr class="{% cycle 'row-even' 'row-odd' %}">
|
|
147
|
+
<td>{{ row.name }}</td>
|
|
148
|
+
</tr>
|
|
149
|
+
{% endfor %}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Named cycle for resumable state:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
{% for item in items %}
|
|
156
|
+
{% cycle 'a' 'b' 'c' as marker silent %}
|
|
157
|
+
{% if marker == 'b' %}
|
|
158
|
+
<strong>{{ item }}</strong>
|
|
159
|
+
{% else %}
|
|
160
|
+
{{ item }}
|
|
161
|
+
{% endif %}
|
|
162
|
+
{% endfor %}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `{% firstof %}`
|
|
168
|
+
|
|
169
|
+
Outputs the first argument that evaluates to `true`.
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
{% firstof user.display_name user.username "Anonymous" %}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
With `{% else %}` for a fallback:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
{% firstof user.display_name user.username %}
|
|
179
|
+
{{ firstof_output }}
|
|
180
|
+
{% else %}
|
|
181
|
+
Anonymous
|
|
182
|
+
{% endif %}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Template Inheritance Tags
|
|
188
|
+
|
|
189
|
+
### `{% extends %}`
|
|
190
|
+
|
|
191
|
+
Must be the first tag in a child template. Specifies the parent template.
|
|
192
|
+
|
|
193
|
+
```html
|
|
194
|
+
{% extends "base.html" %}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Can use expressions for dynamic parent (e.g., mobile vs desktop):
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
{% extends device|default:"base.html" %}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Security:** Path traversal is blocked — the template name must resolve within the configured `views` directories.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### `{% block %} / {% endblock %}`
|
|
208
|
+
|
|
209
|
+
Defines a replaceable section that child templates can override.
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<!-- base.html -->
|
|
213
|
+
{% block content %}
|
|
214
|
+
Default content
|
|
215
|
+
{% endblock %}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```html
|
|
219
|
+
<!-- child.html -->
|
|
220
|
+
{% extends "base.html" %}
|
|
221
|
+
{% block content %}
|
|
222
|
+
Overridden content
|
|
223
|
+
{% endblock %}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**`{{ block.super }}`** — renders the parent template's block content within an override:
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
{% block content %}
|
|
230
|
+
{{ block.super }}
|
|
231
|
+
<p>Additional content from child</p>
|
|
232
|
+
{% endblock %}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### `{% block.super %}`
|
|
238
|
+
|
|
239
|
+
A special variable, not a tag. When used inside a `{% block %}`, it renders the parent template's version of that block.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Include and Partial Tags
|
|
244
|
+
|
|
245
|
+
### `{% include %}`
|
|
246
|
+
|
|
247
|
+
Includes another template file at render time. The included template gets a copy of the current context.
|
|
248
|
+
|
|
249
|
+
```html
|
|
250
|
+
{% include "header.html" %}
|
|
251
|
+
{% include "sidebar.html" with active_section="home" %}
|
|
252
|
+
{% include "footer.html" without context %}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Security:** Path traversal is blocked to prevent reading arbitrary files outside the views directory.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### `{% partialdef %} / {% endpartialdef %}`
|
|
260
|
+
|
|
261
|
+
Defines a reusable fragment that can be rendered later via `{% partial %}`.
|
|
262
|
+
|
|
263
|
+
```html
|
|
264
|
+
{% partialdef card %}
|
|
265
|
+
<div class="card">
|
|
266
|
+
<h3>{{ title }}</h3>
|
|
267
|
+
<p>{{ description }}</p>
|
|
268
|
+
</div>
|
|
269
|
+
{% endpartialdef %}
|
|
270
|
+
|
|
271
|
+
{% partial card with title="Hello" description="World" %}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Options:**
|
|
275
|
+
|
|
276
|
+
| Option | Description |
|
|
277
|
+
|--------|-------------|
|
|
278
|
+
| `inline` | Renders the definition inline at its location during parse. |
|
|
279
|
+
| `lazy` | Defers parsing until first use (default is eager parsing). |
|
|
280
|
+
|
|
281
|
+
**Programmatic access:**
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
const compiled = compile(template);
|
|
285
|
+
compiled.renderPartial('card', { title: 'Hi', description: 'There' });
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### `{% partial %}`
|
|
291
|
+
|
|
292
|
+
Renders a previously defined partial.
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
{% partial card %}
|
|
296
|
+
{% partial card with title="Custom" %}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Supports passing context variables:
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
{% partial greeting with name=user.name %}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Utility Tags
|
|
308
|
+
|
|
309
|
+
### `{% comment %} / {% endcomment %}`
|
|
310
|
+
|
|
311
|
+
Block comment that is stripped from the output entirely.
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
{% comment %}
|
|
315
|
+
This section is deprecated.
|
|
316
|
+
It will be removed in the next release.
|
|
317
|
+
{% endcomment %}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Short form (single tag, self-closing):
|
|
321
|
+
|
|
322
|
+
```html
|
|
323
|
+
{% comment %} This will not appear in output {% endcomment %}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### `{% verbatim %} / {% endverbatim %}`
|
|
329
|
+
|
|
330
|
+
Prevents all tag/variable parsing inside the block.
|
|
331
|
+
|
|
332
|
+
```html
|
|
333
|
+
{% verbatim %}
|
|
334
|
+
{{ this_is_not_a_variable }}
|
|
335
|
+
{% if this_is_not_a_tag %}Ignored{% endif %}
|
|
336
|
+
{% endverbatim %}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### `{% load %}`
|
|
342
|
+
|
|
343
|
+
Loads additional filter libraries (for future extensibility).
|
|
344
|
+
|
|
345
|
+
```html
|
|
346
|
+
{% load i18n %}
|
|
347
|
+
{% load custom_filters %}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### `{% spaceless %} / {% endspaceless %}`
|
|
353
|
+
|
|
354
|
+
Removes whitespace between HTML tags.
|
|
355
|
+
|
|
356
|
+
```html
|
|
357
|
+
{% spaceless %}
|
|
358
|
+
<div> <p>Hello</p> </div>
|
|
359
|
+
{% endspaceless %}
|
|
360
|
+
<!-- → <div><p>Hello</p></div> -->
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### `{% static %}`
|
|
366
|
+
|
|
367
|
+
Generates the URL for a static asset.
|
|
368
|
+
|
|
369
|
+
```html
|
|
370
|
+
<img src="{% static "css/app.css" %}">
|
|
371
|
+
<script src="{% static "js/bundle.js" %}"></script>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Configure the prefix:
|
|
375
|
+
```javascript
|
|
376
|
+
compile(template, { staticUrl: '/assets/' });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
### `{% url %}`
|
|
382
|
+
|
|
383
|
+
Generates a URL for a named route using the provided `urlHelper` function.
|
|
384
|
+
|
|
385
|
+
```html
|
|
386
|
+
<a href="{% url "home" %}">Home</a>
|
|
387
|
+
<a href="{% url "user-profile" user.id %}">Profile</a>
|
|
388
|
+
<a href="{% url "search" query=search_query %}">Search</a>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Configure:
|
|
392
|
+
```javascript
|
|
393
|
+
compile(template, {
|
|
394
|
+
urlHelper: (name, params, kwargs) => {
|
|
395
|
+
// return resolved URL string
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### `{% csrf_token %}`
|
|
403
|
+
|
|
404
|
+
Outputs a CSRF token hidden input for forms.
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<form method="post">
|
|
408
|
+
{% csrf_token %}
|
|
409
|
+
<input type="text" name="title">
|
|
410
|
+
<button type="submit">Submit</button>
|
|
411
|
+
</form>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Provide `csrf_token` in context:
|
|
415
|
+
```javascript
|
|
416
|
+
res.render('form', { csrf_token: req.csrfToken() });
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
### `{% csp_nonce_attr %}`
|
|
422
|
+
|
|
423
|
+
Outputs a `nonce="..."` attribute for Content Security Policy.
|
|
424
|
+
|
|
425
|
+
```html
|
|
426
|
+
<script {% csp_nonce_attr %}>
|
|
427
|
+
console.log('CSP nonce');
|
|
428
|
+
</script>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Provide `csp_nonce` in context:
|
|
432
|
+
```javascript
|
|
433
|
+
res.render('page', { csp_nonce: req.nonce });
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### `{% regroup %}`
|
|
439
|
+
|
|
440
|
+
Regroups a list by a common attribute.
|
|
441
|
+
|
|
442
|
+
```html
|
|
443
|
+
{% regroup users by department as departments %}
|
|
444
|
+
{% for dept in departments %}
|
|
445
|
+
<h3>{{ dept.grouper }}</h3>
|
|
446
|
+
{% for user in dept.list %}
|
|
447
|
+
<p>{{ user.name }}</p>
|
|
448
|
+
{% endfor %}
|
|
449
|
+
{% endfor %}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### `{% trans "key" %}`
|
|
455
|
+
|
|
456
|
+
Outputs a translated string from the i18n registry.
|
|
457
|
+
|
|
458
|
+
```html
|
|
459
|
+
{% trans "Hello, World!" %}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
With arguments:
|
|
463
|
+
|
|
464
|
+
```html
|
|
465
|
+
{% trans "Hello, %s!" name=user.name %}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
With context:
|
|
469
|
+
|
|
470
|
+
```html
|
|
471
|
+
{% trans context "verb" "He runs" %}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### `{% blocktrans %}...{% endblocktrans %}`
|
|
477
|
+
|
|
478
|
+
Translates a block of text. Supports `{% with name=value %}` and `{% plural count name=value %}`.
|
|
479
|
+
|
|
480
|
+
```html
|
|
481
|
+
{% blocktrans with name=user.name count items|length %}
|
|
482
|
+
{{ name }} has {{ items|length }} item.
|
|
483
|
+
{% plural %}
|
|
484
|
+
{{ name }} has {{ items|length }} items.
|
|
485
|
+
{% endblocktrans %}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
### `{% language "xx" %}...{% endlanguage %}`
|
|
491
|
+
|
|
492
|
+
Switches the active language for the enclosed block.
|
|
493
|
+
|
|
494
|
+
```html
|
|
495
|
+
{% language "fr" %}
|
|
496
|
+
{% trans "Welcome" %}
|
|
497
|
+
{% endlanguage %}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
### `{% widthratio value max max_width %}`
|
|
503
|
+
|
|
504
|
+
Calculates a proportional width, commonly used for bar charts or progress indicators.
|
|
505
|
+
|
|
506
|
+
```html
|
|
507
|
+
{% widthratio 25 100 150 %} <!-- → 37 (floor of 25/100*150) -->
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
### `{% debug %}`
|
|
513
|
+
|
|
514
|
+
Dumps the current template context as a formatted HTML `<pre>` block. Useful during development.
|
|
515
|
+
|
|
516
|
+
```html
|
|
517
|
+
<pre>
|
|
518
|
+
{% debug %}
|
|
519
|
+
</pre>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
### `{% load library1 library2 %}`
|
|
525
|
+
|
|
526
|
+
Loads one or more plugin libraries, making their tags, filters, and helpers available.
|
|
527
|
+
|
|
528
|
+
```html
|
|
529
|
+
{% load i18n humanize cache %}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Built-in libraries:
|
|
533
|
+
- `i18n` — `trans`, `blocktrans`, `language`
|
|
534
|
+
- `humanize` — `intcomma`, `intword`, `apnumber`, `ordinal`, `naturalday`
|
|
535
|
+
- `cache` — `{% cache timeout key %}...{% endcache %}`
|
|
536
|
+
- `lorem` — `lorem` filter for placeholder text
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Custom Filters
|
|
541
|
+
|
|
542
|
+
Register custom filters with `registerFilter`:
|
|
543
|
+
|
|
544
|
+
```javascript
|
|
545
|
+
const { registerFilter } = require('miki-template');
|
|
546
|
+
|
|
547
|
+
// Simple filter
|
|
548
|
+
registerFilter('reverse', (val) => String(val).split('').reverse().join(''));
|
|
549
|
+
|
|
550
|
+
// Filter with argument
|
|
551
|
+
registerFilter('truncate', (val, length) => {
|
|
552
|
+
const str = String(val);
|
|
553
|
+
if (str.length <= length) return str;
|
|
554
|
+
return str.slice(0, length) + '...';
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Chaining works automatically:
|
|
558
|
+
// {{ name|reverse|truncate:5 }}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Custom Tags
|
|
564
|
+
|
|
565
|
+
Register custom block tags with `registerTag`:
|
|
566
|
+
|
|
567
|
+
```javascript
|
|
568
|
+
const { registerTag } = require('miki-template');
|
|
569
|
+
|
|
570
|
+
registerTag('markdown', (tagContent, parser) => {
|
|
571
|
+
const body = parser.parse(['endmarkdown']);
|
|
572
|
+
const next = parser.peek();
|
|
573
|
+
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endmarkdown') {
|
|
574
|
+
parser.advance();
|
|
575
|
+
}
|
|
576
|
+
const md = require('markdown-it')();
|
|
577
|
+
return {
|
|
578
|
+
render(context) {
|
|
579
|
+
const html = body.map(n => n.render(context)).join('');
|
|
580
|
+
return md.render(html);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Usage in templates:
|
|
587
|
+
```html
|
|
588
|
+
{% markdown %}
|
|
589
|
+
# Hello World
|
|
590
|
+
{% endmarkdown %}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Filter Argument Types
|
|
596
|
+
|
|
597
|
+
Filters accept the following argument types:
|
|
598
|
+
|
|
599
|
+
| Syntax | Type | Example |
|
|
600
|
+
|--------|------|---------|
|
|
601
|
+
| Unquoted | Variable lookup | `{{ value|filter:count }}` |
|
|
602
|
+
| Double-quoted | String literal | `{{ value|filter:"hello" }}` |
|
|
603
|
+
| Single-quoted | String literal | `{{ value|filter:'world' }}` |
|
|
604
|
+
| Number | Integer literal | `{{ value|truncatewords:10 }}` |
|
|
605
|
+
|
|
606
|
+
```html
|
|
607
|
+
{{ user.name|default:"Guest" }} <!-- String default -->
|
|
608
|
+
{{ items|slice:"1:3" }} <!-- Slice notation -->
|
|
609
|
+
{{ price|floatformat:2 }} <!-- Decimal places -->
|
|
610
|
+
```
|