miki-template 2.0.1 → 2.2.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/.github/workflows/ci.yml +13 -37
- package/.github/workflows/docs.yml +105 -0
- package/.github/workflows/npm-publish-github-packages.yml +36 -0
- package/README.md +69 -14
- package/assets/logo.png +0 -0
- package/benchmarks/ejs-results.json +4 -4
- package/benchmarks/handlebars-results.json +6 -6
- package/benchmarks/miki-results.json +4 -4
- package/benchmarks/pug-results.json +4 -4
- package/benchmarks/stress.mjs +1 -1
- package/docs/api/async-render.md +85 -0
- package/docs/api/cache.md +87 -0
- package/docs/api/compile.md +128 -0
- package/docs/api/context-processors.md +77 -0
- package/docs/api/filters.md +217 -0
- package/docs/api/finder.md +94 -0
- package/docs/api/helpers.md +53 -0
- package/docs/api/i18n.md +157 -0
- package/docs/api/index.md +54 -0
- package/docs/api/libraries.md +207 -0
- package/docs/api/render-partial.md +81 -0
- package/docs/api/render.md +92 -0
- package/docs/api/security.md +145 -0
- package/docs/api/setup-express.md +76 -0
- package/docs/api/tags.md +134 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/guide/advanced-usage.md +397 -0
- package/docs/guide/async-rendering.md +308 -0
- package/docs/guide/context-processors.md +257 -0
- package/docs/guide/custom-filters.md +311 -0
- package/docs/guide/custom-tags.md +271 -0
- package/docs/guide/filters.md +642 -0
- package/docs/guide/getting-started.md +102 -0
- package/docs/guide/installation.md +95 -0
- package/docs/guide/partial-templates.md +367 -0
- package/docs/guide/quick-start.md +222 -0
- package/docs/guide/security.md +345 -0
- package/docs/guide/tags.md +783 -0
- package/docs/guide/template-discovery.md +170 -0
- package/docs/guide/template-inheritance.md +273 -0
- package/docs/guide/what-is-miki-template.md +28 -0
- package/docs/guide/why-miki-template.md +75 -0
- package/docs/index.md +104 -0
- package/docs/integrations/elysia.md +78 -0
- package/docs/integrations/express.md +219 -0
- package/docs/integrations/fastify.md +77 -0
- package/docs/integrations/hono.md +78 -0
- package/docs/integrations/index.md +68 -0
- package/docs/integrations/koa.md +88 -0
- package/docs/integrations/nestjs.md +78 -0
- package/docs/integrations/tsed.md +81 -0
- package/docs/javascripts/extra.js +174 -0
- package/docs/performance.md +37 -0
- package/docs/stylesheets/extra.css +819 -0
- package/mkdocs.yml +217 -0
- package/overrides/main.html +26 -0
- package/overrides/partials/footer.html +9 -0
- package/package.json +4 -2
- package/requirements-docs.txt +1 -0
- package/docs/README.md +0 -18
- package/docs/advanced_usage.md +0 -71
- package/docs/api.md +0 -122
- package/docs/filters.md +0 -708
- package/docs/installation.md +0 -106
- package/docs/integrations.md +0 -214
- package/docs/overview.md +0 -79
- package/docs/partialdef.md +0 -70
- package/docs/security.md +0 -27
- package/docs/tags.md +0 -673
- package/docs/usage.md +0 -646
package/docs/usage.md
DELETED
|
@@ -1,646 +0,0 @@
|
|
|
1
|
-
# Usage Guide
|
|
2
|
-
|
|
3
|
-
This guide covers all usage patterns for **miki-template**, from basic variable rendering to advanced Express integration.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Quick Reference
|
|
8
|
-
|
|
9
|
-
### One-off Rendering
|
|
10
|
-
|
|
11
|
-
**CommonJS:**
|
|
12
|
-
```javascript
|
|
13
|
-
const { render } = require('miki-template');
|
|
14
|
-
|
|
15
|
-
const output = render('Hello {{ name }}!', { name: 'World' });
|
|
16
|
-
// → "Hello World!"
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
**ESM:**
|
|
20
|
-
```javascript
|
|
21
|
-
import { render } from 'miki-template';
|
|
22
|
-
|
|
23
|
-
const output = render('Hello {{ name }}!', { name: 'World' });
|
|
24
|
-
// → "Hello World!"
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Compiled Templates (recommended for repeated use)
|
|
28
|
-
|
|
29
|
-
**CommonJS:**
|
|
30
|
-
```javascript
|
|
31
|
-
const { compile } = require('miki-template');
|
|
32
|
-
|
|
33
|
-
const template = compile('Welcome, {{ user.name }}!');
|
|
34
|
-
|
|
35
|
-
// Render 1
|
|
36
|
-
console.log(template.render({ user: { name: 'Alice' } }));
|
|
37
|
-
// → "Welcome, Alice!"
|
|
38
|
-
|
|
39
|
-
// Render 2
|
|
40
|
-
console.log(template.render({ user: { name: 'Bob' } }));
|
|
41
|
-
// → "Welcome, Bob!"
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**ESM:**
|
|
45
|
-
```javascript
|
|
46
|
-
import { compile } from 'miki-template';
|
|
47
|
-
|
|
48
|
-
const template = compile('Welcome, {{ user.name }}!');
|
|
49
|
-
|
|
50
|
-
console.log(template.render({ user: { name: 'Alice' } }));
|
|
51
|
-
// → "Welcome, Alice!"
|
|
52
|
-
|
|
53
|
-
console.log(template.render({ user: { name: 'Bob' } }));
|
|
54
|
-
// → "Welcome, Bob!"
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## Variables and Lookups
|
|
60
|
-
|
|
61
|
-
### Basic Variables
|
|
62
|
-
|
|
63
|
-
```html
|
|
64
|
-
<p>Hello, {{ name }}!</p>
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Dotted Lookups (nested properties)
|
|
68
|
-
|
|
69
|
-
```html
|
|
70
|
-
<p>{{ user.profile.displayName }}</p>
|
|
71
|
-
<p>{{ config.site.title }}</p>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Array Indexing
|
|
75
|
-
|
|
76
|
-
```html
|
|
77
|
-
<p>First item: {{ items.0 }}</p>
|
|
78
|
-
<p>Third item: {{ items.2 }}</p>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Function Call
|
|
82
|
-
|
|
83
|
-
If a resolved value is a function, it is called automatically with zero arguments:
|
|
84
|
-
|
|
85
|
-
```javascript
|
|
86
|
-
// Context: { user: { getName: () => 'Miki' } }
|
|
87
|
-
{{ user.getName }} // → "Miki"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Filters
|
|
93
|
-
|
|
94
|
-
Filters transform variable output. Apply them with the pipe `|` character:
|
|
95
|
-
|
|
96
|
-
```html
|
|
97
|
-
{{ name|upper }} → "MIKI"
|
|
98
|
-
{{ title|slugify }} → "hello-world"
|
|
99
|
-
{{ text|truncatewords:20 }} → truncated to 20 words
|
|
100
|
-
{{ date|date:"Y-m-d" }} → "2026-08-31"
|
|
101
|
-
{{ user.name|default:"Anonymous" }} → "Miki" or "Anonymous"
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Filter Chaining
|
|
105
|
-
|
|
106
|
-
Filters apply left-to-right:
|
|
107
|
-
|
|
108
|
-
```html
|
|
109
|
-
{{ name|lower|capfirst }} → "miki" → "Miki"
|
|
110
|
-
{{ bio|striptags|truncatewords:50 }} → strip HTML, then truncate
|
|
111
|
-
{{ price|floatformat:2|add:10 }} → format, then add 10
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Filter Arguments
|
|
115
|
-
|
|
116
|
-
Most filters accept optional arguments after a colon:
|
|
117
|
-
|
|
118
|
-
```html
|
|
119
|
-
{{ items|join:", " }} → "a, b, c"
|
|
120
|
-
{{ text|truncatewords:10 }} → 10 words max
|
|
121
|
-
{{ date|date:"F j, Y" }} → "August 31, 2026"
|
|
122
|
-
{{ value|default:"N/A" }} → fallback if falsy
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## Control Flow Tags
|
|
128
|
-
|
|
129
|
-
### `{% if %} / {% elif %} / {% else %} / {% endif %}`
|
|
130
|
-
|
|
131
|
-
```html
|
|
132
|
-
{% if user.is_active %}
|
|
133
|
-
<p>Welcome back!</p>
|
|
134
|
-
{% elif user.is_pending %}
|
|
135
|
-
<p>Please verify your email.</p>
|
|
136
|
-
{% else %}
|
|
137
|
-
<p>Contact support.</p>
|
|
138
|
-
{% endif %}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Supported operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `and`, `or`, `not`, `in`, `not in`
|
|
142
|
-
|
|
143
|
-
```html
|
|
144
|
-
{% if user.role == 'admin' or user.is_staff %}
|
|
145
|
-
<a href="/admin">Admin Panel</a>
|
|
146
|
-
{% endif %}
|
|
147
|
-
|
|
148
|
-
{% if item in cart_items %}
|
|
149
|
-
<span>In cart</span>
|
|
150
|
-
{% endif %}
|
|
151
|
-
|
|
152
|
-
{% if not user.is_banned %}
|
|
153
|
-
<p>You may post.</p>
|
|
154
|
-
{% endif %}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### `{% for %} / {% empty %} / {% endfor %}`
|
|
158
|
-
|
|
159
|
-
Loop over arrays:
|
|
160
|
-
|
|
161
|
-
```html
|
|
162
|
-
<ul>
|
|
163
|
-
{% for item in items %}
|
|
164
|
-
<li>{{ item }}</li>
|
|
165
|
-
{% empty %}
|
|
166
|
-
<li>No items found.</li>
|
|
167
|
-
{% endfor %}
|
|
168
|
-
</ul>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Loop with unpacking (arrays):
|
|
172
|
-
|
|
173
|
-
```html
|
|
174
|
-
{% for name, index in items %}
|
|
175
|
-
{{ forloop.counter }}. {{ name }}
|
|
176
|
-
{% endfor %}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Loop over objects (key, value):
|
|
180
|
-
|
|
181
|
-
```html
|
|
182
|
-
{% for key, value in config %}
|
|
183
|
-
<tr>
|
|
184
|
-
<td>{{ key }}</td>
|
|
185
|
-
<td>{{ value }}</td>
|
|
186
|
-
</tr>
|
|
187
|
-
{% endfor %}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Loop metadata (`forloop`):
|
|
191
|
-
|
|
192
|
-
```html
|
|
193
|
-
{% for item in items %}
|
|
194
|
-
{% if forloop.first %}<ul>{% endif %}
|
|
195
|
-
<li>{% if forloop.last %}last!{% else %}{{ item }}{% endif %}</li>
|
|
196
|
-
{% if forloop.last %}</ul>{% endif %}
|
|
197
|
-
{% endfor %}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Available `forloop` properties:
|
|
201
|
-
| Property | Description |
|
|
202
|
-
|----------|-------------|
|
|
203
|
-
| `forloop.counter` | 1-indexed position |
|
|
204
|
-
| `forloop.counter0` | 0-indexed position |
|
|
205
|
-
| `forloop.revcounter` | Countdown from end (1-indexed) |
|
|
206
|
-
| `forloop.revcounter0` | Countdown from end (0-indexed) |
|
|
207
|
-
| `forloop.first` | `true` on first iteration |
|
|
208
|
-
| `forloop.last` | `true` on last iteration |
|
|
209
|
-
| `forloop.parentloop` | Reference to parent loop's metadata |
|
|
210
|
-
|
|
211
|
-
Nested loops:
|
|
212
|
-
|
|
213
|
-
```html
|
|
214
|
-
{% for group in groups %}
|
|
215
|
-
{% for item in group.items %}
|
|
216
|
-
{{ forloop.parentloop.counter }}.{{ forloop.counter }}: {{ item }}
|
|
217
|
-
{% endfor %}
|
|
218
|
-
{% endfor %}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### `{% with %} / {% endwith %}`
|
|
222
|
-
|
|
223
|
-
Create scoped aliases:
|
|
224
|
-
|
|
225
|
-
```html
|
|
226
|
-
{% with user.profile.address as addr %}
|
|
227
|
-
<p>{{ addr.city }}, {{ addr.country }}</p>
|
|
228
|
-
{% endwith %}
|
|
229
|
-
|
|
230
|
-
{% with a=x b=y c=z %}
|
|
231
|
-
{{ a }} + {{ b }} + {{ c }}
|
|
232
|
-
{% endwith %}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### `{% cycle %}`
|
|
236
|
-
|
|
237
|
-
Cycle through values on each iteration:
|
|
238
|
-
|
|
239
|
-
```html
|
|
240
|
-
{% for row in rows %}
|
|
241
|
-
<tr class="{% cycle 'row-even' 'row-odd' %}">
|
|
242
|
-
<td>{{ row.name }}</td>
|
|
243
|
-
</tr>
|
|
244
|
-
{% endfor %}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Cycle with named state:
|
|
248
|
-
|
|
249
|
-
```html
|
|
250
|
-
{% for item in items %}
|
|
251
|
-
{% cycle 'row1' 'row2' as row_class %}
|
|
252
|
-
<tr class="{{ row_class }}">{{ item }}</tr>
|
|
253
|
-
{% endfor %}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### `{% firstof %}`
|
|
257
|
-
|
|
258
|
-
Return the first truthy value:
|
|
259
|
-
|
|
260
|
-
```html
|
|
261
|
-
{% firstof user.display_name user.username "Guest" %}
|
|
262
|
-
<!-- Returns first non-falsy value -->
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
---
|
|
266
|
-
|
|
267
|
-
## Template Inheritance
|
|
268
|
-
|
|
269
|
-
### Base Template
|
|
270
|
-
|
|
271
|
-
```html
|
|
272
|
-
<!-- base.html -->
|
|
273
|
-
<html>
|
|
274
|
-
<head>
|
|
275
|
-
<title>{% block title %}Default Title{% endblock %}</title>
|
|
276
|
-
{% block extra_head %}{% endblock %}
|
|
277
|
-
</head>
|
|
278
|
-
<body>
|
|
279
|
-
<header>{% block header %}Site Header{% endblock %}</header>
|
|
280
|
-
<main>{% block content %}{% endblock %}</main>
|
|
281
|
-
<footer>{% block footer %}{% endblock %}</footer>
|
|
282
|
-
</body>
|
|
283
|
-
</html>
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Child Template
|
|
287
|
-
|
|
288
|
-
```html
|
|
289
|
-
<!-- home.html -->
|
|
290
|
-
{% extends "base.html" %}
|
|
291
|
-
|
|
292
|
-
{% block title %}Home Page{% endblock %}
|
|
293
|
-
|
|
294
|
-
{% block content %}
|
|
295
|
-
<h1>Welcome!</h1>
|
|
296
|
-
{{ block.super }} <!-- renders parent's block content -->
|
|
297
|
-
{% endblock %}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
`{{ block.super }}` renders the parent template's block content within the override.
|
|
301
|
-
|
|
302
|
-
### Multi-level Inheritance
|
|
303
|
-
|
|
304
|
-
```
|
|
305
|
-
base.html
|
|
306
|
-
└── base_blog.html {% extends "base.html" %}
|
|
307
|
-
└── post.html {% extends "base_blog.html" %}
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## Include and Partials
|
|
313
|
-
|
|
314
|
-
### `{% include %}`
|
|
315
|
-
|
|
316
|
-
Include another template file:
|
|
317
|
-
|
|
318
|
-
```html
|
|
319
|
-
{% include "header.html" %}
|
|
320
|
-
{% include "sidebar.html" with active="home" %}
|
|
321
|
-
{% include user.theme|add:".html" %} <!-- dynamic template name -->
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
Path traversal is blocked for security.
|
|
325
|
-
|
|
326
|
-
### `{% partialdef %} / {% partial %}`
|
|
327
|
-
|
|
328
|
-
Define and render reusable partial snippets within a template:
|
|
329
|
-
|
|
330
|
-
```html
|
|
331
|
-
{% partialdef card %}
|
|
332
|
-
<div class="card">
|
|
333
|
-
<h3>{{ title }}</h3>
|
|
334
|
-
<p>{{ description }}</p>
|
|
335
|
-
</div>
|
|
336
|
-
{% endpartialdef %}
|
|
337
|
-
|
|
338
|
-
{% partial card with title="Hello" description="World" %}
|
|
339
|
-
{% partial card with title="Foo" description="Bar" %}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
Inline partials render immediately:
|
|
343
|
-
|
|
344
|
-
```html
|
|
345
|
-
{% partialdef greeting inline %}
|
|
346
|
-
Hello {{ name }}!
|
|
347
|
-
{% endpartialdef %}
|
|
348
|
-
<!-- Output: "Hello !" (name not yet defined) -->
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Programmatic Partial Rendering
|
|
352
|
-
|
|
353
|
-
```javascript
|
|
354
|
-
const { compile } = require('miki-template');
|
|
355
|
-
|
|
356
|
-
const template = `{% partialdef my_partial %}Hello {{ who }}!{% endpartialdef %}`;
|
|
357
|
-
const compiled = compile(template);
|
|
358
|
-
|
|
359
|
-
console.log(compiled.renderPartial('my_partial', { who: 'World' }));
|
|
360
|
-
// → "Hello World!"
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
## Block Partial Rendering (HTMX / AJAX)
|
|
366
|
-
|
|
367
|
-
Render a specific block from a compiled template for AJAX responses:
|
|
368
|
-
|
|
369
|
-
```javascript
|
|
370
|
-
const { compile } = require('miki-template');
|
|
371
|
-
|
|
372
|
-
const template = compile(`
|
|
373
|
-
{% extends "base.html" %}
|
|
374
|
-
{% block main %}
|
|
375
|
-
<h1>{{ title }}</h1>
|
|
376
|
-
<div class="content">{{ content }}</div>
|
|
377
|
-
{% endblock %}
|
|
378
|
-
`, { views: './templates' });
|
|
379
|
-
|
|
380
|
-
// Full page render
|
|
381
|
-
res.send(template.render({ title: 'Home', content: '...' }));
|
|
382
|
-
|
|
383
|
-
// Partial render — only the 'main' block
|
|
384
|
-
res.send(template.renderBlock('main', { title: 'Home', content: '...' }));
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
---
|
|
388
|
-
|
|
389
|
-
## Async Rendering
|
|
390
|
-
|
|
391
|
-
For templates with async helpers (database lookups, API calls):
|
|
392
|
-
|
|
393
|
-
```javascript
|
|
394
|
-
const { asyncRender, registerHelper } = require('miki-template');
|
|
395
|
-
|
|
396
|
-
registerHelper('fetch-user', async (content, ctx) => {
|
|
397
|
-
const userId = content.trim();
|
|
398
|
-
const user = await db.users.findById(userId);
|
|
399
|
-
return `User: ${user.name}`;
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// Template: {% fetch-user %}123{% endfetch-user %}
|
|
403
|
-
const html = await asyncRender(template, { db });
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
---
|
|
407
|
-
|
|
408
|
-
## Express Integration
|
|
409
|
-
|
|
410
|
-
### One-Line Setup (recommended)
|
|
411
|
-
|
|
412
|
-
`miki.setupExpress(app, opts)` wires the view engine, the `views` directory, and a `res.render` shim that makes `res.render('view#partial', ...)` return just the named `{% partialdef %}` body — perfect for HTMX.
|
|
413
|
-
|
|
414
|
-
```javascript
|
|
415
|
-
const express = require('express');
|
|
416
|
-
const miki = require('miki-template');
|
|
417
|
-
|
|
418
|
-
const app = express();
|
|
419
|
-
|
|
420
|
-
// That single line: registers the engine, sets views dir, enables #partial selectors.
|
|
421
|
-
miki.setupExpress(app, { extension: 'html', views: './views' });
|
|
422
|
-
|
|
423
|
-
// Full-page render
|
|
424
|
-
app.get('/', (req, res) => res.render('home', { user: req.user }));
|
|
425
|
-
|
|
426
|
-
// HTMX partial response — just append `#partialName` to the view name.
|
|
427
|
-
// Internally this calls the {% partialdef card %} body inside views/home.html.
|
|
428
|
-
app.get('/partials/:name', (req, res) =>
|
|
429
|
-
res.render(`home#${req.params.name}`, { user: req.user })
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
app.listen(3000);
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
Options:
|
|
436
|
-
|
|
437
|
-
| Option | Default | Description |
|
|
438
|
-
|---|---|---|
|
|
439
|
-
| `extension` | `'html'` | File extension for views. Use `'miki'` if you prefer `.miki` files. |
|
|
440
|
-
| `views` | `app.get('views')` | Views directory (passed to `app.set('views', ...)`). |
|
|
441
|
-
| `async` | `false` | Use the async engine (`__expressAsync`). For Express 5 with async helpers. |
|
|
442
|
-
|
|
443
|
-
> The `res.render` shim intercepts **only** view names containing a `#`. Everything else (full pages, `res.render(view, cb)`, callback forms) goes through Express's normal view lookup, so the integration is fully compatible with existing Express middleware.
|
|
444
|
-
|
|
445
|
-
### Just-the-Middleware Variant
|
|
446
|
-
|
|
447
|
-
If you already have your own `app.engine()` setup and just want partial responses, add the middleware:
|
|
448
|
-
|
|
449
|
-
```javascript
|
|
450
|
-
const miki = require('miki-template');
|
|
451
|
-
app.use(miki.expressPartialRenderer());
|
|
452
|
-
|
|
453
|
-
app.get('/card', (req, res) => res.renderPartial('home#card', { user: req.user }));
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Manual Setup (still supported)
|
|
457
|
-
|
|
458
|
-
```javascript
|
|
459
|
-
const express = require('express');
|
|
460
|
-
const { __express } = require('miki-template');
|
|
461
|
-
|
|
462
|
-
const app = express();
|
|
463
|
-
app.engine('html', __express);
|
|
464
|
-
app.set('view engine', 'html');
|
|
465
|
-
app.set('views', './views');
|
|
466
|
-
|
|
467
|
-
app.get('/', (req, res) => {
|
|
468
|
-
res.render('home', {
|
|
469
|
-
title: 'My Site',
|
|
470
|
-
user: req.user,
|
|
471
|
-
items: ['a', 'b', 'c']
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
app.listen(3000);
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Async Express Views
|
|
479
|
-
|
|
480
|
-
Express 5+ supports async route handlers natively. Pass `async: true` to `setupExpress`, or use `__expressAsync` directly:
|
|
481
|
-
|
|
482
|
-
```javascript
|
|
483
|
-
miki.setupExpress(app, { extension: 'html', views: './views', async: true });
|
|
484
|
-
|
|
485
|
-
app.get('/user/:id', async (req, res) => {
|
|
486
|
-
const user = await User.findById(req.params.id);
|
|
487
|
-
if (!user) return res.status(404).send('Not found');
|
|
488
|
-
res.render('user-profile', { user });
|
|
489
|
-
});
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Context Processors
|
|
495
|
-
|
|
496
|
-
Context processors inject variables into every template render, like Django's custom context processors.
|
|
497
|
-
|
|
498
|
-
```javascript
|
|
499
|
-
const { registerContextProcessor } = require('miki-template');
|
|
500
|
-
|
|
501
|
-
// Inject site-wide variables
|
|
502
|
-
registerContextProcessor(() => ({
|
|
503
|
-
site_name: 'MyApp',
|
|
504
|
-
current_year: new Date().getFullYear()
|
|
505
|
-
}));
|
|
506
|
-
|
|
507
|
-
// Access request-specific data
|
|
508
|
-
registerContextProcessor((ctx) => ({
|
|
509
|
-
is_authenticated: ctx.user !== null,
|
|
510
|
-
user_display: ctx.user ? ctx.user.name : 'Guest'
|
|
511
|
-
}));
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
Now `{{ site_name }}` and `{{ current_year }}` are available in every template automatically.
|
|
515
|
-
|
|
516
|
-
---
|
|
517
|
-
|
|
518
|
-
## Security
|
|
519
|
-
|
|
520
|
-
### Auto-escaping
|
|
521
|
-
|
|
522
|
-
HTML auto-escaping is **enabled by default**. All variable output is escaped:
|
|
523
|
-
|
|
524
|
-
```html
|
|
525
|
-
{{ user_input }} → <script>alert()</script>
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### Marking Values as Safe
|
|
529
|
-
|
|
530
|
-
Use `|safe` for trusted HTML content:
|
|
531
|
-
|
|
532
|
-
```html
|
|
533
|
-
{{ trusted_html|safe }}
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
In JavaScript:
|
|
537
|
-
|
|
538
|
-
```javascript
|
|
539
|
-
const { markSafe } = require('miki-template');
|
|
540
|
-
|
|
541
|
-
res.render('email', {
|
|
542
|
-
body: markSafe('<b>Welcome!</b>') // Won't be escaped
|
|
543
|
-
});
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### CSRF Protection
|
|
547
|
-
|
|
548
|
-
```html
|
|
549
|
-
<form method="post">
|
|
550
|
-
{% csrf_token %}
|
|
551
|
-
<!-- renders: <input type="hidden" name="csrfmiddlewaretoken" value="..."> -->
|
|
552
|
-
...
|
|
553
|
-
</form>
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
Provide `csrf_token` in context:
|
|
557
|
-
|
|
558
|
-
```javascript
|
|
559
|
-
res.render('form', { csrf_token: req.csrfToken() });
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
### CSP Nonce
|
|
563
|
-
|
|
564
|
-
```html
|
|
565
|
-
<script {% csp_nonce %} src="/app.js"></script>
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
Provide `csp_nonce` in context:
|
|
569
|
-
|
|
570
|
-
```javascript
|
|
571
|
-
res.render('page', { csp_nonce: res.locals.nonce });
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
---
|
|
575
|
-
|
|
576
|
-
## Static Files and URLs
|
|
577
|
-
|
|
578
|
-
Configure the static URL prefix:
|
|
579
|
-
|
|
580
|
-
```javascript
|
|
581
|
-
compile(template, { staticUrl: '/static/assets/' });
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
Then in templates:
|
|
585
|
-
|
|
586
|
-
```html
|
|
587
|
-
<img src="{% static "images/logo.png" %}" alt="Logo">
|
|
588
|
-
<!-- → /static/assets/images/logo.png -->
|
|
589
|
-
|
|
590
|
-
<script src="{% static "js/app.js" %}"></script>
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
### URL Resolution
|
|
594
|
-
|
|
595
|
-
```javascript
|
|
596
|
-
compile(template, {
|
|
597
|
-
urlHelper: (routeName, ...args) => {
|
|
598
|
-
const routes = {
|
|
599
|
-
'home': '/',
|
|
600
|
-
'user-profile': (id) => `/users/${id}`
|
|
601
|
-
};
|
|
602
|
-
const handler = routes[routeName];
|
|
603
|
-
return typeof handler === 'function' ? handler(...args) : handler;
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
```html
|
|
609
|
-
<a href="{% url "home" %}">Home</a>
|
|
610
|
-
<a href="{% url "user-profile" user.id %}">Profile</a>
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
---
|
|
614
|
-
|
|
615
|
-
## Error Handling
|
|
616
|
-
|
|
617
|
-
### Unclosed Tags
|
|
618
|
-
|
|
619
|
-
Unclosed block tags produce an error:
|
|
620
|
-
|
|
621
|
-
```html
|
|
622
|
-
{% if user.is_active %}
|
|
623
|
-
<p>Active</p>
|
|
624
|
-
<!-- Missing {% endif %} → throws "Unexpected end of template"
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### Missing Partial
|
|
628
|
-
|
|
629
|
-
```html
|
|
630
|
-
{% partial missing_name %}
|
|
631
|
-
<!-- throws: Partial 'missing_name' not found -->
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### Missing Block
|
|
635
|
-
|
|
636
|
-
```javascript
|
|
637
|
-
template.renderBlock('nonexistent', {});
|
|
638
|
-
// throws: Block 'nonexistent' not found in template
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
### Path Traversal Protection
|
|
642
|
-
|
|
643
|
-
```html
|
|
644
|
-
{% include "../etc/passwd" %}
|
|
645
|
-
<!-- throws: Include tag attempted path traversal outside allowed views -->
|
|
646
|
-
```
|