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.
- package/.github/workflows/ci.yml +14 -10
- package/.github/workflows/docs.yml +105 -0
- package/.github/workflows/npm-publish-github-packages.yml +36 -0
- package/README.md +142 -26
- package/assets/logo.png +0 -0
- package/benchmarks/ejs-results.json +17 -0
- package/benchmarks/ejs.js +36 -0
- package/benchmarks/handlebars-results.json +17 -0
- package/benchmarks/handlebars.js +48 -0
- package/benchmarks/miki-results.json +17 -0
- package/benchmarks/miki.js +36 -0
- package/benchmarks/pug-results.json +17 -0
- package/benchmarks/pug.js +36 -0
- package/benchmarks/run.js +69 -37
- package/benchmarks/stress.mjs +1 -1
- package/docs/api/async-render.md +85 -0
- package/docs/api/cache.md +87 -0
- package/docs/api/compile.md +128 -0
- package/docs/api/context-processors.md +77 -0
- package/docs/api/filters.md +217 -0
- package/docs/api/finder.md +94 -0
- package/docs/api/helpers.md +53 -0
- package/docs/api/i18n.md +157 -0
- package/docs/api/index.md +54 -0
- package/docs/api/libraries.md +207 -0
- package/docs/api/render-partial.md +81 -0
- package/docs/api/render.md +92 -0
- package/docs/api/security.md +145 -0
- package/docs/api/setup-express.md +76 -0
- package/docs/api/tags.md +134 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/guide/advanced-usage.md +397 -0
- package/docs/guide/async-rendering.md +308 -0
- package/docs/guide/context-processors.md +257 -0
- package/docs/guide/custom-filters.md +311 -0
- package/docs/guide/custom-tags.md +271 -0
- package/docs/guide/filters.md +642 -0
- package/docs/guide/getting-started.md +102 -0
- package/docs/guide/installation.md +95 -0
- package/docs/guide/partial-templates.md +367 -0
- package/docs/guide/quick-start.md +222 -0
- package/docs/guide/security.md +345 -0
- package/docs/guide/tags.md +783 -0
- package/docs/guide/template-discovery.md +170 -0
- package/docs/guide/template-inheritance.md +273 -0
- package/docs/guide/what-is-miki-template.md +28 -0
- package/docs/guide/why-miki-template.md +75 -0
- package/docs/index.md +104 -0
- package/docs/integrations/elysia.md +78 -0
- package/docs/integrations/express.md +219 -0
- package/docs/integrations/fastify.md +77 -0
- package/docs/integrations/hono.md +78 -0
- package/docs/integrations/index.md +68 -0
- package/docs/integrations/koa.md +88 -0
- package/docs/integrations/nestjs.md +78 -0
- package/docs/integrations/tsed.md +81 -0
- package/docs/javascripts/extra.js +174 -0
- package/docs/performance.md +37 -0
- package/docs/stylesheets/extra.css +819 -0
- package/live-test/integrations/elysia-example.js +16 -0
- package/live-test/integrations/express-example.js +24 -0
- package/live-test/integrations/fastify-example.js +20 -0
- package/live-test/integrations/hono-example.js +16 -0
- package/live-test/integrations/koa-example.js +30 -0
- package/live-test/integrations/nestjs-example.js +25 -0
- package/live-test/integrations/smoke-test.js +166 -0
- package/live-test/integrations/tsed-example.js +23 -0
- package/live-test/package-lock.json +235 -0
- package/live-test/package.json +4 -0
- package/live-test/views/home.html +17 -0
- package/mkdocs.yml +217 -0
- package/overrides/main.html +26 -0
- package/overrides/partials/footer.html +9 -0
- package/package.json +16 -6
- package/requirements-docs.txt +1 -0
- package/tests/integration/partial-render.test.cjs +13 -0
- package/docs/README.md +0 -18
- package/docs/advanced_usage.md +0 -71
- package/docs/api.md +0 -122
- package/docs/filters.md +0 -708
- package/docs/installation.md +0 -106
- package/docs/overview.md +0 -79
- package/docs/partialdef.md +0 -70
- package/docs/security.md +0 -27
- package/docs/tags.md +0 -673
- package/docs/usage.md +0 -646
package/.github/workflows/ci.yml
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
32
|
+
- name: Use Node.js
|
|
33
33
|
uses: actions/setup-node@v4
|
|
34
34
|
with:
|
|
35
|
-
node-version:
|
|
35
|
+
node-version: ${{ matrix.node-version }}
|
|
36
|
+
cache: 'npm'
|
|
36
37
|
- run: npm ci
|
|
37
|
-
- name: Run
|
|
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
|

|
|
3
3
|
 
|
|
4
|
-
|
|
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
|
|
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
|
-
- [
|
|
57
|
-
- [
|
|
58
|
-
- [
|
|
59
|
-
- [
|
|
60
|
-
- [
|
|
61
|
-
- [
|
|
62
|
-
- [
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
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
|
-
- **
|
|
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`.
|
package/assets/logo.png
ADDED
|
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}`);
|