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,308 @@
1
+ # Async Rendering
2
+
3
+ miki-template supports async rendering for templates that use async filters, async custom tags, or async library components. Use `asyncRender()` instead of `render()` to await these operations.
4
+
5
+ ## Table of Contents
6
+
7
+ - [When to Use Async Rendering](#when-to-use-async-rendering)
8
+ - [asyncRender()](#asyncrender)
9
+ - [compiled.asyncRender()](#compiledasyncrender)
10
+ - [Async Filters](#async-filters)
11
+ - [Async Custom Tags](#async-custom-tags)
12
+ - [Express Async Engine](#express-async-engine)
13
+
14
+ ---
15
+
16
+ ## When to Use Async Rendering
17
+
18
+ Use `asyncRender()` when your templates contain any of the following:
19
+
20
+ - **Async filters** — filters that return Promises
21
+ - **Async custom tags** — custom tags whose `render()` returns a Promise
22
+ - **Async library components** — i18n translations loaded dynamically
23
+ - **Async helpers** — helpers that perform I/O
24
+
25
+ If you use async features with `render()` or `compiled.render()`, the engine throws:
26
+
27
+ > Async node encountered during sync render. Use asyncRender() instead.
28
+
29
+ ## asyncRender()
30
+
31
+ === "CommonJS"
32
+
33
+ ```javascript
34
+ const { asyncRender } = require('miki-template');
35
+
36
+ const html = await asyncRender('Hello, {{ name }}!', { name: 'World' });
37
+ console.log(html);
38
+ // → "Hello, World!"
39
+ ```
40
+
41
+ === "ES Modules"
42
+
43
+ ```javascript
44
+ import { asyncRender } from 'miki-template';
45
+
46
+ const html = await asyncRender('Hello, {{ name }}!', { name: 'World' });
47
+ console.log(html);
48
+ // → "Hello, World!"
49
+ ```
50
+
51
+ ### With Views and Partials
52
+
53
+ === "CommonJS"
54
+
55
+ ```javascript
56
+ const { asyncRender } = require('miki-template');
57
+
58
+ // Render a single partial from a file
59
+ const html = await asyncRender('home#card', { user: userData }, {
60
+ views: './templates'
61
+ });
62
+ ```
63
+
64
+ === "ES Modules"
65
+
66
+ ```javascript
67
+ import { asyncRender } from 'miki-template';
68
+
69
+ const html = await asyncRender('home#card', { user: userData }, {
70
+ views: './templates'
71
+ });
72
+ ```
73
+
74
+ ## compiled.asyncRender()
75
+
76
+ When you pre-compile a template, the returned object has `asyncRender()` and `asyncRenderWith()` methods:
77
+
78
+ === "CommonJS"
79
+
80
+ ```javascript
81
+ const { compile } = require('miki-template');
82
+
83
+ const template = `
84
+ {% load markdown %}
85
+ {{ content|markdown }}
86
+ `;
87
+
88
+ const compiled = compile(template);
89
+ const html = await compiled.asyncRender({ content: '# Hello World' });
90
+ ```
91
+
92
+ === "ES Modules"
93
+
94
+ ```javascript
95
+ import { compile } from 'miki-template';
96
+
97
+ const template = `
98
+ {% load markdown %}
99
+ {{ content|markdown }}
100
+ `;
101
+
102
+ const compiled = compile(template);
103
+ const html = await compiled.asyncRender({ content: '# Hello World' });
104
+ ```
105
+
106
+ ## Async Filters
107
+
108
+ Filters that return Promises are automatically awaited when using `asyncRender()`:
109
+
110
+ === "CommonJS"
111
+
112
+ ```javascript
113
+ const { registerFilter, asyncRender } = require('miki-template');
114
+
115
+ registerFilter('to_upper', (val) => val.toUpperCase());
116
+ registerFilter('fetch_url', async (url) => {
117
+ const res = await fetch(url);
118
+ return res.text();
119
+ });
120
+ ```
121
+
122
+ === "ES Modules"
123
+
124
+ ```javascript
125
+ import { registerFilter, asyncRender } from 'miki-template';
126
+
127
+ registerFilter('to_upper', (val) => val.toUpperCase());
128
+ registerFilter('fetch_url', async (url) => {
129
+ const res = await fetch(url);
130
+ return res.text();
131
+ });
132
+ ```
133
+
134
+ Usage:
135
+
136
+ ```html
137
+ {{ api_endpoint|fetch_url }}
138
+ ```
139
+
140
+ **Real-world CMS content fetch:**
141
+
142
+ === "CommonJS"
143
+
144
+ ```javascript
145
+ const { registerFilter, asyncRender } = require('miki-template');
146
+
147
+ registerFilter('cms_content', async (id) => {
148
+ const res = await fetch(`https://cms.example.com/api/content/${id}`);
149
+ const data = await res.json();
150
+ return data.html;
151
+ });
152
+
153
+ const html = await asyncRender(
154
+ '{% autoescape off %}{{ page_id|cms_content }}{% endautoescape %}',
155
+ { page_id: 'about' }
156
+ );
157
+ ```
158
+
159
+ === "ES Modules"
160
+
161
+ ```javascript
162
+ import { registerFilter, asyncRender } from 'miki-template';
163
+
164
+ registerFilter('cms_content', async (id) => {
165
+ const res = await fetch(`https://cms.example.com/api/content/${id}`);
166
+ const data = await res.json();
167
+ return data.html;
168
+ });
169
+
170
+ const html = await asyncRender(
171
+ '{% autoescape off %}{{ page_id|cms_content }}{% endautoescape %}',
172
+ { page_id: 'about' }
173
+ );
174
+ ```
175
+
176
+ ## Async Custom Tags
177
+
178
+ Custom tags whose `render()` returns a Promise work with `asyncRender()`:
179
+
180
+ === "CommonJS"
181
+
182
+ ```javascript
183
+ const { registerTag, asyncRender } = require('miki-template');
184
+
185
+ registerTag('api_data', (tagContent, parser) => {
186
+ const endpoint = tagContent.trim();
187
+ return {
188
+ async render(context) {
189
+ const res = await fetch(context.get(endpoint));
190
+ const data = await res.json();
191
+ return JSON.stringify(data, null, 2);
192
+ }
193
+ };
194
+ });
195
+
196
+ const html = await asyncRender(
197
+ '{% api_data api_url %}',
198
+ { api_url: 'https://api.example.com/users' }
199
+ );
200
+ ```
201
+
202
+ === "ES Modules"
203
+
204
+ ```javascript
205
+ import { registerTag, asyncRender } from 'miki-template';
206
+
207
+ registerTag('api_data', (tagContent, parser) => {
208
+ const endpoint = tagContent.trim();
209
+ return {
210
+ async render(context) {
211
+ const res = await fetch(context.get(endpoint));
212
+ const data = await res.json();
213
+ return JSON.stringify(data, null, 2);
214
+ }
215
+ };
216
+ });
217
+
218
+ const html = await asyncRender(
219
+ '{% api_data api_url %}',
220
+ { api_url: 'https://api.example.com/users' }
221
+ );
222
+ ```
223
+
224
+ ## Express Async Engine
225
+
226
+ For Express apps with async templates, use `__expressAsync` or `express({ async: true })`:
227
+
228
+ === "CommonJS"
229
+
230
+ ```javascript
231
+ const express = require('express');
232
+ const miki = require('miki-template');
233
+
234
+ const app = express();
235
+ app.engine('html', miki.express({ async: true }));
236
+ app.set('view engine', 'html');
237
+ app.set('views', './views');
238
+ ```
239
+
240
+ === "ES Modules"
241
+
242
+ ```javascript
243
+ import express from 'express';
244
+ import miki from 'miki-template';
245
+
246
+ const app = express();
247
+ app.engine('html', miki.express({ async: true }));
248
+ app.set('view engine', 'html');
249
+ app.set('views', './views');
250
+ ```
251
+
252
+ ### Express 5+ Native Promise Support
253
+
254
+ If you're using Express 5 (which supports Promise-based view engines), use `__expressAsync` directly:
255
+
256
+ === "CommonJS"
257
+
258
+ ```javascript
259
+ const express = require('express');
260
+ const miki = require('miki-template');
261
+
262
+ app.engine('html', miki.__expressAsync);
263
+ ```
264
+
265
+ === "ES Modules"
266
+
267
+ ```javascript
268
+ import express from 'express';
269
+ import miki from 'miki-template';
270
+
271
+ app.engine('html', miki.__expressAsync);
272
+ ```
273
+
274
+ ## asyncRenderWith()
275
+
276
+ Override compile-time options at render time:
277
+
278
+ === "CommonJS"
279
+
280
+ ```javascript
281
+ const { compile } = require('miki-template');
282
+
283
+ const compiled = compile(template, { views: './views' });
284
+
285
+ const html = await compiled.asyncRenderWith(
286
+ { user: userData },
287
+ { views: './other-views', customOption: true }
288
+ );
289
+ ```
290
+
291
+ === "ES Modules"
292
+
293
+ ```javascript
294
+ import { compile } from 'miki-template';
295
+
296
+ const compiled = compile(template, { views: './views' });
297
+
298
+ const html = await compiled.asyncRenderWith(
299
+ { user: userData },
300
+ { views: './other-views', customOption: true }
301
+ );
302
+ ```
303
+
304
+ ## Next Steps
305
+
306
+ - [Custom Filters: Async Filters](./custom-filters#async-filters)
307
+ - [Custom Tags: Async Custom Tags](./custom-tags#async-custom-tags)
308
+ - [API Reference: asyncRender](../api/async-render)
@@ -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)