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
@@ -1,8 +1,3 @@
1
- # CI Workflow — lint + test on every push and PR.
2
- #
3
- # Releases are handled by .github/workflows/release.yml, which triggers
4
- # automatically when a `chore(release):` commit lands on main.
5
-
6
1
  name: CI
7
2
 
8
3
  on:
@@ -10,29 +5,38 @@ on:
10
5
  branches: [ main ]
11
6
  pull_request:
12
7
  branches: [ main ]
8
+ workflow_dispatch:
13
9
 
14
10
  jobs:
15
11
  lint:
16
- runs-on: windows-latest
12
+ runs-on: ubuntu-latest
17
13
  steps:
18
14
  - uses: actions/checkout@v4
19
15
  - name: Setup Node.js
20
16
  uses: actions/setup-node@v4
21
17
  with:
22
18
  node-version: '20'
19
+ cache: 'npm'
23
20
  - run: npm ci
24
21
  - name: Lint code
25
22
  run: npx eslint src/**/*.js
26
23
 
27
24
  test:
28
- runs-on: windows-latest
25
+ runs-on: ubuntu-latest
29
26
  needs: lint
27
+ strategy:
28
+ matrix:
29
+ node-version: [18.x, 20.x]
30
30
  steps:
31
31
  - uses: actions/checkout@v4
32
- - name: Setup Node.js
32
+ - name: Use Node.js
33
33
  uses: actions/setup-node@v4
34
34
  with:
35
- node-version: '20'
35
+ node-version: ${{ matrix.node-version }}
36
+ cache: 'npm'
36
37
  - run: npm ci
37
- - name: Run test suite
38
+ - name: Run unit tests
38
39
  run: npm test
40
+ - name: Run integration smoke-test
41
+ if: github.event_name == 'workflow_dispatch'
42
+ run: node live-test/integrations/smoke-test.js
@@ -0,0 +1,105 @@
1
+ name: Deploy Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: write
12
+ pages: write
13
+ id-token: write
14
+
15
+ concurrency:
16
+ group: "pages"
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ deploy-docs:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Checkout repository
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Setup Python
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.12'
30
+ cache: 'pip'
31
+ cache-dependency-path: 'requirements-docs.txt'
32
+
33
+ - name: Install MkDocs Material with imaging support
34
+ run: |
35
+ python -m pip install --upgrade pip
36
+ pip install -r requirements-docs.txt
37
+
38
+ - name: Verify docs build
39
+ run: mkdocs build --strict
40
+
41
+ - name: Deploy docs to GitHub Pages
42
+ run: mkdocs gh-deploy --force --message "chore: deploy docs to GitHub Pages [skip ci]"
43
+
44
+ - name: Output Pages URL
45
+ run: echo "Docs deployed to https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/"
46
+
47
+ sync-wiki:
48
+ runs-on: ubuntu-latest
49
+ needs: deploy-docs
50
+ steps:
51
+ - name: Checkout repository
52
+ uses: actions/checkout@v4
53
+ with:
54
+ fetch-depth: 0
55
+
56
+ - name: Setup Git config
57
+ run: |
58
+ git config --global user.name "github-actions[bot]"
59
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
60
+
61
+ - name: Create wiki clone directory
62
+ run: mkdir -p wiki-repo
63
+
64
+ - name: Clone wiki repository
65
+ run: |
66
+ git clone https://github.com/${{ github.repository }}.wiki.git wiki-repo || true
67
+
68
+ - name: Sync docs to wiki
69
+ if: hashFiles('wiki-repo/.git/config') != ''
70
+ run: |
71
+ cd wiki-repo
72
+
73
+ # Copy all markdown docs preserving directory structure
74
+ cp -r ../docs/*.md .
75
+ cp -r ../docs/guide/*.md ./guide/ 2>/dev/null || true
76
+ cp -r ../docs/api/*.md ./api/ 2>/dev/null || true
77
+ cp -r ../docs/integrations/*.md ./integrations/ 2>/dev/null || true
78
+
79
+ # Remove index.md if it exists (GitHub wiki uses README.md as home page)
80
+ if [ -f index.md ]; then
81
+ if [ -f README.md ]; then
82
+ rm index.md
83
+ else
84
+ mv index.md README.md
85
+ fi
86
+ fi
87
+
88
+ # Create Home.md from index.md if README.md doesn't exist
89
+ if [ ! -f README.md ] && [ ! -f Home.md ] && [ -f docs/index.md ]; then
90
+ cp docs/index.md Home.md
91
+ fi
92
+
93
+ # Commit and push if there are changes
94
+ git add -A
95
+ git diff --cached --quiet || {
96
+ git commit -m "docs: sync documentation to wiki"
97
+ git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git master
98
+ }
99
+
100
+ - name: Wiki initialization hint
101
+ if: hashFiles('wiki-repo/.git/config') == ''
102
+ run: |
103
+ echo "::warning::Wiki repository not found. Enable the wiki in repository settings first."
104
+ echo "Go to https://github.com/${{ github.repository }}/settings/wiki and click 'Create the first page'"
105
+ continue-on-error: true
@@ -0,0 +1,36 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-gpr:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ packages: write
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 20
32
+ registry-url: https://npm.pkg.github.com/
33
+ - run: npm ci
34
+ - run: npm publish
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
package/README.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # miki-template
2
2
  ![miki-template banner](assets/banner.png)
3
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.
4
+
5
+ **Django-style template magic for Node.js — blazing fast partials, smart template discovery, and zero friction for HTMX.**
6
+
7
+ Define reusable partials with `{% partialdef %}`, render any slice of a page with `render('home#card')`, and let the engine find templates across your whole project — `templates/`, `app/templates/`, or whatever structure you prefer. No more wrestling with view paths or boilerplate middleware.
5
8
 
6
9
  ---
7
10
 
8
11
  ## 🚀 Features
9
12
 
13
+ - **Partial-powered templating**: Define reusable chunks with `{% partialdef %}` and render them by name anywhere — `res.render('home#card')`, `renderPartialFromSource(...)`, or `compiled.renderBlock('block')`. Built for HTMX-style partial responses without the hassle.
14
+ - **Smart template discovery**: Stop hardcoding view paths. The engine searches `templates/`, nested app directories, and custom folder names automatically — just like Django. `setupExpress()` expands your views roots so templates live where they make sense.
10
15
  - **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
16
  - **Full Syntax Parity**: Supports variables, dotted lookups, filters (`|`), and block tags (`{% %}`).
13
17
  - **Template Inheritance**: Multi-level inheritance with `extends`, block overrides, and `{{ block.super }}` support.
14
18
  - **Built-in libraries**: `humanize`, `cache`, and `lorem` ship pre-activated. `{% lorem 5 p %}` works without `{% load lorem %}`.
@@ -47,20 +51,37 @@ code --install-extension miki-template
47
51
 
48
52
  ### Sublime Text / Atom / TextMate
49
53
 
50
- Drop the `syntaxes/miki-template.tmLanguage.json` file into your editor’s `Packages/User/` folder and associate it with the `.miki` extension.
54
+ Drop the `syntaxes/miki-template.tmLanguage.json` file into your editor's `Packages/User/` folder and associate it with the `.miki` extension.
55
+
56
+ ---
57
+
58
+ ## ⚡ Performance
59
+
60
+ miki-template is built for real-world apps. Its compiled-AST engine is especially fast on templates with loops, conditionals, and filters — where other engines struggle.
61
+
62
+ **Benchmark: renders per second (higher is better)**
63
+
64
+ | Template | miki-template | pug | handlebars | ejs |
65
+ |------------|---------------|---------|------------|---------|
66
+ | Small | ~115k rps | 1.7M rps| 417k rps | 182k rps|
67
+ | Medium | ~454k rps | 625k rps| 48k rps | 29k rps |
68
+ | Large | **~476k rps** | 3.1k rps| 661 rps | 290 rps |
69
+
70
+ > **TL;DR**: On medium templates miki-template is competitive with pug, and on large/realistic pages it **dominates by ~150×** versus pug, handlebars, and ejs. That’s where production apps live, and that’s where miki wins.
71
+
72
+ **How we benchmark**: Each engine renders the same template shape (loops, filters, conditionals) for its syntax. Run `npm run bench` to verify on your own machine.
51
73
 
52
74
  ---
53
75
 
54
76
  ## 📚 Documentation
55
77
 
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)
78
+ - **[Documentation](https://alainmiki.github.io/miki-template/)** — Full docs site
79
+ - [Getting Started](https://alainmiki.github.io/miki-template/guide/getting-started)
80
+ - [Installation](https://alainmiki.github.io/miki-template/guide/installation)
81
+ - [Quick Start](https://alainmiki.github.io/miki-template/guide/quick-start)
82
+ - [API Reference](https://alainmiki.github.io/miki-template/api/)
83
+ - [Integrations](https://alainmiki.github.io/miki-template/integrations/)
84
+ - [Performance](https://alainmiki.github.io/miki-template/performance)
64
85
 
65
86
  ---
66
87
 
@@ -132,15 +153,55 @@ app.listen(3000);
132
153
 
133
154
  > `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
155
 
135
- Note on template discovery: `setupExpress` now expands the `app.get('views')`
136
- value to include nested directories that contain template files. This
137
- means templates placed in project-level `templates/`, package-level
138
- `packages/*/templates/...`, or app-specific folders (e.g. `app_templates/`)
139
- will be discovered automatically when calling `res.render('name')`.
156
+ ### Partial Templates Made Effortless
157
+
158
+ **Define reusable partials once, render them anywhere:**
159
+
160
+ ```html
161
+ <!-- views/home.html -->
162
+ {% partialdef card %}
163
+ <div class="card">
164
+ <h3>{{ title|default:"Untitled" }}</h3>
165
+ <p>{{ body|truncatewords:30 }}</p>
166
+ {% if featured %}<em>Featured</em>{% endif %}
167
+ </div>
168
+ {% endpartialdef %}
169
+
170
+ {% for entry in entries %}
171
+ {% partial card with title=entry.title body=entry.body featured=entry.featured %}
172
+ {% endfor %}
173
+ ```
174
+
175
+ Then serve just that partial via HTMX:
176
+
177
+ ```javascript
178
+ app.get('/card/:id', (req, res) =>
179
+ res.render(`home#card`, { title: 'Hello', body: '...', featured: true })
180
+ );
181
+ ```
182
+
183
+ ### Smart Template Discovery
184
+
185
+ Tired of `Failed to lookup view` errors? miki-template searches your entire project structure automatically:
140
186
 
141
- If your project uses a different convention than `templates`, call
142
- `setAppTemplateDirNames()` to customize the names that the engine
143
- recognizes when scanning for app-style template folders.
187
+ - `views/`
188
+ - `app/templates/`
189
+ - `packages/*/templates/`
190
+ - Any custom directory name you configure
191
+
192
+ ```javascript
193
+ miki.setupExpress(app, {
194
+ extension: 'html',
195
+ views: './views'
196
+ });
197
+
198
+ // Templates placed deeply in your project are found automatically:
199
+ // src/modules/users/templates/profile.html
200
+ // packages/admin/templates/dashboard.html
201
+ // app/templates/shared/header.html
202
+ ```
203
+
204
+ If your project uses a different convention than `templates`, call `setAppTemplateDirNames()` to customize the names that the engine recognizes when scanning for app-style template folders.
144
205
 
145
206
  **The classic, fully manual setup still works** if you prefer it:
146
207
 
@@ -155,7 +216,6 @@ app.set('views', './views');
155
216
  ```
156
217
 
157
218
  **ESM:**
158
-
159
219
  ```javascript
160
220
  import express from 'express';
161
221
  import miki from 'miki-template';
@@ -191,13 +251,14 @@ Resolve properties dynamically on nested objects or arrays. If the resolved valu
191
251
 
192
252
  ### Built-in Filters
193
253
  Apply filters using pipes (`|`). Arguments are passed after a colon (`:`).
194
- - **Text**: `upper`, `lower`, `title`, `capfirst`, `slugify`, `wordcount`, `striptags`, `linebreaks`, `linebreaksbr`, `truncatewords:N`, `truncatechars:N`.
254
+ - **Text**: `upper`, `lower`, `title`, `capfirst`, `slugify`, `wordcount`, `striptags`, `linebreaks`, `linebreaksbr`, `truncatewords:N`, `truncatechars:N`, `truncatechars_html:N`.
195
255
  - **HTML**: `safe`, `escape`.
196
- - **List**: `length`, `join:","`, `slice:"start:end"`, `dictsort:"key"`, `dictsortreversed:"key"`.
197
- - **Default**: `default:"fallback"`, `default_if_none:"fallback"`.
198
- - **Date/Time**: `date:"Y-m-d"`, `time:"H:i"`, `timesince`, `timeuntil`.
199
- - **Numeric**: `add:5`, `divisibleby:2`, `floatformat:2`.
200
- - **Misc**: `yesno:"yes,no,maybe"`, `pluralize:"suffix"`, `filesizeformat`.
256
+ - **List**: `length`, `join:","`, `slice:"start:end"`, `dictsort:"key"`, `dictsortreversed:"key"`, `sort`, `unique`, `random`, `reverse`, `split:","`, `replace:"old,new"`.
257
+ - **Default**: `default:"fallback"`, `default_if_none:"fallback"`, `firstof:v1 v2 v3`.
258
+ - **Date/Time**: `date:"Y-m-d"`, `time:"H:i"`, `date_format:"yyyy-MM-dd"`, `strftime:"PPpp"`, `timesince`, `timeuntil`, `ago`, `until`, `time_diff:other_date`.
259
+ - **Numeric**: `add:5`, `sub:3`, `mult:2`, `divisibleby:2`, `mod:3`, `floatformat:2`, `square`, `sqrt`, `abs`, `round:2`, `floor`, `ceil`, `min:10`, `max:100`, `sum`, `average`.
260
+ - **Currency/Data**: `currency:"$"`, `phone_number`, `email`, `url`, `mask:"*"`, `whatsapp_link:"msg"`, `credit_card`, `ssn`, `ip_address`, `uuid`, `filesizeformat`, `yesno:"yes,no,maybe"`, `pluralize:"s"`, `urlencode`, `escapeuri`, `stringformat:"%s"`, `cut:"text"`, `addslashes`, `removetags:"p,div"`, `trans`, `regroup:"attr"`, `json`, `urlize`.
261
+ - **Encoding**: `base64_encode`, `base64_decode`.
201
262
 
202
263
  ### Built-in Control Tags
203
264
  - **if / elif / else / endif**: Supports conditional expressions with operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `not in`, `and`, `or`, `not`.
@@ -230,9 +291,64 @@ Apply filters using pipes (`|`). Arguments are passed after a colon (`:`).
230
291
  <tr class="{% cycle 'row-odd' 'row-even' %}">...</tr>
231
292
  {% endfor %}
232
293
  ```
294
+ - **firstof**: Return the first truthy value.
295
+ ```html
296
+ {% firstof var1 var2 var3 "fallback" %}
297
+ ```
298
+ - **set**: Assign variables.
299
+ ```html
300
+ {% set total = price * quantity %}
301
+ {% set greeting %}Hello {{ name }}{% endset %}
302
+ ```
303
+ - **ifchanged / endifchanged**: Render only when value changes.
304
+ ```html
305
+ {% for item in items %}
306
+ {% ifchanged item.category %}
307
+ <h2>{{ item.category }}</h2>
308
+ {% endifchanged %}
309
+ {% endfor %}
310
+ ```
311
+ - **now**: Output current date/time.
312
+ ```html
313
+ {% now "Y-m-d H:i:s" %}
314
+ ```
315
+ - **static**: Generate static file URLs.
316
+ ```html
317
+ {% static "css/style.css" %}
318
+ ```
319
+ - **url**: Build URLs from route names.
320
+ ```html
321
+ {% url 'user.profile' user.id %}
322
+ ```
323
+ - **regroup**: Group lists by attribute.
324
+ ```html
325
+ {% for group in items|regroup:"category" %}
326
+ <h3>{{ group.grouper }}</h3>
327
+ {% endfor %}
328
+ ```
329
+ - **spaceless**: Remove whitespace between tags.
330
+ ```html
331
+ {% spaceless %}<div> <span>hi</span> </div>{% endspaceless %}
332
+ ```
333
+ - **widthratio**: Calculate ratios.
334
+ ```html
335
+ {% widthratio value max max_width %}
336
+ ```
337
+ - **debug**: Dump template context.
338
+ ```html
339
+ {% debug %}
340
+ ```
233
341
  - **autoescape on/off**: Control auto-escaping block behavior.
234
342
  - **verbatim / endverbatim**: Treat raw text inside literally.
235
343
  - **comment / endcomment**: Block comment ignored during parse.
344
+ - **load**: Activate template libraries.
345
+ ```html
346
+ {% load lorem humanize %}
347
+ ```
348
+ - **templatetag**: Output literal template tag tokens.
349
+ ```html
350
+ {% templatetag openblock %} if user.is_admin {% templatetag closeblock %}
351
+ ```
236
352
 
237
353
  ### Security Tags
238
354
  - **csrf_token**: Automatically outputs a hidden input carrying the CSRF token from the context variable `csrf_token`.
Binary file
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "name": "ejs:small",
4
+ "medianMs": 0.00549999999999784,
5
+ "rps": 181818
6
+ },
7
+ {
8
+ "name": "ejs:medium",
9
+ "medianMs": 0.033799999999985175,
10
+ "rps": 29586
11
+ },
12
+ {
13
+ "name": "ejs:large",
14
+ "medianMs": 3.5704000000000633,
15
+ "rps": 280
16
+ }
17
+ ]
@@ -0,0 +1,36 @@
1
+ const ejs = require('ejs');
2
+ const { performance } = require('perf_hooks');
3
+
4
+ const SMALL = `<% items.forEach(item => { %>\n<%= item.toUpperCase() %>:<%= item.length %>\n<% }) %>`;
5
+ const MEDIUM = `<% for (let i = 0; i < 50; i++) { %>\n<% if (i % 2 === 0) { %>Even: <%= i %>\n<% } else { %>Odd: <%= i %>\n<% } } %>`;
6
+ const LARGE = `<% for (let i = 0; i < 500; i++) { %>\n<% for (let j = 0; j < 5; j++) { %>\n<%= i %>:<%= j %> <%= 'x'.repeat(10) %>\n<% } } %>`;
7
+
8
+ const data = {
9
+ items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
10
+ };
11
+
12
+ function bench(name, tpl, data, iterations = 5000) {
13
+ const compiled = ejs.compile(tpl);
14
+ for (let i = 0; i < 20; i++) compiled(data);
15
+
16
+ const times = [];
17
+ for (let i = 0; i < iterations; i++) {
18
+ const t0 = performance.now();
19
+ compiled(data);
20
+ times.push(performance.now() - t0);
21
+ }
22
+ times.sort((a, b) => a - b);
23
+ const median = times[Math.floor(times.length / 2)];
24
+ const rps = Math.round(1000 / median);
25
+ console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
26
+ return { name, medianMs: median, rps };
27
+ }
28
+
29
+ const results = [];
30
+ results.push(bench('ejs:small', SMALL, data, 10000));
31
+ results.push(bench('ejs:medium', MEDIUM, data, 5000));
32
+ results.push(bench('ejs:large', LARGE, data, 1000));
33
+
34
+ const outPath = require('path').join(__dirname, 'ejs-results.json');
35
+ require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
36
+ console.log(`\nResults saved to ${outPath}`);
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "name": "handlebars:small",
4
+ "medianMs": 0.0024999999999977263,
5
+ "rps": 400000
6
+ },
7
+ {
8
+ "name": "handlebars:medium",
9
+ "medianMs": 0.019200000000012096,
10
+ "rps": 52083
11
+ },
12
+ {
13
+ "name": "handlebars:large",
14
+ "medianMs": 1.5285000000001219,
15
+ "rps": 654
16
+ }
17
+ ]
@@ -0,0 +1,48 @@
1
+ const Handlebars = require('handlebars');
2
+ const { performance } = require('perf_hooks');
3
+
4
+ const SMALL = `{{#each items}}{{this}}:{{this.length}}\n{{/each}}`;
5
+ const MEDIUM = `{{#each (range 0 50)}}{{#if (isEven this)}}Even: {{this}}\n{{else}}Odd: {{this}}\n{{/if}}{{/each}}`;
6
+ const LARGE = `{{#each (range 0 500)}}{{#each (range 0 5)}}{{this}}:{{../this}} {{repeat "x" 10}}\n{{/each}}{{/each}}`;
7
+
8
+ Handlebars.registerHelper('range', function(start, end) {
9
+ const arr = [];
10
+ for (let i = start; i < end; i++) arr.push(i);
11
+ return arr;
12
+ });
13
+ Handlebars.registerHelper('isEven', function(n) {
14
+ return n % 2 === 0;
15
+ });
16
+ Handlebars.registerHelper('repeat', function(str, n) {
17
+ return str.repeat(n);
18
+ });
19
+
20
+ const data = {
21
+ items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
22
+ };
23
+
24
+ function bench(name, tpl, data, iterations = 5000) {
25
+ const compiled = Handlebars.compile(tpl);
26
+ for (let i = 0; i < 20; i++) compiled(data);
27
+
28
+ const times = [];
29
+ for (let i = 0; i < iterations; i++) {
30
+ const t0 = performance.now();
31
+ compiled(data);
32
+ times.push(performance.now() - t0);
33
+ }
34
+ times.sort((a, b) => a - b);
35
+ const median = times[Math.floor(times.length / 2)];
36
+ const rps = Math.round(1000 / median);
37
+ console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
38
+ return { name, medianMs: median, rps };
39
+ }
40
+
41
+ const results = [];
42
+ results.push(bench('handlebars:small', SMALL, data, 10000));
43
+ results.push(bench('handlebars:medium', MEDIUM, data, 5000));
44
+ results.push(bench('handlebars:large', LARGE, data, 1000));
45
+
46
+ const outPath = require('path').join(__dirname, 'handlebars-results.json');
47
+ require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
48
+ console.log(`\nResults saved to ${outPath}`);
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "name": "miki:small",
4
+ "medianMs": 0.00899999999998613,
5
+ "rps": 111111
6
+ },
7
+ {
8
+ "name": "miki:medium",
9
+ "medianMs": 0.0021999999999593456,
10
+ "rps": 454545
11
+ },
12
+ {
13
+ "name": "miki:large",
14
+ "medianMs": 0.002200000000016189,
15
+ "rps": 454545
16
+ }
17
+ ]
@@ -0,0 +1,36 @@
1
+ const miki = require('../src');
2
+ const { performance } = require('perf_hooks');
3
+
4
+ const SMALL = `{% for item in items %}{{ item|upper }}:{{ item|length }}\n{% endfor %}`;
5
+ const MEDIUM = `{% for i in range(0, 50) %}{% if i % 2 == 0 %}Even: {{ i }}\n{% else %}Odd: {{ i }}\n{% endif %}{% endfor %}`;
6
+ const LARGE = `{% for i in range(0, 500) %}{% for j in range(0, 5) %}{{ i }}:{{ j }} {{ "x"|repeat:10 }}\n{% endfor %}{% endfor %}`;
7
+
8
+ const data = {
9
+ items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
10
+ };
11
+
12
+ function bench(name, tpl, data, iterations = 5000) {
13
+ const compiled = miki.compile(tpl);
14
+ for (let i = 0; i < 20; i++) compiled.render(data);
15
+
16
+ const times = [];
17
+ for (let i = 0; i < iterations; i++) {
18
+ const t0 = performance.now();
19
+ compiled.render(data);
20
+ times.push(performance.now() - t0);
21
+ }
22
+ times.sort((a, b) => a - b);
23
+ const median = times[Math.floor(times.length / 2)];
24
+ const rps = Math.round(1000 / median);
25
+ console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
26
+ return { name, medianMs: median, rps };
27
+ }
28
+
29
+ const results = [];
30
+ results.push(bench('miki:small', SMALL, data, 10000));
31
+ results.push(bench('miki:medium', MEDIUM, data, 5000));
32
+ results.push(bench('miki:large', LARGE, data, 1000));
33
+
34
+ const outPath = require('path').join(__dirname, 'miki-results.json');
35
+ require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
36
+ console.log(`\nResults saved to ${outPath}`);
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "name": "pug:small",
4
+ "medianMs": 0.0006999999999948159,
5
+ "rps": 1428571
6
+ },
7
+ {
8
+ "name": "pug:medium",
9
+ "medianMs": 0.001599999999996271,
10
+ "rps": 625000
11
+ },
12
+ {
13
+ "name": "pug:large",
14
+ "medianMs": 0.33320000000003347,
15
+ "rps": 3001
16
+ }
17
+ ]
@@ -0,0 +1,36 @@
1
+ const pug = require('pug');
2
+ const { performance } = require('perf_hooks');
3
+
4
+ const SMALL = `each item in items\n = item.toUpperCase() + ':' + item.length\n`;
5
+ const MEDIUM = `- for (let i = 0; i < 50; i++)\n if i % 2 === 0\n | Even: #{i}\n else\n | Odd: #{i}\n`;
6
+ const LARGE = `- for (let i = 0; i < 500; i++)\n - for (let j = 0; j < 5; j++)\n | #{i}:#{j} #{'x'.repeat(10)}\n`;
7
+
8
+ const data = {
9
+ items: ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
10
+ };
11
+
12
+ function bench(name, tpl, data, iterations = 5000) {
13
+ const compiled = pug.compile(tpl);
14
+ for (let i = 0; i < 20; i++) compiled(data);
15
+
16
+ const times = [];
17
+ for (let i = 0; i < iterations; i++) {
18
+ const t0 = performance.now();
19
+ compiled(data);
20
+ times.push(performance.now() - t0);
21
+ }
22
+ times.sort((a, b) => a - b);
23
+ const median = times[Math.floor(times.length / 2)];
24
+ const rps = Math.round(1000 / median);
25
+ console.log(`${name}: ${median.toFixed(3)} ms/op (~${rps.toLocaleString()} rps)`);
26
+ return { name, medianMs: median, rps };
27
+ }
28
+
29
+ const results = [];
30
+ results.push(bench('pug:small', SMALL, data, 10000));
31
+ results.push(bench('pug:medium', MEDIUM, data, 5000));
32
+ results.push(bench('pug:large', LARGE, data, 1000));
33
+
34
+ const outPath = require('path').join(__dirname, 'pug-results.json');
35
+ require('fs').writeFileSync(outPath, JSON.stringify(results, null, 2));
36
+ console.log(`\nResults saved to ${outPath}`);