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.
- package/.github/workflows/ci.yml +13 -37
- package/.github/workflows/docs.yml +105 -0
- package/.github/workflows/release.yml +6 -0
- package/README.md +69 -14
- package/assets/logo.png +0 -0
- package/benchmarks/ejs-results.json +4 -4
- package/benchmarks/handlebars-results.json +6 -6
- package/benchmarks/miki-results.json +4 -4
- package/benchmarks/pug-results.json +4 -4
- 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/mkdocs.yml +217 -0
- package/overrides/main.html +26 -0
- package/overrides/partials/footer.html +9 -0
- package/package.json +4 -2
- package/requirements-docs.txt +1 -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/integrations.md +0 -214
- 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,257 @@
|
|
|
1
|
+
# Context Processors
|
|
2
|
+
|
|
3
|
+
Context processors are functions that automatically inject variables into every template render. This follows Django's context processor pattern — ideal for injecting global settings, user data, or feature flags.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [How Context Processors Work](#how-context-processors-work)
|
|
8
|
+
- [Register a Context Processor](#register-a-context-processor)
|
|
9
|
+
- [Context Processor Signature](#context-processor-signature)
|
|
10
|
+
- [Overriding Behavior](#overriding-behavior)
|
|
11
|
+
- [Real-World Examples](#real-world-examples)
|
|
12
|
+
- [Clearing Processors](#clearing-processors)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## How Context Processors Work
|
|
17
|
+
|
|
18
|
+
Context processors run on **every render** (both `render()` and `compile().render()`). They return an object of key/value pairs that are merged into the rendering context **before** your template's local context is applied.
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
graph LR
|
|
22
|
+
A[Your Context] --> B[Apply Processors]
|
|
23
|
+
B --> C[Processor adds global vars]
|
|
24
|
+
C --> D[Your Context wins]
|
|
25
|
+
D --> E[Template renders]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Key behavior:** Your explicit context values **always win** over processor values. This means you can override global defaults per-render without fighting the processor.
|
|
29
|
+
|
|
30
|
+
## Register a Context Processor
|
|
31
|
+
|
|
32
|
+
=== "CommonJS"
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
const { registerContextProcessor } = require('miki-template');
|
|
36
|
+
|
|
37
|
+
registerContextProcessor((context) => {
|
|
38
|
+
return {
|
|
39
|
+
siteName: 'My App',
|
|
40
|
+
currentYear: new Date().getFullYear(),
|
|
41
|
+
debug: process.env.NODE_ENV !== 'production'
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
=== "ES Modules"
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
import { registerContextProcessor } from 'miki-template';
|
|
50
|
+
|
|
51
|
+
registerContextProcessor((context) => {
|
|
52
|
+
return {
|
|
53
|
+
siteName: 'My App',
|
|
54
|
+
currentYear: new Date().getFullYear(),
|
|
55
|
+
debug: process.env.NODE_ENV !== 'production'
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Multiple Processors
|
|
61
|
+
|
|
62
|
+
You can register multiple processors. They run in order — later processors can overwrite earlier ones:
|
|
63
|
+
|
|
64
|
+
=== "CommonJS"
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
const { registerContextProcessor } = require('miki-template');
|
|
68
|
+
|
|
69
|
+
registerContextProcessor(() => ({ siteName: 'My App' }));
|
|
70
|
+
registerContextProcessor(() => ({ version: '2.0.0' }));
|
|
71
|
+
registerContextProcessor(() => ({
|
|
72
|
+
footerText: '© 2024 My App. All rights reserved.'
|
|
73
|
+
}));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
=== "ES Modules"
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
import { registerContextProcessor } from 'miki-template';
|
|
80
|
+
|
|
81
|
+
registerContextProcessor(() => ({ siteName: 'My App' }));
|
|
82
|
+
registerContextProcessor(() => ({ version: '2.0.0' }));
|
|
83
|
+
registerContextProcessor(() => ({
|
|
84
|
+
footerText: '© 2024 My App. All rights reserved.'
|
|
85
|
+
}));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Context Processor Signature
|
|
89
|
+
|
|
90
|
+
The processor function receives the rendering `context` as an argument and must return a plain object:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
registerContextProcessor((context) => {
|
|
94
|
+
// context is the full Context object — you can inspect contextObj
|
|
95
|
+
// but don't mutate it
|
|
96
|
+
return {
|
|
97
|
+
key: 'value'
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Important:** If a processor returns `null`, `undefined`, or nothing, it's treated as returning an empty object `{}`. Processors must **not** return a Promise — if you need async data, compute it before rendering and pass it as context.
|
|
103
|
+
|
|
104
|
+
## Overriding Behavior
|
|
105
|
+
|
|
106
|
+
Since your explicit context always wins, you can override global defaults per-render:
|
|
107
|
+
|
|
108
|
+
=== "CommonJS"
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const { render } = require('miki-template');
|
|
112
|
+
|
|
113
|
+
// processor sets debug: false
|
|
114
|
+
// but this render overrides it:
|
|
115
|
+
render(template, { debug: true });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
=== "ES Modules"
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
import { render } from 'miki-template';
|
|
122
|
+
|
|
123
|
+
render(template, { debug: true });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Real-World Examples
|
|
127
|
+
|
|
128
|
+
### App-wide Settings
|
|
129
|
+
|
|
130
|
+
=== "CommonJS"
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const { registerContextProcessor } = require('miki-template');
|
|
134
|
+
|
|
135
|
+
registerContextProcessor(() => ({
|
|
136
|
+
appName: process.env.APP_NAME || 'MyApp',
|
|
137
|
+
appVersion: require('./package.json').version,
|
|
138
|
+
environment: process.env.NODE_ENV || 'development',
|
|
139
|
+
apiUrl: process.env.API_URL || 'http://localhost:3000/api',
|
|
140
|
+
assetsUrl: process.env.ASSETS_URL || '/assets'
|
|
141
|
+
}));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
=== "ES Modules"
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import { registerContextProcessor } from 'miki-template';
|
|
148
|
+
import pkg from './package.json' with { type: 'json' };
|
|
149
|
+
|
|
150
|
+
registerContextProcessor(() => ({
|
|
151
|
+
appName: process.env.APP_NAME || 'MyApp',
|
|
152
|
+
appVersion: pkg.version,
|
|
153
|
+
environment: process.env.NODE_ENV || 'development',
|
|
154
|
+
apiUrl: process.env.API_URL || 'http://localhost:3000/api',
|
|
155
|
+
assetsUrl: process.env.ASSETS_URL || '/assets'
|
|
156
|
+
}));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### User Authentication
|
|
160
|
+
|
|
161
|
+
=== "CommonJS"
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
const { registerContextProcessor } = require('miki-template');
|
|
165
|
+
|
|
166
|
+
registerContextProcessor((context) => {
|
|
167
|
+
const user = context.get('user');
|
|
168
|
+
if (!user) return {};
|
|
169
|
+
return {
|
|
170
|
+
user_name: user.name,
|
|
171
|
+
user_avatar: user.avatar || '/default-avatar.png',
|
|
172
|
+
user_is_admin: user.isAdmin || false
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
=== "ES Modules"
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
import { registerContextProcessor } from 'miki-template';
|
|
181
|
+
|
|
182
|
+
registerContextProcessor((context) => {
|
|
183
|
+
const user = context.get('user');
|
|
184
|
+
if (!user) return {};
|
|
185
|
+
return {
|
|
186
|
+
user_name: user.name,
|
|
187
|
+
user_avatar: user.avatar || '/default-avatar.png',
|
|
188
|
+
user_is_admin: user.isAdmin || false
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Feature Flags
|
|
194
|
+
|
|
195
|
+
=== "CommonJS"
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
const { registerContextProcessor } = require('miki-template');
|
|
199
|
+
|
|
200
|
+
registerContextProcessor(() => ({
|
|
201
|
+
flags: {
|
|
202
|
+
newDashboard: process.env.FEATURE_NEW_DASHBOARD === 'true',
|
|
203
|
+
betaFeature: process.env.FEATURE_BETA === 'true',
|
|
204
|
+
darkModeDefault: process.env.FEATURE_DARK_MODE === 'true'
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
=== "ES Modules"
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
import { registerContextProcessor } from 'miki-template';
|
|
213
|
+
|
|
214
|
+
registerContextProcessor(() => ({
|
|
215
|
+
flags: {
|
|
216
|
+
newDashboard: process.env.FEATURE_NEW_DASHBOARD === 'true',
|
|
217
|
+
betaFeature: process.env.FEATURE_BETA === 'true',
|
|
218
|
+
darkModeDefault: process.env.FEATURE_DARK_MODE === 'true'
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Template usage:
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
{% if flags.newDashboard %}
|
|
227
|
+
<a href="/new-dashboard">New Dashboard</a>
|
|
228
|
+
{% else %}
|
|
229
|
+
<a href="/dashboard">Classic Dashboard</a>
|
|
230
|
+
{% endif %}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Clearing Processors
|
|
234
|
+
|
|
235
|
+
Clear all registered processors (useful in tests or dynamic configuration):
|
|
236
|
+
|
|
237
|
+
=== "CommonJS"
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
const { clearContextProcessors } = require('miki-template');
|
|
241
|
+
|
|
242
|
+
clearContextProcessors();
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
=== "ES Modules"
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
import { clearContextProcessors } from 'miki-template';
|
|
249
|
+
|
|
250
|
+
clearContextProcessors();
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Next Steps
|
|
254
|
+
|
|
255
|
+
- [Advanced Usage: Context Processors](./advanced-usage)
|
|
256
|
+
- [Async Rendering](./async-rendering)
|
|
257
|
+
- [API Reference: Context Processors](../api/context-processors)
|
|
@@ -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)
|