@rhinostone/swig-twig 2.4.1 → 2.4.2
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/lib/tags/from.js +67 -11
- package/lib/tags/import.js +87 -22
- package/package.json +2 -2
package/lib/tags/from.js
CHANGED
|
@@ -50,9 +50,12 @@ exports.block = true;
|
|
|
50
50
|
* for each requested macro, invokes its `compile` to get the IRMacro
|
|
51
51
|
* node and renders that node to JS through `backend.compile`. A
|
|
52
52
|
* macro requested by name but not found in the imported template
|
|
53
|
-
* raises a filename-aware throw. The
|
|
54
|
-
* `
|
|
55
|
-
* `
|
|
53
|
+
* raises a filename-aware throw. The imported file's own nested
|
|
54
|
+
* `{% import %}` / `{% from %}` tokens are carried through (flagged
|
|
55
|
+
* `isImport`, with `boundNames` + a synthesized private `slot`) so the
|
|
56
|
+
* requested macros can reference them at call time. The resulting list
|
|
57
|
+
* (nested entries + `[{compiled, origName, aliasName}, ...]`) is stashed
|
|
58
|
+
* on `token.args` for the compile step to rewrite.
|
|
56
59
|
*
|
|
57
60
|
* @param {string} str Tag body.
|
|
58
61
|
* @param {number} line Source line of the opening `{%`.
|
|
@@ -157,9 +160,34 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
157
160
|
// Index the imported template's macros by name so we can look up
|
|
158
161
|
// each requested entry once. Raises a filename-aware throw if an
|
|
159
162
|
// entry names a macro that doesn't exist in the imported template.
|
|
163
|
+
//
|
|
164
|
+
// The imported file may have its own `{% import %}` / `{% from %}`.
|
|
165
|
+
// Unlike `{% import %}`, `{% from %}` binds bare names with no namespace
|
|
166
|
+
// alias to hang those nested imports under, so they re-home under a
|
|
167
|
+
// synthesized private slot keyed off the path (compile() applies the
|
|
168
|
+
// rewrite). This keeps the inner names out of the parent's bare scope —
|
|
169
|
+
// bare `{{ name }}` in the parent stays undefined, matching Twig's rule
|
|
170
|
+
// that imports are local to the importing template.
|
|
171
|
+
var privateSlot = '__nsfrom_' + path.replace(/[^a-zA-Z0-9]/g, '_');
|
|
172
|
+
var nested = [];
|
|
160
173
|
var macroIndex = {};
|
|
161
174
|
utils.each(parsed.tokens, function (tk) {
|
|
162
|
-
if (!tk ||
|
|
175
|
+
if (!tk || typeof tk.compile !== 'function') {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (tk.name === 'import' || tk.name === 'from') {
|
|
179
|
+
var bn = (tk.name === 'import')
|
|
180
|
+
? [tk.args[tk.args.length - 1]]
|
|
181
|
+
: utils.map(tk.args, function (a) { return a.aliasName; });
|
|
182
|
+
nested.push({
|
|
183
|
+
compiled: tk.compile(null, tk.args.slice(), tk.content, [], compileOpts) + '\n',
|
|
184
|
+
isImport: true,
|
|
185
|
+
boundNames: bn,
|
|
186
|
+
slot: privateSlot
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (tk.name !== 'macro') {
|
|
163
191
|
return;
|
|
164
192
|
}
|
|
165
193
|
macroIndex[tk.args[0]] = tk;
|
|
@@ -181,7 +209,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
181
209
|
});
|
|
182
210
|
}
|
|
183
211
|
|
|
184
|
-
token.args = resolved;
|
|
212
|
+
token.args = nested.concat(resolved);
|
|
185
213
|
return true;
|
|
186
214
|
};
|
|
187
215
|
|
|
@@ -201,6 +229,11 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
201
229
|
* `undefined`, matching Twig's "unimported macros are not available"
|
|
202
230
|
* semantic.
|
|
203
231
|
*
|
|
232
|
+
* The imported file's own nested imports (flagged `isImport`) are emitted
|
|
233
|
+
* first, re-homed under a private slot (`_ctx.<boundName>` ->
|
|
234
|
+
* `_ctx.<slot>.<boundName>`) so they resolve for the imported macros at
|
|
235
|
+
* call time without leaking into the parent's bare scope.
|
|
236
|
+
*
|
|
204
237
|
* @return {string} JS source that assigns every imported macro into
|
|
205
238
|
* `_ctx.<aliasName>`. Backend lifts it into
|
|
206
239
|
* `IRLegacyJS`.
|
|
@@ -225,20 +258,43 @@ exports.compile = function (compiler, args, content, parents, options) {
|
|
|
225
258
|
options.filename || ''
|
|
226
259
|
);
|
|
227
260
|
}
|
|
228
|
-
var
|
|
229
|
-
var
|
|
261
|
+
var macros = [];
|
|
262
|
+
var nested = [];
|
|
263
|
+
utils.each(args, function (a) { (a.isImport ? nested : macros).push(a); });
|
|
264
|
+
var allOrigNames = utils.map(macros, function (arg) { return arg.origName; }).join('|');
|
|
265
|
+
var replacements = utils.map(macros, function (arg) {
|
|
230
266
|
return {
|
|
231
267
|
ex: new RegExp('_ctx\\.' + arg.origName + '(\\W)(?!' + allOrigNames + ')', 'g'),
|
|
232
268
|
re: '_ctx.' + arg.aliasName + '$1'
|
|
233
269
|
};
|
|
234
270
|
});
|
|
271
|
+
// The imported file's own nested imports re-home under a private slot so
|
|
272
|
+
// they never leak into the parent's bare scope: `_ctx.<boundName>` ->
|
|
273
|
+
// `_ctx.<slot>.<boundName>`, applied to the nested setup JS and to every
|
|
274
|
+
// imported macro body that references a bound name.
|
|
275
|
+
var innerReplacements = [];
|
|
276
|
+
utils.each(nested, function (a) {
|
|
277
|
+
utils.each(a.boundNames, function (nm) {
|
|
278
|
+
innerReplacements.push({
|
|
279
|
+
ex: new RegExp('_ctx\\.' + nm + '(\\W)', 'g'),
|
|
280
|
+
re: '_ctx.' + a.slot + '.' + nm + '$1'
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
235
284
|
|
|
236
285
|
var out = ' var _output = "";\n';
|
|
237
|
-
|
|
286
|
+
// Nested imports first (under the private slot), so the imported macros
|
|
287
|
+
// below resolve them at call time.
|
|
288
|
+
utils.each(nested, function (a) {
|
|
289
|
+
out += '_ctx.' + a.slot + ' = _ctx.' + a.slot + ' || {};\n';
|
|
290
|
+
var c = a.compiled;
|
|
291
|
+
utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
|
|
292
|
+
out += c;
|
|
293
|
+
});
|
|
294
|
+
utils.each(macros, function (arg) {
|
|
238
295
|
var c = arg.compiled;
|
|
239
|
-
utils.each(replacements, function (re) {
|
|
240
|
-
|
|
241
|
-
});
|
|
296
|
+
utils.each(replacements, function (re) { c = c.replace(re.ex, re.re); });
|
|
297
|
+
utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
|
|
242
298
|
out += c;
|
|
243
299
|
});
|
|
244
300
|
|
package/lib/tags/import.js
CHANGED
|
@@ -47,13 +47,16 @@ exports.block = true;
|
|
|
47
47
|
* against the bare-identifier rule and the CVE-2023-25345
|
|
48
48
|
* `_dangerousProps` blocklist.
|
|
49
49
|
*
|
|
50
|
-
* Walks the imported template's token list (via `swig.parseFile`)
|
|
51
|
-
* `{% macro %}`
|
|
52
|
-
*
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
50
|
+
* Walks the imported template's token list (via `swig.parseFile`). For
|
|
51
|
+
* each `{% macro %}` token, invokes its `compile` to get the IRMacro node
|
|
52
|
+
* and renders it to JS through `backend.compile`. For each nested
|
|
53
|
+
* `{% import %}` / `{% from %}` token (the imported file's own imports),
|
|
54
|
+
* carries its compiled setup through flagged `isImport` (with `boundNames`)
|
|
55
|
+
* so macros defined here can reference it at call time. The resulting
|
|
56
|
+
* entries + the alias string are stashed on `token.args` —
|
|
57
|
+
* `exports.compile` pops the alias off the tail, re-homes any nested
|
|
58
|
+
* imports under it, and performs the namespace-prefix rewrite on each
|
|
59
|
+
* macro's compiled JS.
|
|
57
60
|
*
|
|
58
61
|
* @param {string} str Tag body.
|
|
59
62
|
* @param {number} line Source line of the opening `{%`.
|
|
@@ -130,7 +133,37 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
130
133
|
var macros = [];
|
|
131
134
|
|
|
132
135
|
utils.each(parsed.tokens, function (tk) {
|
|
133
|
-
if (!tk ||
|
|
136
|
+
if (!tk || typeof tk.compile !== 'function') {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// The imported file may itself import macros, via `{% import "x" as y %}`
|
|
140
|
+
// or `{% from "x" import a, b %}`. Carry those nested imports through so
|
|
141
|
+
// a macro defined here that references a name they bind resolves at call
|
|
142
|
+
// time — without them the call compiles against a namespace/binding that
|
|
143
|
+
// was never set up, and silently renders empty.
|
|
144
|
+
//
|
|
145
|
+
// Unlike native `lib/tags/import.js` (which emits the nested bindings
|
|
146
|
+
// straight into the caller's `_ctx` and thereby leaks them into the
|
|
147
|
+
// parent scope), swig-twig keeps imports local to their template per
|
|
148
|
+
// Twig's scoping rule: compile() re-homes every bound name under THIS
|
|
149
|
+
// import's alias (`_ctx.<alias>.<boundName>`), so none is visible bare
|
|
150
|
+
// in the parent. `tk.args` is already parsed; slice() avoids the pop()
|
|
151
|
+
// in compile() mutating the cached token.
|
|
152
|
+
if (tk.name === 'import' || tk.name === 'from') {
|
|
153
|
+
// Names the nested import binds into _ctx: `{% import %}` binds one
|
|
154
|
+
// namespace alias (the tail of args); `{% from %}` binds one per
|
|
155
|
+
// requested entry (its alias name).
|
|
156
|
+
var boundNames = (tk.name === 'import')
|
|
157
|
+
? [tk.args[tk.args.length - 1]]
|
|
158
|
+
: utils.map(tk.args, function (a) { return a.aliasName; });
|
|
159
|
+
macros.push({
|
|
160
|
+
compiled: tk.compile(null, tk.args.slice(), tk.content, [], compileOpts) + '\n',
|
|
161
|
+
isImport: true,
|
|
162
|
+
boundNames: boundNames
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (tk.name !== 'macro') {
|
|
134
167
|
return;
|
|
135
168
|
}
|
|
136
169
|
var macroName = tk.args[0];
|
|
@@ -145,15 +178,20 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
145
178
|
|
|
146
179
|
/**
|
|
147
180
|
* Emit the namespace-prefix rewrite. Pops the alias off the tail of
|
|
148
|
-
* `args
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* `
|
|
181
|
+
* `args` and splits the rest into macros and nested imports (flagged
|
|
182
|
+
* `isImport` by parse()). Builds a `_ctx.<name>(\\W)(?!<allMacros>)`
|
|
183
|
+
* regex for each imported macro and rewrites every occurrence in each
|
|
184
|
+
* macro's compiled JS to `_ctx.<alias>.<name>`. Nested imports are
|
|
185
|
+
* emitted first, re-homed under the alias (`_ctx.<boundName>` ->
|
|
186
|
+
* `_ctx.<alias>.<boundName>`) so a file's own imports stay local and
|
|
187
|
+
* never leak bare into the parent scope; the same re-homing is applied
|
|
188
|
+
* to macro bodies that reference any bound name. Concatenates after the
|
|
189
|
+
* `_ctx.<alias> = {};` namespace-init line and returns a JS source string
|
|
190
|
+
* (the backend lifts it into `IRLegacyJS`).
|
|
154
191
|
*
|
|
155
|
-
* @return {string} JS source that initialises `_ctx.<alias
|
|
156
|
-
*
|
|
192
|
+
* @return {string} JS source that initialises `_ctx.<alias>`, re-homes
|
|
193
|
+
* any nested imports under it, and assigns every
|
|
194
|
+
* imported macro into it.
|
|
157
195
|
*/
|
|
158
196
|
exports.compile = function (compiler, args, content, parents, options) {
|
|
159
197
|
// Phase 2 (#T22): async-codegen branch. Parse stashed `[path, alias]`
|
|
@@ -167,20 +205,47 @@ exports.compile = function (compiler, args, content, parents, options) {
|
|
|
167
205
|
);
|
|
168
206
|
}
|
|
169
207
|
var ctx = args.pop();
|
|
170
|
-
var
|
|
208
|
+
var macros = [];
|
|
209
|
+
var nested = [];
|
|
210
|
+
utils.each(args, function (a) {
|
|
211
|
+
(a.isImport ? nested : macros).push(a);
|
|
212
|
+
});
|
|
213
|
+
var allMacros = utils.map(macros, function (arg) { return arg.name; }).join('|');
|
|
171
214
|
var out = '_ctx.' + ctx + ' = {};\n var _output = "";\n';
|
|
172
|
-
var replacements = utils.map(
|
|
215
|
+
var replacements = utils.map(macros, function (arg) {
|
|
173
216
|
return {
|
|
174
217
|
ex: new RegExp('_ctx\\.' + arg.name + '(\\W)(?!' + allMacros + ')', 'g'),
|
|
175
218
|
re: '_ctx.' + ctx + '.' + arg.name + '$1'
|
|
176
219
|
};
|
|
177
220
|
});
|
|
221
|
+
// Re-home every name a nested import binds under THIS alias so it stays
|
|
222
|
+
// local to the imported file (Twig scoping) and never leaks bare into the
|
|
223
|
+
// parent: `_ctx.<boundName>` -> `_ctx.<alias>.<boundName>`, applied to both
|
|
224
|
+
// the nested setup JS and every macro body below that references it.
|
|
225
|
+
// Compounds across import depth (a 3-level chain re-homes at each layer
|
|
226
|
+
// with no special-casing).
|
|
227
|
+
var innerReplacements = [];
|
|
228
|
+
utils.each(nested, function (a) {
|
|
229
|
+
utils.each(a.boundNames, function (nm) {
|
|
230
|
+
innerReplacements.push({
|
|
231
|
+
ex: new RegExp('_ctx\\.' + nm + '(\\W)', 'g'),
|
|
232
|
+
re: '_ctx.' + ctx + '.' + nm + '$1'
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Nested imports first, re-homed under the alias, so the macros defined
|
|
238
|
+
// below resolve them at call time.
|
|
239
|
+
utils.each(nested, function (a) {
|
|
240
|
+
var c = a.compiled;
|
|
241
|
+
utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
|
|
242
|
+
out += c;
|
|
243
|
+
});
|
|
178
244
|
|
|
179
|
-
utils.each(
|
|
245
|
+
utils.each(macros, function (arg) {
|
|
180
246
|
var c = arg.compiled;
|
|
181
|
-
utils.each(replacements, function (re) {
|
|
182
|
-
|
|
183
|
-
});
|
|
247
|
+
utils.each(replacements, function (re) { c = c.replace(re.ex, re.re); });
|
|
248
|
+
utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
|
|
184
249
|
out += c;
|
|
185
250
|
});
|
|
186
251
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig-twig",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Twig-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=12"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@rhinostone/swig-core": "2.4.
|
|
25
|
+
"@rhinostone/swig-core": "2.4.2"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|