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
@@ -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)