miki-template 1.2.0 → 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.
- package/.github/release-notes/v1.3.1.md +55 -0
- package/.github/release-notes/v1.3.3.md +77 -0
- package/.github/workflows/ci.yml +38 -54
- package/.github/workflows/release.yml +106 -0
- package/AGENT.md +71 -71
- package/API_REFERENCE.md +314 -314
- package/CHANGELOG.md +173 -97
- package/CODE_OF_CONDUCT.md +14 -14
- package/CONTRIBUTING.md +27 -27
- package/README.md +342 -304
- package/ROADMAP.md +40 -40
- package/assets/banner.png +0 -0
- package/benchmarks/report.json +16 -16
- package/benchmarks/run.js +49 -49
- package/benchmarks/stress.mjs +647 -0
- package/benchmarks/templates/large.dtpl +7 -7
- package/benchmarks/templates/medium.dtpl +3 -3
- package/benchmarks/templates/small.dtpl +7 -7
- package/context/component.md +109 -109
- package/context/prd.md +131 -131
- package/context/project-structure.md +33 -33
- package/dir/base.html +23 -0
- package/dir/cmpnt.html +11 -0
- package/dir/footer.html +3 -0
- package/dir/home.html +80 -0
- package/dir/index.html +80 -0
- package/dir/navbar.html +9 -0
- package/docs/README.md +18 -18
- package/docs/advanced_usage.md +71 -71
- package/docs/api.md +119 -102
- package/docs/filters.md +708 -540
- package/docs/installation.md +106 -106
- package/docs/overview.md +57 -57
- package/docs/partialdef.md +70 -41
- package/docs/security.md +27 -27
- package/docs/tags.md +673 -610
- package/docs/usage.md +646 -599
- package/eslint.config.mjs +42 -34
- package/ex.mjs +33 -0
- package/miki-template-extension/.github/workflows/ci.yml +116 -0
- package/miki-template-extension/.vscodeignore +7 -0
- package/miki-template-extension/CHANGELOG.md +99 -0
- package/miki-template-extension/LICENSE +21 -21
- package/miki-template-extension/README.md +273 -82
- package/miki-template-extension/extension.js +1013 -0
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +10 -10
- package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
- package/miki-template-extension/package.json +280 -46
- package/miki-template-extension/snippets/miki-template.json +717 -177
- package/miki-template-extension/syntaxes/language-configuration.json +114 -26
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -146
- package/miki-template-extension/tests/grammar-tests.json +162 -0
- package/miki-template-extension/tests/run-grammar-tests.js +82 -0
- package/package.json +37 -31
- package/sample-app/package-lock.json +901 -0
- package/sample-app/package.json +9 -0
- package/sample-app/server.js +14 -0
- package/sample-app/views/index.html +1 -0
- package/scripts/build-vsix.js +129 -0
- package/scripts/build-vsix.ps1 +15 -0
- package/snippets/miki-template.json +177 -177
- package/src/asyncRender.js +20 -20
- package/src/cache.js +80 -41
- package/src/context.js +126 -122
- package/src/context_processors.js +48 -41
- package/src/esm.mjs +84 -72
- package/src/filters.js +975 -527
- package/src/i18n.js +171 -171
- package/src/index.js +974 -454
- package/src/lexer.js +114 -92
- package/src/libraries.js +371 -240
- package/src/parser.js +270 -250
- package/src/security.js +53 -51
- package/src/tags/control.js +719 -590
- package/src/tags/extra.js +154 -0
- package/src/tags/helpers.js +26 -26
- package/src/tags/i18n.js +256 -230
- package/src/tags/inheritance.js +335 -216
- package/src/tags/registry.js +18 -18
- package/src/tags/util.js +400 -322
- package/src/types.d.ts +107 -107
- package/syntaxes/language-configuration.json +26 -26
- package/syntaxes/miki-template.tmLanguage.json +146 -146
- package/tests/asyncRender.test.js +17 -17
- package/tests/base.html +6 -6
- package/tests/child.html +3 -3
- package/tests/context_processors.test.js +13 -13
- package/tests/esm.test.mjs +61 -26
- package/tests/filters.test.js +254 -99
- package/tests/include_security.test.js +9 -9
- package/tests/integration/README.md +32 -0
- package/tests/integration/features.test.cjs +1681 -0
- package/tests/integration/features.test.mjs +1697 -0
- package/tests/integration/templates/base.miki +6 -0
- package/tests/integration/templates/child.miki +6 -0
- package/tests/integration/templates/index.html +17 -0
- package/tests/lexer.test.js +45 -45
- package/tests/parser.test.js +57 -55
- package/tests/partial.html +1 -1
- package/tests/partialdef.test.js +79 -40
- package/tests/production_checks.js +57 -57
- package/tests/security.test.js +28 -28
- package/tests/tags.test.js +233 -203
- package/miki-template-1.2.0.vsix +0 -0
package/README.md
CHANGED
|
@@ -1,304 +1,342 @@
|
|
|
1
|
-
# miki-template
|
|
2
|
-

|
|
3
|
+
 
|
|
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.
|