miki-template 2.3.2 → 2.3.4

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.
@@ -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
  }
@@ -365,6 +333,12 @@ class FirstofNode {
365
333
  }
366
334
  }
367
335
 
336
+ class EndFirstofNode {
337
+ render(_context) {
338
+ return '';
339
+ }
340
+ }
341
+
368
342
  class CommentNode {
369
343
  constructor(body) {
370
344
  this.body = body;
@@ -576,7 +550,7 @@ function parseFor(tagContent, parser) {
576
550
 
577
551
  // Parse the iterable expression to support filters: items|regroup:"category"
578
552
  const parsedIterable = parseVariableExpression(iterableExpr);
579
- return new ForNode(loopVars, parsedIterable.varPath, body, emptyBody, parsedIterable.filters);
553
+ return new ForNode(loopVars, parsedIterable.varPath, body, emptyBody, parsedIterable.filters, parsedIterable.isLiteral, parsedIterable.literalValue);
580
554
  }
581
555
 
582
556
  function parseWith(tagContent, parser) {
@@ -658,13 +632,36 @@ function parseKeyValuePairs(content) {
658
632
  val += content[i++];
659
633
  }
660
634
  if (content[i] === quote) i++;
661
- // Keep the quotes around the literal so WithNode can recognize it
662
635
  val = quote + val + quote;
663
636
  } else {
664
637
  while (i < content.length && content[i] !== ',' && !/\s/.test(content[i])) {
665
638
  val += content[i++];
666
639
  }
667
640
  }
641
+
642
+ while (i < content.length && /\s/.test(content[i])) i++;
643
+ if (i < content.length && content[i] === '|') {
644
+ val += '|';
645
+ i++;
646
+ while (i < content.length) {
647
+ if (/\s/.test(content[i])) { i++; continue; }
648
+ if (content[i] === ',') break;
649
+ if (content[i] === '|') { val += '|'; i++; continue; }
650
+ if (content[i] === ':') { val += ':'; i++; continue; }
651
+ if (content[i] === '"' || content[i] === '\'') {
652
+ const quote = content[i++];
653
+ val += quote;
654
+ while (i < content.length && content[i] !== quote) {
655
+ if (content[i] === '\\' && i + 1 < content.length) { val += content[i]; i++; }
656
+ val += content[i++];
657
+ }
658
+ if (content[i] === quote) { val += quote; i++; }
659
+ } else {
660
+ val += content[i++];
661
+ }
662
+ }
663
+ }
664
+
668
665
  tokens.push({ name: key, valPath: val });
669
666
  while (i < content.length && /\s/.test(content[i])) i++;
670
667
  if (content[i] === ',') i++;
@@ -674,17 +671,16 @@ function parseKeyValuePairs(content) {
674
671
 
675
672
  function parseCycle(tagContent, _parser) {
676
673
  const content = tagContent.slice(5).trim();
677
- const argRegex = /(".*?"|'.*?'|[^\s]+)/g;
678
- const matches = content.match(argRegex) || [];
674
+ const parts = content.split(/\s+/).filter(Boolean);
679
675
  let asName = null;
680
676
  const args = [];
681
677
 
682
- for (let i = 0; i < matches.length; i++) {
683
- if (matches[i] === 'as' && i < matches.length - 1) {
684
- asName = matches[i + 1];
678
+ for (let i = 0; i < parts.length; i++) {
679
+ if (parts[i] === 'as' && i < parts.length - 1) {
680
+ asName = parts[i + 1];
685
681
  break;
686
682
  }
687
- args.push(matches[i]);
683
+ args.push(parts[i]);
688
684
  }
689
685
 
690
686
  return new CycleNode(args, asName);
@@ -692,17 +688,115 @@ function parseCycle(tagContent, _parser) {
692
688
 
693
689
  function parseFirstof(tagContent, _parser) {
694
690
  const content = tagContent.slice(8).trim();
695
- const argRegex = /(".*?"|'.*?'|[^\s]+)/g;
696
- const args = (content.match(argRegex) || []).map(a => a.trim()).filter(Boolean);
691
+ const args = [];
692
+ let i = 0;
693
+ const len = content.length;
694
+
695
+ while (i < len) {
696
+ while (i < len && /\s/.test(content[i])) i++;
697
+ if (i >= len) break;
698
+
699
+ let expr = '';
700
+ if (content[i] === '"' || content[i] === '\'') {
701
+ const quote = content[i];
702
+ expr += content[i];
703
+ i++;
704
+ while (i < len && content[i] !== quote) {
705
+ if (content[i] === '\\' && i + 1 < len) {
706
+ expr += content[i];
707
+ i++;
708
+ }
709
+ expr += content[i];
710
+ i++;
711
+ }
712
+ if (i < len) {
713
+ expr += content[i];
714
+ i++;
715
+ }
716
+ while (i < len && !/\s/.test(content[i])) {
717
+ if (content[i] === '|') {
718
+ expr += content[i];
719
+ i++;
720
+ while (i < len && content[i] !== ':' && content[i] !== '|' && !/\s/.test(content[i])) {
721
+ expr += content[i];
722
+ i++;
723
+ }
724
+ if (i < len && content[i] === ':') {
725
+ expr += content[i];
726
+ i++;
727
+ if (i < len && (content[i] === '"' || content[i] === '\'')) {
728
+ const q = content[i];
729
+ expr += content[i];
730
+ i++;
731
+ while (i < len && content[i] !== q) {
732
+ if (content[i] === '\\' && i + 1 < len) {
733
+ expr += content[i];
734
+ i++;
735
+ }
736
+ expr += content[i];
737
+ i++;
738
+ }
739
+ if (i < len) {
740
+ expr += content[i];
741
+ i++;
742
+ }
743
+ } else {
744
+ while (i < len && content[i] !== '|' && !/\s/.test(content[i])) {
745
+ expr += content[i];
746
+ i++;
747
+ }
748
+ }
749
+ }
750
+ } else {
751
+ break;
752
+ }
753
+ }
754
+ } else {
755
+ while (i < len && !/\s/.test(content[i])) {
756
+ expr += content[i];
757
+ i++;
758
+ }
759
+ }
760
+
761
+ if (expr) args.push(expr);
762
+ }
763
+
697
764
  return new FirstofNode(args);
698
765
  }
699
766
 
767
+ class ResetCycleNode {
768
+ constructor(key) {
769
+ this.key = key ? key.trim() : null;
770
+ }
771
+
772
+ render(context) {
773
+ if (!context.cycleStates) context.cycleStates = new Map();
774
+ if (this.key) {
775
+ context.cycleStates.set(this.key, 0);
776
+ } else {
777
+ context.cycleStates.clear();
778
+ }
779
+ return '';
780
+ }
781
+ }
782
+
783
+ function parseResetCycle(tagContent, _parser) {
784
+ const key = tagContent.slice(11).trim() || null;
785
+ return new ResetCycleNode(key);
786
+ }
787
+
788
+ function parseEndFirstof(_tagContent, _parser) {
789
+ return new EndFirstofNode();
790
+ }
791
+
700
792
  module.exports = {
701
793
  IfNode,
702
794
  ForNode,
703
795
  WithNode,
704
796
  CycleNode,
705
797
  FirstofNode,
798
+ EndFirstofNode,
799
+ ResetCycleNode,
706
800
  CommentNode,
707
801
  AutoescapeNode,
708
802
  PartialDefNode,
@@ -718,6 +812,8 @@ module.exports = {
718
812
  comment: parseComment,
719
813
  partialdef: parsePartialDef,
720
814
  partial: parsePartial,
721
- firstof: parseFirstof
815
+ firstof: parseFirstof,
816
+ endfirstof: parseEndFirstof,
817
+ resetcycle: parseResetCycle
722
818
  }
723
819
  };
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
@@ -130,6 +119,76 @@ function parseTrans(tagContent, _parser) {
130
119
  return new TransNode(key, Object.keys(args).length ? args : null);
131
120
  }
132
121
 
122
+ function parseTranslate(tagContent, _parser) {
123
+ const trimmed = tagContent.slice(9).trim(); // strip "translate"
124
+ const tokens = trimmed.match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
125
+
126
+ let keyIdx = 0;
127
+ if (tokens[0] && tokens[0] === 'context') {
128
+ keyIdx = 2;
129
+ }
130
+ const key = tokens[keyIdx] || '';
131
+ const args = {};
132
+ for (let i = keyIdx + 1; i < tokens.length; i++) {
133
+ const eq = tokens[i].indexOf('=');
134
+ if (eq > 0) {
135
+ const name = tokens[i].slice(0, eq);
136
+ let val = tokens[i].slice(eq + 1);
137
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
138
+ val = val.slice(1, -1);
139
+ }
140
+ args[name] = val;
141
+ }
142
+ }
143
+ return new TransNode(key, Object.keys(args).length ? args : null);
144
+ }
145
+
146
+ function parseBlockTranslate(tagContent, parser) {
147
+ const body = parser.parse(['endblocktranslate']);
148
+ const next = parser.peek();
149
+ if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endblocktranslate') {
150
+ parser.advance();
151
+ }
152
+
153
+ const withMappings = [];
154
+ const pluralMappings = [];
155
+ const afterTrans = tagContent.replace(/^blocktranslate\s*/, '').trim();
156
+ const tokens = afterTrans.match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
157
+ let i = 0;
158
+ if (tokens[i] === 'context') i += 2;
159
+ while (i < tokens.length) {
160
+ const tok = tokens[i];
161
+ if (tok === 'with') {
162
+ i++;
163
+ while (i < tokens.length && tokens[i].indexOf('=') > 0) {
164
+ const pair = tokens[i];
165
+ const eq = pair.indexOf('=');
166
+ const name = pair.slice(0, eq);
167
+ let val = pair.slice(eq + 1);
168
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
169
+ val = val.slice(1, -1);
170
+ }
171
+ withMappings.push({ name, valPath: val });
172
+ i++;
173
+ }
174
+ } else if (tok === 'count') {
175
+ i++;
176
+ const countVal = tokens[i] || '1';
177
+ pluralMappings.push({ name: 'count', valPath: countVal });
178
+ i++;
179
+ } else {
180
+ i++;
181
+ }
182
+ }
183
+
184
+ return new BlockTransNode(
185
+ extractTextAndVars(body),
186
+ withMappings.length ? withMappings : extractWithMappings(body),
187
+ pluralMappings,
188
+ body
189
+ );
190
+ }
191
+
133
192
  function parseLanguage(tagContent, parser) {
134
193
  const lang = tagContent.slice(9).trim(); // strip "language"
135
194
  const body = parser.parse(['endlanguage']);
@@ -249,8 +308,10 @@ module.exports = {
249
308
  PluralMappingNode,
250
309
  parsers: {
251
310
  trans: parseTrans,
311
+ translate: parseTranslate,
252
312
  language: parseLanguage,
253
313
  blocktrans: parseBlockTrans,
314
+ blocktranslate: parseBlockTranslate,
254
315
  plural: parsePlural
255
316
  }
256
317
  };
@@ -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