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.
Files changed (167) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -2
  3. package/src/codegen.js +140 -9
  4. package/src/parser.js +69 -1
  5. package/src/tags/control.js +112 -51
  6. package/src/tags/extra.js +4 -17
  7. package/src/tags/i18n.js +6 -17
  8. package/src/tags/inheritance.js +6 -20
  9. package/src/tags/util.js +5 -20
  10. package/.eslintrc.json +0 -16
  11. package/.github/release-notes/v1.3.1.md +0 -55
  12. package/.github/release-notes/v1.3.3.md +0 -77
  13. package/.github/release-notes/v1.3.7.md +0 -20
  14. package/.github/workflows/ci.yml +0 -42
  15. package/.github/workflows/docs.yml +0 -107
  16. package/.github/workflows/release.yml +0 -107
  17. package/AGENT.md +0 -71
  18. package/API_REFERENCE.md +0 -341
  19. package/CHANGELOG.md +0 -173
  20. package/CODE_OF_CONDUCT.md +0 -14
  21. package/CONTRIBUTING.md +0 -27
  22. package/ROADMAP.md +0 -40
  23. package/assets/banner.png +0 -0
  24. package/assets/logo.png +0 -0
  25. package/benchmarks/ejs-results.json +0 -17
  26. package/benchmarks/ejs.js +0 -38
  27. package/benchmarks/handlebars-results.json +0 -17
  28. package/benchmarks/handlebars.js +0 -45
  29. package/benchmarks/miki-results.json +0 -17
  30. package/benchmarks/miki.js +0 -39
  31. package/benchmarks/pug-results.json +0 -17
  32. package/benchmarks/pug.js +0 -38
  33. package/benchmarks/report.json +0 -17
  34. package/benchmarks/run.js +0 -81
  35. package/benchmarks/stress.mjs +0 -647
  36. package/benchmarks/templates/large.dtpl +0 -7
  37. package/benchmarks/templates/medium.dtpl +0 -3
  38. package/benchmarks/templates/small.dtpl +0 -7
  39. package/context/component.md +0 -109
  40. package/context/prd.md +0 -131
  41. package/context/project-structure.md +0 -33
  42. package/dir/base.html +0 -23
  43. package/dir/cmpnt.html +0 -11
  44. package/dir/footer.html +0 -3
  45. package/dir/home.html +0 -80
  46. package/dir/index.html +0 -80
  47. package/dir/navbar.html +0 -9
  48. package/docs/api/async-render.md +0 -170
  49. package/docs/api/cache.md +0 -174
  50. package/docs/api/compile.md +0 -256
  51. package/docs/api/context-processors.md +0 -154
  52. package/docs/api/filters.md +0 -437
  53. package/docs/api/finder.md +0 -188
  54. package/docs/api/helpers.md +0 -106
  55. package/docs/api/i18n.md +0 -314
  56. package/docs/api/index.md +0 -108
  57. package/docs/api/libraries.md +0 -414
  58. package/docs/api/render-partial.md +0 -162
  59. package/docs/api/render.md +0 -184
  60. package/docs/api/security.md +0 -290
  61. package/docs/api/setup-express.md +0 -152
  62. package/docs/api/tags.md +0 -268
  63. package/docs/assets/banner.png +0 -0
  64. package/docs/assets/logo.png +0 -0
  65. package/docs/filter.md +0 -0
  66. package/docs/guide/advanced-usage.md +0 -794
  67. package/docs/guide/async-rendering.md +0 -616
  68. package/docs/guide/context-processors.md +0 -514
  69. package/docs/guide/custom-filters.md +0 -622
  70. package/docs/guide/custom-tags.md +0 -542
  71. package/docs/guide/filters.md +0 -1314
  72. package/docs/guide/getting-started.md +0 -204
  73. package/docs/guide/installation.md +0 -190
  74. package/docs/guide/partial-templates.md +0 -734
  75. package/docs/guide/quick-start.md +0 -444
  76. package/docs/guide/security.md +0 -690
  77. package/docs/guide/tags.md +0 -1566
  78. package/docs/guide/template-discovery.md +0 -340
  79. package/docs/guide/template-inheritance.md +0 -546
  80. package/docs/guide/what-is-miki-template.md +0 -28
  81. package/docs/guide/why-miki-template.md +0 -75
  82. package/docs/index.md +0 -86
  83. package/docs/integrations/elysia.md +0 -80
  84. package/docs/integrations/express.md +0 -219
  85. package/docs/integrations/fastify.md +0 -79
  86. package/docs/integrations/hono.md +0 -80
  87. package/docs/integrations/index.md +0 -68
  88. package/docs/integrations/koa.md +0 -90
  89. package/docs/integrations/nestjs.md +0 -80
  90. package/docs/integrations/tsed.md +0 -83
  91. package/docs/performance.md +0 -74
  92. package/eslint.config.mjs +0 -42
  93. package/ex.mjs +0 -36
  94. package/live-test/integrations/elysia-example.js +0 -16
  95. package/live-test/integrations/express-example.js +0 -24
  96. package/live-test/integrations/fastify-example.js +0 -20
  97. package/live-test/integrations/hono-example.js +0 -16
  98. package/live-test/integrations/koa-example.js +0 -30
  99. package/live-test/integrations/nestjs-example.js +0 -25
  100. package/live-test/integrations/smoke-test.js +0 -166
  101. package/live-test/integrations/tsed-example.js +0 -23
  102. package/live-test/package-lock.json +0 -1150
  103. package/live-test/package.json +0 -13
  104. package/live-test/packages/product/templates/product/detail.html +0 -7
  105. package/live-test/server.js +0 -38
  106. package/live-test/templates/app_templates/detail.html +0 -6
  107. package/live-test/views/base.html +0 -8
  108. package/live-test/views/child.html +0 -7
  109. package/live-test/views/home.html +0 -17
  110. package/live-test/views/index.html +0 -1
  111. package/miki-template-extension/.github/workflows/ci.yml +0 -116
  112. package/miki-template-extension/.vscodeignore +0 -7
  113. package/miki-template-extension/CHANGELOG.md +0 -99
  114. package/miki-template-extension/LICENSE +0 -21
  115. package/miki-template-extension/README.md +0 -273
  116. package/miki-template-extension/extension.js +0 -1013
  117. package/miki-template-extension/icon.png +0 -0
  118. package/miki-template-extension/icon.svg +0 -10
  119. package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
  120. package/miki-template-extension/package.json +0 -280
  121. package/miki-template-extension/snippets/miki-template.json +0 -717
  122. package/miki-template-extension/syntaxes/language-configuration.json +0 -114
  123. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +0 -355
  124. package/miki-template-extension/tests/grammar-tests.json +0 -162
  125. package/miki-template-extension/tests/run-grammar-tests.js +0 -82
  126. package/mkdocs.yml +0 -195
  127. package/overrides/main.html +0 -26
  128. package/requirements-docs.txt +0 -2
  129. package/sample-app/package-lock.json +0 -901
  130. package/sample-app/package.json +0 -9
  131. package/sample-app/server.js +0 -14
  132. package/sample-app/views/index.html +0 -1
  133. package/scripts/build-vsix.js +0 -129
  134. package/scripts/build-vsix.ps1 +0 -15
  135. package/snippets/miki-template.json +0 -177
  136. package/src/asyncRender.js +0 -21
  137. package/src/types.d.ts +0 -107
  138. package/syntaxes/language-configuration.json +0 -26
  139. package/syntaxes/miki-template.tmLanguage.json +0 -146
  140. package/tests/asyncRender.test.js +0 -17
  141. package/tests/base.html +0 -6
  142. package/tests/child.html +0 -3
  143. package/tests/context_processors.test.js +0 -13
  144. package/tests/esm.test.mjs +0 -61
  145. package/tests/filters.test.js +0 -263
  146. package/tests/finder-appdirs.test.js +0 -19
  147. package/tests/finder.test.js +0 -17
  148. package/tests/fixtures/views/nested/index.html +0 -1
  149. package/tests/fixtures/views/partial.html +0 -1
  150. package/tests/fixtures/views/sub/deepfile.html +0 -1
  151. package/tests/fixtures/views-appdirs/product/site/detail.html +0 -1
  152. package/tests/include_security.test.js +0 -9
  153. package/tests/integration/README.md +0 -32
  154. package/tests/integration/features.test.cjs +0 -1681
  155. package/tests/integration/features.test.mjs +0 -1697
  156. package/tests/integration/finder.esm.test.mjs +0 -13
  157. package/tests/integration/partial-render.test.cjs +0 -13
  158. package/tests/integration/templates/base.miki +0 -6
  159. package/tests/integration/templates/child.miki +0 -6
  160. package/tests/integration/templates/index.html +0 -17
  161. package/tests/lexer.test.js +0 -45
  162. package/tests/parser.test.js +0 -57
  163. package/tests/partial.html +0 -1
  164. package/tests/partialdef.test.js +0 -79
  165. package/tests/production_checks.js +0 -57
  166. package/tests/security.test.js +0 -28
  167. 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.1",
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
- if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
674
- return token.slice(1, -1);
675
- }
676
- if (token === 'true' || token === 'True') return true;
677
- if (token === 'false' || token === 'False') return false;
678
- if (token === 'none' || token === 'None' || token === 'null') return null;
679
- if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
680
- return context.get(token);
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
  };
@@ -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(resolveValue(t, context));
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
- // The valPath may be a literal string (e.g. "Hello" with quotes) or
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 = resolveValue(token, context);
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 = resolveValue(arg, context);
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 argRegex = /(".*?"|'.*?'|[^\s]+)/g;
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 < matches.length; i++) {
683
- if (matches[i] === 'as' && i < matches.length - 1) {
684
- asName = matches[i + 1];
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(matches[i]);
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 argRegex = /(".*?"|'.*?'|[^\s]+)/g;
696
- const args = (content.match(argRegex) || []).map(a => a.trim()).filter(Boolean);
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
- // Helper to resolve expression values (literals, numbers, booleans, or context lookups)
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 = resolveValue(this.formatExpr, context);
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 ? resolveValue(this.valueExpr, context) : '';
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 ? resolveValue(this.conditionStr, context) : undefined;
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] = context.get(v);
22
+ params[k] = evaluateExpression(v, context);
22
23
  }
23
24
  }
24
- // For unquoted keys: if the key is a variable name, resolve it
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
- // Strip quotes
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] = context.get(m.valPath);
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] = context.get(m.valPath);
71
+ localParams[m.name] = evaluateExpression(m.valPath, context);
83
72
  }
84
73
 
85
74
  // 3. Reassemble the full text