miki-template 1.2.0 → 1.3.6
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/release-notes/v1.3.1.md +55 -0
- 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 -97
- package/CODE_OF_CONDUCT.md +14 -14
- package/CONTRIBUTING.md +27 -27
- package/README.md +342 -304
- package/ROADMAP.md +40 -40
- package/assets/banner.png +0 -0
- package/benchmarks/report.json +16 -16
- package/benchmarks/run.js +49 -49
- package/benchmarks/stress.mjs +647 -0
- 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 +23 -0
- package/dir/cmpnt.html +11 -0
- package/dir/footer.html +3 -0
- package/dir/home.html +80 -0
- package/dir/index.html +80 -0
- package/dir/navbar.html +9 -0
- package/docs/README.md +18 -18
- package/docs/advanced_usage.md +71 -71
- package/docs/api.md +119 -102
- package/docs/filters.md +708 -540
- package/docs/installation.md +106 -106
- package/docs/overview.md +57 -57
- package/docs/partialdef.md +70 -41
- package/docs/security.md +27 -27
- package/docs/tags.md +673 -610
- package/docs/usage.md +646 -599
- package/eslint.config.mjs +42 -34
- package/ex.mjs +33 -0
- package/miki-template-extension/.github/workflows/ci.yml +116 -0
- package/miki-template-extension/.vscodeignore +7 -0
- package/miki-template-extension/CHANGELOG.md +99 -0
- package/miki-template-extension/LICENSE +21 -21
- package/miki-template-extension/README.md +273 -82
- package/miki-template-extension/extension.js +1013 -0
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +10 -10
- package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
- package/miki-template-extension/package.json +280 -46
- package/miki-template-extension/snippets/miki-template.json +717 -177
- package/miki-template-extension/syntaxes/language-configuration.json +114 -26
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -146
- package/miki-template-extension/tests/grammar-tests.json +162 -0
- package/miki-template-extension/tests/run-grammar-tests.js +82 -0
- package/package.json +37 -31
- 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 -0
- package/scripts/build-vsix.ps1 +15 -0
- package/snippets/miki-template.json +177 -177
- package/src/asyncRender.js +20 -20
- package/src/cache.js +80 -41
- package/src/context.js +126 -122
- package/src/context_processors.js +48 -41
- package/src/esm.mjs +84 -72
- package/src/filters.js +975 -527
- package/src/i18n.js +171 -171
- package/src/index.js +974 -454
- package/src/lexer.js +114 -92
- package/src/libraries.js +371 -240
- package/src/parser.js +270 -250
- package/src/security.js +53 -51
- package/src/tags/control.js +719 -590
- package/src/tags/extra.js +154 -0
- package/src/tags/helpers.js +26 -26
- package/src/tags/i18n.js +256 -230
- package/src/tags/inheritance.js +335 -216
- package/src/tags/registry.js +18 -18
- package/src/tags/util.js +400 -322
- 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 -26
- package/tests/filters.test.js +254 -99
- package/tests/include_security.test.js +9 -9
- package/tests/integration/README.md +32 -0
- package/tests/integration/features.test.cjs +1681 -0
- package/tests/integration/features.test.mjs +1697 -0
- package/tests/integration/templates/base.miki +6 -0
- package/tests/integration/templates/child.miki +6 -0
- package/tests/integration/templates/index.html +17 -0
- package/tests/lexer.test.js +45 -45
- package/tests/parser.test.js +57 -55
- package/tests/partial.html +1 -1
- package/tests/partialdef.test.js +79 -40
- package/tests/production_checks.js +57 -57
- package/tests/security.test.js +28 -28
- package/tests/tags.test.js +233 -203
- package/miki-template-1.2.0.vsix +0 -0
package/src/tags/inheritance.js
CHANGED
|
@@ -1,216 +1,335 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inheritance template tags: extends, block, include.
|
|
3
|
-
*/
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
context.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Inheritance template tags: extends, block, include.
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
class ExtendsNode {
|
|
24
|
+
constructor(parentTemplateExpr) {
|
|
25
|
+
this.parentTemplateExpr = parentTemplateExpr;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render(context) {
|
|
29
|
+
const parentName = resolveValue(this.parentTemplateExpr, context);
|
|
30
|
+
if (!parentName) return '';
|
|
31
|
+
context.parentTemplate = parentName;
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class BlockNode {
|
|
37
|
+
constructor(name, body) {
|
|
38
|
+
this.name = name;
|
|
39
|
+
this.body = body;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(context) {
|
|
43
|
+
const blockStack = context.blocks[this.name];
|
|
44
|
+
if (!blockStack || blockStack.length === 0) {
|
|
45
|
+
// If block is not overridden, render its default body
|
|
46
|
+
return this.body.map(n => n.render(context)).join('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!context.blockRenderIndices) {
|
|
50
|
+
context.blockRenderIndices = {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const currentIndex = context.blockRenderIndices[this.name] !== undefined
|
|
54
|
+
? context.blockRenderIndices[this.name]
|
|
55
|
+
: -1;
|
|
56
|
+
|
|
57
|
+
if (currentIndex === -1) {
|
|
58
|
+
// Direct rendering entrypoint: start at the child-most block (index 0)
|
|
59
|
+
context.blockRenderIndices[this.name] = 0;
|
|
60
|
+
|
|
61
|
+
let superVal = '';
|
|
62
|
+
if (blockStack.length > 1) {
|
|
63
|
+
context.blockRenderIndices[this.name] = 1;
|
|
64
|
+
superVal = blockStack[1].render(context);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
context.push({ block: { super: superVal } });
|
|
68
|
+
context.blockRenderIndices[this.name] = 0;
|
|
69
|
+
const result = blockStack[0].body.map(n => n.render(context)).join('');
|
|
70
|
+
context.pop();
|
|
71
|
+
|
|
72
|
+
context.blockRenderIndices[this.name] = -1;
|
|
73
|
+
return result;
|
|
74
|
+
} else {
|
|
75
|
+
// Rendering a parent/super block in the inheritance stack
|
|
76
|
+
const nextIndex = currentIndex + 1;
|
|
77
|
+
let superVal = '';
|
|
78
|
+
if (nextIndex < blockStack.length) {
|
|
79
|
+
context.blockRenderIndices[this.name] = nextIndex;
|
|
80
|
+
superVal = blockStack[nextIndex].render(context);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
context.push({ block: { super: superVal } });
|
|
84
|
+
context.blockRenderIndices[this.name] = currentIndex;
|
|
85
|
+
const result = blockStack[currentIndex].body.map(n => n.render(context)).join('');
|
|
86
|
+
context.pop();
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class IncludeNode {
|
|
94
|
+
constructor(templateNameExpr, extraMappings, partialName = null) {
|
|
95
|
+
this.templateNameExpr = templateNameExpr;
|
|
96
|
+
this.extraMappings = extraMappings; // Array of { name, valPath }
|
|
97
|
+
this.partialName = partialName; // When set, render only this partial
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
render(context) {
|
|
101
|
+
const rawName = resolveValue(this.templateNameExpr, context);
|
|
102
|
+
if (!rawName) return '';
|
|
103
|
+
|
|
104
|
+
// Django-style partial selector: "home.html#card" means render
|
|
105
|
+
// only the `card` partial defined inside `home.html`.
|
|
106
|
+
let templateName = rawName;
|
|
107
|
+
let partialName = this.partialName;
|
|
108
|
+
const hashIdx = rawName.indexOf('#');
|
|
109
|
+
if (hashIdx >= 0) {
|
|
110
|
+
templateName = rawName.slice(0, hashIdx);
|
|
111
|
+
partialName = rawName.slice(hashIdx + 1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let viewsDirs = ['.'];
|
|
115
|
+
if (context.options && context.options.settings && context.options.settings.views) {
|
|
116
|
+
const views = context.options.settings.views;
|
|
117
|
+
viewsDirs = Array.isArray(views) ? views : [views];
|
|
118
|
+
} else if (context.options && context.options.views) {
|
|
119
|
+
const views = context.options.views;
|
|
120
|
+
viewsDirs = Array.isArray(views) ? views : [views];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let fileContent = '';
|
|
124
|
+
let loaded = false;
|
|
125
|
+
for (const dir of viewsDirs) {
|
|
126
|
+
try {
|
|
127
|
+
const fullPath = path.resolve(dir, templateName);
|
|
128
|
+
// Security check: ensure the resolved path is within the allowed view directory
|
|
129
|
+
const relative = path.relative(path.resolve(dir), fullPath);
|
|
130
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
131
|
+
throw new Error(`Include tag attempted path traversal outside allowed views: '${templateName}'`);
|
|
132
|
+
}
|
|
133
|
+
fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
134
|
+
loaded = true;
|
|
135
|
+
break;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (e.message && e.message.startsWith('Include tag attempted path traversal')) {
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!loaded) {
|
|
144
|
+
throw new Error(`Template not found: '${templateName}' in directories ${JSON.stringify(viewsDirs)}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Parse the included file. We use the same lexer/parser/registry
|
|
148
|
+
// as the outer compile, which means {% load %}, custom tags, and
|
|
149
|
+
// all partialdefs are registered when we parse the file.
|
|
150
|
+
const { tokenize } = require('../lexer');
|
|
151
|
+
const { Parser } = require('../parser');
|
|
152
|
+
const { getTagRegistry } = require('./registry');
|
|
153
|
+
|
|
154
|
+
const tokens = tokenize(fileContent);
|
|
155
|
+
const parser = new Parser(tokens, getTagRegistry());
|
|
156
|
+
const nodes = parser.parse();
|
|
157
|
+
|
|
158
|
+
// If a partial selector was specified, compile the file so its
|
|
159
|
+
// partialdefs register themselves, then render only the named
|
|
160
|
+
// partial using the current context.
|
|
161
|
+
if (partialName) {
|
|
162
|
+
// Use a fresh context so partials from the included file do NOT
|
|
163
|
+
// leak into the caller's partial registry. Only the requested
|
|
164
|
+
// partial is rendered, and it can reference other partials from
|
|
165
|
+
// the same included file via the temp context's registry.
|
|
166
|
+
const tempContext = new Context({}, context.options || {});
|
|
167
|
+
nodes.map(n => n.render(tempContext));
|
|
168
|
+
const partial = tempContext.getPartial(partialName);
|
|
169
|
+
if (!partial) {
|
|
170
|
+
throw new Error(`Partial '${partialName}' not found in template '${templateName}'`);
|
|
171
|
+
}
|
|
172
|
+
// Build a render context that carries the caller's state but
|
|
173
|
+
// only the included file's partial definitions.
|
|
174
|
+
const renderCtx = new Context({}, context.options || {});
|
|
175
|
+
renderCtx.scopes = context.scopes.slice();
|
|
176
|
+
renderCtx.blocks = context.blocks;
|
|
177
|
+
renderCtx.cycleStates = context.cycleStates;
|
|
178
|
+
renderCtx.autoescape = context.autoescape;
|
|
179
|
+
renderCtx.parentTemplate = context.parentTemplate;
|
|
180
|
+
renderCtx.blockRenderIndices = context.blockRenderIndices;
|
|
181
|
+
renderCtx.partialDefs = tempContext.partialDefs;
|
|
182
|
+
// Apply include's `with` extra mappings on top of caller's ctx
|
|
183
|
+
if (this.extraMappings && this.extraMappings.length > 0) {
|
|
184
|
+
const extraScope = {};
|
|
185
|
+
for (const m of this.extraMappings) {
|
|
186
|
+
extraScope[m.name] = resolveValue(m.valPath, context);
|
|
187
|
+
}
|
|
188
|
+
renderCtx.push(extraScope);
|
|
189
|
+
const out = partial.body.map(n => n.render(renderCtx)).join('');
|
|
190
|
+
renderCtx.pop();
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
return partial.body.map(n => n.render(renderCtx)).join('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Plain include (no partial selector): render the included body
|
|
197
|
+
// inside the caller's context, applying any `with` extra mappings.
|
|
198
|
+
if (this.extraMappings && this.extraMappings.length > 0) {
|
|
199
|
+
const extraScope = {};
|
|
200
|
+
for (const mapping of this.extraMappings) {
|
|
201
|
+
extraScope[mapping.name] = resolveValue(mapping.valPath, context);
|
|
202
|
+
}
|
|
203
|
+
context.push(extraScope);
|
|
204
|
+
const res = nodes.map(n => n.render(context)).join('');
|
|
205
|
+
context.pop();
|
|
206
|
+
return res;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return nodes.map(n => n.render(context)).join('');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
ExtendsNode,
|
|
215
|
+
BlockNode,
|
|
216
|
+
IncludeNode,
|
|
217
|
+
parsers: {
|
|
218
|
+
extends: parseExtends,
|
|
219
|
+
block: parseBlock,
|
|
220
|
+
include: parseInclude
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function parseExtends(tagContent, _parser) {
|
|
225
|
+
// tagContent: "extends 'base.html'"
|
|
226
|
+
const parentTemplateExpr = tagContent.slice(8).trim();
|
|
227
|
+
return new ExtendsNode(parentTemplateExpr);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseBlock(tagContent, parser) {
|
|
231
|
+
// tagContent: "block content"
|
|
232
|
+
const name = tagContent.slice(6).trim();
|
|
233
|
+
const body = parser.parse(['endblock']);
|
|
234
|
+
|
|
235
|
+
const next = parser.peek();
|
|
236
|
+
if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endblock') {
|
|
237
|
+
parser.advance(); // Consume endblock
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const blockNode = new BlockNode(name, body);
|
|
241
|
+
|
|
242
|
+
// Register the block stack on the parser so we track overriding chain
|
|
243
|
+
parser.blocks = parser.blocks || {};
|
|
244
|
+
if (!parser.blocks[name]) {
|
|
245
|
+
parser.blocks[name] = [];
|
|
246
|
+
}
|
|
247
|
+
parser.blocks[name].push(blockNode);
|
|
248
|
+
|
|
249
|
+
return blockNode;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseInclude(tagContent, _parser) {
|
|
253
|
+
// tagContent: "include 'header.html'" or
|
|
254
|
+
// "include 'header.html' with val1=var1 val2='literal'"
|
|
255
|
+
// "include 'header.html#partial_name'"
|
|
256
|
+
const content = tagContent.slice(8).trim();
|
|
257
|
+
|
|
258
|
+
// Extract template expression (may be quoted)
|
|
259
|
+
const tmplMatch = content.match(/^(".*?"|'.*?'|\S+)/);
|
|
260
|
+
if (!tmplMatch) {
|
|
261
|
+
throw new Error('include tag requires a template name');
|
|
262
|
+
}
|
|
263
|
+
const templateNameExpr = tmplMatch[1];
|
|
264
|
+
const rest = content.slice(tmplMatch[0].length).trim();
|
|
265
|
+
|
|
266
|
+
// Optional `with k1=v1 k2='v2' k3=v3`
|
|
267
|
+
let extraMappings = [];
|
|
268
|
+
if (rest) {
|
|
269
|
+
const withMatch = rest.match(/^with\s+(.+)$/s);
|
|
270
|
+
if (withMatch) {
|
|
271
|
+
extraMappings = parseKeyValuePairs(withMatch[1]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return new IncludeNode(templateNameExpr, extraMappings);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Parse a whitespace-separated list of `key=value` pairs. Values may be
|
|
280
|
+
* quoted (single or double quotes) or bare identifiers. The
|
|
281
|
+
* control.js parseKeyValuePairs handles commas, so prefer that — but
|
|
282
|
+
* include's `with` historically used a whitespace separator. Support
|
|
283
|
+
* both for backwards compatibility.
|
|
284
|
+
*/
|
|
285
|
+
function parseKeyValuePairs(str) {
|
|
286
|
+
const pairs = [];
|
|
287
|
+
let i = 0;
|
|
288
|
+
while (i < str.length) {
|
|
289
|
+
while (i < str.length && /\s/.test(str[i])) i++;
|
|
290
|
+
if (i >= str.length) break;
|
|
291
|
+
|
|
292
|
+
// Read key
|
|
293
|
+
let key = '';
|
|
294
|
+
while (i < str.length && /[a-zA-Z0-9_]/.test(str[i])) {
|
|
295
|
+
key += str[i++];
|
|
296
|
+
}
|
|
297
|
+
if (!key) { i++; continue; }
|
|
298
|
+
while (i < str.length && /\s/.test(str[i])) i++;
|
|
299
|
+
if (str[i] !== '=') { i++; continue; }
|
|
300
|
+
i++; // consume '='
|
|
301
|
+
while (i < str.length && /\s/.test(str[i])) i++;
|
|
302
|
+
|
|
303
|
+
let val = '';
|
|
304
|
+
if (str[i] === '"' || str[i] === '\'') {
|
|
305
|
+
const quote = str[i++];
|
|
306
|
+
while (i < str.length && str[i] !== quote) {
|
|
307
|
+
if (str[i] === '\\' && i + 1 < str.length) i++;
|
|
308
|
+
val += str[i++];
|
|
309
|
+
}
|
|
310
|
+
if (str[i] === quote) i++;
|
|
311
|
+
// Preserve quotes so resolveValue can recognize the literal
|
|
312
|
+
val = quote + val + quote;
|
|
313
|
+
} else {
|
|
314
|
+
// Unquoted value: read until comma or whitespace
|
|
315
|
+
while (i < str.length && str[i] !== ',' && !/\s/.test(str[i])) {
|
|
316
|
+
val += str[i++];
|
|
317
|
+
}
|
|
318
|
+
// Consume trailing comma if present
|
|
319
|
+
if (str[i] === ',') i++;
|
|
320
|
+
}
|
|
321
|
+
pairs.push({ name: key, valPath: val });
|
|
322
|
+
}
|
|
323
|
+
return pairs;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
ExtendsNode,
|
|
328
|
+
BlockNode,
|
|
329
|
+
IncludeNode,
|
|
330
|
+
parsers: {
|
|
331
|
+
extends: parseExtends,
|
|
332
|
+
block: parseBlock,
|
|
333
|
+
include: parseInclude
|
|
334
|
+
}
|
|
335
|
+
};
|
package/src/tags/registry.js
CHANGED
|
@@ -1,18 +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
|
-
};
|
|
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
|
+
};
|