miki-template 2.0.1 → 2.2.3

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/release.yml +6 -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
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 }} → &lt;script&gt;alert()&lt;/script&gt;
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
- ```