miki-template 1.3.3 → 1.3.7
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/.eslintrc.json +16 -0
- package/.github/release-notes/v1.3.1.md +55 -55
- package/.github/release-notes/v1.3.3.md +77 -0
- package/.github/workflows/ci.yml +38 -54
- package/.github/workflows/release.yml +106 -0
- package/AGENT.md +71 -71
- package/API_REFERENCE.md +314 -314
- package/CHANGELOG.md +173 -169
- package/CODE_OF_CONDUCT.md +14 -14
- package/CONTRIBUTING.md +27 -27
- package/README.md +342 -321
- package/ROADMAP.md +40 -40
- package/benchmarks/report.json +16 -16
- package/benchmarks/run.js +49 -49
- package/benchmarks/stress.mjs +647 -647
- package/benchmarks/templates/large.dtpl +7 -7
- package/benchmarks/templates/medium.dtpl +3 -3
- package/benchmarks/templates/small.dtpl +7 -7
- package/context/component.md +109 -109
- package/context/prd.md +131 -131
- package/context/project-structure.md +33 -33
- package/dir/base.html +22 -22
- package/dir/cmpnt.html +10 -10
- package/dir/footer.html +2 -2
- package/dir/home.html +80 -80
- package/dir/index.html +80 -0
- package/dir/navbar.html +8 -8
- package/docs/README.md +18 -18
- package/docs/advanced_usage.md +71 -71
- package/docs/api.md +119 -119
- package/docs/filters.md +708 -708
- package/docs/installation.md +106 -106
- package/docs/overview.md +79 -57
- package/docs/partialdef.md +70 -70
- package/docs/security.md +27 -27
- package/docs/tags.md +673 -673
- package/docs/usage.md +646 -646
- package/eslint.config.mjs +42 -42
- package/ex.mjs +34 -32
- package/live-test/package-lock.json +915 -0
- package/live-test/package.json +9 -0
- package/live-test/server.js +14 -0
- package/live-test/views/base.html +8 -0
- package/live-test/views/child.html +7 -0
- package/live-test/views/index.html +1 -0
- package/miki-template-extension/.github/workflows/ci.yml +116 -116
- package/miki-template-extension/.vscodeignore +7 -7
- package/miki-template-extension/CHANGELOG.md +99 -99
- package/miki-template-extension/LICENSE +21 -21
- package/miki-template-extension/README.md +273 -273
- package/miki-template-extension/extension.js +1013 -1013
- package/miki-template-extension/icon.svg +10 -10
- package/miki-template-extension/package.json +280 -280
- package/miki-template-extension/snippets/miki-template.json +717 -717
- package/miki-template-extension/syntaxes/language-configuration.json +114 -114
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -355
- package/miki-template-extension/tests/grammar-tests.json +162 -162
- package/miki-template-extension/tests/run-grammar-tests.js +82 -82
- package/package.json +40 -34
- package/sample-app/package-lock.json +901 -0
- package/sample-app/package.json +9 -0
- package/sample-app/server.js +14 -0
- package/sample-app/views/index.html +1 -0
- package/scripts/build-vsix.js +129 -129
- package/scripts/build-vsix.ps1 +15 -15
- package/snippets/miki-template.json +177 -177
- package/src/asyncRender.js +20 -20
- package/src/cache.js +80 -80
- package/src/context.js +126 -126
- package/src/context_processors.js +48 -48
- package/src/esm.mjs +89 -84
- package/src/filters.js +975 -975
- package/src/i18n.js +171 -171
- package/src/index.js +1112 -940
- package/src/lexer.js +114 -114
- package/src/libraries.js +371 -371
- package/src/parser.js +270 -270
- package/src/security.js +53 -53
- package/src/tags/control.js +719 -719
- package/src/tags/extra.js +154 -154
- package/src/tags/helpers.js +26 -26
- package/src/tags/i18n.js +256 -256
- package/src/tags/inheritance.js +335 -335
- package/src/tags/registry.js +18 -18
- package/src/tags/util.js +400 -400
- package/src/types.d.ts +107 -107
- package/syntaxes/language-configuration.json +26 -26
- package/syntaxes/miki-template.tmLanguage.json +146 -146
- package/tests/asyncRender.test.js +17 -17
- package/tests/base.html +6 -6
- package/tests/child.html +3 -3
- package/tests/context_processors.test.js +13 -13
- package/tests/esm.test.mjs +61 -61
- package/tests/filters.test.js +254 -254
- package/tests/finder-appdirs.test.js +19 -0
- package/tests/finder.test.js +17 -0
- package/tests/fixtures/views/nested/index.html +1 -0
- package/tests/fixtures/views/partial.html +1 -0
- package/tests/fixtures/views/sub/deepfile.html +1 -0
- package/tests/fixtures/views-appdirs/product/site/detail.html +1 -0
- package/tests/include_security.test.js +9 -9
- package/tests/integration/README.md +32 -32
- package/tests/integration/features.test.cjs +1681 -1681
- package/tests/integration/features.test.mjs +1697 -1697
- package/tests/integration/finder.esm.test.mjs +13 -0
- package/tests/integration/templates/base.miki +6 -6
- package/tests/integration/templates/child.miki +6 -6
- package/tests/integration/templates/index.html +17 -17
- package/tests/lexer.test.js +45 -45
- package/tests/parser.test.js +57 -57
- package/tests/partial.html +1 -1
- package/tests/partialdef.test.js +79 -79
- package/tests/production_checks.js +57 -57
- package/tests/security.test.js +28 -28
- package/tests/tags.test.js +233 -233
package/src/tags/i18n.js
CHANGED
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* i18n template tags: trans, blocktrans, language.
|
|
3
|
-
*/
|
|
4
|
-
const i18n = require('../i18n');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* {% trans "translation key" %}
|
|
8
|
-
* {% trans "Hello, %s!" name %}
|
|
9
|
-
* Optional context: {% trans context "key" %}
|
|
10
|
-
*/
|
|
11
|
-
class TransNode {
|
|
12
|
-
constructor(key, args) {
|
|
13
|
-
this.key = key;
|
|
14
|
-
this.args = args; // { name: 'userName', ... }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
render(context) {
|
|
18
|
-
const params = {};
|
|
19
|
-
if (this.args) {
|
|
20
|
-
for (const [k, v] of Object.entries(this.args)) {
|
|
21
|
-
params[k] = context.get(v);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
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
|
-
}
|
|
34
|
-
return i18n.lookup(key, params);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
class LanguageNode {
|
|
39
|
-
constructor(lang, body) {
|
|
40
|
-
this.lang = lang;
|
|
41
|
-
this.body = body;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
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);
|
|
49
|
-
const previous = i18n.getLanguage();
|
|
50
|
-
i18n.setLanguage(lang);
|
|
51
|
-
try {
|
|
52
|
-
return this.body.map(n => n.render(context)).join('');
|
|
53
|
-
} finally {
|
|
54
|
-
i18n.setLanguage(previous);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* {% blocktrans %}...{% endblocktrans %}
|
|
61
|
-
* Supports {% with name=value %}, {% plural count %}, {% endblocktrans %}
|
|
62
|
-
* Inner text may contain {% with %} and {% plural %} tags.
|
|
63
|
-
*/
|
|
64
|
-
class BlockTransNode {
|
|
65
|
-
constructor(textParts, withMappings, pluralMappings, body) {
|
|
66
|
-
// textParts: array of raw string segments between variable interpolations
|
|
67
|
-
// e.g. "Hello, " + [name] + "!" => ['Hello, ', { type: 'var', name: 'name' }, '!']
|
|
68
|
-
this.textParts = textParts;
|
|
69
|
-
this.withMappings = withMappings || [];
|
|
70
|
-
this.pluralMappings = pluralMappings || [];
|
|
71
|
-
this.body = body; // for nested tags (unused currently)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
render(context) {
|
|
75
|
-
// 1. Apply {% with %} mappings to a local scope
|
|
76
|
-
const localParams = {};
|
|
77
|
-
for (const m of this.withMappings) {
|
|
78
|
-
localParams[m.name] = context.get(m.valPath);
|
|
79
|
-
}
|
|
80
|
-
// 2. Apply {% plural count %} mappings
|
|
81
|
-
for (const m of this.pluralMappings) {
|
|
82
|
-
localParams[m.name] = context.get(m.valPath);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 3. Reassemble the full text
|
|
86
|
-
let fullText = '';
|
|
87
|
-
for (const part of this.textParts) {
|
|
88
|
-
if (typeof part === 'string') {
|
|
89
|
-
fullText += part;
|
|
90
|
-
} else if (part.type === 'var') {
|
|
91
|
-
const val = localParams[part.name] !== undefined
|
|
92
|
-
? localParams[part.name]
|
|
93
|
-
: context.get(part.name);
|
|
94
|
-
fullText += String(val);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 4. Look up translation
|
|
99
|
-
const count = Object.values(localParams).find(v => typeof v === 'number');
|
|
100
|
-
return i18n.lookup(fullText, localParams, count);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// --- Parsers ---
|
|
105
|
-
|
|
106
|
-
function parseTrans(tagContent, _parser) {
|
|
107
|
-
// {% trans "key" %}
|
|
108
|
-
// {% trans "Hello %s" name=user.name %}
|
|
109
|
-
// {% trans context "key" %}
|
|
110
|
-
const trimmed = tagContent.slice(5).trim(); // strip "trans"
|
|
111
|
-
const tokens = trimmed.match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
|
|
112
|
-
|
|
113
|
-
let keyIdx = 0;
|
|
114
|
-
if (tokens[0] && tokens[0] === 'context') {
|
|
115
|
-
keyIdx = 2;
|
|
116
|
-
}
|
|
117
|
-
const key = tokens[keyIdx] || '';
|
|
118
|
-
const args = {};
|
|
119
|
-
for (let i = keyIdx + 1; i < tokens.length; i++) {
|
|
120
|
-
const eq = tokens[i].indexOf('=');
|
|
121
|
-
if (eq > 0) {
|
|
122
|
-
const name = tokens[i].slice(0, eq);
|
|
123
|
-
let val = tokens[i].slice(eq + 1);
|
|
124
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
|
|
125
|
-
val = val.slice(1, -1);
|
|
126
|
-
}
|
|
127
|
-
args[name] = val;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return new TransNode(key, Object.keys(args).length ? args : null);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function parseLanguage(tagContent, parser) {
|
|
134
|
-
const lang = tagContent.slice(9).trim(); // strip "language"
|
|
135
|
-
const body = parser.parse(['endlanguage']);
|
|
136
|
-
const next = parser.peek();
|
|
137
|
-
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endlanguage') {
|
|
138
|
-
parser.advance();
|
|
139
|
-
}
|
|
140
|
-
return new LanguageNode(lang, body);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function parseBlockTrans(tagContent, parser) {
|
|
144
|
-
// Collect body until {% endblocktrans %}. The parser will stop at the
|
|
145
|
-
// first matching end tag it encounters in its untilTags list.
|
|
146
|
-
const body = parser.parse(['endblocktrans']);
|
|
147
|
-
// Consume the endblocktrans token if it's still pending.
|
|
148
|
-
const next = parser.peek();
|
|
149
|
-
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endblocktrans') {
|
|
150
|
-
parser.advance();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Parse `with name=value` and `count` from the blocktrans tag content.
|
|
154
|
-
// Format: {% blocktrans with name1=var1 name2=var2 count n %}...{% endblocktrans %}
|
|
155
|
-
const withMappings = [];
|
|
156
|
-
const pluralMappings = [];
|
|
157
|
-
const afterTrans = tagContent.replace(/^blocktrans\s*/, '').trim();
|
|
158
|
-
// Split on "with" / "count" boundaries, keeping the keywords
|
|
159
|
-
const tokens = afterTrans.match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
|
|
160
|
-
let i = 0;
|
|
161
|
-
// Skip any leading context (e.g. "context")
|
|
162
|
-
if (tokens[i] === 'context') i += 2;
|
|
163
|
-
// Check for `with` or `count` after the main text
|
|
164
|
-
while (i < tokens.length) {
|
|
165
|
-
const tok = tokens[i];
|
|
166
|
-
if (tok === 'with') {
|
|
167
|
-
i++;
|
|
168
|
-
// Read name=var pairs until we hit end or another keyword
|
|
169
|
-
while (i < tokens.length && tokens[i].indexOf('=') > 0) {
|
|
170
|
-
const pair = tokens[i];
|
|
171
|
-
const eq = pair.indexOf('=');
|
|
172
|
-
const name = pair.slice(0, eq);
|
|
173
|
-
let val = pair.slice(eq + 1);
|
|
174
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
|
|
175
|
-
val = val.slice(1, -1);
|
|
176
|
-
}
|
|
177
|
-
withMappings.push({ name, valPath: val });
|
|
178
|
-
i++;
|
|
179
|
-
}
|
|
180
|
-
} else if (tok === 'count') {
|
|
181
|
-
i++;
|
|
182
|
-
// Next token is the count variable
|
|
183
|
-
if (i < tokens.length) {
|
|
184
|
-
pluralMappings.push({ name: 'count', valPath: tokens[i++] });
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
i++;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return new BlockTransNode(
|
|
192
|
-
extractTextAndVars(body),
|
|
193
|
-
withMappings.length ? withMappings : extractWithMappings(body),
|
|
194
|
-
pluralMappings,
|
|
195
|
-
body
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Extract raw text + {% with %}-bound variable references from a body. */
|
|
200
|
-
function extractTextAndVars(body) {
|
|
201
|
-
const parts = [];
|
|
202
|
-
for (const node of body) {
|
|
203
|
-
if (node.constructor.name === 'TextNode') {
|
|
204
|
-
parts.push(node.content);
|
|
205
|
-
} else if (node.constructor.name === 'VariableNode') {
|
|
206
|
-
// Top-level var: bind it to the parsed varPath so it can be filled
|
|
207
|
-
parts.push({ type: 'var', name: node.varPath });
|
|
208
|
-
} else {
|
|
209
|
-
parts.push('');
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return parts;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function extractWithMappings(body) {
|
|
216
|
-
const mappings = [];
|
|
217
|
-
for (const node of body) {
|
|
218
|
-
if (node.constructor.name === 'WithNode' && node.mappings) {
|
|
219
|
-
for (const m of node.mappings) {
|
|
220
|
-
mappings.push({ name: m.name, valPath: m.valPath });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return mappings;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
class PluralMappingNode {
|
|
228
|
-
constructor(name, valPath) {
|
|
229
|
-
this.name = name;
|
|
230
|
-
this.valPath = valPath;
|
|
231
|
-
}
|
|
232
|
-
render() { return ''; }
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function parsePlural(tagContent, _parser) {
|
|
236
|
-
// {% plural count %}
|
|
237
|
-
const rest = tagContent.slice(6).trim();
|
|
238
|
-
const eq = rest.indexOf('=');
|
|
239
|
-
if (eq > 0) {
|
|
240
|
-
return new PluralMappingNode(rest.slice(0, eq).trim(), rest.slice(eq + 1).trim());
|
|
241
|
-
}
|
|
242
|
-
return new PluralMappingNode('count', rest);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
module.exports = {
|
|
246
|
-
TransNode,
|
|
247
|
-
LanguageNode,
|
|
248
|
-
BlockTransNode,
|
|
249
|
-
PluralMappingNode,
|
|
250
|
-
parsers: {
|
|
251
|
-
trans: parseTrans,
|
|
252
|
-
language: parseLanguage,
|
|
253
|
-
blocktrans: parseBlockTrans,
|
|
254
|
-
plural: parsePlural
|
|
255
|
-
}
|
|
256
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* i18n template tags: trans, blocktrans, language.
|
|
3
|
+
*/
|
|
4
|
+
const i18n = require('../i18n');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* {% trans "translation key" %}
|
|
8
|
+
* {% trans "Hello, %s!" name %}
|
|
9
|
+
* Optional context: {% trans context "key" %}
|
|
10
|
+
*/
|
|
11
|
+
class TransNode {
|
|
12
|
+
constructor(key, args) {
|
|
13
|
+
this.key = key;
|
|
14
|
+
this.args = args; // { name: 'userName', ... }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
render(context) {
|
|
18
|
+
const params = {};
|
|
19
|
+
if (this.args) {
|
|
20
|
+
for (const [k, v] of Object.entries(this.args)) {
|
|
21
|
+
params[k] = context.get(v);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
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
|
+
}
|
|
34
|
+
return i18n.lookup(key, params);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class LanguageNode {
|
|
39
|
+
constructor(lang, body) {
|
|
40
|
+
this.lang = lang;
|
|
41
|
+
this.body = body;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
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);
|
|
49
|
+
const previous = i18n.getLanguage();
|
|
50
|
+
i18n.setLanguage(lang);
|
|
51
|
+
try {
|
|
52
|
+
return this.body.map(n => n.render(context)).join('');
|
|
53
|
+
} finally {
|
|
54
|
+
i18n.setLanguage(previous);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* {% blocktrans %}...{% endblocktrans %}
|
|
61
|
+
* Supports {% with name=value %}, {% plural count %}, {% endblocktrans %}
|
|
62
|
+
* Inner text may contain {% with %} and {% plural %} tags.
|
|
63
|
+
*/
|
|
64
|
+
class BlockTransNode {
|
|
65
|
+
constructor(textParts, withMappings, pluralMappings, body) {
|
|
66
|
+
// textParts: array of raw string segments between variable interpolations
|
|
67
|
+
// e.g. "Hello, " + [name] + "!" => ['Hello, ', { type: 'var', name: 'name' }, '!']
|
|
68
|
+
this.textParts = textParts;
|
|
69
|
+
this.withMappings = withMappings || [];
|
|
70
|
+
this.pluralMappings = pluralMappings || [];
|
|
71
|
+
this.body = body; // for nested tags (unused currently)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render(context) {
|
|
75
|
+
// 1. Apply {% with %} mappings to a local scope
|
|
76
|
+
const localParams = {};
|
|
77
|
+
for (const m of this.withMappings) {
|
|
78
|
+
localParams[m.name] = context.get(m.valPath);
|
|
79
|
+
}
|
|
80
|
+
// 2. Apply {% plural count %} mappings
|
|
81
|
+
for (const m of this.pluralMappings) {
|
|
82
|
+
localParams[m.name] = context.get(m.valPath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Reassemble the full text
|
|
86
|
+
let fullText = '';
|
|
87
|
+
for (const part of this.textParts) {
|
|
88
|
+
if (typeof part === 'string') {
|
|
89
|
+
fullText += part;
|
|
90
|
+
} else if (part.type === 'var') {
|
|
91
|
+
const val = localParams[part.name] !== undefined
|
|
92
|
+
? localParams[part.name]
|
|
93
|
+
: context.get(part.name);
|
|
94
|
+
fullText += String(val);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Look up translation
|
|
99
|
+
const count = Object.values(localParams).find(v => typeof v === 'number');
|
|
100
|
+
return i18n.lookup(fullText, localParams, count);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Parsers ---
|
|
105
|
+
|
|
106
|
+
function parseTrans(tagContent, _parser) {
|
|
107
|
+
// {% trans "key" %}
|
|
108
|
+
// {% trans "Hello %s" name=user.name %}
|
|
109
|
+
// {% trans context "key" %}
|
|
110
|
+
const trimmed = tagContent.slice(5).trim(); // strip "trans"
|
|
111
|
+
const tokens = trimmed.match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
|
|
112
|
+
|
|
113
|
+
let keyIdx = 0;
|
|
114
|
+
if (tokens[0] && tokens[0] === 'context') {
|
|
115
|
+
keyIdx = 2;
|
|
116
|
+
}
|
|
117
|
+
const key = tokens[keyIdx] || '';
|
|
118
|
+
const args = {};
|
|
119
|
+
for (let i = keyIdx + 1; i < tokens.length; i++) {
|
|
120
|
+
const eq = tokens[i].indexOf('=');
|
|
121
|
+
if (eq > 0) {
|
|
122
|
+
const name = tokens[i].slice(0, eq);
|
|
123
|
+
let val = tokens[i].slice(eq + 1);
|
|
124
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
|
|
125
|
+
val = val.slice(1, -1);
|
|
126
|
+
}
|
|
127
|
+
args[name] = val;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return new TransNode(key, Object.keys(args).length ? args : null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseLanguage(tagContent, parser) {
|
|
134
|
+
const lang = tagContent.slice(9).trim(); // strip "language"
|
|
135
|
+
const body = parser.parse(['endlanguage']);
|
|
136
|
+
const next = parser.peek();
|
|
137
|
+
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endlanguage') {
|
|
138
|
+
parser.advance();
|
|
139
|
+
}
|
|
140
|
+
return new LanguageNode(lang, body);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseBlockTrans(tagContent, parser) {
|
|
144
|
+
// Collect body until {% endblocktrans %}. The parser will stop at the
|
|
145
|
+
// first matching end tag it encounters in its untilTags list.
|
|
146
|
+
const body = parser.parse(['endblocktrans']);
|
|
147
|
+
// Consume the endblocktrans token if it's still pending.
|
|
148
|
+
const next = parser.peek();
|
|
149
|
+
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endblocktrans') {
|
|
150
|
+
parser.advance();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse `with name=value` and `count` from the blocktrans tag content.
|
|
154
|
+
// Format: {% blocktrans with name1=var1 name2=var2 count n %}...{% endblocktrans %}
|
|
155
|
+
const withMappings = [];
|
|
156
|
+
const pluralMappings = [];
|
|
157
|
+
const afterTrans = tagContent.replace(/^blocktrans\s*/, '').trim();
|
|
158
|
+
// Split on "with" / "count" boundaries, keeping the keywords
|
|
159
|
+
const tokens = afterTrans.match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
|
|
160
|
+
let i = 0;
|
|
161
|
+
// Skip any leading context (e.g. "context")
|
|
162
|
+
if (tokens[i] === 'context') i += 2;
|
|
163
|
+
// Check for `with` or `count` after the main text
|
|
164
|
+
while (i < tokens.length) {
|
|
165
|
+
const tok = tokens[i];
|
|
166
|
+
if (tok === 'with') {
|
|
167
|
+
i++;
|
|
168
|
+
// Read name=var pairs until we hit end or another keyword
|
|
169
|
+
while (i < tokens.length && tokens[i].indexOf('=') > 0) {
|
|
170
|
+
const pair = tokens[i];
|
|
171
|
+
const eq = pair.indexOf('=');
|
|
172
|
+
const name = pair.slice(0, eq);
|
|
173
|
+
let val = pair.slice(eq + 1);
|
|
174
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
|
|
175
|
+
val = val.slice(1, -1);
|
|
176
|
+
}
|
|
177
|
+
withMappings.push({ name, valPath: val });
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
180
|
+
} else if (tok === 'count') {
|
|
181
|
+
i++;
|
|
182
|
+
// Next token is the count variable
|
|
183
|
+
if (i < tokens.length) {
|
|
184
|
+
pluralMappings.push({ name: 'count', valPath: tokens[i++] });
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return new BlockTransNode(
|
|
192
|
+
extractTextAndVars(body),
|
|
193
|
+
withMappings.length ? withMappings : extractWithMappings(body),
|
|
194
|
+
pluralMappings,
|
|
195
|
+
body
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Extract raw text + {% with %}-bound variable references from a body. */
|
|
200
|
+
function extractTextAndVars(body) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
for (const node of body) {
|
|
203
|
+
if (node.constructor.name === 'TextNode') {
|
|
204
|
+
parts.push(node.content);
|
|
205
|
+
} else if (node.constructor.name === 'VariableNode') {
|
|
206
|
+
// Top-level var: bind it to the parsed varPath so it can be filled
|
|
207
|
+
parts.push({ type: 'var', name: node.varPath });
|
|
208
|
+
} else {
|
|
209
|
+
parts.push('');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return parts;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function extractWithMappings(body) {
|
|
216
|
+
const mappings = [];
|
|
217
|
+
for (const node of body) {
|
|
218
|
+
if (node.constructor.name === 'WithNode' && node.mappings) {
|
|
219
|
+
for (const m of node.mappings) {
|
|
220
|
+
mappings.push({ name: m.name, valPath: m.valPath });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return mappings;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class PluralMappingNode {
|
|
228
|
+
constructor(name, valPath) {
|
|
229
|
+
this.name = name;
|
|
230
|
+
this.valPath = valPath;
|
|
231
|
+
}
|
|
232
|
+
render() { return ''; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parsePlural(tagContent, _parser) {
|
|
236
|
+
// {% plural count %}
|
|
237
|
+
const rest = tagContent.slice(6).trim();
|
|
238
|
+
const eq = rest.indexOf('=');
|
|
239
|
+
if (eq > 0) {
|
|
240
|
+
return new PluralMappingNode(rest.slice(0, eq).trim(), rest.slice(eq + 1).trim());
|
|
241
|
+
}
|
|
242
|
+
return new PluralMappingNode('count', rest);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
TransNode,
|
|
247
|
+
LanguageNode,
|
|
248
|
+
BlockTransNode,
|
|
249
|
+
PluralMappingNode,
|
|
250
|
+
parsers: {
|
|
251
|
+
trans: parseTrans,
|
|
252
|
+
language: parseLanguage,
|
|
253
|
+
blocktrans: parseBlockTrans,
|
|
254
|
+
plural: parsePlural
|
|
255
|
+
}
|
|
256
|
+
};
|