miki-template 2.3.2 → 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 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.2",
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": {
@@ -32,28 +32,6 @@
32
32
  "src/tags/i18n.js",
33
33
  "src/tags/extra.js"
34
34
  ],
35
- "files": [
36
- "src/index.js",
37
- "src/esm.mjs",
38
- "src/lexer.js",
39
- "src/parser.js",
40
- "src/context.js",
41
- "src/context_processors.js",
42
- "src/filters.js",
43
- "src/security.js",
44
- "src/cache.js",
45
- "src/utils.js",
46
- "src/codegen.js",
47
- "src/i18n.js",
48
- "src/libraries.js",
49
- "src/tags/helpers.js",
50
- "src/tags/registry.js",
51
- "src/tags/control.js",
52
- "src/tags/inheritance.js",
53
- "src/tags/util.js",
54
- "src/tags/i18n.js",
55
- "src/tags/extra.js"
56
- ],
57
35
  "keywords": [
58
36
  "django",
59
37
  "template",
@@ -91,4 +69,4 @@
91
69
  "release:minor": "npm version minor --no-git-tag-version -m \"chore(release): v%s\" && git push origin main",
92
70
  "release:major": "npm version major --no-git-tag-version -m \"chore(release): v%s\" && git push origin main"
93
71
  }
94
- }
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
@@ -4,21 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { Context } = require('../context');
7
-
8
- // Helper to resolve expression values (literals, numbers, booleans, or context lookups)
9
- function resolveValue(token, context) {
10
- if (token === undefined || token === null) return '';
11
- if (typeof token !== 'string') return token;
12
- if (token === '') return '';
13
- if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
14
- return token.slice(1, -1);
15
- }
16
- if (token === 'true' || token === 'True') return true;
17
- if (token === 'false' || token === 'False') return false;
18
- if (token === 'none' || token === 'None' || token === 'null') return null;
19
- if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
20
- return context.get(token);
21
- }
7
+ const { evaluateExpression } = require('../parser');
22
8
 
23
9
  class ExtendsNode {
24
10
  constructor(parentTemplateExpr) {
@@ -26,7 +12,7 @@ class ExtendsNode {
26
12
  }
27
13
 
28
14
  render(context) {
29
- const parentName = resolveValue(this.parentTemplateExpr, context);
15
+ const parentName = evaluateExpression(this.parentTemplateExpr, context);
30
16
  if (!parentName) return '';
31
17
  context.parentTemplate = parentName;
32
18
  return '';
@@ -98,7 +84,7 @@ class IncludeNode {
98
84
  }
99
85
 
100
86
  render(context) {
101
- const rawName = resolveValue(this.templateNameExpr, context);
87
+ const rawName = evaluateExpression(this.templateNameExpr, context);
102
88
  if (!rawName) return '';
103
89
 
104
90
  // Django-style partial selector: "home.html#card" means render
@@ -183,7 +169,7 @@ class IncludeNode {
183
169
  if (this.extraMappings && this.extraMappings.length > 0) {
184
170
  const extraScope = {};
185
171
  for (const m of this.extraMappings) {
186
- extraScope[m.name] = resolveValue(m.valPath, context);
172
+ extraScope[m.name] = evaluateExpression(m.valPath, context);
187
173
  }
188
174
  renderCtx.push(extraScope);
189
175
  const out = partial.body.map(n => n.render(renderCtx)).join('');
@@ -198,7 +184,7 @@ class IncludeNode {
198
184
  if (this.extraMappings && this.extraMappings.length > 0) {
199
185
  const extraScope = {};
200
186
  for (const mapping of this.extraMappings) {
201
- extraScope[mapping.name] = resolveValue(mapping.valPath, context);
187
+ extraScope[mapping.name] = evaluateExpression(mapping.valPath, context);
202
188
  }
203
189
  context.push(extraScope);
204
190
  const res = nodes.map(n => n.render(context)).join('');
@@ -308,7 +294,7 @@ function parseKeyValuePairs(str) {
308
294
  val += str[i++];
309
295
  }
310
296
  if (str[i] === quote) i++;
311
- // Preserve quotes so resolveValue can recognize the literal
297
+ // Preserve quotes so evaluateExpression can recognize the literal
312
298
  val = quote + val + quote;
313
299
  } else {
314
300
  // Unquoted value: read until comma or whitespace
package/src/tags/util.js CHANGED
@@ -2,22 +2,7 @@
2
2
  * Utility template tags: static, url, regroup, spaceless.
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
- // Numeric literal (integer or float)
17
- if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
18
- // Otherwise treat as a context variable lookup
19
- return context.get(token);
20
- }
5
+ const { evaluateExpression } = require('../parser');
21
6
 
22
7
  class StaticNode {
23
8
  constructor(pathExpr) {
@@ -25,7 +10,7 @@ class StaticNode {
25
10
  }
26
11
 
27
12
  render(context) {
28
- const resolvedPath = resolveValue(this.pathExpr, context);
13
+ const resolvedPath = evaluateExpression(this.pathExpr, context);
29
14
 
30
15
  // Resolve static prefix
31
16
  let prefix = '/static/';
@@ -55,11 +40,11 @@ class UrlNode {
55
40
  }
56
41
 
57
42
  render(context) {
58
- const routeName = resolveValue(this.routeNameExpr, context);
59
- const resolvedPositional = this.positionalArgs.map(arg => resolveValue(arg, context));
43
+ const routeName = evaluateExpression(this.routeNameExpr, context);
44
+ const resolvedPositional = this.positionalArgs.map(arg => evaluateExpression(arg, context));
60
45
  const resolvedKwargs = {};
61
46
  for (const kw of this.kwargs) {
62
- resolvedKwargs[kw.name] = resolveValue(kw.valueExpr, context);
47
+ resolvedKwargs[kw.name] = evaluateExpression(kw.valueExpr, context);
63
48
  }
64
49
  const hasKwargs = Object.keys(resolvedKwargs).length > 0;
65
50