miki-template 1.3.3 → 1.3.7

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 (115) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.github/release-notes/v1.3.1.md +55 -55
  3. package/.github/release-notes/v1.3.3.md +77 -0
  4. package/.github/workflows/ci.yml +38 -54
  5. package/.github/workflows/release.yml +106 -0
  6. package/AGENT.md +71 -71
  7. package/API_REFERENCE.md +314 -314
  8. package/CHANGELOG.md +173 -169
  9. package/CODE_OF_CONDUCT.md +14 -14
  10. package/CONTRIBUTING.md +27 -27
  11. package/README.md +342 -321
  12. package/ROADMAP.md +40 -40
  13. package/benchmarks/report.json +16 -16
  14. package/benchmarks/run.js +49 -49
  15. package/benchmarks/stress.mjs +647 -647
  16. package/benchmarks/templates/large.dtpl +7 -7
  17. package/benchmarks/templates/medium.dtpl +3 -3
  18. package/benchmarks/templates/small.dtpl +7 -7
  19. package/context/component.md +109 -109
  20. package/context/prd.md +131 -131
  21. package/context/project-structure.md +33 -33
  22. package/dir/base.html +22 -22
  23. package/dir/cmpnt.html +10 -10
  24. package/dir/footer.html +2 -2
  25. package/dir/home.html +80 -80
  26. package/dir/index.html +80 -0
  27. package/dir/navbar.html +8 -8
  28. package/docs/README.md +18 -18
  29. package/docs/advanced_usage.md +71 -71
  30. package/docs/api.md +119 -119
  31. package/docs/filters.md +708 -708
  32. package/docs/installation.md +106 -106
  33. package/docs/overview.md +79 -57
  34. package/docs/partialdef.md +70 -70
  35. package/docs/security.md +27 -27
  36. package/docs/tags.md +673 -673
  37. package/docs/usage.md +646 -646
  38. package/eslint.config.mjs +42 -42
  39. package/ex.mjs +34 -32
  40. package/live-test/package-lock.json +915 -0
  41. package/live-test/package.json +9 -0
  42. package/live-test/server.js +14 -0
  43. package/live-test/views/base.html +8 -0
  44. package/live-test/views/child.html +7 -0
  45. package/live-test/views/index.html +1 -0
  46. package/miki-template-extension/.github/workflows/ci.yml +116 -116
  47. package/miki-template-extension/.vscodeignore +7 -7
  48. package/miki-template-extension/CHANGELOG.md +99 -99
  49. package/miki-template-extension/LICENSE +21 -21
  50. package/miki-template-extension/README.md +273 -273
  51. package/miki-template-extension/extension.js +1013 -1013
  52. package/miki-template-extension/icon.svg +10 -10
  53. package/miki-template-extension/package.json +280 -280
  54. package/miki-template-extension/snippets/miki-template.json +717 -717
  55. package/miki-template-extension/syntaxes/language-configuration.json +114 -114
  56. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -355
  57. package/miki-template-extension/tests/grammar-tests.json +162 -162
  58. package/miki-template-extension/tests/run-grammar-tests.js +82 -82
  59. package/package.json +40 -34
  60. package/sample-app/package-lock.json +901 -0
  61. package/sample-app/package.json +9 -0
  62. package/sample-app/server.js +14 -0
  63. package/sample-app/views/index.html +1 -0
  64. package/scripts/build-vsix.js +129 -129
  65. package/scripts/build-vsix.ps1 +15 -15
  66. package/snippets/miki-template.json +177 -177
  67. package/src/asyncRender.js +20 -20
  68. package/src/cache.js +80 -80
  69. package/src/context.js +126 -126
  70. package/src/context_processors.js +48 -48
  71. package/src/esm.mjs +89 -84
  72. package/src/filters.js +975 -975
  73. package/src/i18n.js +171 -171
  74. package/src/index.js +1112 -940
  75. package/src/lexer.js +114 -114
  76. package/src/libraries.js +371 -371
  77. package/src/parser.js +270 -270
  78. package/src/security.js +53 -53
  79. package/src/tags/control.js +719 -719
  80. package/src/tags/extra.js +154 -154
  81. package/src/tags/helpers.js +26 -26
  82. package/src/tags/i18n.js +256 -256
  83. package/src/tags/inheritance.js +335 -335
  84. package/src/tags/registry.js +18 -18
  85. package/src/tags/util.js +400 -400
  86. package/src/types.d.ts +107 -107
  87. package/syntaxes/language-configuration.json +26 -26
  88. package/syntaxes/miki-template.tmLanguage.json +146 -146
  89. package/tests/asyncRender.test.js +17 -17
  90. package/tests/base.html +6 -6
  91. package/tests/child.html +3 -3
  92. package/tests/context_processors.test.js +13 -13
  93. package/tests/esm.test.mjs +61 -61
  94. package/tests/filters.test.js +254 -254
  95. package/tests/finder-appdirs.test.js +19 -0
  96. package/tests/finder.test.js +17 -0
  97. package/tests/fixtures/views/nested/index.html +1 -0
  98. package/tests/fixtures/views/partial.html +1 -0
  99. package/tests/fixtures/views/sub/deepfile.html +1 -0
  100. package/tests/fixtures/views-appdirs/product/site/detail.html +1 -0
  101. package/tests/include_security.test.js +9 -9
  102. package/tests/integration/README.md +32 -32
  103. package/tests/integration/features.test.cjs +1681 -1681
  104. package/tests/integration/features.test.mjs +1697 -1697
  105. package/tests/integration/finder.esm.test.mjs +13 -0
  106. package/tests/integration/templates/base.miki +6 -6
  107. package/tests/integration/templates/child.miki +6 -6
  108. package/tests/integration/templates/index.html +17 -17
  109. package/tests/lexer.test.js +45 -45
  110. package/tests/parser.test.js +57 -57
  111. package/tests/partial.html +1 -1
  112. package/tests/partialdef.test.js +79 -79
  113. package/tests/production_checks.js +57 -57
  114. package/tests/security.test.js +28 -28
  115. 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.