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.
Files changed (71) hide show
  1. package/.github/workflows/ci.yml +13 -37
  2. package/.github/workflows/docs.yml +105 -0
  3. package/.github/workflows/npm-publish-github-packages.yml +36 -0
  4. package/README.md +69 -14
  5. package/assets/logo.png +0 -0
  6. package/benchmarks/ejs-results.json +4 -4
  7. package/benchmarks/handlebars-results.json +6 -6
  8. package/benchmarks/miki-results.json +4 -4
  9. package/benchmarks/pug-results.json +4 -4
  10. package/benchmarks/stress.mjs +1 -1
  11. package/docs/api/async-render.md +85 -0
  12. package/docs/api/cache.md +87 -0
  13. package/docs/api/compile.md +128 -0
  14. package/docs/api/context-processors.md +77 -0
  15. package/docs/api/filters.md +217 -0
  16. package/docs/api/finder.md +94 -0
  17. package/docs/api/helpers.md +53 -0
  18. package/docs/api/i18n.md +157 -0
  19. package/docs/api/index.md +54 -0
  20. package/docs/api/libraries.md +207 -0
  21. package/docs/api/render-partial.md +81 -0
  22. package/docs/api/render.md +92 -0
  23. package/docs/api/security.md +145 -0
  24. package/docs/api/setup-express.md +76 -0
  25. package/docs/api/tags.md +134 -0
  26. package/docs/assets/banner.png +0 -0
  27. package/docs/assets/logo.png +0 -0
  28. package/docs/guide/advanced-usage.md +397 -0
  29. package/docs/guide/async-rendering.md +308 -0
  30. package/docs/guide/context-processors.md +257 -0
  31. package/docs/guide/custom-filters.md +311 -0
  32. package/docs/guide/custom-tags.md +271 -0
  33. package/docs/guide/filters.md +642 -0
  34. package/docs/guide/getting-started.md +102 -0
  35. package/docs/guide/installation.md +95 -0
  36. package/docs/guide/partial-templates.md +367 -0
  37. package/docs/guide/quick-start.md +222 -0
  38. package/docs/guide/security.md +345 -0
  39. package/docs/guide/tags.md +783 -0
  40. package/docs/guide/template-discovery.md +170 -0
  41. package/docs/guide/template-inheritance.md +273 -0
  42. package/docs/guide/what-is-miki-template.md +28 -0
  43. package/docs/guide/why-miki-template.md +75 -0
  44. package/docs/index.md +104 -0
  45. package/docs/integrations/elysia.md +78 -0
  46. package/docs/integrations/express.md +219 -0
  47. package/docs/integrations/fastify.md +77 -0
  48. package/docs/integrations/hono.md +78 -0
  49. package/docs/integrations/index.md +68 -0
  50. package/docs/integrations/koa.md +88 -0
  51. package/docs/integrations/nestjs.md +78 -0
  52. package/docs/integrations/tsed.md +81 -0
  53. package/docs/javascripts/extra.js +174 -0
  54. package/docs/performance.md +37 -0
  55. package/docs/stylesheets/extra.css +819 -0
  56. package/mkdocs.yml +217 -0
  57. package/overrides/main.html +26 -0
  58. package/overrides/partials/footer.html +9 -0
  59. package/package.json +4 -2
  60. package/requirements-docs.txt +1 -0
  61. package/docs/README.md +0 -18
  62. package/docs/advanced_usage.md +0 -71
  63. package/docs/api.md +0 -122
  64. package/docs/filters.md +0 -708
  65. package/docs/installation.md +0 -106
  66. package/docs/integrations.md +0 -214
  67. package/docs/overview.md +0 -79
  68. package/docs/partialdef.md +0 -70
  69. package/docs/security.md +0 -27
  70. package/docs/tags.md +0 -673
  71. 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
+ &lt;script&gt;alert(&quot;1&quot;)&lt;/script&gt;
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 &lt;b&gt;Bold&lt;/b&gt;
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
+ | `&` | `&amp;` |
272
+ | `<` | `&lt;` |
273
+ | `>` | `&gt;` |
274
+ | `"` | `&quot;` |
275
+ | `'` | `&#x27;` |
276
+ | `` ` `` | `&#96;` |
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
+ // → "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
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: &lt;script&gt;
300
+ ```
301
+
302
+ === "ES Modules"
303
+
304
+ ```javascript
305
+ import { escapeHtml } from 'miki-template';
306
+
307
+ const escaped = escapeHtml('<script>');
308
+ // Output: &lt;script&gt;
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)