miki-template 2.3.1 → 2.3.3
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/README.md +1 -1
- package/package.json +24 -2
- package/src/codegen.js +140 -9
- package/src/parser.js +69 -1
- package/src/tags/control.js +112 -51
- package/src/tags/extra.js +4 -17
- package/src/tags/i18n.js +6 -17
- package/src/tags/inheritance.js +6 -20
- package/src/tags/util.js +5 -20
- package/.eslintrc.json +0 -16
- package/.github/release-notes/v1.3.1.md +0 -55
- package/.github/release-notes/v1.3.3.md +0 -77
- package/.github/release-notes/v1.3.7.md +0 -20
- package/.github/workflows/ci.yml +0 -42
- package/.github/workflows/docs.yml +0 -107
- package/.github/workflows/release.yml +0 -107
- package/AGENT.md +0 -71
- package/API_REFERENCE.md +0 -341
- package/CHANGELOG.md +0 -173
- package/CODE_OF_CONDUCT.md +0 -14
- package/CONTRIBUTING.md +0 -27
- package/ROADMAP.md +0 -40
- package/assets/banner.png +0 -0
- package/assets/logo.png +0 -0
- package/benchmarks/ejs-results.json +0 -17
- package/benchmarks/ejs.js +0 -38
- package/benchmarks/handlebars-results.json +0 -17
- package/benchmarks/handlebars.js +0 -45
- package/benchmarks/miki-results.json +0 -17
- package/benchmarks/miki.js +0 -39
- package/benchmarks/pug-results.json +0 -17
- package/benchmarks/pug.js +0 -38
- package/benchmarks/report.json +0 -17
- package/benchmarks/run.js +0 -81
- package/benchmarks/stress.mjs +0 -647
- package/benchmarks/templates/large.dtpl +0 -7
- package/benchmarks/templates/medium.dtpl +0 -3
- package/benchmarks/templates/small.dtpl +0 -7
- package/context/component.md +0 -109
- package/context/prd.md +0 -131
- package/context/project-structure.md +0 -33
- package/dir/base.html +0 -23
- package/dir/cmpnt.html +0 -11
- package/dir/footer.html +0 -3
- package/dir/home.html +0 -80
- package/dir/index.html +0 -80
- package/dir/navbar.html +0 -9
- package/docs/api/async-render.md +0 -170
- package/docs/api/cache.md +0 -174
- package/docs/api/compile.md +0 -256
- package/docs/api/context-processors.md +0 -154
- package/docs/api/filters.md +0 -437
- package/docs/api/finder.md +0 -188
- package/docs/api/helpers.md +0 -106
- package/docs/api/i18n.md +0 -314
- package/docs/api/index.md +0 -108
- package/docs/api/libraries.md +0 -414
- package/docs/api/render-partial.md +0 -162
- package/docs/api/render.md +0 -184
- package/docs/api/security.md +0 -290
- package/docs/api/setup-express.md +0 -152
- package/docs/api/tags.md +0 -268
- package/docs/assets/banner.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/filter.md +0 -0
- package/docs/guide/advanced-usage.md +0 -794
- package/docs/guide/async-rendering.md +0 -616
- package/docs/guide/context-processors.md +0 -514
- package/docs/guide/custom-filters.md +0 -622
- package/docs/guide/custom-tags.md +0 -542
- package/docs/guide/filters.md +0 -1314
- package/docs/guide/getting-started.md +0 -204
- package/docs/guide/installation.md +0 -190
- package/docs/guide/partial-templates.md +0 -734
- package/docs/guide/quick-start.md +0 -444
- package/docs/guide/security.md +0 -690
- package/docs/guide/tags.md +0 -1566
- package/docs/guide/template-discovery.md +0 -340
- package/docs/guide/template-inheritance.md +0 -546
- package/docs/guide/what-is-miki-template.md +0 -28
- package/docs/guide/why-miki-template.md +0 -75
- package/docs/index.md +0 -86
- package/docs/integrations/elysia.md +0 -80
- package/docs/integrations/express.md +0 -219
- package/docs/integrations/fastify.md +0 -79
- package/docs/integrations/hono.md +0 -80
- package/docs/integrations/index.md +0 -68
- package/docs/integrations/koa.md +0 -90
- package/docs/integrations/nestjs.md +0 -80
- package/docs/integrations/tsed.md +0 -83
- package/docs/performance.md +0 -74
- package/eslint.config.mjs +0 -42
- package/ex.mjs +0 -36
- package/live-test/integrations/elysia-example.js +0 -16
- package/live-test/integrations/express-example.js +0 -24
- package/live-test/integrations/fastify-example.js +0 -20
- package/live-test/integrations/hono-example.js +0 -16
- package/live-test/integrations/koa-example.js +0 -30
- package/live-test/integrations/nestjs-example.js +0 -25
- package/live-test/integrations/smoke-test.js +0 -166
- package/live-test/integrations/tsed-example.js +0 -23
- package/live-test/package-lock.json +0 -1150
- package/live-test/package.json +0 -13
- package/live-test/packages/product/templates/product/detail.html +0 -7
- package/live-test/server.js +0 -38
- package/live-test/templates/app_templates/detail.html +0 -6
- package/live-test/views/base.html +0 -8
- package/live-test/views/child.html +0 -7
- package/live-test/views/home.html +0 -17
- package/live-test/views/index.html +0 -1
- package/miki-template-extension/.github/workflows/ci.yml +0 -116
- package/miki-template-extension/.vscodeignore +0 -7
- package/miki-template-extension/CHANGELOG.md +0 -99
- package/miki-template-extension/LICENSE +0 -21
- package/miki-template-extension/README.md +0 -273
- package/miki-template-extension/extension.js +0 -1013
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +0 -10
- package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
- package/miki-template-extension/package.json +0 -280
- package/miki-template-extension/snippets/miki-template.json +0 -717
- package/miki-template-extension/syntaxes/language-configuration.json +0 -114
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +0 -355
- package/miki-template-extension/tests/grammar-tests.json +0 -162
- package/miki-template-extension/tests/run-grammar-tests.js +0 -82
- package/mkdocs.yml +0 -195
- package/overrides/main.html +0 -26
- package/requirements-docs.txt +0 -2
- package/sample-app/package-lock.json +0 -901
- package/sample-app/package.json +0 -9
- package/sample-app/server.js +0 -14
- package/sample-app/views/index.html +0 -1
- package/scripts/build-vsix.js +0 -129
- package/scripts/build-vsix.ps1 +0 -15
- package/snippets/miki-template.json +0 -177
- package/src/asyncRender.js +0 -21
- package/src/types.d.ts +0 -107
- package/syntaxes/language-configuration.json +0 -26
- package/syntaxes/miki-template.tmLanguage.json +0 -146
- package/tests/asyncRender.test.js +0 -17
- package/tests/base.html +0 -6
- package/tests/child.html +0 -3
- package/tests/context_processors.test.js +0 -13
- package/tests/esm.test.mjs +0 -61
- package/tests/filters.test.js +0 -263
- package/tests/finder-appdirs.test.js +0 -19
- package/tests/finder.test.js +0 -17
- package/tests/fixtures/views/nested/index.html +0 -1
- package/tests/fixtures/views/partial.html +0 -1
- package/tests/fixtures/views/sub/deepfile.html +0 -1
- package/tests/fixtures/views-appdirs/product/site/detail.html +0 -1
- package/tests/include_security.test.js +0 -9
- package/tests/integration/README.md +0 -32
- package/tests/integration/features.test.cjs +0 -1681
- package/tests/integration/features.test.mjs +0 -1697
- package/tests/integration/finder.esm.test.mjs +0 -13
- package/tests/integration/partial-render.test.cjs +0 -13
- package/tests/integration/templates/base.miki +0 -6
- package/tests/integration/templates/child.miki +0 -6
- package/tests/integration/templates/index.html +0 -17
- package/tests/lexer.test.js +0 -45
- package/tests/parser.test.js +0 -57
- package/tests/partial.html +0 -1
- package/tests/partialdef.test.js +0 -79
- package/tests/production_checks.js +0 -57
- package/tests/security.test.js +0 -28
- package/tests/tags.test.js +0 -233
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ you can register your own custom filters and filters in miki templates.
|
|
|
18
18
|
- **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.
|
|
19
19
|
- **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.
|
|
20
20
|
- **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.**
|
|
21
|
-
- **Full Syntax Parity**: Supports variables, dotted lookups, filters (`|`), and block tags (`{% %}`).
|
|
21
|
+
- **Full Syntax Parity**: Supports variables, dotted lookups, filters (`|`), and block tags (`{% %}`). **All tag arguments now support filter chains** — e.g. `{% set x = " hello "|trim|upper %}`, `{% for i in "5,1,2"|split:","|sort %}`, `{% with greeting="hello"|upper %}`.
|
|
22
22
|
- **Template Inheritance**: Multi-level inheritance with `extends`, block overrides, and `{{ block.super }}` support.
|
|
23
23
|
- **Built-in libraries**: `humanize`, `cache`, and `lorem` ship pre-activated. `{% lorem 5 p %}` works without `{% load lorem %}`.
|
|
24
24
|
- **ESM & CommonJS**: Works seamlessly with both `import` and `require` syntax.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miki-template",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "Django-Style template engine for Node.js and Express",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -10,6 +10,28 @@
|
|
|
10
10
|
"default": "./src/index.js"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src/index.js",
|
|
15
|
+
"src/esm.mjs",
|
|
16
|
+
"src/lexer.js",
|
|
17
|
+
"src/parser.js",
|
|
18
|
+
"src/context.js",
|
|
19
|
+
"src/context_processors.js",
|
|
20
|
+
"src/filters.js",
|
|
21
|
+
"src/security.js",
|
|
22
|
+
"src/cache.js",
|
|
23
|
+
"src/utils.js",
|
|
24
|
+
"src/codegen.js",
|
|
25
|
+
"src/i18n.js",
|
|
26
|
+
"src/libraries.js",
|
|
27
|
+
"src/tags/helpers.js",
|
|
28
|
+
"src/tags/registry.js",
|
|
29
|
+
"src/tags/control.js",
|
|
30
|
+
"src/tags/inheritance.js",
|
|
31
|
+
"src/tags/util.js",
|
|
32
|
+
"src/tags/i18n.js",
|
|
33
|
+
"src/tags/extra.js"
|
|
34
|
+
],
|
|
13
35
|
"keywords": [
|
|
14
36
|
"django",
|
|
15
37
|
"template",
|
|
@@ -47,4 +69,4 @@
|
|
|
47
69
|
"release:minor": "npm version minor --no-git-tag-version -m \"chore(release): v%s\" && git push origin main",
|
|
48
70
|
"release:major": "npm version major --no-git-tag-version -m \"chore(release): v%s\" && git push origin main"
|
|
49
71
|
}
|
|
50
|
-
}
|
|
72
|
+
}
|
package/src/codegen.js
CHANGED
|
@@ -212,7 +212,7 @@ function genNode(node, stmts, level, buf = 'out', loopVarMap = {}) {
|
|
|
212
212
|
case 'ForNode': {
|
|
213
213
|
const s = '_' + level;
|
|
214
214
|
stmts.push(pad('{', level));
|
|
215
|
-
const iterExpr = (loopVarMap && loopVarMap[node.iterablePath]) ? loopVarMap[node.iterablePath] : `_get(_ctx, ${js(node.iterablePath)})
|
|
215
|
+
const iterExpr = node.isLiteral ? js(node.literalValue) : ((loopVarMap && loopVarMap[node.iterablePath]) ? loopVarMap[node.iterablePath] : `_get(_ctx, ${js(node.iterablePath)})`);
|
|
216
216
|
stmts.push(pad(`let _raw${s} = ${iterExpr};`, level + 1));
|
|
217
217
|
for (let i = 0; i < node.filters.length; i++) {
|
|
218
218
|
const f = node.filters[i];
|
|
@@ -670,14 +670,145 @@ function resolveVal(token, context) {
|
|
|
670
670
|
if (token === undefined || token === null) return '';
|
|
671
671
|
if (typeof token !== 'string') return token;
|
|
672
672
|
if (token === '') return '';
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
673
|
+
|
|
674
|
+
let idx = 0;
|
|
675
|
+
const len = token.length;
|
|
676
|
+
|
|
677
|
+
function skipWs() {
|
|
678
|
+
while (idx < len && /\s/.test(token[idx])) idx++;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let isLiteral = false;
|
|
682
|
+
let literalValue = null;
|
|
683
|
+
let varPath = '';
|
|
684
|
+
|
|
685
|
+
if (idx < len && (token[idx] === '"' || token[idx] === '\'')) {
|
|
686
|
+
const quote = token[idx];
|
|
687
|
+
idx++;
|
|
688
|
+
let str = '';
|
|
689
|
+
while (idx < len && token[idx] !== quote) {
|
|
690
|
+
if (token[idx] === '\\' && idx + 1 < len) idx++;
|
|
691
|
+
str += token[idx];
|
|
692
|
+
idx++;
|
|
693
|
+
}
|
|
694
|
+
idx++;
|
|
695
|
+
isLiteral = true;
|
|
696
|
+
literalValue = str;
|
|
697
|
+
skipWs();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (!isLiteral && idx < len) {
|
|
701
|
+
const remaining = token.slice(idx);
|
|
702
|
+
if (remaining.startsWith('true') || remaining.startsWith('True')) {
|
|
703
|
+
isLiteral = true;
|
|
704
|
+
literalValue = true;
|
|
705
|
+
idx += remaining.startsWith('True') ? 4 : 5;
|
|
706
|
+
skipWs();
|
|
707
|
+
} else if (remaining.startsWith('false') || remaining.startsWith('False')) {
|
|
708
|
+
isLiteral = true;
|
|
709
|
+
literalValue = false;
|
|
710
|
+
idx += remaining.startsWith('False') ? 5 : 6;
|
|
711
|
+
skipWs();
|
|
712
|
+
} else if (remaining.startsWith('null') || remaining.startsWith('None') || remaining.startsWith('none')) {
|
|
713
|
+
isLiteral = true;
|
|
714
|
+
literalValue = null;
|
|
715
|
+
idx += 4;
|
|
716
|
+
skipWs();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!isLiteral && idx < len && /[\d.-]/.test(token[idx])) {
|
|
721
|
+
let numStr = '';
|
|
722
|
+
if (token[idx] === '-') {
|
|
723
|
+
numStr += '-';
|
|
724
|
+
idx++;
|
|
725
|
+
}
|
|
726
|
+
while (idx < len && /[\d]/.test(token[idx])) {
|
|
727
|
+
numStr += token[idx];
|
|
728
|
+
idx++;
|
|
729
|
+
}
|
|
730
|
+
if (token[idx] === '.') {
|
|
731
|
+
numStr += '.';
|
|
732
|
+
idx++;
|
|
733
|
+
while (idx < len && /[\d]/.test(token[idx])) {
|
|
734
|
+
numStr += token[idx];
|
|
735
|
+
idx++;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
isLiteral = true;
|
|
739
|
+
literalValue = Number(numStr);
|
|
740
|
+
skipWs();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!isLiteral) {
|
|
744
|
+
while (idx < len && token[idx] !== '|') {
|
|
745
|
+
varPath += token[idx];
|
|
746
|
+
idx++;
|
|
747
|
+
}
|
|
748
|
+
varPath = varPath.trim();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let val = isLiteral ? literalValue : (varPath ? context.get(varPath) : undefined);
|
|
752
|
+
|
|
753
|
+
while (idx < len) {
|
|
754
|
+
if (token[idx] === '|') {
|
|
755
|
+
idx++;
|
|
756
|
+
skipWs();
|
|
757
|
+
let name = '';
|
|
758
|
+
while (idx < len && token[idx] !== ':' && token[idx] !== '|' && !/\s/.test(token[idx])) {
|
|
759
|
+
name += token[idx];
|
|
760
|
+
idx++;
|
|
761
|
+
}
|
|
762
|
+
skipWs();
|
|
763
|
+
let argVal = undefined;
|
|
764
|
+
if (idx < len && token[idx] === ':') {
|
|
765
|
+
idx++;
|
|
766
|
+
skipWs();
|
|
767
|
+
if (idx < len && (token[idx] === '"' || token[idx] === '\'')) {
|
|
768
|
+
const quote = token[idx];
|
|
769
|
+
idx++;
|
|
770
|
+
let s = '';
|
|
771
|
+
while (idx < len && token[idx] !== quote) {
|
|
772
|
+
if (token[idx] === '\\' && idx + 1 < len) idx++;
|
|
773
|
+
s += token[idx];
|
|
774
|
+
idx++;
|
|
775
|
+
}
|
|
776
|
+
idx++;
|
|
777
|
+
argVal = s;
|
|
778
|
+
} else {
|
|
779
|
+
let s = '';
|
|
780
|
+
while (idx < len && token[idx] !== '|' && !/\s/.test(token[idx])) {
|
|
781
|
+
s += token[idx];
|
|
782
|
+
idx++;
|
|
783
|
+
}
|
|
784
|
+
s = s.trim();
|
|
785
|
+
if (s !== '' && !isNaN(s)) argVal = Number(s);
|
|
786
|
+
else if (s === 'true') argVal = true;
|
|
787
|
+
else if (s === 'false') argVal = false;
|
|
788
|
+
else if (s === 'null' || s === 'None' || s === 'none') argVal = null;
|
|
789
|
+
else argVal = s;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const fn = filtersModule.getFilter(name);
|
|
793
|
+
if (!fn) throw new Error(`Unknown filter: '${name}'`);
|
|
794
|
+
val = fn(val, argVal, context);
|
|
795
|
+
skipWs();
|
|
796
|
+
} else {
|
|
797
|
+
idx++;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (val === undefined) {
|
|
802
|
+
if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
|
|
803
|
+
return token.slice(1, -1);
|
|
804
|
+
}
|
|
805
|
+
if (token === 'true' || token === 'True') return true;
|
|
806
|
+
if (token === 'false' || token === 'False') return false;
|
|
807
|
+
if (token === 'none' || token === 'None' || token === 'null') return null;
|
|
808
|
+
if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
|
|
809
|
+
return context.get(token);
|
|
810
|
+
}
|
|
811
|
+
return val;
|
|
681
812
|
}
|
|
682
813
|
|
|
683
814
|
function getVal(ctx, path) {
|
package/src/parser.js
CHANGED
|
@@ -88,6 +88,51 @@ function parseVariableExpression(expr) {
|
|
|
88
88
|
skipWhitespace();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Check for boolean/null literals as base value
|
|
92
|
+
if (!isLiteral && idx < len) {
|
|
93
|
+
const remaining = expr.slice(idx);
|
|
94
|
+
if (remaining.startsWith('true') || remaining.startsWith('True')) {
|
|
95
|
+
isLiteral = true;
|
|
96
|
+
literalValue = true;
|
|
97
|
+
idx += remaining.startsWith('True') ? 4 : 5;
|
|
98
|
+
skipWhitespace();
|
|
99
|
+
} else if (remaining.startsWith('false') || remaining.startsWith('False')) {
|
|
100
|
+
isLiteral = true;
|
|
101
|
+
literalValue = false;
|
|
102
|
+
idx += remaining.startsWith('False') ? 5 : 6;
|
|
103
|
+
skipWhitespace();
|
|
104
|
+
} else if (remaining.startsWith('null') || remaining.startsWith('None') || remaining.startsWith('none')) {
|
|
105
|
+
isLiteral = true;
|
|
106
|
+
literalValue = null;
|
|
107
|
+
idx += remaining.startsWith('None') ? 4 : (remaining.startsWith('null') ? 4 : 4);
|
|
108
|
+
skipWhitespace();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for numeric literal as base value
|
|
113
|
+
if (!isLiteral && idx < len && /[\d.-]/.test(expr[idx])) {
|
|
114
|
+
let numStr = '';
|
|
115
|
+
if (expr[idx] === '-') {
|
|
116
|
+
numStr += '-';
|
|
117
|
+
idx++;
|
|
118
|
+
}
|
|
119
|
+
while (idx < len && /[\d]/.test(expr[idx])) {
|
|
120
|
+
numStr += expr[idx];
|
|
121
|
+
idx++;
|
|
122
|
+
}
|
|
123
|
+
if (expr[idx] === '.') {
|
|
124
|
+
numStr += '.';
|
|
125
|
+
idx++;
|
|
126
|
+
while (idx < len && /[\d]/.test(expr[idx])) {
|
|
127
|
+
numStr += expr[idx];
|
|
128
|
+
idx++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
isLiteral = true;
|
|
132
|
+
literalValue = Number(numStr);
|
|
133
|
+
skipWhitespace();
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
// Parse main variable path
|
|
92
137
|
if (!isLiteral) {
|
|
93
138
|
while (idx < len && expr[idx] !== '|') {
|
|
@@ -262,9 +307,32 @@ class Parser {
|
|
|
262
307
|
}
|
|
263
308
|
}
|
|
264
309
|
|
|
310
|
+
function evaluateExpression(exprStr, context) {
|
|
311
|
+
if (!exprStr || typeof exprStr !== 'string') return exprStr === undefined || exprStr === null ? '' : exprStr;
|
|
312
|
+
const parsed = parseVariableExpression(exprStr);
|
|
313
|
+
let val = parsed.isLiteral ? parsed.literalValue : (parsed.varPath ? context.get(parsed.varPath) : undefined);
|
|
314
|
+
for (const filterInfo of parsed.filters) {
|
|
315
|
+
const filterFn = filtersModule.getFilter(filterInfo.name);
|
|
316
|
+
if (!filterFn) {
|
|
317
|
+
throw new Error(`Unknown filter: '${filterInfo.name}'`);
|
|
318
|
+
}
|
|
319
|
+
let argVal = undefined;
|
|
320
|
+
if (filterInfo.arg) {
|
|
321
|
+
if (filterInfo.arg.type === 'literal') {
|
|
322
|
+
argVal = filterInfo.arg.value;
|
|
323
|
+
} else if (filterInfo.arg.type === 'variable') {
|
|
324
|
+
argVal = context.get(filterInfo.arg.value);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
val = filterFn(val, argVal, context);
|
|
328
|
+
}
|
|
329
|
+
return val === undefined ? '' : val;
|
|
330
|
+
}
|
|
331
|
+
|
|
265
332
|
module.exports = {
|
|
266
333
|
Parser,
|
|
267
334
|
TextNode,
|
|
268
335
|
VariableNode,
|
|
269
|
-
parseVariableExpression
|
|
336
|
+
parseVariableExpression,
|
|
337
|
+
evaluateExpression
|
|
270
338
|
};
|
package/src/tags/control.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Control flow template tags: if, for, with, cycle, comment, with, firstof.
|
|
3
3
|
*/
|
|
4
|
-
const { parseVariableExpression } = require('../parser');
|
|
4
|
+
const { parseVariableExpression, evaluateExpression } = require('../parser');
|
|
5
5
|
const { getFilter } = require('../filters');
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -81,23 +81,6 @@ function tokenizeExpr(exprStr) {
|
|
|
81
81
|
return tokens;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
* Helper to resolve a token to its runtime value.
|
|
86
|
-
*/
|
|
87
|
-
function resolveValue(token, context) {
|
|
88
|
-
if (!token) return '';
|
|
89
|
-
if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
|
|
90
|
-
return token.slice(1, -1);
|
|
91
|
-
}
|
|
92
|
-
if (!isNaN(token) && token !== '') {
|
|
93
|
-
return Number(token);
|
|
94
|
-
}
|
|
95
|
-
if (token === 'true' || token === 'True') return true;
|
|
96
|
-
if (token === 'false' || token === 'False') return false;
|
|
97
|
-
if (token === 'none' || token === 'None' || token === 'null') return null;
|
|
98
|
-
return context.get(token);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
84
|
/**
|
|
102
85
|
* Shunting-yard style expression evaluator supporting:
|
|
103
86
|
* == != < <= > >= in not in and or not
|
|
@@ -161,7 +144,7 @@ function evaluateCondition(exprStr, context) {
|
|
|
161
144
|
continue;
|
|
162
145
|
}
|
|
163
146
|
|
|
164
|
-
vals.push(
|
|
147
|
+
vals.push(evaluateExpression(t, context));
|
|
165
148
|
}
|
|
166
149
|
|
|
167
150
|
while (ops.length > 0) {
|
|
@@ -201,12 +184,14 @@ class IfNode {
|
|
|
201
184
|
}
|
|
202
185
|
|
|
203
186
|
class ForNode {
|
|
204
|
-
constructor(loopVars, iterablePath, body, emptyBody, filters = []) {
|
|
187
|
+
constructor(loopVars, iterablePath, body, emptyBody, filters = [], isLiteral = false, literalValue = null) {
|
|
205
188
|
this.loopVars = loopVars;
|
|
206
189
|
this.iterablePath = iterablePath;
|
|
207
190
|
this.filters = filters;
|
|
208
191
|
this.body = body;
|
|
209
192
|
this.emptyBody = emptyBody;
|
|
193
|
+
this.isLiteral = isLiteral;
|
|
194
|
+
this.literalValue = literalValue;
|
|
210
195
|
this._filterFns = filters.map(f => {
|
|
211
196
|
const fn = getFilter(f.name);
|
|
212
197
|
return { fn, arg: f.arg };
|
|
@@ -214,7 +199,7 @@ class ForNode {
|
|
|
214
199
|
}
|
|
215
200
|
|
|
216
201
|
render(context) {
|
|
217
|
-
let rawItems = context.get(this.iterablePath);
|
|
202
|
+
let rawItems = this.isLiteral ? this.literalValue : context.get(this.iterablePath);
|
|
218
203
|
|
|
219
204
|
for (const { fn, arg } of this._filterFns) {
|
|
220
205
|
if (!fn) {
|
|
@@ -293,24 +278,7 @@ class WithNode {
|
|
|
293
278
|
render(context) {
|
|
294
279
|
const scope = {};
|
|
295
280
|
for (const mapping of this.mappings) {
|
|
296
|
-
|
|
297
|
-
// a variable lookup (e.g. user.name). Parse quotes here.
|
|
298
|
-
const raw = mapping.valPath;
|
|
299
|
-
let value;
|
|
300
|
-
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith('\'') && raw.endsWith('\''))) {
|
|
301
|
-
value = raw.slice(1, -1);
|
|
302
|
-
} else if (raw === 'true' || raw === 'True') {
|
|
303
|
-
value = true;
|
|
304
|
-
} else if (raw === 'false' || raw === 'False') {
|
|
305
|
-
value = false;
|
|
306
|
-
} else if (raw === 'none' || raw === 'None' || raw === 'null') {
|
|
307
|
-
value = null;
|
|
308
|
-
} else if (raw !== '' && !isNaN(Number(raw))) {
|
|
309
|
-
value = Number(raw);
|
|
310
|
-
} else {
|
|
311
|
-
value = context.get(raw);
|
|
312
|
-
}
|
|
313
|
-
scope[mapping.name] = value;
|
|
281
|
+
scope[mapping.name] = evaluateExpression(mapping.valPath, context);
|
|
314
282
|
}
|
|
315
283
|
// If an `as alias` was used after key=value pairs, alias the last
|
|
316
284
|
// key to the alias name. This supports `{% with a=5 as answ %}` →
|
|
@@ -336,7 +304,7 @@ class CycleNode {
|
|
|
336
304
|
const key = this.args.join(',');
|
|
337
305
|
let idx = context.cycleStates.get(key) || 0;
|
|
338
306
|
const token = this.args[idx % this.args.length];
|
|
339
|
-
const val =
|
|
307
|
+
const val = evaluateExpression(token, context);
|
|
340
308
|
context.cycleStates.set(key, idx + 1);
|
|
341
309
|
|
|
342
310
|
if (this.asName) {
|
|
@@ -356,7 +324,7 @@ class FirstofNode {
|
|
|
356
324
|
|
|
357
325
|
render(context) {
|
|
358
326
|
for (const arg of this.args) {
|
|
359
|
-
const val =
|
|
327
|
+
const val = evaluateExpression(arg, context);
|
|
360
328
|
if (val && val !== '' && val !== null && val !== undefined) {
|
|
361
329
|
return String(val);
|
|
362
330
|
}
|
|
@@ -576,7 +544,7 @@ function parseFor(tagContent, parser) {
|
|
|
576
544
|
|
|
577
545
|
// Parse the iterable expression to support filters: items|regroup:"category"
|
|
578
546
|
const parsedIterable = parseVariableExpression(iterableExpr);
|
|
579
|
-
return new ForNode(loopVars, parsedIterable.varPath, body, emptyBody, parsedIterable.filters);
|
|
547
|
+
return new ForNode(loopVars, parsedIterable.varPath, body, emptyBody, parsedIterable.filters, parsedIterable.isLiteral, parsedIterable.literalValue);
|
|
580
548
|
}
|
|
581
549
|
|
|
582
550
|
function parseWith(tagContent, parser) {
|
|
@@ -658,13 +626,36 @@ function parseKeyValuePairs(content) {
|
|
|
658
626
|
val += content[i++];
|
|
659
627
|
}
|
|
660
628
|
if (content[i] === quote) i++;
|
|
661
|
-
// Keep the quotes around the literal so WithNode can recognize it
|
|
662
629
|
val = quote + val + quote;
|
|
663
630
|
} else {
|
|
664
631
|
while (i < content.length && content[i] !== ',' && !/\s/.test(content[i])) {
|
|
665
632
|
val += content[i++];
|
|
666
633
|
}
|
|
667
634
|
}
|
|
635
|
+
|
|
636
|
+
while (i < content.length && /\s/.test(content[i])) i++;
|
|
637
|
+
if (i < content.length && content[i] === '|') {
|
|
638
|
+
val += '|';
|
|
639
|
+
i++;
|
|
640
|
+
while (i < content.length) {
|
|
641
|
+
if (/\s/.test(content[i])) { i++; continue; }
|
|
642
|
+
if (content[i] === ',') break;
|
|
643
|
+
if (content[i] === '|') { val += '|'; i++; continue; }
|
|
644
|
+
if (content[i] === ':') { val += ':'; i++; continue; }
|
|
645
|
+
if (content[i] === '"' || content[i] === '\'') {
|
|
646
|
+
const quote = content[i++];
|
|
647
|
+
val += quote;
|
|
648
|
+
while (i < content.length && content[i] !== quote) {
|
|
649
|
+
if (content[i] === '\\' && i + 1 < content.length) { val += content[i]; i++; }
|
|
650
|
+
val += content[i++];
|
|
651
|
+
}
|
|
652
|
+
if (content[i] === quote) { val += quote; i++; }
|
|
653
|
+
} else {
|
|
654
|
+
val += content[i++];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
668
659
|
tokens.push({ name: key, valPath: val });
|
|
669
660
|
while (i < content.length && /\s/.test(content[i])) i++;
|
|
670
661
|
if (content[i] === ',') i++;
|
|
@@ -674,17 +665,16 @@ function parseKeyValuePairs(content) {
|
|
|
674
665
|
|
|
675
666
|
function parseCycle(tagContent, _parser) {
|
|
676
667
|
const content = tagContent.slice(5).trim();
|
|
677
|
-
const
|
|
678
|
-
const matches = content.match(argRegex) || [];
|
|
668
|
+
const parts = content.split(/\s+/).filter(Boolean);
|
|
679
669
|
let asName = null;
|
|
680
670
|
const args = [];
|
|
681
671
|
|
|
682
|
-
for (let i = 0; i <
|
|
683
|
-
if (
|
|
684
|
-
asName =
|
|
672
|
+
for (let i = 0; i < parts.length; i++) {
|
|
673
|
+
if (parts[i] === 'as' && i < parts.length - 1) {
|
|
674
|
+
asName = parts[i + 1];
|
|
685
675
|
break;
|
|
686
676
|
}
|
|
687
|
-
args.push(
|
|
677
|
+
args.push(parts[i]);
|
|
688
678
|
}
|
|
689
679
|
|
|
690
680
|
return new CycleNode(args, asName);
|
|
@@ -692,8 +682,79 @@ function parseCycle(tagContent, _parser) {
|
|
|
692
682
|
|
|
693
683
|
function parseFirstof(tagContent, _parser) {
|
|
694
684
|
const content = tagContent.slice(8).trim();
|
|
695
|
-
const
|
|
696
|
-
|
|
685
|
+
const args = [];
|
|
686
|
+
let i = 0;
|
|
687
|
+
const len = content.length;
|
|
688
|
+
|
|
689
|
+
while (i < len) {
|
|
690
|
+
while (i < len && /\s/.test(content[i])) i++;
|
|
691
|
+
if (i >= len) break;
|
|
692
|
+
|
|
693
|
+
let expr = '';
|
|
694
|
+
if (content[i] === '"' || content[i] === '\'') {
|
|
695
|
+
const quote = content[i];
|
|
696
|
+
expr += content[i];
|
|
697
|
+
i++;
|
|
698
|
+
while (i < len && content[i] !== quote) {
|
|
699
|
+
if (content[i] === '\\' && i + 1 < len) {
|
|
700
|
+
expr += content[i];
|
|
701
|
+
i++;
|
|
702
|
+
}
|
|
703
|
+
expr += content[i];
|
|
704
|
+
i++;
|
|
705
|
+
}
|
|
706
|
+
if (i < len) {
|
|
707
|
+
expr += content[i];
|
|
708
|
+
i++;
|
|
709
|
+
}
|
|
710
|
+
while (i < len && !/\s/.test(content[i])) {
|
|
711
|
+
if (content[i] === '|') {
|
|
712
|
+
expr += content[i];
|
|
713
|
+
i++;
|
|
714
|
+
while (i < len && content[i] !== ':' && content[i] !== '|' && !/\s/.test(content[i])) {
|
|
715
|
+
expr += content[i];
|
|
716
|
+
i++;
|
|
717
|
+
}
|
|
718
|
+
if (i < len && content[i] === ':') {
|
|
719
|
+
expr += content[i];
|
|
720
|
+
i++;
|
|
721
|
+
if (i < len && (content[i] === '"' || content[i] === '\'')) {
|
|
722
|
+
const q = content[i];
|
|
723
|
+
expr += content[i];
|
|
724
|
+
i++;
|
|
725
|
+
while (i < len && content[i] !== q) {
|
|
726
|
+
if (content[i] === '\\' && i + 1 < len) {
|
|
727
|
+
expr += content[i];
|
|
728
|
+
i++;
|
|
729
|
+
}
|
|
730
|
+
expr += content[i];
|
|
731
|
+
i++;
|
|
732
|
+
}
|
|
733
|
+
if (i < len) {
|
|
734
|
+
expr += content[i];
|
|
735
|
+
i++;
|
|
736
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
while (i < len && content[i] !== '|' && !/\s/.test(content[i])) {
|
|
739
|
+
expr += content[i];
|
|
740
|
+
i++;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
while (i < len && !/\s/.test(content[i])) {
|
|
750
|
+
expr += content[i];
|
|
751
|
+
i++;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (expr) args.push(expr);
|
|
756
|
+
}
|
|
757
|
+
|
|
697
758
|
return new FirstofNode(args);
|
|
698
759
|
}
|
|
699
760
|
|
package/src/tags/extra.js
CHANGED
|
@@ -2,20 +2,7 @@
|
|
|
2
2
|
* Additional utility template tags: now, set, ifchanged.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
function resolveValue(token, context) {
|
|
7
|
-
if (token === undefined || token === null) return '';
|
|
8
|
-
if (typeof token !== 'string') return token;
|
|
9
|
-
if (token === '') return '';
|
|
10
|
-
if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
|
|
11
|
-
return token.slice(1, -1);
|
|
12
|
-
}
|
|
13
|
-
if (token === 'true' || token === 'True') return true;
|
|
14
|
-
if (token === 'false' || token === 'False') return false;
|
|
15
|
-
if (token === 'none' || token === 'None' || token === 'null') return null;
|
|
16
|
-
if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
|
|
17
|
-
return context.get(token);
|
|
18
|
-
}
|
|
5
|
+
const { evaluateExpression } = require('../parser');
|
|
19
6
|
|
|
20
7
|
// --- {% now %} tag ---
|
|
21
8
|
|
|
@@ -25,7 +12,7 @@ class NowNode {
|
|
|
25
12
|
}
|
|
26
13
|
|
|
27
14
|
render(context) {
|
|
28
|
-
const fmt =
|
|
15
|
+
const fmt = evaluateExpression(this.formatExpr, context);
|
|
29
16
|
const now = new Date();
|
|
30
17
|
if (!fmt) return now.toISOString();
|
|
31
18
|
const dateFilter = require('../filters');
|
|
@@ -49,7 +36,7 @@ class SetNode {
|
|
|
49
36
|
|
|
50
37
|
render(context) {
|
|
51
38
|
const name = String(this.nameExpr);
|
|
52
|
-
const value = this.valueExpr !== undefined ?
|
|
39
|
+
const value = this.valueExpr !== undefined ? evaluateExpression(this.valueExpr, context) : '';
|
|
53
40
|
if (name) {
|
|
54
41
|
context.scopes[0][name] = value;
|
|
55
42
|
}
|
|
@@ -90,7 +77,7 @@ class IfChangedNode {
|
|
|
90
77
|
}
|
|
91
78
|
|
|
92
79
|
render(context) {
|
|
93
|
-
const currentVal = this.conditionStr ?
|
|
80
|
+
const currentVal = this.conditionStr ? evaluateExpression(this.conditionStr, context) : undefined;
|
|
94
81
|
|
|
95
82
|
if (!context.ifChangedState) {
|
|
96
83
|
context.ifChangedState = new Map();
|
package/src/tags/i18n.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* i18n template tags: trans, blocktrans, language.
|
|
3
3
|
*/
|
|
4
4
|
const i18n = require('../i18n');
|
|
5
|
+
const { evaluateExpression } = require('../parser');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* {% trans "translation key" %}
|
|
@@ -18,19 +19,10 @@ class TransNode {
|
|
|
18
19
|
const params = {};
|
|
19
20
|
if (this.args) {
|
|
20
21
|
for (const [k, v] of Object.entries(this.args)) {
|
|
21
|
-
params[k] =
|
|
22
|
+
params[k] = evaluateExpression(v, context);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
let key = this.key;
|
|
26
|
-
if (!key.startsWith('"') && !key.startsWith('\'') && !key.includes(' ')) {
|
|
27
|
-
const resolved = context.get(key);
|
|
28
|
-
if (typeof resolved === 'string' && resolved.length > 0) {
|
|
29
|
-
key = resolved;
|
|
30
|
-
}
|
|
31
|
-
} else {
|
|
32
|
-
key = key.slice(1, -1);
|
|
33
|
-
}
|
|
25
|
+
const key = evaluateExpression(this.key, context);
|
|
34
26
|
return i18n.lookup(key, params);
|
|
35
27
|
}
|
|
36
28
|
}
|
|
@@ -42,10 +34,7 @@ class LanguageNode {
|
|
|
42
34
|
}
|
|
43
35
|
|
|
44
36
|
render(context) {
|
|
45
|
-
|
|
46
|
-
const lang = this.lang.startsWith('"') || this.lang.startsWith('\'')
|
|
47
|
-
? this.lang.slice(1, -1)
|
|
48
|
-
: (context.get(this.lang) || this.lang);
|
|
37
|
+
const lang = evaluateExpression(this.lang, context);
|
|
49
38
|
const previous = i18n.getLanguage();
|
|
50
39
|
i18n.setLanguage(lang);
|
|
51
40
|
try {
|
|
@@ -75,11 +64,11 @@ class BlockTransNode {
|
|
|
75
64
|
// 1. Apply {% with %} mappings to a local scope
|
|
76
65
|
const localParams = {};
|
|
77
66
|
for (const m of this.withMappings) {
|
|
78
|
-
localParams[m.name] =
|
|
67
|
+
localParams[m.name] = evaluateExpression(m.valPath, context);
|
|
79
68
|
}
|
|
80
69
|
// 2. Apply {% plural count %} mappings
|
|
81
70
|
for (const m of this.pluralMappings) {
|
|
82
|
-
localParams[m.name] =
|
|
71
|
+
localParams[m.name] = evaluateExpression(m.valPath, context);
|
|
83
72
|
}
|
|
84
73
|
|
|
85
74
|
// 3. Reassemble the full text
|