miki-template 2.0.0 → 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 (87) hide show
  1. package/.github/workflows/ci.yml +14 -10
  2. package/.github/workflows/docs.yml +105 -0
  3. package/.github/workflows/npm-publish-github-packages.yml +36 -0
  4. package/README.md +142 -26
  5. package/assets/logo.png +0 -0
  6. package/benchmarks/ejs-results.json +17 -0
  7. package/benchmarks/ejs.js +36 -0
  8. package/benchmarks/handlebars-results.json +17 -0
  9. package/benchmarks/handlebars.js +48 -0
  10. package/benchmarks/miki-results.json +17 -0
  11. package/benchmarks/miki.js +36 -0
  12. package/benchmarks/pug-results.json +17 -0
  13. package/benchmarks/pug.js +36 -0
  14. package/benchmarks/run.js +69 -37
  15. package/benchmarks/stress.mjs +1 -1
  16. package/docs/api/async-render.md +85 -0
  17. package/docs/api/cache.md +87 -0
  18. package/docs/api/compile.md +128 -0
  19. package/docs/api/context-processors.md +77 -0
  20. package/docs/api/filters.md +217 -0
  21. package/docs/api/finder.md +94 -0
  22. package/docs/api/helpers.md +53 -0
  23. package/docs/api/i18n.md +157 -0
  24. package/docs/api/index.md +54 -0
  25. package/docs/api/libraries.md +207 -0
  26. package/docs/api/render-partial.md +81 -0
  27. package/docs/api/render.md +92 -0
  28. package/docs/api/security.md +145 -0
  29. package/docs/api/setup-express.md +76 -0
  30. package/docs/api/tags.md +134 -0
  31. package/docs/assets/banner.png +0 -0
  32. package/docs/assets/logo.png +0 -0
  33. package/docs/guide/advanced-usage.md +397 -0
  34. package/docs/guide/async-rendering.md +308 -0
  35. package/docs/guide/context-processors.md +257 -0
  36. package/docs/guide/custom-filters.md +311 -0
  37. package/docs/guide/custom-tags.md +271 -0
  38. package/docs/guide/filters.md +642 -0
  39. package/docs/guide/getting-started.md +102 -0
  40. package/docs/guide/installation.md +95 -0
  41. package/docs/guide/partial-templates.md +367 -0
  42. package/docs/guide/quick-start.md +222 -0
  43. package/docs/guide/security.md +345 -0
  44. package/docs/guide/tags.md +783 -0
  45. package/docs/guide/template-discovery.md +170 -0
  46. package/docs/guide/template-inheritance.md +273 -0
  47. package/docs/guide/what-is-miki-template.md +28 -0
  48. package/docs/guide/why-miki-template.md +75 -0
  49. package/docs/index.md +104 -0
  50. package/docs/integrations/elysia.md +78 -0
  51. package/docs/integrations/express.md +219 -0
  52. package/docs/integrations/fastify.md +77 -0
  53. package/docs/integrations/hono.md +78 -0
  54. package/docs/integrations/index.md +68 -0
  55. package/docs/integrations/koa.md +88 -0
  56. package/docs/integrations/nestjs.md +78 -0
  57. package/docs/integrations/tsed.md +81 -0
  58. package/docs/javascripts/extra.js +174 -0
  59. package/docs/performance.md +37 -0
  60. package/docs/stylesheets/extra.css +819 -0
  61. package/live-test/integrations/elysia-example.js +16 -0
  62. package/live-test/integrations/express-example.js +24 -0
  63. package/live-test/integrations/fastify-example.js +20 -0
  64. package/live-test/integrations/hono-example.js +16 -0
  65. package/live-test/integrations/koa-example.js +30 -0
  66. package/live-test/integrations/nestjs-example.js +25 -0
  67. package/live-test/integrations/smoke-test.js +166 -0
  68. package/live-test/integrations/tsed-example.js +23 -0
  69. package/live-test/package-lock.json +235 -0
  70. package/live-test/package.json +4 -0
  71. package/live-test/views/home.html +17 -0
  72. package/mkdocs.yml +217 -0
  73. package/overrides/main.html +26 -0
  74. package/overrides/partials/footer.html +9 -0
  75. package/package.json +16 -6
  76. package/requirements-docs.txt +1 -0
  77. package/tests/integration/partial-render.test.cjs +13 -0
  78. package/docs/README.md +0 -18
  79. package/docs/advanced_usage.md +0 -71
  80. package/docs/api.md +0 -122
  81. package/docs/filters.md +0 -708
  82. package/docs/installation.md +0 -106
  83. package/docs/overview.md +0 -79
  84. package/docs/partialdef.md +0 -70
  85. package/docs/security.md +0 -27
  86. package/docs/tags.md +0 -673
  87. package/docs/usage.md +0 -646
@@ -0,0 +1,311 @@
1
+ # Custom Filters
2
+
3
+ Add your own filters to transform values in templates. miki-template's filter API mirrors Django's — filters are simply functions that receive a value and optional argument, and return the transformed value.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Register a Simple Filter](#register-a-simple-filter)
8
+ - [Filters with Arguments](#filters-with-arguments)
9
+ - [Multiple Arguments](#multiple-arguments)
10
+ - [Context-Aware Filters](#context-aware-filters)
11
+ - [SafeString Filters](#safestring-filters)
12
+ - [Async Filters](#async-filters)
13
+ - [Filter Registration Best Practices](#filter-registration-best-practices)
14
+ - [Chaining Custom Filters](#chaining-custom-filters)
15
+
16
+ ---
17
+
18
+ ## Register a Simple Filter
19
+
20
+ === "CommonJS"
21
+
22
+ ```javascript
23
+ const { registerFilter } = require('miki-template');
24
+
25
+ registerFilter('reverse', (val) => {
26
+ return String(val).split('').reverse().join('');
27
+ });
28
+ ```
29
+
30
+ === "ES Modules"
31
+
32
+ ```javascript
33
+ import { registerFilter } from 'miki-template';
34
+
35
+ registerFilter('reverse', (val) => {
36
+ return String(val).split('').reverse().join('');
37
+ });
38
+ ```
39
+
40
+ Use it in templates:
41
+
42
+ ```html
43
+ {{ name|reverse }}
44
+ ```
45
+
46
+ ## Filters with Arguments
47
+
48
+ Filters can accept arguments after a colon:
49
+
50
+ === "CommonJS"
51
+
52
+ ```javascript
53
+ const { registerFilter } = require('miki-template');
54
+
55
+ registerFilter('multiply', (val, factor) => {
56
+ return Number(val) * Number(factor);
57
+ });
58
+ ```
59
+
60
+ === "ES Modules"
61
+
62
+ ```javascript
63
+ import { registerFilter } from 'miki-template';
64
+
65
+ registerFilter('multiply', (val, factor) => {
66
+ return Number(val) * Number(factor);
67
+ });
68
+ ```
69
+
70
+ Usage:
71
+
72
+ ```html
73
+ {{ price|multiply:1.2 }}
74
+ ```
75
+
76
+ ## Multiple Arguments
77
+
78
+ Pass multiple arguments separated by commas:
79
+
80
+ === "CommonJS"
81
+
82
+ ```javascript
83
+ const { registerFilter } = require('miki-template');
84
+
85
+ registerFilter('format', (val, prefix, suffix) => {
86
+ return `${prefix}${val}${suffix}`;
87
+ });
88
+ ```
89
+
90
+ === "ES Modules"
91
+
92
+ ```javascript
93
+ import { registerFilter } from 'miki-template';
94
+
95
+ registerFilter('format', (val, prefix, suffix) => {
96
+ return `${prefix}${val}${suffix}`;
97
+ });
98
+ ```
99
+
100
+ Usage:
101
+
102
+ ```html
103
+ {{ name|format:"<b>","</b>" }}
104
+ <!-- → "<b>Alice</b>" -->
105
+ ```
106
+
107
+ ### Real-World Example: Dynamic Currency Filter
108
+
109
+ === "CommonJS"
110
+
111
+ ```javascript
112
+ const { registerFilter, markSafe } = require('miki-template');
113
+
114
+ registerFilter('currency_dynamic', (val, code, locale = 'en-US') => {
115
+ const num = Number(val);
116
+ if (isNaN(num)) return '';
117
+ return new Intl.NumberFormat(locale, {
118
+ style: 'currency',
119
+ currency: code
120
+ }).format(num);
121
+ });
122
+ ```
123
+
124
+ === "ES Modules"
125
+
126
+ ```javascript
127
+ import { registerFilter } from 'miki-template';
128
+
129
+ registerFilter('currency_dynamic', (val, code, locale = 'en-US') => {
130
+ const num = Number(val);
131
+ if (isNaN(num)) return '';
132
+ return new Intl.NumberFormat(locale, {
133
+ style: 'currency',
134
+ currency: code
135
+ }).format(num);
136
+ });
137
+ ```
138
+
139
+ Template usage:
140
+
141
+ ```html
142
+ <!-- €1,234.56 -->
143
+ {{ 1234.5|currency_dynamic:"EUR", "de-DE" }}
144
+
145
+ <!-- $1,234.56 -->
146
+ {{ 1234.5|currency_dynamic:"USD" }}
147
+ ```
148
+
149
+ ## Context-Aware Filters
150
+
151
+ Filters receive the rendering `context` as the third argument, enabling context-aware transformations:
152
+
153
+ === "CommonJS"
154
+
155
+ ```javascript
156
+ const { registerFilter } = require('miki-template');
157
+
158
+ registerFilter('currency', (val, symbol, ctx) => {
159
+ const num = Number(val);
160
+ if (isNaN(num)) return '';
161
+ const sym = symbol || ctx.currencySymbol || '$';
162
+ return sym + num.toFixed(2);
163
+ });
164
+ ```
165
+
166
+ === "ES Modules"
167
+
168
+ ```javascript
169
+ import { registerFilter } from 'miki-template';
170
+
171
+ registerFilter('currency', (val, symbol, ctx) => {
172
+ const num = Number(val);
173
+ if (isNaN(num)) return '';
174
+ const sym = symbol || ctx.currencySymbol || '$';
175
+ return sym + num.toFixed(2);
176
+ });
177
+ ```
178
+
179
+ Usage:
180
+
181
+ ```html
182
+ {{ price|currency:"€" }}
183
+ <!-- The filter can also read ctx.currencySymbol for a default -->
184
+ ```
185
+
186
+ **Real-world locale-aware formatter:**
187
+
188
+ ```javascript
189
+ registerFilter('datetime', (val, format, ctx) => {
190
+ const locale = ctx.locale || 'en-US';
191
+ const d = new Date(val);
192
+ return new Intl.DateTimeFormat(locale, {
193
+ dateStyle: format === 'short' ? 'short' : 'full',
194
+ timeStyle: format === 'short' ? 'short' : undefined
195
+ }).format(d);
196
+ });
197
+ ```
198
+
199
+ ```html
200
+ {{ post.created_at|datetime:"full" }}
201
+ ```
202
+
203
+ ## SafeString Filters
204
+
205
+ Filters can return `SafeString` to prevent escaping — useful when generating HTML:
206
+
207
+ === "CommonJS"
208
+
209
+ ```javascript
210
+ const { registerFilter, markSafe } = require('miki-template');
211
+
212
+ registerFilter('badge', (val) => {
213
+ const color = val === 'active' ? 'green' : 'gray';
214
+ return markSafe(`<span class="badge badge-${color}">${val}</span>`);
215
+ });
216
+ ```
217
+
218
+ === "ES Modules"
219
+
220
+ ```javascript
221
+ import { registerFilter, markSafe } from 'miki-template';
222
+
223
+ registerFilter('badge', (val) => {
224
+ const color = val === 'active' ? 'green' : 'gray';
225
+ return markSafe(`<span class="badge badge-${color}">${val}</span>`);
226
+ });
227
+ ```
228
+
229
+ Usage:
230
+
231
+ ```html
232
+ {{ user.status|badge }}
233
+ ```
234
+
235
+ ## Async Filters
236
+
237
+ Filters can be async by returning a Promise. Use `asyncRender()` to render templates with async filters:
238
+
239
+ === "CommonJS"
240
+
241
+ ```javascript
242
+ const { registerFilter } = require('miki-template');
243
+
244
+ registerFilter('fetch_user', async (val) => {
245
+ const res = await fetch(`https://api.example.com/users/${val}`);
246
+ const data = await res.json();
247
+ return data.display_name;
248
+ });
249
+ ```
250
+
251
+ === "ES Modules"
252
+
253
+ ```javascript
254
+ import { registerFilter } from 'miki-template';
255
+
256
+ registerFilter('fetch_user', async (val) => {
257
+ const res = await fetch(`https://api.example.com/users/${val}`);
258
+ const data = await res.json();
259
+ return data.display_name;
260
+ });
261
+ ```
262
+
263
+ Usage:
264
+
265
+ === "CommonJS (asyncRender)"
266
+
267
+ ```javascript
268
+ const { asyncRender } = require('miki-template');
269
+
270
+ const html = await asyncRender('Author: {{ user.id|fetch_user }}', { user: { id: 42 } });
271
+ ```
272
+
273
+ === "ES Modules"
274
+
275
+ ```javascript
276
+ import { asyncRender } from 'miki-template';
277
+
278
+ const html = await asyncRender('Author: {{ user.id|fetch_user }}', { user: { id: 42 } });
279
+ ```
280
+
281
+ > **Note:** Async filters only work with `asyncRender()` or `compiled.asyncRender()`. Using them with `render()` or `compiled.render()` will throw.
282
+
283
+ ## Filter Registration Best Practices
284
+
285
+ 1. **Handle null/undefined gracefully** — Return empty string or a fallback value.
286
+ 2. **Return strings** — Filters should generally return string representations for template output.
287
+ 3. **Don't mutate the input** — Treat values as immutable.
288
+ 4. **Use `markSafe()` for HTML output** — Prevent auto-escaping when returning HTML.
289
+ 5. **Validate arguments** — Coerce numeric arguments with `Number()` and handle `NaN`.
290
+
291
+ ## Chaining Custom Filters
292
+
293
+ Custom filters chain the same way as built-in filters:
294
+
295
+ ```html
296
+ {{ text|trim|highlight:"important"|safe }}
297
+ ```
298
+
299
+ ```javascript
300
+ registerFilter('trim', (val) => String(val || '').trim());
301
+ registerFilter('highlight', (val, term) => {
302
+ const re = new RegExp(`(${term})`, 'gi');
303
+ return markSafe(String(val).replace(re, '<mark>$1</mark>'));
304
+ });
305
+ ```
306
+
307
+ ## Next Steps
308
+
309
+ - [Custom Tags](./custom-tags)
310
+ - [Advanced Usage](./advanced-usage)
311
+ - [API Reference: Filters](../api/filters)
@@ -0,0 +1,271 @@
1
+ # Custom Tags
2
+
3
+ Create your own template tags by registering a parser function. miki-template's tag API mirrors Django's — a tag is a parser that returns a Node object with a `render(context)` method.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Register a Simple Tag](#register-a-simple-tag)
8
+ - [Async Custom Tags](#async-custom-tags)
9
+ - [Parsing Complex Tags](#parsing-complex-tags)
10
+ - [Accessing the Parser](#accessing-the-parser)
11
+ - [Tag Registration Best Practices](#tag-registration-best-practices)
12
+
13
+ ---
14
+
15
+ ## Register a Simple Tag
16
+
17
+ === "CommonJS"
18
+
19
+ ```javascript
20
+ const { registerTag } = require('miki-template');
21
+
22
+ registerTag('hello', (tagContent, parser) => {
23
+ return {
24
+ render: (context) => 'Hello World!'
25
+ };
26
+ });
27
+ ```
28
+
29
+ === "ES Modules"
30
+
31
+ ```javascript
32
+ import { registerTag } from 'miki-template';
33
+
34
+ registerTag('hello', (tagContent, parser) => {
35
+ return {
36
+ render: (context) => 'Hello World!'
37
+ };
38
+ });
39
+ ```
40
+
41
+ Usage in templates:
42
+
43
+ ```html
44
+ {% hello %}
45
+ ```
46
+
47
+ ### Passing Arguments
48
+
49
+ ```javascript
50
+ registerTag('greet', (tagContent, parser) => {
51
+ // tagContent is the full text after the tag name: "user.name"
52
+ const varName = tagContent.trim();
53
+ return {
54
+ render: (context) => {
55
+ const value = context.get(varName);
56
+ return `Hello, ${value}!`;
57
+ }
58
+ };
59
+ });
60
+ ```
61
+
62
+ ```html
63
+ {% greet user.name %}
64
+ ```
65
+
66
+ ## Returning a Node Class
67
+
68
+ For more complex tags, return a Node class instance:
69
+
70
+ === "CommonJS"
71
+
72
+ ```javascript
73
+ const { registerTag } = require('miki-template');
74
+
75
+ class GreetNode {
76
+ constructor(varName) {
77
+ this.varName = varName;
78
+ }
79
+ render(context) {
80
+ const value = context.get(this.varName);
81
+ return `Hello, ${value || 'Guest'}!`;
82
+ }
83
+ }
84
+
85
+ registerTag('greet', (tagContent, parser) => {
86
+ const varName = tagContent.trim();
87
+ return new GreetNode(varName);
88
+ });
89
+ ```
90
+
91
+ === "ES Modules"
92
+
93
+ ```javascript
94
+ import { registerTag } from 'miki-template';
95
+
96
+ class GreetNode {
97
+ constructor(varName) {
98
+ this.varName = varName;
99
+ }
100
+ render(context) {
101
+ const value = context.get(this.varName);
102
+ return `Hello, ${value || 'Guest'}!`;
103
+ }
104
+ }
105
+
106
+ registerTag('greet', (tagContent, parser) => {
107
+ const varName = tagContent.trim();
108
+ return new GreetNode(varName);
109
+ });
110
+ ```
111
+
112
+ ## Async Custom Tags
113
+
114
+ If your `render()` method returns a Promise, the template must be rendered with `asyncRender()`:
115
+
116
+ === "CommonJS"
117
+
118
+ ```javascript
119
+ const { registerTag, asyncRender } = require('miki-template');
120
+
121
+ registerTag('fetch_greeting', (tagContent, parser) => {
122
+ const urlVar = tagContent.trim();
123
+ return {
124
+ async render(context) {
125
+ const url = context.get(urlVar);
126
+ const res = await fetch(url);
127
+ const data = await res.json();
128
+ return data.message;
129
+ }
130
+ };
131
+ });
132
+
133
+ // Must use asyncRender
134
+ const html = await asyncRender('{% fetch_greeting api_url %}', { api_url: 'https://...' });
135
+ ```
136
+
137
+ === "ES Modules"
138
+
139
+ ```javascript
140
+ import { registerTag, asyncRender } from 'miki-template';
141
+
142
+ registerTag('fetch_greeting', (tagContent, parser) => {
143
+ const urlVar = tagContent.trim();
144
+ return {
145
+ async render(context) {
146
+ const url = context.get(urlVar);
147
+ const res = await fetch(url);
148
+ const data = await res.json();
149
+ return data.message;
150
+ }
151
+ };
152
+ });
153
+
154
+ const html = await asyncRender('{% fetch_greeting api_url %}', { api_url: 'https://...' });
155
+ ```
156
+
157
+ ## Parsing Complex Tags
158
+
159
+ Use the `parser` object to consume tokens and build multi-part tags:
160
+
161
+ === "CommonJS"
162
+
163
+ ```javascript
164
+ const { registerTag } = require('miki-template');
165
+
166
+ registerTag('panel', (tagContent, parser) => {
167
+ const classes = tagContent.trim() || '';
168
+ const nodelist = parser.parse(['endpanel']);
169
+ parser.skipTag(); // consume endpanel
170
+
171
+ return {
172
+ render: (context) => {
173
+ const body = nodelist.map(n => n.render(context)).join('');
174
+ return `<div class="panel ${classes}">${body}</div>`;
175
+ }
176
+ };
177
+ });
178
+ ```
179
+
180
+ === "ES Modules"
181
+
182
+ ```javascript
183
+ import { registerTag } from 'miki-template';
184
+
185
+ registerTag('panel', (tagContent, parser) => {
186
+ const classes = tagContent.trim() || '';
187
+ const nodelist = parser.parse(['endpanel']);
188
+ parser.skipTag();
189
+
190
+ return {
191
+ render: (context) => {
192
+ const body = nodelist.map(n => n.render(context)).join('');
193
+ return `<div class="panel ${classes}">${body}</div>`;
194
+ }
195
+ };
196
+ });
197
+ ```
198
+
199
+ Usage with nested content:
200
+
201
+ ```html
202
+ {% panel "card" %}
203
+ <h2>{{ title }}</h2>
204
+ <p>{{ description }}</p>
205
+ {% endpanel %}
206
+ ```
207
+
208
+ ### Real-World Example: Cache Tag
209
+
210
+ === "CommonJS"
211
+
212
+ ```javascript
213
+ const { registerTag } = require('miki-template');
214
+
215
+ registerTag('cache_block', (tagContent, parser) => {
216
+ const [key, ...rest] = tagContent.trim().split(/\s+/);
217
+ const nodelist = parser.parse(['endcache_block']);
218
+ parser.skipTag();
219
+
220
+ return {
221
+ render: (context) => {
222
+ const cacheKey = key;
223
+ const cache = context.get('cache') || global.__cache__;
224
+ if (!cache) return nodelist.map(n => n.render(context)).join('');
225
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
226
+ const output = nodelist.map(n => n.render(context)).join('');
227
+ cache.set(cacheKey, output, rest[0] || 300);
228
+ return output;
229
+ }
230
+ };
231
+ });
232
+ ```
233
+
234
+ === "ES Modules"
235
+
236
+ ```javascript
237
+ import { registerTag } from 'miki-template';
238
+
239
+ registerTag('cache_block', (tagContent, parser) => {
240
+ const [key, ...rest] = tagContent.trim().split(/\s+/);
241
+ const nodelist = parser.parse(['endcache_block']);
242
+ parser.skipTag();
243
+
244
+ return {
245
+ render: (context) => {
246
+ const cacheKey = key;
247
+ const cache = context.get('cache') || global.__cache__;
248
+ if (!cache) return nodelist.map(n => n.render(context)).join('');
249
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
250
+ const output = nodelist.map(n => n.render(context)).join('');
251
+ cache.set(cacheKey, output, rest[0] || 300);
252
+ return output;
253
+ }
254
+ };
255
+ });
256
+ ```
257
+
258
+ ## Tag Registration Best Practices
259
+
260
+ 1. **Return objects with `render(context)`** — the render signature must accept a context object.
261
+ 2. **Use `parser.parse([...terminators])`** for tags with bodies — this lets the parser consume nested content correctly.
262
+ 3. **Always call `parser.skipTag()`** after `parser.parse` to consume the end tag.
263
+ 4. **Handle whitespace** — `tagContent.trim()` for single-argument tags.
264
+ 5. **Async tags need asyncRender** — return a Promise from `render()` and use `asyncRender()` to render.
265
+ 6. **Access context values** — use `context.get('key')` or `context.resolve('expr')`.
266
+
267
+ ## Next Steps
268
+
269
+ - [Built-in Tags Reference](../api/tags)
270
+ - [Custom Filters](./custom-filters)
271
+ - [Guide: Tags](./tags)