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.
- package/.github/workflows/ci.yml +14 -10
- package/.github/workflows/docs.yml +105 -0
- package/.github/workflows/npm-publish-github-packages.yml +36 -0
- package/README.md +142 -26
- package/assets/logo.png +0 -0
- package/benchmarks/ejs-results.json +17 -0
- package/benchmarks/ejs.js +36 -0
- package/benchmarks/handlebars-results.json +17 -0
- package/benchmarks/handlebars.js +48 -0
- package/benchmarks/miki-results.json +17 -0
- package/benchmarks/miki.js +36 -0
- package/benchmarks/pug-results.json +17 -0
- package/benchmarks/pug.js +36 -0
- package/benchmarks/run.js +69 -37
- 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/live-test/integrations/elysia-example.js +16 -0
- package/live-test/integrations/express-example.js +24 -0
- package/live-test/integrations/fastify-example.js +20 -0
- package/live-test/integrations/hono-example.js +16 -0
- package/live-test/integrations/koa-example.js +30 -0
- package/live-test/integrations/nestjs-example.js +25 -0
- package/live-test/integrations/smoke-test.js +166 -0
- package/live-test/integrations/tsed-example.js +23 -0
- package/live-test/package-lock.json +235 -0
- package/live-test/package.json +4 -0
- package/live-test/views/home.html +17 -0
- package/mkdocs.yml +217 -0
- package/overrides/main.html +26 -0
- package/overrides/partials/footer.html +9 -0
- package/package.json +16 -6
- package/requirements-docs.txt +1 -0
- package/tests/integration/partial-render.test.cjs +13 -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/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,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)
|