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.
- package/README.md +3 -1
- package/package.json +2 -24
- package/src/codegen.js +189 -10
- package/src/filters.js +243 -4
- package/src/libraries.js +1 -1
- package/src/parser.js +69 -1
- package/src/tags/control.js +148 -52
- package/src/tags/extra.js +4 -17
- package/src/tags/i18n.js +78 -17
- package/src/tags/inheritance.js +6 -20
- package/src/tags/util.js +155 -21
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
|
}
|
|
@@ -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
|
|
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 <
|
|
683
|
-
if (
|
|
684
|
-
asName =
|
|
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(
|
|
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
|
|
696
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
};
|
package/src/tags/inheritance.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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] =
|
|
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] =
|
|
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
|
|
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
|