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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Quick Start
|
|
2
|
+
|
|
3
|
+
A hands-on tour of the most common miki-template workflows. Each example shows **CommonJS** and **ES Modules** side by side — pick the tab that matches your project.
|
|
4
|
+
|
|
5
|
+
## 1. Render a Template String
|
|
6
|
+
|
|
7
|
+
The `render()` function compiles and renders in one call. Perfect for email templates, static-site generation, or testing snippets.
|
|
8
|
+
|
|
9
|
+
=== "CommonJS (require)"
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
const { render } = require('miki-template');
|
|
13
|
+
|
|
14
|
+
const template = 'Hello {{ user.name|title }}! Roles: {{ user.roles|join:", " }}';
|
|
15
|
+
const context = {
|
|
16
|
+
user: {
|
|
17
|
+
name: 'miki coder',
|
|
18
|
+
roles: ['admin', 'developer']
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const result = render(template, context);
|
|
23
|
+
console.log(result);
|
|
24
|
+
// Output: "Hello Miki Coder! Roles: admin, developer"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
=== "ES Modules (import)"
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import { render } from 'miki-template';
|
|
31
|
+
|
|
32
|
+
const template = 'Hello {{ user.name|title }}! Roles: {{ user.roles|join:", " }}';
|
|
33
|
+
const context = {
|
|
34
|
+
user: {
|
|
35
|
+
name: 'miki coder',
|
|
36
|
+
roles: ['admin', 'developer']
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const result = render(template, context);
|
|
41
|
+
console.log(result);
|
|
42
|
+
// Output: "Hello Miki Coder! Roles: admin, developer"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 2. Compile and Reuse
|
|
46
|
+
|
|
47
|
+
When you render the same template many times (e.g. an email template or a partial), use `compile()` to parse it once and reuse the compiled AST across many renders.
|
|
48
|
+
|
|
49
|
+
=== "CommonJS"
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const { compile } = require('miki-template');
|
|
53
|
+
|
|
54
|
+
const template = compile(
|
|
55
|
+
'<h1>Hello {{ name|title }}!</h1><p>{{ body|truncatewords:20 }}</p>'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
console.log(template.render({ name: 'alice', body: 'A long body of text...' }));
|
|
59
|
+
console.log(template.render({ name: 'bob', body: 'Another long body...' }));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
=== "ES Modules"
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
import { compile } from 'miki-template';
|
|
66
|
+
|
|
67
|
+
const template = compile(
|
|
68
|
+
'<h1>Hello {{ name|title }}!</h1><p>{{ body|truncatewords:20 }}</p>'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
console.log(template.render({ name: 'alice', body: 'A long body of text...' }));
|
|
72
|
+
console.log(template.render({ name: 'bob', body: 'Another long body...' }));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Compiled Template Methods
|
|
76
|
+
|
|
77
|
+
The object returned by `compile()` exposes several render methods:
|
|
78
|
+
|
|
79
|
+
| Method | Description |
|
|
80
|
+
|--------|-------------|
|
|
81
|
+
| `render(context)` | Synchronous render. |
|
|
82
|
+
| `renderWith(context, callOptions)` | Sync render with per-call option overrides (e.g. a different `views` root). |
|
|
83
|
+
| `asyncRender(context)` | Async render — awaits Promise-returning helpers/filters. |
|
|
84
|
+
| `asyncRenderWith(context, callOptions)` | Async render with per-call option overrides. |
|
|
85
|
+
| `renderBlock(blockName, context)` | Render only a single `{% block %}` — ideal for HTMX/AJAX slices. |
|
|
86
|
+
| `renderPartial(partialName, context)` | Render only a `{% partialdef %}` block by name. |
|
|
87
|
+
|
|
88
|
+
## 3. Express: Full Page + HTMX Partials
|
|
89
|
+
|
|
90
|
+
`setupExpress()` wires everything in one call. After that, `res.render('home')` renders the full template, and `res.render('home#card')` renders only the `card` partial — no extra middleware required.
|
|
91
|
+
|
|
92
|
+
=== "CommonJS"
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
const express = require('express');
|
|
96
|
+
const miki = require('miki-template');
|
|
97
|
+
|
|
98
|
+
const app = express();
|
|
99
|
+
miki.setupExpress(app, { extension: 'html', views: './views' });
|
|
100
|
+
|
|
101
|
+
// Full page
|
|
102
|
+
app.get('/', (req, res) => res.render('home', { user: req.user }));
|
|
103
|
+
|
|
104
|
+
// HTMX / partial response — just append #partialName to the view name
|
|
105
|
+
app.get('/partials/:name', (req, res) =>
|
|
106
|
+
res.render(`home#${req.params.name}`, { user: req.user })
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
app.listen(3000);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
=== "ES Modules"
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
import express from 'express';
|
|
116
|
+
import miki from 'miki-template';
|
|
117
|
+
|
|
118
|
+
const app = express();
|
|
119
|
+
miki.setupExpress(app, { extension: 'html', views: './views' });
|
|
120
|
+
|
|
121
|
+
app.get('/', (req, res) => res.render('home', { user: req.user }));
|
|
122
|
+
app.get('/partials/:name', (req, res) =>
|
|
123
|
+
res.render(`home#${req.params.name}`, { user: req.user })
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
app.listen(3000);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Manual Express Setup (if you prefer full control)
|
|
130
|
+
|
|
131
|
+
=== "CommonJS"
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const express = require('express');
|
|
135
|
+
const { __express } = require('miki-template');
|
|
136
|
+
|
|
137
|
+
const app = express();
|
|
138
|
+
app.engine('html', __express);
|
|
139
|
+
app.set('view engine', 'html');
|
|
140
|
+
app.set('views', './views');
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
=== "ES Modules"
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
import express from 'express';
|
|
147
|
+
import { __express } from 'miki-template';
|
|
148
|
+
|
|
149
|
+
const app = express();
|
|
150
|
+
app.engine('html', __express);
|
|
151
|
+
app.set('view engine', 'html');
|
|
152
|
+
app.set('views', './views');
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 4. Async Rendering
|
|
156
|
+
|
|
157
|
+
When your templates use async helpers or async filters, use `asyncRender()` (or `compiled.asyncRender()`).
|
|
158
|
+
|
|
159
|
+
=== "CommonJS"
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const { asyncRender } = require('miki-template');
|
|
163
|
+
|
|
164
|
+
const html = await asyncRender(
|
|
165
|
+
'Hello {{ name }} — {{ fetchGreeting user.id }}',
|
|
166
|
+
{ name: 'World', userId: 42 }
|
|
167
|
+
);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
=== "ES Modules"
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
import { asyncRender } from 'miki-template';
|
|
174
|
+
|
|
175
|
+
const html = await asyncRender(
|
|
176
|
+
'Hello {{ name }} — {{ fetchGreeting userId }}',
|
|
177
|
+
{ name: 'World', userId: 42 }
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 5. Defining and Rendering a Partial
|
|
182
|
+
|
|
183
|
+
Partials are reusable template fragments defined with `{% partialdef %}`.
|
|
184
|
+
|
|
185
|
+
=== "Template (home.html)"
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
{% partialdef card %}
|
|
189
|
+
<div class="card">
|
|
190
|
+
<h3>{{ title|default:"Untitled" }}</h3>
|
|
191
|
+
<p>{{ body|truncatewords:30 }}</p>
|
|
192
|
+
</div>
|
|
193
|
+
{% endpartialdef %}
|
|
194
|
+
|
|
195
|
+
{% partial card with title=entry.title body=entry.body %}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
=== "CommonJS"
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
const { compile } = require('miki-template');
|
|
202
|
+
|
|
203
|
+
const compiled = compile('template string here', { views: './views' });
|
|
204
|
+
const html = compiled.renderPartial('card', { title: 'Hi', body: 'World' });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
=== "ES Modules"
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
import { compile } from 'miki-template';
|
|
211
|
+
|
|
212
|
+
const compiled = compile('template string here', { views: './views' });
|
|
213
|
+
const html = compiled.renderPartial('card', { title: 'Hi', body: 'World' });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Next Steps
|
|
217
|
+
|
|
218
|
+
- [Partial Templates](./partial-templates)
|
|
219
|
+
- [Template Inheritance](./template-inheritance)
|
|
220
|
+
- [Filters](./filters)
|
|
221
|
+
- [Tags](./tags)
|
|
222
|
+
- [API Reference](../api/)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
miki-template follows Django's security semantics to protect against common web vulnerabilities.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Auto-Escaping](#auto-escaping)
|
|
8
|
+
- [SafeString](#safestring)
|
|
9
|
+
- [CSRF Protection](#csrf-protection)
|
|
10
|
+
- [CSP Nonce](#csp-nonce)
|
|
11
|
+
- [Path Traversal Protection](#path-traversal-protection)
|
|
12
|
+
- [No Unsafe Code Execution](#no-unsafe-code-execution)
|
|
13
|
+
- [HTML Escaping Details](#html-escaping-details)
|
|
14
|
+
- [Context Processor Security](#context-processor-security)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Auto-Escaping
|
|
19
|
+
|
|
20
|
+
All variable output is HTML-escaped by default. This means any `<`, `>`, `&`, `"`, `'`, and `` ` `` characters in your data are converted to HTML entities before rendering.
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
{{ user_input }}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If `user_input` is `<script>alert(1)</script>`, the output is:
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<script>alert("1")</script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This prevents XSS (Cross-Site Scripting) attacks where malicious users inject executable JavaScript.
|
|
33
|
+
|
|
34
|
+
### Disabling Auto-Escaping
|
|
35
|
+
|
|
36
|
+
Use `{% autoescape off %}` to disable escaping for a block:
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
{% autoescape off %}
|
|
40
|
+
{{ trusted_html }} {# not escaped #}
|
|
41
|
+
{% endautoescape %}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Re-enabling Auto-Escaping
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
{% autoescape on %}
|
|
48
|
+
{{ user_input }} {# escaped again #}
|
|
49
|
+
{% endautoescape %}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Real-world blog post:**
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<article>
|
|
56
|
+
<!-- Post body is trusted CMS content -->
|
|
57
|
+
{% autoescape off %}
|
|
58
|
+
{{ post.body_html }}
|
|
59
|
+
{% endautoescape %}
|
|
60
|
+
|
|
61
|
+
<!-- User comment is untrusted -->
|
|
62
|
+
<div class="comments">
|
|
63
|
+
{% for comment in comments %}
|
|
64
|
+
<p>{{ comment.text }}</p>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
</div>
|
|
67
|
+
</article>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## SafeString
|
|
71
|
+
|
|
72
|
+
Use the `safe` filter or `markSafe()` to mark content as trusted (bypassing auto-escaping):
|
|
73
|
+
|
|
74
|
+
=== "Template (safe filter)"
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
{{ trusted_html|safe }}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
=== "CommonJS (markSafe)"
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
const { markSafe } = require('miki-template');
|
|
84
|
+
|
|
85
|
+
const html = markSafe('<b>ok</b>');
|
|
86
|
+
// Will not be escaped when rendered
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
=== "ES Modules (markSafe)"
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
import { markSafe } from 'miki-template';
|
|
93
|
+
|
|
94
|
+
const html = markSafe('<b>ok</b>');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### SafeString Class
|
|
98
|
+
|
|
99
|
+
You can also create `SafeString` instances directly:
|
|
100
|
+
|
|
101
|
+
=== "CommonJS"
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const { SafeString } = require('miki-template');
|
|
105
|
+
|
|
106
|
+
const html = new SafeString('<b>Bold</b>');
|
|
107
|
+
// {{ html }} renders as <b>Bold</b>, NOT <b>Bold</b>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
=== "ES Modules"
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
import { SafeString } from 'miki-template';
|
|
114
|
+
|
|
115
|
+
const html = new SafeString('<b>Bold</b>');
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Checking if a value is safe
|
|
119
|
+
|
|
120
|
+
=== "CommonJS"
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
const { isSafe } = require('miki-template');
|
|
124
|
+
|
|
125
|
+
if (isSafe(value)) {
|
|
126
|
+
// value is marked safe
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
=== "ES Modules"
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
import { isSafe } from 'miki-template';
|
|
134
|
+
|
|
135
|
+
if (isSafe(value)) {
|
|
136
|
+
// value is marked safe
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## HTML Filters
|
|
141
|
+
|
|
142
|
+
### safe
|
|
143
|
+
|
|
144
|
+
Mark a string as safe (no escaping):
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
{{ content|safe }}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### escape
|
|
151
|
+
|
|
152
|
+
Force HTML escaping, even on SafeString values. This matches Django's `{{ value|escape }}` semantics:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<!-- Even if content is marked safe, escape forces HTML entities -->
|
|
156
|
+
{{ content|escape }}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Real-world: render user-generated content with a safe wrapper**
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<!-- In a filter -->
|
|
163
|
+
{{ user.bio|default:"No bio yet."|escape }}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## CSRF Protection
|
|
167
|
+
|
|
168
|
+
Use the `{% csrf_token %}` tag to output a hidden input with the CSRF token:
|
|
169
|
+
|
|
170
|
+
```html
|
|
171
|
+
<form method="post">
|
|
172
|
+
{% csrf_token %}
|
|
173
|
+
<button type="submit">Submit</button>
|
|
174
|
+
</form>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The token value is HTML-escaped to prevent attribute injection. The output is:
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<input type="hidden" name="csrfmiddlewaretoken" value="escaped_token_value">
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### How it works
|
|
184
|
+
|
|
185
|
+
- The tag looks for `csrf_token` in the template context.
|
|
186
|
+
- If found, it outputs a hidden input with the escaped token value.
|
|
187
|
+
- If not found, it outputs an empty hidden input.
|
|
188
|
+
|
|
189
|
+
Provide `csrf_token` in context:
|
|
190
|
+
|
|
191
|
+
=== "CommonJS (Express + csurf)"
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
const csrf = require('csurf');
|
|
195
|
+
|
|
196
|
+
app.use(csrf({ cookie: true }));
|
|
197
|
+
app.use((req, res, next) => {
|
|
198
|
+
res.locals.csrf_token = req.csrfToken();
|
|
199
|
+
next();
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
=== "ES Modules"
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
app.use((req, res, next) => {
|
|
207
|
+
res.locals.csrf_token = req.csrfToken();
|
|
208
|
+
next();
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## CSP Nonce
|
|
213
|
+
|
|
214
|
+
Use the `{% csp_nonce_attr %}` tag to output a `nonce` attribute when `csp_nonce` is in the context. This is essential for Content-Security-Policy-compliant inline scripts:
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<script {% csp_nonce_attr %} src="/js/app.js"></script>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
If `csp_nonce` is present in context, the output is:
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<script nonce="abc123" src="/js/app.js"></script>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
If `csp_nonce` is missing, the tag outputs nothing — the `<script>` tag is rendered without a nonce.
|
|
227
|
+
|
|
228
|
+
Provide `csp_nonce` in context:
|
|
229
|
+
|
|
230
|
+
=== "CommonJS"
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
app.use((req, res, next) => {
|
|
234
|
+
res.locals.csp_nonce = crypto.randomBytes(16).toString('base64');
|
|
235
|
+
next();
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
=== "ES Modules"
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
import crypto from 'node:crypto';
|
|
243
|
+
|
|
244
|
+
app.use((req, res, next) => {
|
|
245
|
+
res.locals.csp_nonce = crypto.randomBytes(16).toString('base64');
|
|
246
|
+
next();
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Path Traversal Protection
|
|
251
|
+
|
|
252
|
+
`{% extends %}`, `{% include %}`, and `{% extends %}` paths are validated to prevent directory traversal attacks:
|
|
253
|
+
|
|
254
|
+
```html
|
|
255
|
+
{% extends "../../etc/passwd" %} {# REJECTED #}
|
|
256
|
+
{% include "../../secrets" %} {# REJECTED #}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The engine checks that resolved paths stay within the allowed views directories. An error with message starting with `path traversal` is thrown if the resolved path escapes the views root.
|
|
260
|
+
|
|
261
|
+
## No Unsafe Code Execution
|
|
262
|
+
|
|
263
|
+
miki-template never uses `eval()`. Expressions are parsed and evaluated safely using the AST-based expression evaluator. This prevents code injection attacks — template expressions like `{{ user.name }}` are resolved through property lookups, never by executing arbitrary JavaScript.
|
|
264
|
+
|
|
265
|
+
## HTML Escaping Details
|
|
266
|
+
|
|
267
|
+
miki-template uses the [`he`](https://github.com/mathiasbynetworks/he) library for HTML escaping, which converts:
|
|
268
|
+
|
|
269
|
+
| Character | Escaped |
|
|
270
|
+
|-----------|---------|
|
|
271
|
+
| `&` | `&` |
|
|
272
|
+
| `<` | `<` |
|
|
273
|
+
| `>` | `>` |
|
|
274
|
+
| `"` | `"` |
|
|
275
|
+
| `'` | `'` |
|
|
276
|
+
| `` ` `` | ``` |
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
// Access escaping directly
|
|
280
|
+
const { escapeHtml } = require('miki-template');
|
|
281
|
+
// or
|
|
282
|
+
import { escapeHtml } from 'miki-template';
|
|
283
|
+
|
|
284
|
+
const escaped = escapeHtml('<script>alert("xss")</script>');
|
|
285
|
+
// → "<script>alert("xss")</script>"
|
|
286
|
+
|
|
287
|
+
// Force-escape even SafeString values (third argument)
|
|
288
|
+
const reescaped = escapeHtml(safeStringInstance, true);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Programmatic Escaping
|
|
292
|
+
|
|
293
|
+
=== "CommonJS"
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
const { escapeHtml } = require('miki-template');
|
|
297
|
+
|
|
298
|
+
const escaped = escapeHtml('<script>');
|
|
299
|
+
// Output: <script>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
=== "ES Modules"
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
import { escapeHtml } from 'miki-template';
|
|
306
|
+
|
|
307
|
+
const escaped = escapeHtml('<script>');
|
|
308
|
+
// Output: <script>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Context Processor Security
|
|
312
|
+
|
|
313
|
+
Context processors run before every render and can inject global variables. Be careful not to expose sensitive data:
|
|
314
|
+
|
|
315
|
+
=== "CommonJS"
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
const { registerContextProcessor } = require('miki-template');
|
|
319
|
+
|
|
320
|
+
registerContextProcessor((context) => {
|
|
321
|
+
return {
|
|
322
|
+
siteName: 'My App',
|
|
323
|
+
// Don't inject secrets here - they'll be available in ALL templates
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
=== "ES Modules"
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
import { registerContextProcessor } from 'miki-template';
|
|
332
|
+
|
|
333
|
+
registerContextProcessor((context) => {
|
|
334
|
+
return {
|
|
335
|
+
siteName: 'My App',
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Key behavior:** Context processor values respect Django semantics — existing context values **win** over processor defaults. If you render with `{ user: req.user }` and a processor returns `{ user: 'Guest' }`, the explicit `req.user` is preserved.
|
|
341
|
+
|
|
342
|
+
## Next Steps
|
|
343
|
+
|
|
344
|
+
- [Integrations](../integrations/)
|
|
345
|
+
- [API Reference: Security](../api/security)
|