miki-template 1.2.0
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/.github/workflows/ci.yml +54 -0
- package/AGENT.md +71 -0
- package/API_REFERENCE.md +314 -0
- package/CHANGELOG.md +97 -0
- package/CODE_OF_CONDUCT.md +14 -0
- package/CONTRIBUTING.md +27 -0
- package/README.md +304 -0
- package/ROADMAP.md +40 -0
- package/benchmarks/report.json +17 -0
- package/benchmarks/run.js +49 -0
- package/benchmarks/templates/large.dtpl +7 -0
- package/benchmarks/templates/medium.dtpl +3 -0
- package/benchmarks/templates/small.dtpl +7 -0
- package/context/component.md +109 -0
- package/context/prd.md +131 -0
- package/context/project-structure.md +33 -0
- package/docs/README.md +18 -0
- package/docs/advanced_usage.md +71 -0
- package/docs/api.md +102 -0
- package/docs/filters.md +540 -0
- package/docs/installation.md +106 -0
- package/docs/overview.md +57 -0
- package/docs/partialdef.md +41 -0
- package/docs/security.md +27 -0
- package/docs/tags.md +610 -0
- package/docs/usage.md +599 -0
- package/eslint.config.mjs +34 -0
- package/miki-template-1.2.0.vsix +0 -0
- package/miki-template-extension/LICENSE +21 -0
- package/miki-template-extension/README.md +82 -0
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +10 -0
- package/miki-template-extension/package.json +46 -0
- package/miki-template-extension/snippets/miki-template.json +177 -0
- package/miki-template-extension/syntaxes/language-configuration.json +26 -0
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +146 -0
- package/package.json +31 -0
- package/snippets/miki-template.json +177 -0
- package/src/asyncRender.js +21 -0
- package/src/cache.js +41 -0
- package/src/context.js +122 -0
- package/src/context_processors.js +41 -0
- package/src/esm.mjs +72 -0
- package/src/filters.js +527 -0
- package/src/i18n.js +171 -0
- package/src/index.js +454 -0
- package/src/lexer.js +92 -0
- package/src/libraries.js +240 -0
- package/src/parser.js +250 -0
- package/src/security.js +51 -0
- package/src/tags/control.js +591 -0
- package/src/tags/helpers.js +27 -0
- package/src/tags/i18n.js +230 -0
- package/src/tags/inheritance.js +216 -0
- package/src/tags/registry.js +18 -0
- package/src/tags/util.js +322 -0
- package/src/types.d.ts +107 -0
- package/syntaxes/language-configuration.json +26 -0
- package/syntaxes/miki-template.tmLanguage.json +146 -0
- package/tests/asyncRender.test.js +17 -0
- package/tests/base.html +6 -0
- package/tests/child.html +3 -0
- package/tests/context_processors.test.js +13 -0
- package/tests/esm.test.mjs +26 -0
- package/tests/filters.test.js +99 -0
- package/tests/include_security.test.js +9 -0
- package/tests/lexer.test.js +45 -0
- package/tests/parser.test.js +55 -0
- package/tests/partial.html +1 -0
- package/tests/partialdef.test.js +40 -0
- package/tests/production_checks.js +57 -0
- package/tests/security.test.js +28 -0
- package/tests/tags.test.js +203 -0
package/src/tags/i18n.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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 %}
|
|
145
|
+
const body = parser.parse(['blocktrans_internal_marker']); // placeholder
|
|
146
|
+
// Actually we need a different approach: scan raw body and parse it ourselves
|
|
147
|
+
// because blocktrans parses its own text content as a template.
|
|
148
|
+
// We'll rewind and use the lexer on the body tokens.
|
|
149
|
+
// For simplicity here, we re-parse the entire slice as a mini-template.
|
|
150
|
+
// The body tokens between {% blocktrans %} and {% endblocktrans %} are still
|
|
151
|
+
// available because parseUntil was called.
|
|
152
|
+
// For now, treat the body as a list of nodes that we'll re-render:
|
|
153
|
+
return new BlockTransNode(
|
|
154
|
+
extractTextAndVars(body),
|
|
155
|
+
extractWithMappings(body),
|
|
156
|
+
extractPluralMappings(body),
|
|
157
|
+
body
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Extract raw text + {% with %}-bound variable references from a body. */
|
|
162
|
+
function extractTextAndVars(body) {
|
|
163
|
+
const parts = [];
|
|
164
|
+
for (const node of body) {
|
|
165
|
+
if (node.constructor.name === 'TextNode') {
|
|
166
|
+
parts.push(node.content);
|
|
167
|
+
} else if (node.constructor.name === 'VariableNode') {
|
|
168
|
+
// Top-level var: bind it to the parsed varPath so it can be filled
|
|
169
|
+
parts.push({ type: 'var', name: node.varPath });
|
|
170
|
+
} else {
|
|
171
|
+
parts.push('');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return parts;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractWithMappings(body) {
|
|
178
|
+
const mappings = [];
|
|
179
|
+
for (const node of body) {
|
|
180
|
+
if (node.constructor.name === 'WithNode' && node.mappings) {
|
|
181
|
+
for (const m of node.mappings) {
|
|
182
|
+
mappings.push({ name: m.name, valPath: m.valPath });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return mappings;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function extractPluralMappings(body) {
|
|
190
|
+
// Look for a custom PluralNode (we mark it via a special tag)
|
|
191
|
+
// For now, treat WithNodes that have a count attribute as plural mappings.
|
|
192
|
+
const mappings = [];
|
|
193
|
+
for (const node of body) {
|
|
194
|
+
if (node.constructor.name === 'PluralMappingNode') {
|
|
195
|
+
mappings.push({ name: node.name, valPath: node.valPath });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return mappings;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class PluralMappingNode {
|
|
202
|
+
constructor(name, valPath) {
|
|
203
|
+
this.name = name;
|
|
204
|
+
this.valPath = valPath;
|
|
205
|
+
}
|
|
206
|
+
render() { return ''; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parsePlural(tagContent, _parser) {
|
|
210
|
+
// {% plural count %}
|
|
211
|
+
const rest = tagContent.slice(6).trim();
|
|
212
|
+
const eq = rest.indexOf('=');
|
|
213
|
+
if (eq > 0) {
|
|
214
|
+
return new PluralMappingNode(rest.slice(0, eq).trim(), rest.slice(eq + 1).trim());
|
|
215
|
+
}
|
|
216
|
+
return new PluralMappingNode('count', rest);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
TransNode,
|
|
221
|
+
LanguageNode,
|
|
222
|
+
BlockTransNode,
|
|
223
|
+
PluralMappingNode,
|
|
224
|
+
parsers: {
|
|
225
|
+
trans: parseTrans,
|
|
226
|
+
language: parseLanguage,
|
|
227
|
+
blocktrans: parseBlockTrans,
|
|
228
|
+
plural: parsePlural
|
|
229
|
+
}
|
|
230
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inheritance template tags: extends, block, include.
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// Helper to resolve expression values
|
|
8
|
+
function resolveValue(token, context) {
|
|
9
|
+
if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
|
|
10
|
+
return token.slice(1, -1);
|
|
11
|
+
}
|
|
12
|
+
return context.get(token);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class ExtendsNode {
|
|
16
|
+
constructor(parentTemplateExpr) {
|
|
17
|
+
this.parentTemplateExpr = parentTemplateExpr;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
render(context) {
|
|
21
|
+
const parentName = resolveValue(this.parentTemplateExpr, context);
|
|
22
|
+
if (!parentName) return '';
|
|
23
|
+
context.parentTemplate = parentName;
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class BlockNode {
|
|
29
|
+
constructor(name, body) {
|
|
30
|
+
this.name = name;
|
|
31
|
+
this.body = body;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render(context) {
|
|
35
|
+
const blockStack = context.blocks[this.name];
|
|
36
|
+
if (!blockStack || blockStack.length === 0) {
|
|
37
|
+
// If block is not overridden, render its default body
|
|
38
|
+
return this.body.map(n => n.render(context)).join('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!context.blockRenderIndices) {
|
|
42
|
+
context.blockRenderIndices = {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const currentIndex = context.blockRenderIndices[this.name] !== undefined
|
|
46
|
+
? context.blockRenderIndices[this.name]
|
|
47
|
+
: -1;
|
|
48
|
+
|
|
49
|
+
if (currentIndex === -1) {
|
|
50
|
+
// Direct rendering entrypoint: start at the child-most block (index 0)
|
|
51
|
+
context.blockRenderIndices[this.name] = 0;
|
|
52
|
+
|
|
53
|
+
let superVal = '';
|
|
54
|
+
if (blockStack.length > 1) {
|
|
55
|
+
context.blockRenderIndices[this.name] = 1;
|
|
56
|
+
superVal = blockStack[1].render(context);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
context.push({ block: { super: superVal } });
|
|
60
|
+
context.blockRenderIndices[this.name] = 0;
|
|
61
|
+
const result = blockStack[0].body.map(n => n.render(context)).join('');
|
|
62
|
+
context.pop();
|
|
63
|
+
|
|
64
|
+
context.blockRenderIndices[this.name] = -1;
|
|
65
|
+
return result;
|
|
66
|
+
} else {
|
|
67
|
+
// Rendering a parent/super block in the inheritance stack
|
|
68
|
+
const nextIndex = currentIndex + 1;
|
|
69
|
+
let superVal = '';
|
|
70
|
+
if (nextIndex < blockStack.length) {
|
|
71
|
+
context.blockRenderIndices[this.name] = nextIndex;
|
|
72
|
+
superVal = blockStack[nextIndex].render(context);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
context.push({ block: { super: superVal } });
|
|
76
|
+
context.blockRenderIndices[this.name] = currentIndex;
|
|
77
|
+
const result = blockStack[currentIndex].body.map(n => n.render(context)).join('');
|
|
78
|
+
context.pop();
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class IncludeNode {
|
|
86
|
+
constructor(templateNameExpr, extraMappings) {
|
|
87
|
+
this.templateNameExpr = templateNameExpr;
|
|
88
|
+
this.extraMappings = extraMappings; // Array of { name, valPath }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
render(context) {
|
|
92
|
+
const templateName = resolveValue(this.templateNameExpr, context);
|
|
93
|
+
if (!templateName) return '';
|
|
94
|
+
|
|
95
|
+
let viewsDirs = ['.'];
|
|
96
|
+
if (context.options && context.options.settings && context.options.settings.views) {
|
|
97
|
+
const views = context.options.settings.views;
|
|
98
|
+
viewsDirs = Array.isArray(views) ? views : [views];
|
|
99
|
+
} else if (context.options && context.options.views) {
|
|
100
|
+
const views = context.options.views;
|
|
101
|
+
viewsDirs = Array.isArray(views) ? views : [views];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let fileContent = '';
|
|
105
|
+
let loaded = false;
|
|
106
|
+
for (const dir of viewsDirs) {
|
|
107
|
+
try {
|
|
108
|
+
const fullPath = path.resolve(dir, templateName);
|
|
109
|
+
// Security check: ensure the resolved path is within the allowed view directory
|
|
110
|
+
const relative = path.relative(path.resolve(dir), fullPath);
|
|
111
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
112
|
+
throw new Error(`Include tag attempted path traversal outside allowed views: '${templateName}'`);
|
|
113
|
+
}
|
|
114
|
+
fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
115
|
+
loaded = true;
|
|
116
|
+
break;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Propagate traversal errors, otherwise try next directory
|
|
119
|
+
if (e.message && e.message.startsWith('Include tag attempted path traversal')) {
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
// continue searching other directories
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!loaded) {
|
|
127
|
+
throw new Error(`Template not found: '${templateName}' in directories ${JSON.stringify(viewsDirs)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse and tokenize dynamically (uses lazy imports to break loops)
|
|
131
|
+
const { tokenize } = require('../lexer');
|
|
132
|
+
const { Parser } = require('../parser');
|
|
133
|
+
const { getTagRegistry } = require('./registry');
|
|
134
|
+
|
|
135
|
+
const tokens = tokenize(fileContent);
|
|
136
|
+
const parser = new Parser(tokens, getTagRegistry());
|
|
137
|
+
const nodes = parser.parse();
|
|
138
|
+
|
|
139
|
+
// Set up include scope
|
|
140
|
+
if (this.extraMappings && this.extraMappings.length > 0) {
|
|
141
|
+
const extraScope = {};
|
|
142
|
+
for (const mapping of this.extraMappings) {
|
|
143
|
+
extraScope[mapping.name] = resolveValue(mapping.valPath, context);
|
|
144
|
+
}
|
|
145
|
+
context.push(extraScope);
|
|
146
|
+
const res = nodes.map(n => n.render(context)).join('');
|
|
147
|
+
context.pop();
|
|
148
|
+
return res;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return nodes.map(n => n.render(context)).join('');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Tag Registry Parsers ---
|
|
156
|
+
|
|
157
|
+
// --- Tag Registry Parsers ---
|
|
158
|
+
|
|
159
|
+
function parseExtends(tagContent, _parser) {
|
|
160
|
+
// tagContent: "extends 'base.html'"
|
|
161
|
+
const parentTemplateExpr = tagContent.slice(8).trim();
|
|
162
|
+
return new ExtendsNode(parentTemplateExpr);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseBlock(tagContent, parser) {
|
|
166
|
+
// tagContent: "block content"
|
|
167
|
+
const name = tagContent.slice(6).trim();
|
|
168
|
+
const body = parser.parse(['endblock']);
|
|
169
|
+
|
|
170
|
+
const next = parser.peek();
|
|
171
|
+
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endblock') {
|
|
172
|
+
parser.advance(); // Consume endblock
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const blockNode = new BlockNode(name, body);
|
|
176
|
+
|
|
177
|
+
// Register the block stack on the parser so we track overriding chain
|
|
178
|
+
parser.blocks = parser.blocks || {};
|
|
179
|
+
if (!parser.blocks[name]) {
|
|
180
|
+
parser.blocks[name] = [];
|
|
181
|
+
}
|
|
182
|
+
parser.blocks[name].push(blockNode);
|
|
183
|
+
|
|
184
|
+
return blockNode;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseInclude(tagContent, _parser) {
|
|
188
|
+
// tagContent: "include 'header.html'" or "include 'header.html' with val1=var1"
|
|
189
|
+
const content = tagContent.slice(8).trim();
|
|
190
|
+
|
|
191
|
+
// Extract template expression
|
|
192
|
+
const parts = content.split(/\s+with\s+/);
|
|
193
|
+
const templateNameExpr = parts[0].trim();
|
|
194
|
+
|
|
195
|
+
const extraMappings = [];
|
|
196
|
+
if (parts[1]) {
|
|
197
|
+
const pairRegex = /(\w+)=([^\s]+)/g;
|
|
198
|
+
let match;
|
|
199
|
+
while ((match = pairRegex.exec(parts[1])) !== null) {
|
|
200
|
+
extraMappings.push({ name: match[1], valPath: match[2] });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return new IncludeNode(templateNameExpr, extraMappings);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
ExtendsNode,
|
|
209
|
+
BlockNode,
|
|
210
|
+
IncludeNode,
|
|
211
|
+
parsers: {
|
|
212
|
+
extends: parseExtends,
|
|
213
|
+
block: parseBlock,
|
|
214
|
+
include: parseInclude
|
|
215
|
+
}
|
|
216
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central tag registry for template tags.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const tagRegistry = {};
|
|
6
|
+
|
|
7
|
+
function registerTag(name, parserFn) {
|
|
8
|
+
tagRegistry[name] = parserFn;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getTagRegistry() {
|
|
12
|
+
return tagRegistry;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
registerTag,
|
|
17
|
+
getTagRegistry
|
|
18
|
+
};
|