miki-template 1.3.3 → 1.3.6

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 (101) hide show
  1. package/.github/release-notes/v1.3.1.md +55 -55
  2. package/.github/release-notes/v1.3.3.md +77 -0
  3. package/.github/workflows/ci.yml +38 -54
  4. package/.github/workflows/release.yml +106 -0
  5. package/AGENT.md +71 -71
  6. package/API_REFERENCE.md +314 -314
  7. package/CHANGELOG.md +173 -169
  8. package/CODE_OF_CONDUCT.md +14 -14
  9. package/CONTRIBUTING.md +27 -27
  10. package/README.md +342 -321
  11. package/ROADMAP.md +40 -40
  12. package/benchmarks/report.json +16 -16
  13. package/benchmarks/run.js +49 -49
  14. package/benchmarks/stress.mjs +647 -647
  15. package/benchmarks/templates/large.dtpl +7 -7
  16. package/benchmarks/templates/medium.dtpl +3 -3
  17. package/benchmarks/templates/small.dtpl +7 -7
  18. package/context/component.md +109 -109
  19. package/context/prd.md +131 -131
  20. package/context/project-structure.md +33 -33
  21. package/dir/base.html +22 -22
  22. package/dir/cmpnt.html +10 -10
  23. package/dir/footer.html +2 -2
  24. package/dir/home.html +80 -80
  25. package/dir/index.html +80 -0
  26. package/dir/navbar.html +8 -8
  27. package/docs/README.md +18 -18
  28. package/docs/advanced_usage.md +71 -71
  29. package/docs/api.md +119 -119
  30. package/docs/filters.md +708 -708
  31. package/docs/installation.md +106 -106
  32. package/docs/overview.md +57 -57
  33. package/docs/partialdef.md +70 -70
  34. package/docs/security.md +27 -27
  35. package/docs/tags.md +673 -673
  36. package/docs/usage.md +646 -646
  37. package/eslint.config.mjs +42 -42
  38. package/ex.mjs +32 -32
  39. package/miki-template-extension/.github/workflows/ci.yml +116 -116
  40. package/miki-template-extension/.vscodeignore +7 -7
  41. package/miki-template-extension/CHANGELOG.md +99 -99
  42. package/miki-template-extension/LICENSE +21 -21
  43. package/miki-template-extension/README.md +273 -273
  44. package/miki-template-extension/extension.js +1013 -1013
  45. package/miki-template-extension/icon.svg +10 -10
  46. package/miki-template-extension/package.json +280 -280
  47. package/miki-template-extension/snippets/miki-template.json +717 -717
  48. package/miki-template-extension/syntaxes/language-configuration.json +114 -114
  49. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -355
  50. package/miki-template-extension/tests/grammar-tests.json +162 -162
  51. package/miki-template-extension/tests/run-grammar-tests.js +82 -82
  52. package/package.json +37 -34
  53. package/sample-app/package-lock.json +901 -0
  54. package/sample-app/package.json +9 -0
  55. package/sample-app/server.js +14 -0
  56. package/sample-app/views/index.html +1 -0
  57. package/scripts/build-vsix.js +129 -129
  58. package/scripts/build-vsix.ps1 +15 -15
  59. package/snippets/miki-template.json +177 -177
  60. package/src/asyncRender.js +20 -20
  61. package/src/cache.js +80 -80
  62. package/src/context.js +126 -126
  63. package/src/context_processors.js +48 -48
  64. package/src/esm.mjs +84 -84
  65. package/src/filters.js +975 -975
  66. package/src/i18n.js +171 -171
  67. package/src/index.js +974 -940
  68. package/src/lexer.js +114 -114
  69. package/src/libraries.js +371 -371
  70. package/src/parser.js +270 -270
  71. package/src/security.js +53 -53
  72. package/src/tags/control.js +719 -719
  73. package/src/tags/extra.js +154 -154
  74. package/src/tags/helpers.js +26 -26
  75. package/src/tags/i18n.js +256 -256
  76. package/src/tags/inheritance.js +335 -335
  77. package/src/tags/registry.js +18 -18
  78. package/src/tags/util.js +400 -400
  79. package/src/types.d.ts +107 -107
  80. package/syntaxes/language-configuration.json +26 -26
  81. package/syntaxes/miki-template.tmLanguage.json +146 -146
  82. package/tests/asyncRender.test.js +17 -17
  83. package/tests/base.html +6 -6
  84. package/tests/child.html +3 -3
  85. package/tests/context_processors.test.js +13 -13
  86. package/tests/esm.test.mjs +61 -61
  87. package/tests/filters.test.js +254 -254
  88. package/tests/include_security.test.js +9 -9
  89. package/tests/integration/README.md +32 -32
  90. package/tests/integration/features.test.cjs +1681 -1681
  91. package/tests/integration/features.test.mjs +1697 -1697
  92. package/tests/integration/templates/base.miki +6 -6
  93. package/tests/integration/templates/child.miki +6 -6
  94. package/tests/integration/templates/index.html +17 -17
  95. package/tests/lexer.test.js +45 -45
  96. package/tests/parser.test.js +57 -57
  97. package/tests/partial.html +1 -1
  98. package/tests/partialdef.test.js +79 -79
  99. package/tests/production_checks.js +57 -57
  100. package/tests/security.test.js +28 -28
  101. package/tests/tags.test.js +233 -233
package/README.md CHANGED
@@ -1,321 +1,342 @@
1
- # miki-template
2
- ![miki-template banner](assets/banner.png)
3
- ![npm version](https://img.shields.io/npm/v/miki-template.svg) ![CI](https://github.com/your-repo/miki-template/workflows/ci.yml/badge.svg)
4
- A robust, production-ready template engine that brings **Django's template language** features and syntax to Node.js and Express, fully compliant with modern JavaScript (ES6+), CommonJS, and **ESM** (`import`) support.
5
-
6
- ---
7
-
8
- ## 🚀 Features
9
-
10
- - **One-line Express integration**: `miki.setupExpress(app, { extension: 'html', views: dir })` — wires the engine, views directory, and a `res.render` shim that makes `res.render('home#card', ...)` Just Work for HTMX-style partial responses. **No boilerplate, no extra middleware.**
11
- - **Partial responses out of the box**: `{% partialdef %}` blocks can be rendered by name with `res.render('view#partial', ...)`, `miki.expressPartialRenderer()` middleware (`res.renderPartial(...)`), or `renderPartialFromSource(...)`.
12
- - **Full Syntax Parity**: Supports variables, dotted lookups, filters (`|`), and block tags (`{% %}`).
13
- - **Template Inheritance**: Multi-level inheritance with `extends`, block overrides, and `{{ block.super }}` support.
14
- - **Built-in libraries**: `humanize`, `cache`, and `lorem` ship pre-activated. `{% lorem 5 p %}` works without `{% load lorem %}`.
15
- - **ESM & CommonJS**: Works seamlessly with both `import` and `require` syntax.
16
- - **Security by Default**: Auto-escaping enabled by default with a `SafeString` wrapper.
17
- - **CSRF & CSP Support**: Native tags for `{% csrf_token %}` and `{% csp_nonce_attr %}` to keep apps secure out-of-the-box.
18
- - **Block Partials**: Render a single block from a compiled template via `compiled.renderBlock('block_name')`.
19
- - **Async Rendering**: Support for async filters/tags with `asyncRender()`.
20
- - **Extensible API**: Easy registration for custom tags and filters.
21
- - **No Unsafe Code Execution**: Evaluates expressions securely without using `eval()`.
22
- - **Editor Support**: First-class syntax highlighting and snippets for VS Code, Sublime Text, Atom, and TextMate-compatible editors.
23
-
24
- ### VS Code
25
-
26
- #### Option A: Install the official extension (recommended)
27
-
28
- Search for **miki-template** in the VS Code Marketplace, or install from the command line:
29
-
30
- ```bash
31
- code --install-extension miki-template
32
- ```
33
-
34
- #### Option B: Manual install from this repo
35
-
36
- 1. Copy the `syntaxes/` and `snippets/` folders from this repo.
37
- 2. In VS Code, run **Preferences: Configure File Associations** and associate `*.miki` with `miki-template`.
38
- 3. Or add a workspace-level `.vscode/settings.json`:
39
-
40
- ```json
41
- {
42
- "files.associations": {
43
- "*.miki": "miki-template"
44
- }
45
- }
46
- ```
47
-
48
- ### Sublime Text / Atom / TextMate
49
-
50
- Drop the `syntaxes/miki-template.tmLanguage.json` file into your editor’s `Packages/User/` folder and associate it with the `.miki` extension.
51
-
52
- ---
53
-
54
- ## 📚 Documentation
55
-
56
- - [Installation](docs/installation.md)
57
- - [Usage Guide](docs/usage.md)
58
- - [Tags Reference](docs/tags.md)
59
- - [Filters Reference](docs/filters.md)
60
- - [Security](docs/security.md)
61
- - [API Reference](docs/api.md)
62
- - [Partial Definitions](docs/partialdef.md)
63
- - [Contributing](docs/advanced_usage.md)
64
-
65
- ---
66
-
67
- ## 📦 Installation
68
-
69
- ```bash
70
- npm install miki-template
71
- ```
72
-
73
- ---
74
-
75
- ## 🛠️ Quick Start
76
-
77
- ### CommonJS (require)
78
-
79
- ```javascript
80
- const { render, compile, __express, SafeString, markSafe } = require('miki-template');
81
-
82
- const template = 'Hello {{ user.name|title }}! Roles: {{ user.roles|join:", " }}';
83
- const context = {
84
- user: {
85
- name: 'miki coder',
86
- roles: ['admin', 'developer']
87
- }
88
- };
89
-
90
- const result = render(template, context);
91
- console.log(result); // Output: "Hello Miki Coder! Roles: admin, developer"
92
- ```
93
-
94
- ### ES Modules (import)
95
-
96
- ```javascript
97
- // Named imports
98
- import { render, compile, __express, SafeString, markSafe } from 'miki-template';
99
-
100
- // Or default import (gets all exports)
101
- import miki from 'miki-template';
102
- const { render: mikiRender } = miki;
103
-
104
- const template = 'Hello {{ user.name|title }}!';
105
- const result = render(template, { user: { name: 'world' } });
106
- console.log(result); // Output: "Hello World!"
107
- ```
108
-
109
- > **Note:** For ESM in Node.js, either name your files `.mjs` or add `"type": "module"` to your `package.json`.
110
-
111
- ### Express Integration
112
-
113
- **The recommended, one-line setup** — wires the view engine, views directory, and partial responses in a single call:
114
-
115
- ```javascript
116
- const express = require('express');
117
- const miki = require('miki-template');
118
-
119
- const app = express();
120
- miki.setupExpress(app, { extension: 'html', views: './views' });
121
-
122
- // Full page
123
- app.get('/', (req, res) => res.render('home', { user: req.user }));
124
-
125
- // HTMX / partial response — just append `#partialName` to the view name
126
- app.get('/partials/:name', (req, res) =>
127
- res.render(`home#${req.params.name}`, { user: req.user })
128
- );
129
-
130
- app.listen(3000);
131
- ```
132
-
133
- > `setupExpress` calls `app.engine()`, `app.set('views')`, and `app.set('view engine')` for you, and patches `res.render` so `view#partial` is dispatched to the partial renderer (not the file system). It works equally well for `.miki` files — just pass `extension: 'miki'`.
134
-
135
- **The classic, fully manual setup still works** if you prefer it:
136
-
137
- ```javascript
138
- const express = require('express');
139
- const { __express } = require('miki-template');
140
-
141
- const app = express();
142
- app.engine('html', __express);
143
- app.set('view engine', 'html');
144
- app.set('views', './views');
145
- ```
146
-
147
- **ESM:**
148
-
149
- ```javascript
150
- import express from 'express';
151
- import miki from 'miki-template';
152
-
153
- const app = express();
154
- miki.setupExpress(app, { extension: 'html', views: './views' });
155
- ```
156
-
157
- **Async Express 5+:**
158
- ```javascript
159
- miki.setupExpress(app, { extension: 'html', views: './views', async: true });
160
- ```
161
-
162
- **Or, if you only want partial responses** without changing your engine registration, add the middleware:
163
-
164
- ```javascript
165
- app.use(miki.expressPartialRenderer());
166
-
167
- app.get('/card', (req, res) => res.renderPartial('home#card', { user: req.user }));
168
- ```
169
-
170
- ---
171
-
172
- ## 📖 Template Syntax & Parity
173
-
174
- ### Variables & Dotted Lookups
175
- Resolve properties dynamically on nested objects or arrays. If the resolved value is a callable/function, it is automatically executed with zero arguments.
176
- ```html
177
- {{ user.profile.name }}
178
- {{ items.0 }} <!-- Array indexing -->
179
- {{ user.getFullName }} <!-- Function resolution -->
180
- ```
181
-
182
- ### Built-in Filters
183
- Apply filters using pipes (`|`). Arguments are passed after a colon (`:`).
184
- - **Text**: `upper`, `lower`, `title`, `capfirst`, `slugify`, `wordcount`, `striptags`, `linebreaks`, `linebreaksbr`, `truncatewords:N`, `truncatechars:N`.
185
- - **HTML**: `safe`, `escape`.
186
- - **List**: `length`, `join:","`, `slice:"start:end"`, `dictsort:"key"`, `dictsortreversed:"key"`.
187
- - **Default**: `default:"fallback"`, `default_if_none:"fallback"`.
188
- - **Date/Time**: `date:"Y-m-d"`, `time:"H:i"`, `timesince`, `timeuntil`.
189
- - **Numeric**: `add:5`, `divisibleby:2`, `floatformat:2`.
190
- - **Misc**: `yesno:"yes,no,maybe"`, `pluralize:"suffix"`, `filesizeformat`.
191
-
192
- ### Built-in Control Tags
193
- - **if / elif / else / endif**: Supports conditional expressions with operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `not in`, `and`, `or`, `not`.
194
- ```html
195
- {% if user.role == 'admin' or user.is_staff %}
196
- <p>Access Granted</p>
197
- {% elif user.age >= 18 %}
198
- <p>Standard Access</p>
199
- {% else %}
200
- <p>Access Denied</p>
201
- {% endif %}
202
- ```
203
- - **for / empty / endfor**: Loop over arrays and objects. Injects `forloop` meta tracking.
204
- ```html
205
- {% for item in items %}
206
- <li>{{ forloop.counter }}: {{ item }}</li>
207
- {% empty %}
208
- <li>No items found</li>
209
- {% endfor %}
210
- ```
211
- - **with / endwith**: Scopes localized variables.
212
- ```html
213
- {% with user.profile.address as addr %}
214
- <p>{{ addr.city }}, {{ addr.zip }}</p>
215
- {% endwith %}
216
- ```
217
- - **cycle**: Cycle through values sequentially.
218
- ```html
219
- {% for row in rows %}
220
- <tr class="{% cycle 'row-odd' 'row-even' %}">...</tr>
221
- {% endfor %}
222
- ```
223
- - **autoescape on/off**: Control auto-escaping block behavior.
224
- - **verbatim / endverbatim**: Treat raw text inside literally.
225
- - **comment / endcomment**: Block comment ignored during parse.
226
-
227
- ### Security Tags
228
- - **csrf_token**: Automatically outputs a hidden input carrying the CSRF token from the context variable `csrf_token`.
229
- ```html
230
- <form method="post">
231
- {% csrf_token %}
232
- ...
233
- </form>
234
- ```
235
- - **csp_nonce_attr**: Dynamically outputs `nonce="value"` if the variable `csp_nonce` is in the context.
236
- ```html
237
- <script {% csp_nonce_attr %} src="app.js"></script>
238
- ```
239
-
240
- ### Inheritance & Block Rendering
241
- Inherit structure from parent templates.
242
- - `base.html`:
243
- ```html
244
- <html>
245
- <body>
246
- {% block content %}Default Content{% endblock %}
247
- </body>
248
- </html>
249
- ```
250
- - `child.html`:
251
- ```html
252
- {% extends "base.html" %}
253
- {% block content %}
254
- <h1>Child Content</h1>
255
- {{ block.super }} <!-- Renders parent's default content -->
256
- {% endblock %}
257
- ```
258
-
259
- #### Rendering a Block-Level Partial (Django 5.1+ / HTMX Style)
260
- You can compile a template and choose to render *only a specific block* (useful for AJAX or HTMX requests):
261
- ```javascript
262
- const compiled = compile(childTemplateStr, { views: './templates' });
263
- const partialHtml = compiled.renderBlock('content', context);
264
- console.log(partialHtml); // Output: "<h1>Child Content</h1> Default Content"
265
- ```
266
-
267
- ---
268
-
269
- ## 🔧 Extensibility API
270
-
271
- ### Register a Custom Filter
272
-
273
- **CommonJS:**
274
- ```javascript
275
- const { registerFilter } = require('miki-template');
276
-
277
- registerFilter('reverse', (val) => {
278
- return String(val).split('').reverse().join('');
279
- });
280
- ```
281
-
282
- **ESM:**
283
- ```javascript
284
- import { registerFilter } from 'miki-template';
285
-
286
- registerFilter('reverse', (val) => {
287
- return String(val).split('').reverse().join('');
288
- });
289
- ```
290
-
291
- ### Register a Custom Tag
292
-
293
- **CommonJS:**
294
- ```javascript
295
- const { registerTag } = require('miki-template');
296
-
297
- // Custom tag parser returning an AST Node
298
- registerTag('hello', (tagContent, parser) => {
299
- return {
300
- render: (context) => 'Hello World!'
301
- };
302
- });
303
- ```
304
-
305
- **ESM:**
306
- ```javascript
307
- import { registerTag } from 'miki-template';
308
-
309
- registerTag('hello', (tagContent, parser) => {
310
- return {
311
- render: (context) => 'Hello World!'
312
- };
313
- });
314
- ```
315
-
316
- ---
317
-
318
- ## 🔒 Security
319
- - **HTML Auto-escaping**: Enabled by default to guard against Cross-Site Scripting (XSS).
320
- - **SafeString Wrapper**: Explicitly bypass escaping using the `|safe` filter or marking variables via `markSafe(val)`.
321
- - **No eval() Execution**: Parser evaluates logic statements securely using standard tokens mapping.
1
+ # miki-template
2
+ ![miki-template banner](assets/banner.png)
3
+ ![npm version](https://img.shields.io/npm/v/miki-template.svg) ![CI](https://github.com/your-repo/miki-template/workflows/ci.yml/badge.svg)
4
+ A robust, production-ready template engine that brings **Django's template language** features and syntax to Node.js and Express, fully compliant with modern JavaScript (ES6+), CommonJS, and **ESM** (`import`) support.
5
+
6
+ ---
7
+
8
+ ## 🚀 Features
9
+
10
+ - **One-line Express integration**: `miki.setupExpress(app, { extension: 'html', views: dir })` — wires the engine, views directory, and a `res.render` shim that makes `res.render('home#card', ...)` Just Work for HTMX-style partial responses. **No boilerplate, no extra middleware.**
11
+ - **Partial responses out of the box**: `{% partialdef %}` blocks can be rendered by name with `res.render('view#partial', ...)`, `miki.expressPartialRenderer()` middleware (`res.renderPartial(...)`), or `renderPartialFromSource(...)`.
12
+ - **Full Syntax Parity**: Supports variables, dotted lookups, filters (`|`), and block tags (`{% %}`).
13
+ - **Template Inheritance**: Multi-level inheritance with `extends`, block overrides, and `{{ block.super }}` support.
14
+ - **Built-in libraries**: `humanize`, `cache`, and `lorem` ship pre-activated. `{% lorem 5 p %}` works without `{% load lorem %}`.
15
+ - **ESM & CommonJS**: Works seamlessly with both `import` and `require` syntax.
16
+ - **Security by Default**: Auto-escaping enabled by default with a `SafeString` wrapper.
17
+ - **CSRF & CSP Support**: Native tags for `{% csrf_token %}` and `{% csp_nonce_attr %}` to keep apps secure out-of-the-box.
18
+ - **Block Partials**: Render a single block from a compiled template via `compiled.renderBlock('block_name')`.
19
+ - **Async Rendering**: Support for async filters/tags with `asyncRender()`.
20
+ - **Extensible API**: Easy registration for custom tags and filters.
21
+ - **No Unsafe Code Execution**: Evaluates expressions securely without using `eval()`.
22
+ - **Editor Support**: First-class syntax highlighting and snippets for VS Code, Sublime Text, Atom, and TextMate-compatible editors.
23
+
24
+ ### VS Code
25
+
26
+ #### Option A: Install the official extension (recommended)
27
+
28
+ Search for **miki-template** in the VS Code Marketplace, or install from the command line:
29
+
30
+ ```bash
31
+ code --install-extension miki-template
32
+ ```
33
+
34
+ #### Option B: Manual install from this repo
35
+
36
+ 1. Copy the `syntaxes/` and `snippets/` folders from this repo.
37
+ 2. In VS Code, run **Preferences: Configure File Associations** and associate `*.miki` with `miki-template`.
38
+ 3. Or add a workspace-level `.vscode/settings.json`:
39
+
40
+ ```json
41
+ {
42
+ "files.associations": {
43
+ "*.miki": "miki-template"
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Sublime Text / Atom / TextMate
49
+
50
+ Drop the `syntaxes/miki-template.tmLanguage.json` file into your editor’s `Packages/User/` folder and associate it with the `.miki` extension.
51
+
52
+ ---
53
+
54
+ ## 📚 Documentation
55
+
56
+ - [Installation](docs/installation.md)
57
+ - [Usage Guide](docs/usage.md)
58
+ - [Tags Reference](docs/tags.md)
59
+ - [Filters Reference](docs/filters.md)
60
+ - [Security](docs/security.md)
61
+ - [API Reference](docs/api.md)
62
+ - [Partial Definitions](docs/partialdef.md)
63
+ - [Contributing](docs/advanced_usage.md)
64
+
65
+ ---
66
+
67
+ ## 📦 Installation
68
+
69
+ ```bash
70
+ npm install miki-template
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 🛠️ Quick Start
76
+
77
+ ### CommonJS (require)
78
+
79
+ ```javascript
80
+ const { render, compile, __express, SafeString, markSafe } = require('miki-template');
81
+
82
+ const template = 'Hello {{ user.name|title }}! Roles: {{ user.roles|join:", " }}';
83
+ const context = {
84
+ user: {
85
+ name: 'miki coder',
86
+ roles: ['admin', 'developer']
87
+ }
88
+ };
89
+
90
+ const result = render(template, context);
91
+ console.log(result); // Output: "Hello Miki Coder! Roles: admin, developer"
92
+ ```
93
+
94
+ ### ES Modules (import)
95
+
96
+ ```javascript
97
+ // Named imports
98
+ import { render, compile, __express, SafeString, markSafe } from 'miki-template';
99
+
100
+ // Or default import (gets all exports)
101
+ import miki from 'miki-template';
102
+ const { render: mikiRender } = miki;
103
+
104
+ const template = 'Hello {{ user.name|title }}!';
105
+ const result = render(template, { user: { name: 'world' } });
106
+ console.log(result); // Output: "Hello World!"
107
+ ```
108
+
109
+ > **Note:** For ESM in Node.js, either name your files `.mjs` or add `"type": "module"` to your `package.json`.
110
+
111
+ ### Express Integration
112
+
113
+ **The recommended, one-line setup** — wires the view engine, views directory, and partial responses in a single call:
114
+
115
+ ```javascript
116
+ const express = require('express');
117
+ const miki = require('miki-template');
118
+
119
+ const app = express();
120
+ miki.setupExpress(app, { extension: 'html', views: './views' });
121
+
122
+ // Full page
123
+ app.get('/', (req, res) => res.render('home', { user: req.user }));
124
+
125
+ // HTMX / partial response — just append `#partialName` to the view name
126
+ app.get('/partials/:name', (req, res) =>
127
+ res.render(`home#${req.params.name}`, { user: req.user })
128
+ );
129
+
130
+ app.listen(3000);
131
+ ```
132
+
133
+ > `setupExpress` calls `app.engine()`, `app.set('views')`, and `app.set('view engine')` for you, and patches `res.render` so `view#partial` is dispatched to the partial renderer (not the file system). It works equally well for `.miki` files — just pass `extension: 'miki'`.
134
+
135
+ **The classic, fully manual setup still works** if you prefer it:
136
+
137
+ ```javascript
138
+ const express = require('express');
139
+ const { __express } = require('miki-template');
140
+
141
+ const app = express();
142
+ app.engine('html', __express);
143
+ app.set('view engine', 'html');
144
+ app.set('views', './views');
145
+ ```
146
+
147
+ **ESM:**
148
+
149
+ ```javascript
150
+ import express from 'express';
151
+ import miki from 'miki-template';
152
+
153
+ const app = express();
154
+ miki.setupExpress(app, { extension: 'html', views: './views' });
155
+ ```
156
+
157
+ **Async Express 5+:**
158
+ ```javascript
159
+ miki.setupExpress(app, { extension: 'html', views: './views', async: true });
160
+ ```
161
+
162
+ **Or, if you only want partial responses** without changing your engine registration, add the middleware:
163
+
164
+ ```javascript
165
+ app.use(miki.expressPartialRenderer());
166
+
167
+ app.get('/card', (req, res) => res.renderPartial('home#card', { user: req.user }));
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 📖 Template Syntax & Parity
173
+
174
+ ### Variables & Dotted Lookups
175
+ Resolve properties dynamically on nested objects or arrays. If the resolved value is a callable/function, it is automatically executed with zero arguments.
176
+ ```html
177
+ {{ user.profile.name }}
178
+ {{ items.0 }} <!-- Array indexing -->
179
+ {{ user.getFullName }} <!-- Function resolution -->
180
+ ```
181
+
182
+ ### Built-in Filters
183
+ Apply filters using pipes (`|`). Arguments are passed after a colon (`:`).
184
+ - **Text**: `upper`, `lower`, `title`, `capfirst`, `slugify`, `wordcount`, `striptags`, `linebreaks`, `linebreaksbr`, `truncatewords:N`, `truncatechars:N`.
185
+ - **HTML**: `safe`, `escape`.
186
+ - **List**: `length`, `join:","`, `slice:"start:end"`, `dictsort:"key"`, `dictsortreversed:"key"`.
187
+ - **Default**: `default:"fallback"`, `default_if_none:"fallback"`.
188
+ - **Date/Time**: `date:"Y-m-d"`, `time:"H:i"`, `timesince`, `timeuntil`.
189
+ - **Numeric**: `add:5`, `divisibleby:2`, `floatformat:2`.
190
+ - **Misc**: `yesno:"yes,no,maybe"`, `pluralize:"suffix"`, `filesizeformat`.
191
+
192
+ ### Built-in Control Tags
193
+ - **if / elif / else / endif**: Supports conditional expressions with operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `not in`, `and`, `or`, `not`.
194
+ ```html
195
+ {% if user.role == 'admin' or user.is_staff %}
196
+ <p>Access Granted</p>
197
+ {% elif user.age >= 18 %}
198
+ <p>Standard Access</p>
199
+ {% else %}
200
+ <p>Access Denied</p>
201
+ {% endif %}
202
+ ```
203
+ - **for / empty / endfor**: Loop over arrays and objects. Injects `forloop` meta tracking.
204
+ ```html
205
+ {% for item in items %}
206
+ <li>{{ forloop.counter }}: {{ item }}</li>
207
+ {% empty %}
208
+ <li>No items found</li>
209
+ {% endfor %}
210
+ ```
211
+ - **with / endwith**: Scopes localized variables.
212
+ ```html
213
+ {% with user.profile.address as addr %}
214
+ <p>{{ addr.city }}, {{ addr.zip }}</p>
215
+ {% endwith %}
216
+ ```
217
+ - **cycle**: Cycle through values sequentially.
218
+ ```html
219
+ {% for row in rows %}
220
+ <tr class="{% cycle 'row-odd' 'row-even' %}">...</tr>
221
+ {% endfor %}
222
+ ```
223
+ - **autoescape on/off**: Control auto-escaping block behavior.
224
+ - **verbatim / endverbatim**: Treat raw text inside literally.
225
+ - **comment / endcomment**: Block comment ignored during parse.
226
+
227
+ ### Security Tags
228
+ - **csrf_token**: Automatically outputs a hidden input carrying the CSRF token from the context variable `csrf_token`.
229
+ ```html
230
+ <form method="post">
231
+ {% csrf_token %}
232
+ ...
233
+ </form>
234
+ ```
235
+ - **csp_nonce_attr**: Dynamically outputs `nonce="value"` if the variable `csp_nonce` is in the context.
236
+ ```html
237
+ <script {% csp_nonce_attr %} src="app.js"></script>
238
+ ```
239
+
240
+ ### Inheritance & Block Rendering
241
+ Inherit structure from parent templates.
242
+ - `base.html`:
243
+ ```html
244
+ <html>
245
+ <body>
246
+ {% block content %}Default Content{% endblock %}
247
+ </body>
248
+ </html>
249
+ ```
250
+ - `child.html`:
251
+ ```html
252
+ {% extends "base.html" %}
253
+ {% block content %}
254
+ <h1>Child Content</h1>
255
+ {{ block.super }} <!-- Renders parent's default content -->
256
+ {% endblock %}
257
+ ```
258
+
259
+ #### Rendering a Block-Level Partial (Django 5.1+ / HTMX Style)
260
+ You can compile a template and choose to render *only a specific block* (useful for AJAX or HTMX requests):
261
+ ```javascript
262
+ const compiled = compile(childTemplateStr, { views: './templates' });
263
+ const partialHtml = compiled.renderBlock('content', context);
264
+ console.log(partialHtml); // Output: "<h1>Child Content</h1> Default Content"
265
+ ```
266
+
267
+ ---
268
+
269
+ ## 🔧 Extensibility API
270
+
271
+ ### Register a Custom Filter
272
+
273
+ **CommonJS:**
274
+ ```javascript
275
+ const { registerFilter } = require('miki-template');
276
+
277
+ registerFilter('reverse', (val) => {
278
+ return String(val).split('').reverse().join('');
279
+ });
280
+ ```
281
+
282
+ **ESM:**
283
+ ```javascript
284
+ import { registerFilter } from 'miki-template';
285
+
286
+ registerFilter('reverse', (val) => {
287
+ return String(val).split('').reverse().join('');
288
+ });
289
+ ```
290
+
291
+ ### Register a Custom Tag
292
+
293
+ **CommonJS:**
294
+ ```javascript
295
+ const { registerTag } = require('miki-template');
296
+
297
+ // Custom tag parser returning an AST Node
298
+ registerTag('hello', (tagContent, parser) => {
299
+ return {
300
+ render: (context) => 'Hello World!'
301
+ };
302
+ });
303
+ ```
304
+
305
+ **ESM:**
306
+ ```javascript
307
+ import { registerTag } from 'miki-template';
308
+
309
+ registerTag('hello', (tagContent, parser) => {
310
+ return {
311
+ render: (context) => 'Hello World!'
312
+ };
313
+ });
314
+ ```
315
+
316
+ ---
317
+
318
+ ## 🚀 Releasing
319
+
320
+ Releases are fully automatic. Pick the bump you want and run one command:
321
+
322
+ ```bash
323
+ npm run release:patch # 1.3.3 → 1.3.4
324
+ npm run release:minor # 1.3.3 → 1.4.0
325
+ npm run release:major # 1.3.3 → 2.0.0
326
+ ```
327
+
328
+ That bumps `package.json`, creates a `chore(release): vX.Y.Z` commit, and pushes to `main`. The CI then:
329
+
330
+ 1. Runs lint + test + the strict benchmark (must pass)
331
+ 2. Creates an annotated `vX.Y.Z` git tag and pushes it
332
+ 3. Creates a GitHub Release with notes from `.github/release-notes/vX.Y.Z.md` (optional)
333
+ 4. Publishes to npm
334
+
335
+ Nothing else to click. The whole pipeline is in `.github/workflows/release.yml`.
336
+
337
+ ---
338
+
339
+ ## 🔒 Security
340
+ - **HTML Auto-escaping**: Enabled by default to guard against Cross-Site Scripting (XSS).
341
+ - **SafeString Wrapper**: Explicitly bypass escaping using the `|safe` filter or marking variables via `markSafe(val)`.
342
+ - **No eval() Execution**: Parser evaluates logic statements securely using standard tokens mapping.