@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.
- package/README.md +56 -0
- package/lib/async/pre-walker.js +267 -0
- package/lib/filters.js +1369 -0
- package/lib/index.js +344 -0
- package/lib/lexer.js +305 -0
- package/lib/parser.js +763 -0
- package/lib/tags/autoescape.js +75 -0
- package/lib/tags/block.js +82 -0
- package/lib/tags/elif.js +33 -0
- package/lib/tags/else.js +27 -0
- package/lib/tags/extends.js +77 -0
- package/lib/tags/filter.js +205 -0
- package/lib/tags/for.js +154 -0
- package/lib/tags/from.js +305 -0
- package/lib/tags/if.js +82 -0
- package/lib/tags/import.js +250 -0
- package/lib/tags/include.js +154 -0
- package/lib/tags/index.js +32 -0
- package/lib/tags/macro.js +174 -0
- package/lib/tags/raw.js +51 -0
- package/lib/tags/set.js +164 -0
- package/lib/tags/with.js +121 -0
- package/lib/tests/index.js +213 -0
- package/lib/tokentypes.js +89 -0
- package/package.json +31 -0
package/lib/tags/from.js
ADDED
|
@@ -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
|
+
};
|