@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,75 @@
1
+ /*!
2
+ * Jinja2 `{% autoescape %}` tag.
3
+ *
4
+ * {% autoescape true %}{{ html }}{% endautoescape %} (escape the region)
5
+ * {% autoescape false %}{{ html }}{% endautoescape %} (don't escape)
6
+ *
7
+ * Controls auto-escaping of variable output within its body. The escape
8
+ * decision is baked at parse time: the parser maintains an escape-value
9
+ * stack that this tag's open pushes onto and `{% endautoescape %}` pops,
10
+ * so each `{{ … }}` inside the region gets (or omits) the `e` filter tail
11
+ * accordingly. The emitted IRAutoescape node is therefore inert at the
12
+ * backend — it exists so the IR tree reflects the region.
13
+ *
14
+ * Only the literal keywords `true` and `false` are accepted. A runtime
15
+ * expression (`{% autoescape some_var %}`) is rejected at parse time —
16
+ * the parse-time escape model can only resolve a literal strategy.
17
+ */
18
+
19
+ var ir = require('@rhinostone/swig-core/lib/ir');
20
+ var utils = require('@rhinostone/swig-core/lib/utils');
21
+
22
+ var lexer = require('../lexer');
23
+
24
+ exports.ends = true;
25
+ exports.block = false;
26
+
27
+ /**
28
+ * Parse the `{% autoescape %}` tag body. Requires a single BOOL token
29
+ * (`true` or `false`) and stashes the resolved boolean on
30
+ * `token.escapeValue` — the parser reads it to push onto the escape-value
31
+ * stack for the region, and compile reads it to build the IRAutoescape
32
+ * node.
33
+ *
34
+ * @param {string} str Tag body.
35
+ * @param {number} line Source line of the opening `{%`.
36
+ * @param {object} parser The Jinja2 parser module (unused).
37
+ * @param {object} types Jinja2 lexer token-type enum.
38
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
39
+ * @param {object} opts Per-call options (honors `opts.filename`).
40
+ * @param {object} swig Swig instance (unused).
41
+ * @param {object} token In-progress TagToken. Gets `token.escapeValue`.
42
+ * @return {boolean} Always `true` on success. Throws otherwise.
43
+ */
44
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
45
+ var tokens = lexer.read(utils.strip(str));
46
+ var pos = 0;
47
+
48
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
49
+ var tok = pos < tokens.length ? tokens[pos] : null;
50
+ if (!tok || tok.type !== types.BOOL) {
51
+ utils.throwError('Expected "true" or "false" in "autoescape" tag', line, opts.filename);
52
+ }
53
+ pos += 1;
54
+
55
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
56
+ if (pos < tokens.length) {
57
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" in "autoescape" tag', line, opts.filename);
58
+ }
59
+
60
+ token.escapeValue = (tok.match === 'true');
61
+ return true;
62
+ };
63
+
64
+ /**
65
+ * Emit an IRAutoescape node wrapping the recursively-compiled body. The
66
+ * body's `{{ … }}` outputs already carry (or omit) their `e` tails from
67
+ * parse time, so the backend emits the body verbatim; the strategy is
68
+ * carried for IR fidelity.
69
+ *
70
+ * @return {object} IRAutoescape node.
71
+ */
72
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
73
+ var bodyJS = compiler(content, parents, options, blockName);
74
+ return ir.autoescape(token.escapeValue, [ir.legacyJS(bodyJS)]);
75
+ };
@@ -0,0 +1,82 @@
1
+ /*!
2
+ * Jinja2 `{% block %}` tag.
3
+ *
4
+ * Named override point for template inheritance:
5
+ *
6
+ * {% block <name> %}…{% endblock %}
7
+ *
8
+ * The block name must be a bare identifier (dotted paths rejected) and
9
+ * passes the CVE-2023-25345 `_dangerousProps` guard. The parser captures
10
+ * the block in `template.blocks[name]` when it appears at the top level
11
+ * (the block-keying branch is triggered by `token.block && !stack.length`).
12
+ *
13
+ * Compile emits an `IRBlock` node with the body wrapped in IRLegacyJS. The
14
+ * backend's `Block` branch emits the body verbatim — block-override
15
+ * resolution happens at parse time via `engine.remapBlocks` /
16
+ * `importNonBlocks`, which substitutes the child's block content into the
17
+ * parent's token tree before backend emission.
18
+ */
19
+
20
+ var ir = require('@rhinostone/swig-core/lib/ir');
21
+ var utils = require('@rhinostone/swig-core/lib/utils');
22
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
23
+
24
+ var lexer = require('../lexer');
25
+
26
+ exports.ends = true;
27
+ exports.block = true;
28
+
29
+ /**
30
+ * Parse the `{% block %}` tag body. Extracts the bare-identifier name,
31
+ * validates it, and stashes it on `token.args` so the parser's top-level
32
+ * block-keying branch can pick it up via `token.args.join('')`.
33
+ *
34
+ * @param {string} str Tag body.
35
+ * @param {number} line Source line of the opening `{%`.
36
+ * @param {object} parser The Jinja2 parser module (unused — names are
37
+ * plain identifiers).
38
+ * @param {object} types Jinja2 lexer token-type enum.
39
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
40
+ * @param {object} opts Per-call options (honors `opts.filename`).
41
+ * @param {object} swig Swig instance (unused).
42
+ * @param {object} token In-progress TagToken. `token.args` gets the
43
+ * block name as its single element.
44
+ * @return {boolean} Always `true` on success. Throws otherwise.
45
+ */
46
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
47
+ var tokens = lexer.read(utils.strip(str));
48
+ var pos = 0;
49
+
50
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
51
+ var nameTok = pos < tokens.length ? tokens[pos] : null;
52
+ if (!nameTok || nameTok.type !== types.VAR) {
53
+ utils.throwError('Expected block name in "block" tag', line, opts.filename);
54
+ }
55
+ if (nameTok.match.indexOf('.') !== -1) {
56
+ utils.throwError('Block name "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
57
+ }
58
+ if (_dangerousProps.indexOf(nameTok.match) !== -1) {
59
+ utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
60
+ }
61
+
62
+ pos += 1;
63
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
64
+ if (pos < tokens.length) {
65
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" after block name', line, opts.filename);
66
+ }
67
+
68
+ token.args = [nameTok.match];
69
+ return true;
70
+ };
71
+
72
+ /**
73
+ * Emit an IRBlock node. Body is the recursively-compiled content wrapped in
74
+ * IRLegacyJS. Mirrors the native `block` compile shape so the backend's
75
+ * `Block` branch treats both frontends the same.
76
+ *
77
+ * @return {object} IRBlock node.
78
+ */
79
+ exports.compile = function (compiler, args, content, parents, options) {
80
+ var name = args.join('');
81
+ return ir.block(name, [ir.legacyJS(compiler(content, parents, options, name))]);
82
+ };
@@ -0,0 +1,33 @@
1
+ /*!
2
+ * Jinja2 `{% elif %}` branch marker.
3
+ *
4
+ * Valid only inside an `{% if %}` body. Parses its test expression onto
5
+ * `token.irExpr`; the enclosing `if` tag's compile consumes the marker and
6
+ * splits its branches at it. The marker never reaches the backend on its
7
+ * own — `compile` only fires if an `elif` somehow escapes an `if`, which
8
+ * the parse-time stack check already prevents.
9
+ */
10
+
11
+ var utils = require('@rhinostone/swig-core/lib/utils');
12
+
13
+ var lexer = require('../lexer');
14
+
15
+ exports.ends = false;
16
+ exports.block = false;
17
+
18
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
19
+ var top = stack[stack.length - 1];
20
+ if (!top || top.name !== 'if') {
21
+ utils.throwError('"elif" is only valid inside an "if" tag', line, opts.filename);
22
+ }
23
+ var tokens = lexer.read(utils.strip(str));
24
+ if (!tokens.length) {
25
+ utils.throwError('Expected conditional expression in "elif" tag', line, opts.filename);
26
+ }
27
+ token.irExpr = parser.parseExpr(tokens);
28
+ return true;
29
+ };
30
+
31
+ exports.compile = function () {
32
+ throw new Error('"elif" used outside an "if" tag.');
33
+ };
@@ -0,0 +1,27 @@
1
+ /*!
2
+ * Jinja2 `{% else %}` branch marker.
3
+ *
4
+ * Valid inside an `{% if %}` body (the final fallback branch) or a
5
+ * `{% for %}` body (the empty-iterable branch). It carries no expression;
6
+ * the enclosing tag's compile consumes the marker and splits its body at
7
+ * it. The marker never reaches the backend on its own — `compile` only
8
+ * fires if an `else` escapes its enclosing tag, which the parse-time stack
9
+ * check already prevents.
10
+ */
11
+
12
+ var utils = require('@rhinostone/swig-core/lib/utils');
13
+
14
+ exports.ends = false;
15
+ exports.block = false;
16
+
17
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
18
+ var top = stack[stack.length - 1];
19
+ if (!top || (top.name !== 'if' && top.name !== 'for')) {
20
+ utils.throwError('"else" is only valid inside an "if" or "for" tag', line, opts.filename);
21
+ }
22
+ return true;
23
+ };
24
+
25
+ exports.compile = function () {
26
+ throw new Error('"else" used outside an "if" or "for" tag.');
27
+ };
@@ -0,0 +1,77 @@
1
+ /*!
2
+ * Jinja2 `{% extends %}` tag.
3
+ *
4
+ * Declares a parent template for inheritance:
5
+ *
6
+ * {% extends "layout.html" %}
7
+ *
8
+ * Only static string paths are supported here. Dynamic extends
9
+ * (`{% extends some_var %}`) is rejected at parse time: the engine's
10
+ * parent-chain resolution (`engine.getParents` + `remapBlocks` +
11
+ * `importNonBlocks`) walks the chain statically at compile time, so a
12
+ * runtime-valued parent cannot be resolved on the sync path. Dynamic
13
+ * extends is the async-codegen path's concern, tracked separately.
14
+ *
15
+ * The parser's splitter reads `token.args[0]` and stashes it on
16
+ * `template.parent`. This tag must push the *unquoted* path as the single
17
+ * `token.args` element.
18
+ *
19
+ * Compile emits nothing — `extends` is a parse-time declaration carried via
20
+ * `template.parent` metadata; no runtime code is generated for the tag.
21
+ */
22
+
23
+ var utils = require('@rhinostone/swig-core/lib/utils');
24
+
25
+ var lexer = require('../lexer');
26
+
27
+ exports.ends = false;
28
+ exports.block = true;
29
+
30
+ /**
31
+ * Parse the `{% extends %}` tag body. Extracts the STRING literal path,
32
+ * strips surrounding quotes, and stashes the result as `token.args[0]`.
33
+ * Rejects anything other than a single STRING token.
34
+ *
35
+ * @param {string} str Tag body.
36
+ * @param {number} line Source line of the opening `{%`.
37
+ * @param {object} parser The Jinja2 parser module (unused — path is a
38
+ * bare string literal).
39
+ * @param {object} types Jinja2 lexer token-type enum.
40
+ * @param {Array} stack Open-tag stack (unused — extends has no body).
41
+ * @param {object} opts Per-call options (honors `opts.filename`).
42
+ * @param {object} swig Swig instance (unused).
43
+ * @param {object} token In-progress TagToken. `token.args` gets the
44
+ * unquoted parent path as its single element.
45
+ * @return {boolean} Always `true` on success. Throws otherwise.
46
+ */
47
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
48
+ var tokens = lexer.read(utils.strip(str));
49
+ var pos = 0;
50
+
51
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
52
+ var pathTok = pos < tokens.length ? tokens[pos] : null;
53
+ if (!pathTok) {
54
+ utils.throwError('Expected parent template path in "extends" tag', line, opts.filename);
55
+ }
56
+ if (pathTok.type !== types.STRING) {
57
+ utils.throwError('Dynamic "extends" is not supported — parent path must be a string literal', line, opts.filename);
58
+ }
59
+
60
+ pos += 1;
61
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
62
+ if (pos < tokens.length) {
63
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" after parent path in "extends" tag', line, opts.filename);
64
+ }
65
+
66
+ token.args = [pathTok.match.replace(/^['"]|['"]$/g, '')];
67
+ return true;
68
+ };
69
+
70
+ /**
71
+ * No-op compile. Extends is a parse-time declaration — the parent path
72
+ * lives on `template.parent`, which the engine's `getParents` reads during
73
+ * compile. The `{% extends %}` tag itself emits no runtime code.
74
+ *
75
+ * @return {undefined}
76
+ */
77
+ exports.compile = function () {};
@@ -0,0 +1,205 @@
1
+ /*!
2
+ * Jinja2 `{% filter name %}…{% endfilter %}` tag.
3
+ *
4
+ * Jinja2 filter-block syntax — pipe the captured body through one or more
5
+ * filters, left-to-right:
6
+ *
7
+ * {% filter upper %}hello{% endfilter %}
8
+ * {% filter upper|trim %} hi {% endfilter %}
9
+ * {% filter replace("a", "b") %}banana{% endfilter %}
10
+ * {% filter replace("a", "b")|upper %}banana{% endfilter %}
11
+ *
12
+ * Emits `IRLegacyJS` rather than `IRFilter` — `IRFilter` is single-filter
13
+ * only (backend wraps the body in one `_filters[name](...)` call), and
14
+ * chains require nested `_filters["f3"](_filters["f2"](_filters["f1"](...),
15
+ * ...), ...)`. Keeping the chain-emission in the tag avoids growing a new
16
+ * IR node for what is a JS plumbing shape; consistent with `{% set %}`'s
17
+ * body-capture form which also emits an IIFE via `IRLegacyJS`.
18
+ *
19
+ * CVE-2023-25345 checkpoint applies to each filter name — prototype-chain
20
+ * names (`__proto__`, `constructor`, `prototype`) are rejected at parse
21
+ * time. Filter argument expressions go through `parser.parseExpr` and
22
+ * inherit the `_dangerousProps` guards the expression parser already
23
+ * applies to VAR / DOTKEY / STRING-in-bracket / FUNCTION callee.
24
+ */
25
+
26
+ var ir = require('@rhinostone/swig-core/lib/ir');
27
+ var utils = require('@rhinostone/swig-core/lib/utils');
28
+ var backend = require('@rhinostone/swig-core/lib/backend');
29
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
30
+
31
+ var lexer = require('../lexer');
32
+ var _t = require('../tokentypes');
33
+
34
+ exports.ends = true;
35
+ exports.block = false;
36
+
37
+ /*!
38
+ * Depth-tracked COMMA-split over the token stream starting at `start`
39
+ * (which should be the position immediately after the FUNCTION or FILTER
40
+ * token's implicit open paren). Returns `{ slices, end }` where `end` is
41
+ * the index of the balancing PARENCLOSE (one past the last consumed arg
42
+ * token). Throws via `utils.throwError` on unclosed paren.
43
+ *
44
+ * Paren / bracket / curly / function all bump depth; PARENCLOSE /
45
+ * BRACKETCLOSE / CURLYCLOSE drop it; COMMA at depth 1 is a top-level
46
+ * separator. @private
47
+ */
48
+ function sliceCallArgs(tokens, start, line, filename) {
49
+ var depth = 1,
50
+ argStart = start,
51
+ slices = [],
52
+ j;
53
+ for (j = start; j < tokens.length; j += 1) {
54
+ var tk = tokens[j];
55
+ if (tk.type === _t.PARENOPEN || tk.type === _t.FUNCTION ||
56
+ tk.type === _t.BRACKETOPEN || tk.type === _t.CURLYOPEN) {
57
+ depth += 1;
58
+ continue;
59
+ }
60
+ if (tk.type === _t.PARENCLOSE || tk.type === _t.BRACKETCLOSE ||
61
+ tk.type === _t.CURLYCLOSE) {
62
+ depth -= 1;
63
+ if (depth === 0) {
64
+ if (j > argStart) {
65
+ slices.push(tokens.slice(argStart, j));
66
+ }
67
+ return { slices: slices, end: j + 1 };
68
+ }
69
+ continue;
70
+ }
71
+ if (tk.type === _t.COMMA && depth === 1) {
72
+ if (j > argStart) {
73
+ slices.push(tokens.slice(argStart, j));
74
+ }
75
+ argStart = j + 1;
76
+ }
77
+ }
78
+ utils.throwError('Unclosed argument list in "filter" tag', line, filename);
79
+ }
80
+
81
+ /**
82
+ * Parse the `{% filter name %}` tag body. Extracts the filter chain and
83
+ * validates each filter name against the CVE-2023-25345 blocklist.
84
+ *
85
+ * Stashes `[{name, args: IRExpr[]}, {name, args: IRExpr[]}, ...]` on
86
+ * `token.args`. `args: []` means the filter takes no arguments.
87
+ *
88
+ * @param {string} str Tag body.
89
+ * @param {number} line Source line of the opening `{%`.
90
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`).
91
+ * @param {object} types Jinja2 lexer token-type enum.
92
+ * @param {Array} stack Open-tag stack (parser.js manages push).
93
+ * @param {object} opts Per-call options (honors `opts.filename`).
94
+ * @param {object} swig Swig instance (unused).
95
+ * @param {object} token In-progress TagToken. `token.args` gets the
96
+ * filter chain descriptor array.
97
+ * @return {boolean} Always `true` on success. Throws otherwise.
98
+ */
99
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
100
+ var tokens = lexer.read(utils.strip(str));
101
+ var pos = 0;
102
+
103
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
104
+
105
+ if (pos >= tokens.length) {
106
+ utils.throwError('Expected filter name in "filter" tag', line, opts.filename);
107
+ }
108
+
109
+ function checkName(name) {
110
+ if (_dangerousProps.indexOf(name) !== -1) {
111
+ utils.throwError('Unsafe filter name "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
112
+ }
113
+ }
114
+
115
+ var chain = [];
116
+
117
+ // Head filter — VAR (bare name), FUNCTIONEMPTY (`name()`), or FUNCTION
118
+ // (`name(args...)`). Subsequent filters must come in via FILTER /
119
+ // FILTEREMPTY — a trailing bare VAR / FUNCTION would be a syntax error.
120
+ var head = tokens[pos];
121
+ if (head.type === types.VAR) {
122
+ checkName(head.match);
123
+ chain.push({ name: head.match, args: [] });
124
+ pos += 1;
125
+ } else if (head.type === types.FUNCTIONEMPTY) {
126
+ checkName(head.match);
127
+ chain.push({ name: head.match, args: [] });
128
+ pos += 1;
129
+ } else if (head.type === types.FUNCTION) {
130
+ checkName(head.match);
131
+ var result = sliceCallArgs(tokens, pos + 1, line, opts.filename);
132
+ var exprs = [];
133
+ for (var i = 0; i < result.slices.length; i += 1) {
134
+ exprs.push(parser.parseExpr(result.slices[i]));
135
+ }
136
+ chain.push({ name: head.match, args: exprs });
137
+ pos = result.end;
138
+ } else {
139
+ utils.throwError('Expected filter name in "filter" tag', line, opts.filename);
140
+ }
141
+
142
+ // Chain tail — each FILTER or FILTEREMPTY appends another filter call.
143
+ while (pos < tokens.length) {
144
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
145
+ if (pos >= tokens.length) { break; }
146
+
147
+ var tk = tokens[pos];
148
+ if (tk.type === types.FILTEREMPTY) {
149
+ checkName(tk.match);
150
+ chain.push({ name: tk.match, args: [] });
151
+ pos += 1;
152
+ continue;
153
+ }
154
+ if (tk.type === types.FILTER) {
155
+ checkName(tk.match);
156
+ var r = sliceCallArgs(tokens, pos + 1, line, opts.filename);
157
+ var e = [];
158
+ for (var k = 0; k < r.slices.length; k += 1) {
159
+ e.push(parser.parseExpr(r.slices[k]));
160
+ }
161
+ chain.push({ name: tk.match, args: e });
162
+ pos = r.end;
163
+ continue;
164
+ }
165
+ utils.throwError('Unexpected token "' + tk.match + '" in "filter" tag filter chain', line, opts.filename);
166
+ }
167
+
168
+ token.args = chain;
169
+ return true;
170
+ };
171
+
172
+ /**
173
+ * Emit an `IRLegacyJS` node that captures the body into a local `_output`
174
+ * via an IIFE and folds the filter chain left-to-right into nested
175
+ * `_filters["<name>"](input, ...args)` calls.
176
+ *
177
+ * For `{% filter upper|trim %}body{% endfilter %}` this produces roughly:
178
+ *
179
+ * _output += _filters["trim"](_filters["upper"](
180
+ * (function () { var _output = ""; <bodyJS> return _output; })()
181
+ * ));
182
+ *
183
+ * @return {object} IRLegacyJS node.
184
+ */
185
+ exports.compile = function (compiler, args, content, parents, options, blockName) {
186
+ var chain = args;
187
+ var bodyJS = compiler(content, parents, options, blockName);
188
+ var input = '(function () {\n var _output = "";\n' + bodyJS + ' return _output;\n})()';
189
+
190
+ var expr = input;
191
+ for (var i = 0; i < chain.length; i += 1) {
192
+ var entry = chain[i];
193
+ var argsJS = '';
194
+ if (entry.args && entry.args.length) {
195
+ var parts = [];
196
+ for (var j = 0; j < entry.args.length; j += 1) {
197
+ parts.push(backend.emitExpr(entry.args[j]));
198
+ }
199
+ argsJS = ', ' + parts.join(', ');
200
+ }
201
+ expr = '_filters["' + entry.name + '"](' + expr + argsJS + ')';
202
+ }
203
+
204
+ return ir.legacyJS('_output += ' + expr + ';\n');
205
+ };
@@ -0,0 +1,154 @@
1
+ /*!
2
+ * Jinja2 `{% for %}` tag.
3
+ *
4
+ * {% for <val> in <iterable> %}…{% endfor %}
5
+ * {% for <key>, <val> in <iterable> %}…{% endfor %}
6
+ * {% for <val> in <iterable> %}…{% else %}…{% endfor %} (empty case)
7
+ *
8
+ * Loop variable names must be bare identifiers — dotted paths (`foo.bar`)
9
+ * are rejected at parse time. The CVE-2023-25345 `_dangerousProps` guard
10
+ * runs on every bound name (key and val).
11
+ *
12
+ * The iterable is lowered through `parser.parseExpr`, so filter chains,
13
+ * binary ops, function calls, and inline-ifs all route through the same
14
+ * path as any other Jinja2 expression. The resulting IRExpr is attached
15
+ * to `token.irExpr`.
16
+ *
17
+ * The backend's `For` branch owns the full IIFE scaffolding: `_utils.each`,
18
+ * the `_ctx.loop.*` bookkeeping (first / last / index / index0 / revindex /
19
+ * revindex0 / length / key), the `Math.random()`-based loopcache that keeps
20
+ * nested loops from clobbering each other's `_ctx.loop` state, and the
21
+ * `emptyBody` (for-else) emission. The tag ships only semantic IR —
22
+ * (val, key, iterable, body, emptyBody).
23
+ *
24
+ * A `{% else %}` inside the for body is captured as a marker token (its
25
+ * stack check in tags/else.js allows `for`); compile splits the content at
26
+ * it into the loop body and the empty body.
27
+ */
28
+
29
+ var ir = require('@rhinostone/swig-core/lib/ir');
30
+ var utils = require('@rhinostone/swig-core/lib/utils');
31
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
32
+
33
+ var lexer = require('../lexer');
34
+
35
+ exports.ends = true;
36
+ exports.block = false;
37
+
38
+ /**
39
+ * Parse the `{% for %}` tag body. Extracts the binding names (val or
40
+ * key+val), validates them, then lowers the iterable expression. Names go
41
+ * on `token.args` (`[val]` or `[key, val]`); the iterable IR on
42
+ * `token.irExpr`.
43
+ *
44
+ * @param {string} str Tag body.
45
+ * @param {number} line Source line of the opening `{%`.
46
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`).
47
+ * @param {object} types Jinja2 lexer token-type enum.
48
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
49
+ * @param {object} opts Per-call options (honors `opts.filename`).
50
+ * @param {object} swig Swig instance (unused).
51
+ * @param {object} token In-progress TagToken.
52
+ * @return {boolean} Always `true` on success. Throws otherwise.
53
+ */
54
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
55
+ var tokens = lexer.read(utils.strip(str));
56
+ var pos = 0;
57
+
58
+ function peek() {
59
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
60
+ return pos < tokens.length ? tokens[pos] : null;
61
+ }
62
+ function consume() {
63
+ var t = peek();
64
+ if (t) { pos += 1; }
65
+ return t;
66
+ }
67
+
68
+ function takeName() {
69
+ var tok = consume();
70
+ if (!tok || tok.type !== types.VAR) {
71
+ utils.throwError('Expected loop variable in "for" tag', line, opts.filename);
72
+ }
73
+ if (tok.match.indexOf('.') !== -1) {
74
+ utils.throwError('Loop variable "' + tok.match + '" must be a bare identifier in "for" tag', line, opts.filename);
75
+ }
76
+ if (_dangerousProps.indexOf(tok.match) !== -1) {
77
+ utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
78
+ }
79
+ return tok.match;
80
+ }
81
+
82
+ var first = takeName();
83
+ var val = first;
84
+ var key;
85
+
86
+ if (peek() && peek().type === types.COMMA) {
87
+ consume();
88
+ key = first;
89
+ val = takeName();
90
+ }
91
+
92
+ // The lexer's COMPARATOR rule requires a trailing `\s` on `in`, so
93
+ // `{% for x in %}` (nothing after `in`) lexes `in` as a VAR. Match on the
94
+ // literal string so the error stays "Expected iterable" for that shape.
95
+ var inTok = consume();
96
+ if (!inTok || inTok.match !== 'in' || (inTok.type !== types.COMPARATOR && inTok.type !== types.VAR)) {
97
+ utils.throwError('Expected "in" in "for" tag', line, opts.filename);
98
+ }
99
+
100
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
101
+
102
+ var iterableTokens = tokens.slice(pos);
103
+ if (!iterableTokens.length) {
104
+ utils.throwError('Expected iterable after "in" in "for" tag', line, opts.filename);
105
+ }
106
+
107
+ token.args = key !== undefined ? [key, val] : [val];
108
+ token.irExpr = parser.parseExpr(iterableTokens);
109
+ return true;
110
+ };
111
+
112
+ /**
113
+ * Emit an IRFor node, splitting `content` at a `{% else %}` marker into the
114
+ * loop body and the empty body. The backend's `For` branch owns the
115
+ * loopcache + `_utils.each` scaffolding.
116
+ *
117
+ * @return {object} IRFor node.
118
+ */
119
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
120
+ var val, key;
121
+ if (args.length === 2) {
122
+ key = args[0];
123
+ val = args[1];
124
+ } else {
125
+ val = args[0];
126
+ key = '__k';
127
+ }
128
+
129
+ var loopContent = [],
130
+ emptyContent = null,
131
+ filename = options && options.filename;
132
+
133
+ utils.each(content, function (child) {
134
+ if (child && child.name === 'else') {
135
+ if (emptyContent !== null) {
136
+ utils.throwError('Multiple "else" branches in "for" tag', null, filename);
137
+ }
138
+ emptyContent = [];
139
+ return;
140
+ }
141
+ if (emptyContent !== null) {
142
+ emptyContent.push(child);
143
+ } else {
144
+ loopContent.push(child);
145
+ }
146
+ });
147
+
148
+ var bodyJS = compiler(loopContent, parents, options, blockName);
149
+ var emptyBody;
150
+ if (emptyContent !== null) {
151
+ emptyBody = [ir.legacyJS(compiler(emptyContent, parents, options, blockName))];
152
+ }
153
+ return ir.forStmt(val, token.irExpr, [ir.legacyJS(bodyJS)], key, emptyBody);
154
+ };