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
package/docs/usage.md
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
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
|
+
### Basic Setup
|
|
411
|
+
|
|
412
|
+
```javascript
|
|
413
|
+
const express = require('express');
|
|
414
|
+
const { __express } = require('miki-template');
|
|
415
|
+
|
|
416
|
+
const app = express();
|
|
417
|
+
|
|
418
|
+
app.engine('html', __express);
|
|
419
|
+
app.set('view engine', 'html');
|
|
420
|
+
app.set('views', './views');
|
|
421
|
+
|
|
422
|
+
app.get('/', (req, res) => {
|
|
423
|
+
res.render('home', {
|
|
424
|
+
title: 'My Site',
|
|
425
|
+
user: req.user,
|
|
426
|
+
items: ['a', 'b', 'c']
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
app.listen(3000);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Async Express Views
|
|
434
|
+
|
|
435
|
+
Express 5+ supports async route handlers natively:
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
app.get('/user/:id', async (req, res) => {
|
|
439
|
+
const user = await User.findById(req.params.id);
|
|
440
|
+
if (!user) return res.status(404).send('Not found');
|
|
441
|
+
res.render('user-profile', { user });
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Context Processors
|
|
448
|
+
|
|
449
|
+
Context processors inject variables into every template render, like Django's custom context processors.
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
const { registerContextProcessor } = require('miki-template');
|
|
453
|
+
|
|
454
|
+
// Inject site-wide variables
|
|
455
|
+
registerContextProcessor(() => ({
|
|
456
|
+
site_name: 'MyApp',
|
|
457
|
+
current_year: new Date().getFullYear()
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
// Access request-specific data
|
|
461
|
+
registerContextProcessor((ctx) => ({
|
|
462
|
+
is_authenticated: ctx.user !== null,
|
|
463
|
+
user_display: ctx.user ? ctx.user.name : 'Guest'
|
|
464
|
+
}));
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Now `{{ site_name }}` and `{{ current_year }}` are available in every template automatically.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Security
|
|
472
|
+
|
|
473
|
+
### Auto-escaping
|
|
474
|
+
|
|
475
|
+
HTML auto-escaping is **enabled by default**. All variable output is escaped:
|
|
476
|
+
|
|
477
|
+
```html
|
|
478
|
+
{{ user_input }} → <script>alert()</script>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Marking Values as Safe
|
|
482
|
+
|
|
483
|
+
Use `|safe` for trusted HTML content:
|
|
484
|
+
|
|
485
|
+
```html
|
|
486
|
+
{{ trusted_html|safe }}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
In JavaScript:
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
const { markSafe } = require('miki-template');
|
|
493
|
+
|
|
494
|
+
res.render('email', {
|
|
495
|
+
body: markSafe('<b>Welcome!</b>') // Won't be escaped
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### CSRF Protection
|
|
500
|
+
|
|
501
|
+
```html
|
|
502
|
+
<form method="post">
|
|
503
|
+
{% csrf_token %}
|
|
504
|
+
<!-- renders: <input type="hidden" name="csrfmiddlewaretoken" value="..."> -->
|
|
505
|
+
...
|
|
506
|
+
</form>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Provide `csrf_token` in context:
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
res.render('form', { csrf_token: req.csrfToken() });
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### CSP Nonce
|
|
516
|
+
|
|
517
|
+
```html
|
|
518
|
+
<script {% csp_nonce %} src="/app.js"></script>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Provide `csp_nonce` in context:
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
res.render('page', { csp_nonce: res.locals.nonce });
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Static Files and URLs
|
|
530
|
+
|
|
531
|
+
Configure the static URL prefix:
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
compile(template, { staticUrl: '/static/assets/' });
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Then in templates:
|
|
538
|
+
|
|
539
|
+
```html
|
|
540
|
+
<img src="{% static "images/logo.png" %}" alt="Logo">
|
|
541
|
+
<!-- → /static/assets/images/logo.png -->
|
|
542
|
+
|
|
543
|
+
<script src="{% static "js/app.js" %}"></script>
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### URL Resolution
|
|
547
|
+
|
|
548
|
+
```javascript
|
|
549
|
+
compile(template, {
|
|
550
|
+
urlHelper: (routeName, ...args) => {
|
|
551
|
+
const routes = {
|
|
552
|
+
'home': '/',
|
|
553
|
+
'user-profile': (id) => `/users/${id}`
|
|
554
|
+
};
|
|
555
|
+
const handler = routes[routeName];
|
|
556
|
+
return typeof handler === 'function' ? handler(...args) : handler;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```html
|
|
562
|
+
<a href="{% url "home" %}">Home</a>
|
|
563
|
+
<a href="{% url "user-profile" user.id %}">Profile</a>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Error Handling
|
|
569
|
+
|
|
570
|
+
### Unclosed Tags
|
|
571
|
+
|
|
572
|
+
Unclosed block tags produce an error:
|
|
573
|
+
|
|
574
|
+
```html
|
|
575
|
+
{% if user.is_active %}
|
|
576
|
+
<p>Active</p>
|
|
577
|
+
<!-- Missing {% endif %} → throws "Unexpected end of template"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Missing Partial
|
|
581
|
+
|
|
582
|
+
```html
|
|
583
|
+
{% partial missing_name %}
|
|
584
|
+
<!-- throws: Partial 'missing_name' not found -->
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Missing Block
|
|
588
|
+
|
|
589
|
+
```javascript
|
|
590
|
+
template.renderBlock('nonexistent', {});
|
|
591
|
+
// throws: Block 'nonexistent' not found in template
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Path Traversal Protection
|
|
595
|
+
|
|
596
|
+
```html
|
|
597
|
+
{% include "../etc/passwd" %}
|
|
598
|
+
<!-- throws: Include tag attempted path traversal outside allowed views -->
|
|
599
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
files: ['src/**/*.js'],
|
|
4
|
+
languageOptions: {
|
|
5
|
+
ecmaVersion: 'latest',
|
|
6
|
+
sourceType: 'commonjs',
|
|
7
|
+
globals: {
|
|
8
|
+
Buffer: 'readonly',
|
|
9
|
+
console: 'readonly',
|
|
10
|
+
exports: 'readonly',
|
|
11
|
+
module: 'readonly',
|
|
12
|
+
process: 'readonly',
|
|
13
|
+
__dirname: 'readonly',
|
|
14
|
+
__filename: 'readonly',
|
|
15
|
+
require: 'readonly',
|
|
16
|
+
setTimeout: 'readonly',
|
|
17
|
+
setInterval: 'readonly',
|
|
18
|
+
clearTimeout: 'readonly',
|
|
19
|
+
clearInterval: 'readonly',
|
|
20
|
+
setImmediate: 'readonly',
|
|
21
|
+
clearImmediate: 'readonly',
|
|
22
|
+
global: 'readonly'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
27
|
+
'no-console': 'off',
|
|
28
|
+
'no-undef': 'error',
|
|
29
|
+
'semi': ['error', 'always'],
|
|
30
|
+
'quotes': ['error', 'single'],
|
|
31
|
+
'indent': ['error', 2]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
];
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 miki-template contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|