@rhinostone/swig-jinja2 2.5.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.
@@ -0,0 +1,305 @@
1
+ /*!
2
+ * Jinja2 `{% from "file" import a, b as c %}` tag.
3
+ *
4
+ * Selective macro import syntax — binds a named subset of an imported
5
+ * template's macros into the current context, optionally renaming each
6
+ * via `as <alias>`:
7
+ *
8
+ * {% from "forms.html" import input %}
9
+ * {% from "forms.html" import input, textarea %}
10
+ * {% from "forms.html" import input as field, textarea %}
11
+ *
12
+ * Differs from `{% import %}` in that each entry lands at top-level
13
+ * `_ctx.<alias-or-name>` rather than inside a shared namespace object.
14
+ * Macros not listed in the `import` clause are not surfaced.
15
+ *
16
+ * Stays on `IRLegacyJS` per the same flavor-invariant test that keeps
17
+ * `{% import %}` on IRLegacyJS — the regex-surgery rewrite of a compiled
18
+ * macro body (`_ctx\.<origName>` → `_ctx\.<aliasName>`) is swig-specific
19
+ * coupling on the exact JS source shape the Macro IR emits. When the
20
+ * macro-name → alias rewrite moves into the IR emitter itself, this tag
21
+ * collapses to a dedicated `IRFromImport` node.
22
+ *
23
+ * Dynamic paths (`{% from dynPath import foo %}`) are rejected at parse
24
+ * time — same rationale as `{% import %}` + `{% extends %}`. Context
25
+ * modifiers (`with` / `without context`) are not honored — imported
26
+ * macros always see the caller context (swig's macro model; a documented
27
+ * divergence from Jinja2's without-context import default).
28
+ *
29
+ * Every requested macro name and every alias is a bare identifier —
30
+ * dotted paths are rejected per the lexer-folded-path bail pattern, and
31
+ * CVE-2023-25345 prototype-chain names are rejected on both slots before
32
+ * the assignment lands on `_ctx`.
33
+ */
34
+
35
+ var utils = require('@rhinostone/swig-core/lib/utils');
36
+ var ir = require('@rhinostone/swig-core/lib/ir');
37
+ var backend = require('@rhinostone/swig-core/lib/backend');
38
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
39
+
40
+ var lexer = require('../lexer');
41
+
42
+ exports.ends = false;
43
+ exports.block = true;
44
+
45
+ /**
46
+ * Parse the `{% from %}` tag body. Extracts the STRING literal path,
47
+ * the `import` keyword, and the comma-separated entry list (each entry
48
+ * is `<name>` or `<name> as <alias>`); validates every name and alias
49
+ * against the bare-identifier rule and the CVE-2023-25345
50
+ * `_dangerousProps` blocklist.
51
+ *
52
+ * Walks the imported template's token list (via `swig.parseFile`) and
53
+ * for each requested macro, invokes its `compile` to get the IRMacro
54
+ * node and renders that node to JS through `backend.compile`. A
55
+ * macro requested by name but not found in the imported template
56
+ * raises a filename-aware throw. The imported file's own nested
57
+ * `{% import %}` / `{% from %}` tokens are carried through (flagged
58
+ * `isImport`, with `boundNames` + a synthesized private `slot`) so the
59
+ * requested macros can reference them at call time. The resulting list
60
+ * (nested entries + `[{compiled, origName, aliasName}, ...]`) is stashed
61
+ * on `token.args` for the compile step to rewrite.
62
+ *
63
+ * @param {string} str Tag body.
64
+ * @param {number} line Source line of the opening `{%`.
65
+ * @param {object} parser The Jinja2 parser module (unused — body is
66
+ * lexed locally).
67
+ * @param {object} types Jinja2 lexer token-type enum.
68
+ * @param {Array} stack Open-tag stack (unused — from has no body).
69
+ * @param {object} opts Per-call options. Honors `opts.filename` for
70
+ * `resolveFrom` + filename-aware throws.
71
+ * @param {object} swig Swig instance. Must expose `parseFile`.
72
+ * @param {object} token In-progress TagToken. `token.args` gets the
73
+ * `[{compiled, origName, aliasName}, ...]` list.
74
+ * @return {boolean} Always `true` on success. Throws otherwise.
75
+ */
76
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
77
+ var tokens = lexer.read(utils.strip(str));
78
+ var pos = 0;
79
+
80
+ function peek() {
81
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
82
+ return pos < tokens.length ? tokens[pos] : null;
83
+ }
84
+ function consume() {
85
+ var t = peek();
86
+ if (t) { pos += 1; }
87
+ return t;
88
+ }
89
+ function guardName(name, role) {
90
+ if (name.indexOf('.') !== -1) {
91
+ utils.throwError(role + ' "' + name + '" must be a bare identifier in "from" tag', line, opts.filename);
92
+ }
93
+ if (_dangerousProps.indexOf(name) !== -1) {
94
+ utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
95
+ }
96
+ }
97
+
98
+ var pathTok = consume();
99
+ if (!pathTok) {
100
+ utils.throwError('Expected template path in "from" tag', line, opts.filename);
101
+ }
102
+ if (pathTok.type !== types.STRING) {
103
+ utils.throwError('Dynamic "from" is not supported — path must be a string literal', line, opts.filename);
104
+ }
105
+
106
+ var importTok = consume();
107
+ if (!importTok || importTok.type !== types.VAR || importTok.match !== 'import') {
108
+ utils.throwError('Expected "import" keyword after path in "from" tag', line, opts.filename);
109
+ }
110
+
111
+ // Collect requested macro entries — each is `<name>` or
112
+ // `<name> as <alias>`. At least one entry is required.
113
+ var entries = [];
114
+ while (true) {
115
+ var nameTok = consume();
116
+ if (!nameTok || nameTok.type !== types.VAR) {
117
+ utils.throwError('Expected macro name in "from" tag', line, opts.filename);
118
+ }
119
+ guardName(nameTok.match, 'Macro name');
120
+
121
+ var origName = nameTok.match;
122
+ var aliasName = origName;
123
+
124
+ var next = peek();
125
+ if (next && next.type === types.VAR && next.match === 'as') {
126
+ consume();
127
+ var aliasTok = consume();
128
+ if (!aliasTok || aliasTok.type !== types.VAR) {
129
+ utils.throwError('Expected alias after "as" in "from" tag', line, opts.filename);
130
+ }
131
+ guardName(aliasTok.match, 'Import alias');
132
+ aliasName = aliasTok.match;
133
+ next = peek();
134
+ }
135
+
136
+ entries.push({ origName: origName, aliasName: aliasName });
137
+
138
+ if (!next) { break; }
139
+ if (next.type !== types.COMMA) {
140
+ utils.throwError('Unexpected token "' + next.match + '" in "from" tag import list', line, opts.filename);
141
+ }
142
+ consume();
143
+ }
144
+
145
+ var path = pathTok.match.replace(/^['"]|['"]$/g, '');
146
+
147
+ if (opts && opts.codegenMode === 'async') {
148
+ // Async mode skips parse-time parseFile + macro pre-render. compile()
149
+ // emits IRFromImportDeferred; runtime resolves the template via
150
+ // _swig.getTemplate and binds each entry on _ctx.
151
+ token.args = [{ path: path, entries: entries }];
152
+ return true;
153
+ }
154
+
155
+ if (!swig || typeof swig.parseFile !== 'function') {
156
+ utils.throwError('"from" tag requires an engine context with a loader', line, opts.filename);
157
+ }
158
+
159
+ var parseOpts = { resolveFrom: opts.filename };
160
+ var compileOpts = utils.extend({}, opts, parseOpts);
161
+ var parsed = swig.parseFile(path, parseOpts);
162
+
163
+ // Index the imported template's macros by name so we can look up
164
+ // each requested entry once. Raises a filename-aware throw if an
165
+ // entry names a macro that doesn't exist in the imported template.
166
+ //
167
+ // The imported file may have its own `{% import %}` / `{% from %}`.
168
+ // Unlike `{% import %}`, `{% from %}` binds bare names with no namespace
169
+ // alias to hang those nested imports under, so they re-home under a
170
+ // synthesized private slot keyed off the path (compile() applies the
171
+ // rewrite). This keeps the inner names out of the parent's bare scope —
172
+ // bare `{{ name }}` in the parent stays undefined, matching Jinja2's rule
173
+ // that imports are local to the importing template.
174
+ var privateSlot = '__nsfrom_' + path.replace(/[^a-zA-Z0-9]/g, '_');
175
+ var nested = [];
176
+ var macroIndex = {};
177
+ utils.each(parsed.tokens, function (tk) {
178
+ if (!tk || typeof tk.compile !== 'function') {
179
+ return;
180
+ }
181
+ if (tk.name === 'import' || tk.name === 'from') {
182
+ var bn = (tk.name === 'import')
183
+ ? [tk.args[tk.args.length - 1]]
184
+ : utils.map(tk.args, function (a) { return a.aliasName; });
185
+ nested.push({
186
+ compiled: tk.compile(null, tk.args.slice(), tk.content, [], compileOpts) + '\n',
187
+ isImport: true,
188
+ boundNames: bn,
189
+ slot: privateSlot
190
+ });
191
+ return;
192
+ }
193
+ if (tk.name !== 'macro') {
194
+ return;
195
+ }
196
+ macroIndex[tk.args[0]] = tk;
197
+ });
198
+
199
+ var resolved = [];
200
+ for (var i = 0; i < entries.length; i += 1) {
201
+ var entry = entries[i];
202
+ var macroTok = macroIndex[entry.origName];
203
+ if (!macroTok) {
204
+ utils.throwError('Macro "' + entry.origName + '" not found in "' + path + '"', line, opts.filename);
205
+ }
206
+ var macroIR = macroTok.compile(backend.compile, macroTok.args, macroTok.content, [], compileOpts);
207
+ var compiled = backend.compile([macroIR], [], compileOpts) + '\n';
208
+ resolved.push({
209
+ compiled: compiled,
210
+ origName: entry.origName,
211
+ aliasName: entry.aliasName
212
+ });
213
+ }
214
+
215
+ token.args = nested.concat(resolved);
216
+ return true;
217
+ };
218
+
219
+ /**
220
+ * Emit the selective-import rewrite. For each imported entry, rewrites
221
+ * every occurrence of `_ctx.<origName>` in the compiled macro body to
222
+ * `_ctx.<aliasName>`, including sibling-macro references to any other
223
+ * imported name. The `(?!<allOrigNames>)` negative lookahead matches
224
+ * `{% import %}` behaviour — guards against the edge case where a
225
+ * rewritten `_ctx.<aliasName>` fragment would itself be re-matched by a
226
+ * later replacement whose `origName` happens to be a prefix of the alias
227
+ * tail.
228
+ *
229
+ * Sibling references to a macro that was NOT in the import list are
230
+ * left as-is — those expand to `_ctx.<origName>` lookups at render time
231
+ * and will either read a user-provided context value or evaluate to
232
+ * `undefined`, matching Jinja2's "unimported macros are not available"
233
+ * semantic.
234
+ *
235
+ * The imported file's own nested imports (flagged `isImport`) are emitted
236
+ * first, re-homed under a private slot (`_ctx.<boundName>` ->
237
+ * `_ctx.<slot>.<boundName>`) so they resolve for the imported macros at
238
+ * call time without leaking into the parent's bare scope.
239
+ *
240
+ * @return {string} JS source that assigns every imported macro into
241
+ * `_ctx.<aliasName>`. Backend lifts it into
242
+ * `IRLegacyJS`.
243
+ */
244
+ exports.compile = function (compiler, args, content, parents, options) {
245
+ // Async-codegen branch. Parse stashed a single bundle
246
+ // `[{path, entries: [{origName, aliasName}, ...]}]` in async mode (no
247
+ // macro pre-render); emit IRFromImportDeferred so the backend's
248
+ // `_swig.getTemplate` + per-entry `_ctx.<bind>` assignment happens at
249
+ // runtime.
250
+ if (options && options.codegenMode === 'async') {
251
+ var bundle = args[0];
252
+ var imports = utils.map(bundle.entries, function (e) {
253
+ return {
254
+ name: e.origName,
255
+ alias: e.aliasName === e.origName ? null : e.aliasName
256
+ };
257
+ });
258
+ return ir.fromImportDeferred(
259
+ ir.literal('string', bundle.path),
260
+ imports,
261
+ options.filename || ''
262
+ );
263
+ }
264
+ var macros = [];
265
+ var nested = [];
266
+ utils.each(args, function (a) { (a.isImport ? nested : macros).push(a); });
267
+ var allOrigNames = utils.map(macros, function (arg) { return arg.origName; }).join('|');
268
+ var replacements = utils.map(macros, function (arg) {
269
+ return {
270
+ ex: new RegExp('_ctx\\.' + arg.origName + '(\\W)(?!' + allOrigNames + ')', 'g'),
271
+ re: '_ctx.' + arg.aliasName + '$1'
272
+ };
273
+ });
274
+ // The imported file's own nested imports re-home under a private slot so
275
+ // they never leak into the parent's bare scope: `_ctx.<boundName>` ->
276
+ // `_ctx.<slot>.<boundName>`, applied to the nested setup JS and to every
277
+ // imported macro body that references a bound name.
278
+ var innerReplacements = [];
279
+ utils.each(nested, function (a) {
280
+ utils.each(a.boundNames, function (nm) {
281
+ innerReplacements.push({
282
+ ex: new RegExp('_ctx\\.' + nm + '(\\W)', 'g'),
283
+ re: '_ctx.' + a.slot + '.' + nm + '$1'
284
+ });
285
+ });
286
+ });
287
+
288
+ var out = ' var _output = "";\n';
289
+ // Nested imports first (under the private slot), so the imported macros
290
+ // below resolve them at call time.
291
+ utils.each(nested, function (a) {
292
+ out += '_ctx.' + a.slot + ' = _ctx.' + a.slot + ' || {};\n';
293
+ var c = a.compiled;
294
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
295
+ out += c;
296
+ });
297
+ utils.each(macros, function (arg) {
298
+ var c = arg.compiled;
299
+ utils.each(replacements, function (re) { c = c.replace(re.ex, re.re); });
300
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
301
+ out += c;
302
+ });
303
+
304
+ return out;
305
+ };
package/lib/tags/if.js ADDED
@@ -0,0 +1,82 @@
1
+ /*!
2
+ * Jinja2 `{% if %}` / `{% elif %}` / `{% else %}` tag.
3
+ *
4
+ * Conditional: `{% if <expr> %}…{% elif <expr> %}…{% else %}…{% endif %}`.
5
+ * The test expression is parsed via `parser.parseExpr` and attached to
6
+ * `token.irExpr`. The body content is captured via the parser's open-tag
7
+ * stack (this tag sets `ends: true`).
8
+ *
9
+ * `{% elif %}` and `{% else %}` lex as their own tags (`ends: false`,
10
+ * registered in tags/index.js) and so appear as marker tokens inside this
11
+ * tag's `content`. The compile path walks `content`, splitting at those
12
+ * markers into one IRIfBranch per segment — the backend's `If` walker then
13
+ * emits the `if (…) { … } else if (…) { … } else { … }` envelope.
14
+ */
15
+
16
+ var ir = require('@rhinostone/swig-core/lib/ir');
17
+ var utils = require('@rhinostone/swig-core/lib/utils');
18
+
19
+ var lexer = require('../lexer');
20
+
21
+ exports.ends = true;
22
+ exports.block = false;
23
+
24
+ /**
25
+ * Parse the `{% if %}` test expression onto `token.irExpr`.
26
+ *
27
+ * @param {string} str Tag body (tag name stripped).
28
+ * @param {number} line Source line of the opening `{%`.
29
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`).
30
+ * @param {object} types Jinja2 lexer token-type enum.
31
+ * @param {Array} stack Open-tag stack (managed by parser.js).
32
+ * @param {object} opts Per-call options (honors `opts.filename`).
33
+ * @param {object} swig Swig instance (unused).
34
+ * @param {object} token In-progress TagToken. `token.irExpr` is set.
35
+ * @return {boolean} Always `true` on success. Throws otherwise.
36
+ */
37
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
38
+ var tokens = lexer.read(utils.strip(str));
39
+ if (!tokens.length) {
40
+ utils.throwError('Expected conditional expression in "if" tag', line, opts.filename);
41
+ }
42
+ token.irExpr = parser.parseExpr(tokens);
43
+ return true;
44
+ };
45
+
46
+ /**
47
+ * Split `content` at `elif` / `else` marker tokens and emit a multi-branch
48
+ * IRIf node.
49
+ *
50
+ * @return {object} IRIf node.
51
+ */
52
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
53
+ var branches = [],
54
+ currentTest = token.irExpr,
55
+ currentBody = [],
56
+ sawElse = false,
57
+ filename = options && options.filename;
58
+
59
+ function flush() {
60
+ branches.push(ir.ifBranch(currentTest, [ir.legacyJS(compiler(currentBody, parents, options, blockName))]));
61
+ }
62
+
63
+ utils.each(content, function (child) {
64
+ if (child && child.name === 'elif') {
65
+ if (sawElse) { utils.throwError('"elif" after "else" in "if" tag', null, filename); }
66
+ flush();
67
+ currentTest = child.irExpr;
68
+ currentBody = [];
69
+ } else if (child && child.name === 'else') {
70
+ if (sawElse) { utils.throwError('Multiple "else" branches in "if" tag', null, filename); }
71
+ flush();
72
+ currentTest = null;
73
+ currentBody = [];
74
+ sawElse = true;
75
+ } else {
76
+ currentBody.push(child);
77
+ }
78
+ });
79
+ flush();
80
+
81
+ return ir.ifStmt(branches);
82
+ };
@@ -0,0 +1,250 @@
1
+ /*!
2
+ * Jinja2 `{% import %}` tag.
3
+ *
4
+ * Jinja2 import syntax:
5
+ *
6
+ * {% import "partial.html" as form %}
7
+ *
8
+ * Loads a template and imports every `{% macro %}` it defines into a
9
+ * namespace bound to `_ctx.<alias>`. Each imported macro is rendered
10
+ * to JS via `backend.compile`; the compile step performs regex surgery
11
+ * on that rendered JS to rewrite `_ctx.<macroName>` →
12
+ * `_ctx.<alias>.<macroName>`, including sibling-macro references
13
+ * (the `(?!<allMacros>)` negative lookahead).
14
+ *
15
+ * The regex surgery is swig-specific coupling on the exact JS source
16
+ * shape a Macro IR emits — it fails the flavor-invariant test and stays
17
+ * on `IRLegacyJS`. The tag returns a JS source string from `compile`; the
18
+ * backend lifts it into `IRLegacyJS` at emit time. When the macro-name →
19
+ * namespace rewrite moves into the emitter itself, the tag collapses to a
20
+ * dedicated `IRImport` node.
21
+ *
22
+ * The plural `{% from "file" import a, b %}` shorthand is the sibling
23
+ * `from` tag. Dynamic paths (`{% import dyn as ns %}`) are deferred.
24
+ * Context modifiers (`with` / `without context`) are not honored here —
25
+ * imported macros always see the caller context (swig's macro model;
26
+ * a documented divergence from Jinja2's without-context import default).
27
+ *
28
+ * The alias is a bare identifier — dotted paths are rejected at parse
29
+ * time, and CVE-2023-25345 prototype-chain names are rejected before
30
+ * the namespace assignment. Single-name binding slots reject any `.`
31
+ * in the match before the `_dangerousProps` check.
32
+ */
33
+
34
+ var utils = require('@rhinostone/swig-core/lib/utils');
35
+ var ir = require('@rhinostone/swig-core/lib/ir');
36
+ var backend = require('@rhinostone/swig-core/lib/backend');
37
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
38
+
39
+ var lexer = require('../lexer');
40
+
41
+ exports.ends = false;
42
+ exports.block = true;
43
+
44
+ /**
45
+ * Parse the `{% import %}` tag body. Extracts the STRING literal path,
46
+ * the `as` keyword, and the bare-identifier alias; validates the alias
47
+ * against the bare-identifier rule and the CVE-2023-25345
48
+ * `_dangerousProps` blocklist.
49
+ *
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.
60
+ *
61
+ * @param {string} str Tag body.
62
+ * @param {number} line Source line of the opening `{%`.
63
+ * @param {object} parser The Jinja2 parser module (unused — the body is
64
+ * lexed locally).
65
+ * @param {object} types Jinja2 lexer token-type enum.
66
+ * @param {Array} stack Open-tag stack (unused — import has no body).
67
+ * @param {object} opts Per-call options. Honors `opts.filename` for
68
+ * `resolveFrom` + filename-aware throws.
69
+ * @param {object} swig Swig instance. Must expose `parseFile`.
70
+ * @param {object} token In-progress TagToken. `token.args` gets the
71
+ * `[{compiled, name}, ..., alias]` list.
72
+ * @return {boolean} Always `true` on success. Throws otherwise.
73
+ */
74
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
75
+ var tokens = lexer.read(utils.strip(str));
76
+ var pos = 0;
77
+
78
+ function peek() {
79
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
80
+ return pos < tokens.length ? tokens[pos] : null;
81
+ }
82
+ function consume() {
83
+ var t = peek();
84
+ if (t) { pos += 1; }
85
+ return t;
86
+ }
87
+
88
+ var pathTok = consume();
89
+ if (!pathTok) {
90
+ utils.throwError('Expected template path in "import" tag', line, opts.filename);
91
+ }
92
+ if (pathTok.type !== types.STRING) {
93
+ utils.throwError('Dynamic "import" is not supported — path must be a string literal', line, opts.filename);
94
+ }
95
+
96
+ var asTok = consume();
97
+ if (!asTok || asTok.type !== types.VAR || asTok.match !== 'as') {
98
+ utils.throwError('Expected "as" keyword after path in "import" tag', line, opts.filename);
99
+ }
100
+
101
+ var aliasTok = consume();
102
+ if (!aliasTok || aliasTok.type !== types.VAR) {
103
+ utils.throwError('Expected namespace alias after "as" in "import" tag', line, opts.filename);
104
+ }
105
+ if (aliasTok.match.indexOf('.') !== -1) {
106
+ utils.throwError('Import alias "' + aliasTok.match + '" must be a bare identifier in "import" tag', line, opts.filename);
107
+ }
108
+ if (_dangerousProps.indexOf(aliasTok.match) !== -1) {
109
+ utils.throwError('Unsafe import alias "' + aliasTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
110
+ }
111
+
112
+ if (peek()) {
113
+ utils.throwError('Unexpected token "' + peek().match + '" after alias in "import" tag', line, opts.filename);
114
+ }
115
+
116
+ var path = pathTok.match.replace(/^['"]|['"]$/g, '');
117
+
118
+ if (opts && opts.codegenMode === 'async') {
119
+ // Async mode skips the parse-time parseFile + macro pre-render.
120
+ // compile() emits IRImportDeferred; runtime resolves the template via
121
+ // _swig.getTemplate and binds .exports under the alias.
122
+ token.args = [path, aliasTok.match];
123
+ return true;
124
+ }
125
+
126
+ if (!swig || typeof swig.parseFile !== 'function') {
127
+ utils.throwError('"import" tag requires an engine context with a loader', line, opts.filename);
128
+ }
129
+
130
+ var parseOpts = { resolveFrom: opts.filename };
131
+ var compileOpts = utils.extend({}, opts, parseOpts);
132
+ var parsed = swig.parseFile(path, parseOpts);
133
+ var macros = [];
134
+
135
+ utils.each(parsed.tokens, function (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
+ // Imports stay local to their defining template: compile() re-homes
146
+ // every bound name under THIS import's alias (`_ctx.<alias>.<boundName>`),
147
+ // so none is visible bare in the parent scope. `tk.args` is already
148
+ // parsed; slice() avoids the pop() in compile() mutating the cached token.
149
+ if (tk.name === 'import' || tk.name === 'from') {
150
+ // Names the nested import binds into _ctx: `{% import %}` binds one
151
+ // namespace alias (the tail of args); `{% from %}` binds one per
152
+ // requested entry (its alias name).
153
+ var boundNames = (tk.name === 'import')
154
+ ? [tk.args[tk.args.length - 1]]
155
+ : utils.map(tk.args, function (a) { return a.aliasName; });
156
+ macros.push({
157
+ compiled: tk.compile(null, tk.args.slice(), tk.content, [], compileOpts) + '\n',
158
+ isImport: true,
159
+ boundNames: boundNames
160
+ });
161
+ return;
162
+ }
163
+ if (tk.name !== 'macro') {
164
+ return;
165
+ }
166
+ var macroName = tk.args[0];
167
+ var macroIR = tk.compile(backend.compile, tk.args, tk.content, [], compileOpts);
168
+ var compiled = backend.compile([macroIR], [], compileOpts) + '\n';
169
+ macros.push({ compiled: compiled, name: macroName });
170
+ });
171
+
172
+ token.args = macros.concat([aliasTok.match]);
173
+ return true;
174
+ };
175
+
176
+ /**
177
+ * Emit the namespace-prefix rewrite. Pops the alias off the tail of
178
+ * `args` and splits the rest into macros and nested imports (flagged
179
+ * `isImport` by parse()). Builds a `_ctx.<name>(\\W)(?!<allMacros>)`
180
+ * regex for each imported macro and rewrites every occurrence in each
181
+ * macro's compiled JS to `_ctx.<alias>.<name>`. Nested imports are
182
+ * emitted first, re-homed under the alias (`_ctx.<boundName>` ->
183
+ * `_ctx.<alias>.<boundName>`) so a file's own imports stay local and
184
+ * never leak bare into the parent scope; the same re-homing is applied
185
+ * to macro bodies that reference any bound name. Concatenates after the
186
+ * `_ctx.<alias> = {};` namespace-init line and returns a JS source string
187
+ * (the backend lifts it into `IRLegacyJS`).
188
+ *
189
+ * @return {string} JS source that initialises `_ctx.<alias>`, re-homes
190
+ * any nested imports under it, and assigns every
191
+ * imported macro into it.
192
+ */
193
+ exports.compile = function (compiler, args, content, parents, options) {
194
+ // Async-codegen branch. Parse stashed `[path, alias]` in async mode (no
195
+ // macro pre-render); emit IRImportDeferred so the backend's
196
+ // `_swig.getTemplate` + `.exports` bind happens at runtime.
197
+ if (options && options.codegenMode === 'async') {
198
+ return ir.importDeferred(
199
+ ir.literal('string', args[0]),
200
+ args[args.length - 1],
201
+ options.filename || ''
202
+ );
203
+ }
204
+ var ctx = args.pop();
205
+ var macros = [];
206
+ var nested = [];
207
+ utils.each(args, function (a) {
208
+ (a.isImport ? nested : macros).push(a);
209
+ });
210
+ var allMacros = utils.map(macros, function (arg) { return arg.name; }).join('|');
211
+ var out = '_ctx.' + ctx + ' = {};\n var _output = "";\n';
212
+ var replacements = utils.map(macros, function (arg) {
213
+ return {
214
+ ex: new RegExp('_ctx\\.' + arg.name + '(\\W)(?!' + allMacros + ')', 'g'),
215
+ re: '_ctx.' + ctx + '.' + arg.name + '$1'
216
+ };
217
+ });
218
+ // Re-home every name a nested import binds under THIS alias so it stays
219
+ // local to the imported file (Jinja2 scoping) and never leaks bare into the
220
+ // parent: `_ctx.<boundName>` -> `_ctx.<alias>.<boundName>`, applied to both
221
+ // the nested setup JS and every macro body below that references it.
222
+ // Compounds across import depth (a 3-level chain re-homes at each layer
223
+ // with no special-casing).
224
+ var innerReplacements = [];
225
+ utils.each(nested, function (a) {
226
+ utils.each(a.boundNames, function (nm) {
227
+ innerReplacements.push({
228
+ ex: new RegExp('_ctx\\.' + nm + '(\\W)', 'g'),
229
+ re: '_ctx.' + ctx + '.' + nm + '$1'
230
+ });
231
+ });
232
+ });
233
+
234
+ // Nested imports first, re-homed under the alias, so the macros defined
235
+ // below resolve them at call time.
236
+ utils.each(nested, function (a) {
237
+ var c = a.compiled;
238
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
239
+ out += c;
240
+ });
241
+
242
+ utils.each(macros, function (arg) {
243
+ var c = arg.compiled;
244
+ utils.each(replacements, function (re) { c = c.replace(re.ex, re.re); });
245
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
246
+ out += c;
247
+ });
248
+
249
+ return out;
250
+ };