@rhinostone/swig-twig 2.0.0-alpha.3

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,206 @@
1
+ /*!
2
+ * Phase 3 Session 11 — Twig `{% apply filter %}…{% endapply %}` tag.
3
+ *
4
+ * Twig apply syntax — pipe the captured body through one or more filters,
5
+ * left-to-right:
6
+ *
7
+ * {% apply upper %}hello{% endapply %}
8
+ * {% apply upper|trim %} hi {% endapply %}
9
+ * {% apply replace({'a': 'b'}) %}banana{% endapply %}
10
+ * {% apply replace({'a': 'b'})|upper %}banana{% endapply %}
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
+ * Pattern mirrors `lib/tags/filter.js:lowerExpr` — paren / bracket /
45
+ * curly / function all bump depth; PARENCLOSE / BRACKETCLOSE /
46
+ * CURLYCLOSE drop it; COMMA at depth 1 is a top-level separator.
47
+ * @private
48
+ */
49
+ function sliceCallArgs(tokens, start, line, filename) {
50
+ var depth = 1,
51
+ argStart = start,
52
+ slices = [],
53
+ j;
54
+ for (j = start; j < tokens.length; j += 1) {
55
+ var tk = tokens[j];
56
+ if (tk.type === _t.PARENOPEN || tk.type === _t.FUNCTION ||
57
+ tk.type === _t.BRACKETOPEN || tk.type === _t.CURLYOPEN) {
58
+ depth += 1;
59
+ continue;
60
+ }
61
+ if (tk.type === _t.PARENCLOSE || tk.type === _t.BRACKETCLOSE ||
62
+ tk.type === _t.CURLYCLOSE) {
63
+ depth -= 1;
64
+ if (depth === 0) {
65
+ if (j > argStart) {
66
+ slices.push(tokens.slice(argStart, j));
67
+ }
68
+ return { slices: slices, end: j + 1 };
69
+ }
70
+ continue;
71
+ }
72
+ if (tk.type === _t.COMMA && depth === 1) {
73
+ if (j > argStart) {
74
+ slices.push(tokens.slice(argStart, j));
75
+ }
76
+ argStart = j + 1;
77
+ }
78
+ }
79
+ utils.throwError('Unclosed argument list in "apply" tag', line, filename);
80
+ }
81
+
82
+ /**
83
+ * Parse the `{% apply filter %}` tag body. Extracts the filter chain
84
+ * and validates each filter name against the CVE-2023-25345 blocklist.
85
+ *
86
+ * Stashes `[{name, args: IRExpr[]}, {name, args: IRExpr[]}, ...]` on
87
+ * `token.args`. `args: []` means the filter takes no arguments.
88
+ *
89
+ * @param {string} str Tag body.
90
+ * @param {number} line Source line of the opening `{%`.
91
+ * @param {object} parser The Twig parser module (exposes `parseExpr`).
92
+ * @param {object} types Twig lexer token-type enum.
93
+ * @param {Array} stack Open-tag stack (parser.js manages push).
94
+ * @param {object} opts Per-call options (honors `opts.filename`).
95
+ * @param {object} swig Swig instance (unused).
96
+ * @param {object} token In-progress TagToken. `token.args` gets the
97
+ * filter chain descriptor array.
98
+ * @return {boolean} Always `true` on success. Throws otherwise.
99
+ */
100
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
101
+ var tokens = lexer.read(utils.strip(str));
102
+ var pos = 0;
103
+
104
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
105
+
106
+ if (pos >= tokens.length) {
107
+ utils.throwError('Expected filter name in "apply" tag', line, opts.filename);
108
+ }
109
+
110
+ function checkName(name) {
111
+ if (_dangerousProps.indexOf(name) !== -1) {
112
+ utils.throwError('Unsafe filter name "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
113
+ }
114
+ }
115
+
116
+ var chain = [];
117
+
118
+ // Head filter — VAR (bare name), FUNCTIONEMPTY (`name()`), or FUNCTION
119
+ // (`name(args...)`). Subsequent filters must come in via FILTER /
120
+ // FILTEREMPTY — a trailing bare VAR / FUNCTION would be a syntax error.
121
+ var head = tokens[pos];
122
+ if (head.type === types.VAR) {
123
+ checkName(head.match);
124
+ chain.push({ name: head.match, args: [] });
125
+ pos += 1;
126
+ } else if (head.type === types.FUNCTIONEMPTY) {
127
+ checkName(head.match);
128
+ chain.push({ name: head.match, args: [] });
129
+ pos += 1;
130
+ } else if (head.type === types.FUNCTION) {
131
+ checkName(head.match);
132
+ var result = sliceCallArgs(tokens, pos + 1, line, opts.filename);
133
+ var exprs = [];
134
+ for (var i = 0; i < result.slices.length; i += 1) {
135
+ exprs.push(parser.parseExpr(result.slices[i]));
136
+ }
137
+ chain.push({ name: head.match, args: exprs });
138
+ pos = result.end;
139
+ } else {
140
+ utils.throwError('Expected filter name in "apply" tag', line, opts.filename);
141
+ }
142
+
143
+ // Chain tail — each FILTER or FILTEREMPTY appends another filter call.
144
+ while (pos < tokens.length) {
145
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
146
+ if (pos >= tokens.length) { break; }
147
+
148
+ var tk = tokens[pos];
149
+ if (tk.type === types.FILTEREMPTY) {
150
+ checkName(tk.match);
151
+ chain.push({ name: tk.match, args: [] });
152
+ pos += 1;
153
+ continue;
154
+ }
155
+ if (tk.type === types.FILTER) {
156
+ checkName(tk.match);
157
+ var r = sliceCallArgs(tokens, pos + 1, line, opts.filename);
158
+ var e = [];
159
+ for (var k = 0; k < r.slices.length; k += 1) {
160
+ e.push(parser.parseExpr(r.slices[k]));
161
+ }
162
+ chain.push({ name: tk.match, args: e });
163
+ pos = r.end;
164
+ continue;
165
+ }
166
+ utils.throwError('Unexpected token "' + tk.match + '" in "apply" tag filter chain', line, opts.filename);
167
+ }
168
+
169
+ token.args = chain;
170
+ return true;
171
+ };
172
+
173
+ /**
174
+ * Emit an `IRLegacyJS` node that captures the body into a local
175
+ * `_output` via an IIFE and folds the filter chain left-to-right into
176
+ * nested `_filters["<name>"](input, ...args)` calls.
177
+ *
178
+ * For `{% apply upper|trim %}body{% endapply %}` this produces roughly:
179
+ *
180
+ * _output += _filters["trim"](_filters["upper"](
181
+ * (function () { var _output = ""; <bodyJS> return _output; })()
182
+ * ));
183
+ *
184
+ * @return {object} IRLegacyJS node.
185
+ */
186
+ exports.compile = function (compiler, args, content, parents, options, blockName) {
187
+ var chain = args;
188
+ var bodyJS = compiler(content, parents, options, blockName);
189
+ var input = '(function () {\n var _output = "";\n' + bodyJS + ' return _output;\n})()';
190
+
191
+ var expr = input;
192
+ for (var i = 0; i < chain.length; i += 1) {
193
+ var entry = chain[i];
194
+ var argsJS = '';
195
+ if (entry.args && entry.args.length) {
196
+ var parts = [];
197
+ for (var j = 0; j < entry.args.length; j += 1) {
198
+ parts.push(backend.emitExpr(entry.args[j]));
199
+ }
200
+ argsJS = ', ' + parts.join(', ');
201
+ }
202
+ expr = '_filters["' + entry.name + '"](' + expr + argsJS + ')';
203
+ }
204
+
205
+ return ir.legacyJS('_output += ' + expr + ';\n');
206
+ };
@@ -0,0 +1,93 @@
1
+ /*!
2
+ * Phase 3 Session 9 — Twig `{% 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
10
+ * captures the block in `template.blocks[name]` when it appears at the
11
+ * top level (see `packages/swig-twig/lib/parser.js` — the block-keying
12
+ * branch is triggered by `token.block && !stack.length`).
13
+ *
14
+ * Compile emits an `IRBlock` node with the body wrapped in IRLegacyJS.
15
+ * The backend's `Block` branch emits the body verbatim — block-override
16
+ * resolution happens at parse time via `engine.remapBlocks` /
17
+ * `importNonBlocks`, which substitutes the child's block content into
18
+ * the parent's token tree before backend emission.
19
+ *
20
+ * Native hardening gap flagged: `lib/tags/block.js` uses
21
+ * `parser.on('*')` and does NOT guard the block name against
22
+ * `_dangerousProps`. A `{% block __proto__ %}` in native Swig would
23
+ * key the blocks map by that name; the override path does not currently
24
+ * reach the prototype chain but the cross-layer invariant is to guard
25
+ * anyway. See .claude/architecture/multi-flavor-ir.md § Phase 3 —
26
+ * Session 9 native hardening follow-up.
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
+ var _t = require('../tokentypes');
35
+
36
+ exports.ends = true;
37
+ exports.block = true;
38
+
39
+ /**
40
+ * Parse the `{% block %}` tag body. Extracts the bare-identifier name,
41
+ * validates it against `_dangerousProps` and the dotted-path rule, and
42
+ * stashes it on `token.args` so the parser's top-level block-keying
43
+ * branch can pick it up via `token.args.join('')`.
44
+ *
45
+ * @param {string} str Tag body.
46
+ * @param {number} line Source line of the opening `{%`.
47
+ * @param {object} parser The Twig parser module (unused here — block
48
+ * names are plain identifiers).
49
+ * @param {object} types Twig lexer token-type enum.
50
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
51
+ * @param {object} opts Per-call options (honors `opts.filename`).
52
+ * @param {object} swig Swig instance (unused).
53
+ * @param {object} token In-progress TagToken. `token.args` gets the
54
+ * block name as its single element.
55
+ * @return {boolean} Always `true` on success. Throws otherwise.
56
+ */
57
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
58
+ var tokens = lexer.read(utils.strip(str));
59
+ var pos = 0;
60
+
61
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
62
+ var nameTok = pos < tokens.length ? tokens[pos] : null;
63
+ if (!nameTok || nameTok.type !== types.VAR) {
64
+ utils.throwError('Expected block name in "block" tag', line, opts.filename);
65
+ }
66
+ if (nameTok.match.indexOf('.') !== -1) {
67
+ utils.throwError('Block name "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
68
+ }
69
+ if (_dangerousProps.indexOf(nameTok.match) !== -1) {
70
+ utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
71
+ }
72
+
73
+ pos += 1;
74
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
75
+ if (pos < tokens.length) {
76
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" after block name', line, opts.filename);
77
+ }
78
+
79
+ token.args = [nameTok.match];
80
+ return true;
81
+ };
82
+
83
+ /**
84
+ * Emit an IRBlock node. Body is the recursively-compiled content
85
+ * wrapped in IRLegacyJS. Mirrors the native `lib/tags/block.js` compile
86
+ * shape so the backend's `Block` branch treats both frontends the same.
87
+ *
88
+ * @return {object} IRBlock node.
89
+ */
90
+ exports.compile = function (compiler, args, content, parents, options) {
91
+ var name = args.join('');
92
+ return ir.block(name, [ir.legacyJS(compiler(content, parents, options, name))]);
93
+ };
@@ -0,0 +1,87 @@
1
+ /*!
2
+ * Phase 3 Session 9 — Twig `{% extends %}` tag.
3
+ *
4
+ * Declares a parent template for inheritance:
5
+ *
6
+ * {% extends "layout.twig" %}
7
+ *
8
+ * Twig supports both static string paths (handled here) and dynamic
9
+ * expressions (`{% extends some_var %}`, `{% extends a ? b : c %}`). This
10
+ * session rejects dynamic extends at parse time — the engine's parent-
11
+ * chain resolution (`engine.getParents` + `remapBlocks` +
12
+ * `importNonBlocks`) walks the chain statically at compile time, so a
13
+ * runtime-valued parent cannot be resolved without reworking the engine.
14
+ * Dynamic extends is tracked for a later session; the rejection is
15
+ * deliberate, not an oversight.
16
+ *
17
+ * The parser's splitter reads `token.args[0]` and stashes it on
18
+ * `template.parent` (see `packages/swig-twig/lib/parser.js` line 609).
19
+ * This tag must therefore push the *unquoted* path as the single
20
+ * `token.args` element.
21
+ *
22
+ * Compile emits nothing — `extends.compile` returns undefined. The
23
+ * backend's emit loop skips undefined returns. Extends is a parse-time
24
+ * declaration carried via `template.parent` metadata; no runtime code
25
+ * is generated for the `{% extends %}` tag itself.
26
+ */
27
+
28
+ var utils = require('@rhinostone/swig-core/lib/utils');
29
+
30
+ var lexer = require('../lexer');
31
+ var _t = require('../tokentypes');
32
+
33
+ exports.ends = false;
34
+ exports.block = true;
35
+
36
+ /**
37
+ * Parse the `{% extends %}` tag body. Extracts the STRING literal path,
38
+ * strips surrounding quotes, and stashes the result as `token.args[0]`
39
+ * for the parser's splitter to pick up.
40
+ *
41
+ * Rejects anything other than a single STRING token — dynamic extends
42
+ * (VAR, FUNCTION, expressions) is not supported in this session.
43
+ *
44
+ * @param {string} str Tag body.
45
+ * @param {number} line Source line of the opening `{%`.
46
+ * @param {object} parser The Twig parser module (unused — path is a
47
+ * bare string literal).
48
+ * @param {object} types Twig lexer token-type enum.
49
+ * @param {Array} stack Open-tag stack (unused — extends has no body).
50
+ * @param {object} opts Per-call options (honors `opts.filename`).
51
+ * @param {object} swig Swig instance (unused).
52
+ * @param {object} token In-progress TagToken. `token.args` gets the
53
+ * unquoted parent path as its single element.
54
+ * @return {boolean} Always `true` on success. Throws otherwise.
55
+ */
56
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
57
+ var tokens = lexer.read(utils.strip(str));
58
+ var pos = 0;
59
+
60
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
61
+ var pathTok = pos < tokens.length ? tokens[pos] : null;
62
+ if (!pathTok) {
63
+ utils.throwError('Expected parent template path in "extends" tag', line, opts.filename);
64
+ }
65
+ if (pathTok.type !== types.STRING) {
66
+ utils.throwError('Dynamic "extends" is not supported — parent path must be a string literal', line, opts.filename);
67
+ }
68
+
69
+ pos += 1;
70
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
71
+ if (pos < tokens.length) {
72
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" after parent path in "extends" tag', line, opts.filename);
73
+ }
74
+
75
+ token.args = [pathTok.match.replace(/^['"]|['"]$/g, '')];
76
+ return true;
77
+ };
78
+
79
+ /**
80
+ * No-op compile. Extends is a parse-time declaration — the parent path
81
+ * lives on `template.parent` (set by the parser's splitter), which the
82
+ * engine's `getParents` reads during compile. The `{% extends %}` tag
83
+ * itself emits no runtime code.
84
+ *
85
+ * @return {undefined}
86
+ */
87
+ exports.compile = function () {};
@@ -0,0 +1,134 @@
1
+ /*!
2
+ * Phase 3 Session 8 — Twig `{% for %}` tag.
3
+ *
4
+ * Twig iteration:
5
+ * {% for <val> in <iterable> %}…{% endfor %}
6
+ * {% for <key>, <val> in <iterable> %}…{% endfor %}
7
+ *
8
+ * Loop variable names must be bare identifiers — dotted paths
9
+ * (`foo.bar`) are rejected at parse time (not valid Twig loop-var
10
+ * syntax; and accepting them would let malformed templates silently
11
+ * through). The CVE-2023-25345 `_dangerousProps` guard runs on every
12
+ * bound name (key and val).
13
+ *
14
+ * The iterable is lowered through `parser.parseExpr`, so filter chains
15
+ * (`list|sort`), BinaryOps (`a + b`), function calls, and ternaries
16
+ * route through the same path as any other Twig expression — no
17
+ * tag-local bail conditions. The resulting IRExpr is attached to
18
+ * `token.irExpr`.
19
+ *
20
+ * The backend's `For` branch (packages/swig-core/lib/backend.js:187)
21
+ * owns the full IIFE scaffolding: `_utils.each`, `_ctx.loop.*`
22
+ * bookkeeping (first/last/index/index0/revindex/revindex0/length/key),
23
+ * and the `Math.random()`-based loopcache identifier that keeps nested
24
+ * loops from clobbering each other's `_ctx.loop` state (gh-433). The
25
+ * tag ships only semantic IR — (val, key, iterable, body).
26
+ */
27
+
28
+ var ir = require('@rhinostone/swig-core/lib/ir');
29
+ var utils = require('@rhinostone/swig-core/lib/utils');
30
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
31
+
32
+ var lexer = require('../lexer');
33
+ var _t = require('../tokentypes');
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 against `_dangerousProps` and the bare-
41
+ * identifier rule, then lowers the iterable expression through
42
+ * `parser.parseExpr`. Names are stashed on `token.args` (`[val]` or
43
+ * `[key, val]`); the iterable IR is stashed on `token.irExpr`.
44
+ *
45
+ * @param {string} str Tag body.
46
+ * @param {number} line Source line of the opening `{%`.
47
+ * @param {object} parser The Twig parser module (exposes `parseExpr`).
48
+ * @param {object} types Twig lexer token-type enum.
49
+ * @param {Array} stack Open-tag stack (parser.js manages the push
50
+ * after parse returns).
51
+ * @param {object} opts Per-call options (honors `opts.filename`).
52
+ * @param {object} swig Swig instance (unused).
53
+ * @param {object} token In-progress TagToken.
54
+ * @return {boolean} Always `true` on success. Throws otherwise.
55
+ */
56
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
57
+ var tokens = lexer.read(utils.strip(str));
58
+ var pos = 0;
59
+
60
+ function peek() {
61
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
62
+ return pos < tokens.length ? tokens[pos] : null;
63
+ }
64
+ function consume() {
65
+ var t = peek();
66
+ if (t) { pos += 1; }
67
+ return t;
68
+ }
69
+
70
+ function takeName() {
71
+ var tok = consume();
72
+ if (!tok || tok.type !== types.VAR) {
73
+ utils.throwError('Expected loop variable in "for" tag', line, opts.filename);
74
+ }
75
+ if (tok.match.indexOf('.') !== -1) {
76
+ utils.throwError('Loop variable "' + tok.match + '" must be a bare identifier in "for" tag', line, opts.filename);
77
+ }
78
+ if (_dangerousProps.indexOf(tok.match) !== -1) {
79
+ utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
80
+ }
81
+ return tok.match;
82
+ }
83
+
84
+ var first = takeName();
85
+ var val = first;
86
+ var key;
87
+
88
+ if (peek() && peek().type === types.COMMA) {
89
+ consume();
90
+ key = first;
91
+ val = takeName();
92
+ }
93
+
94
+ // NB: the Twig lexer's COMPARATOR rule is `^(=== | ... | in\s)` — the
95
+ // trailing `\s` is required, so `{% for x in %}` (nothing after `in`)
96
+ // lexes `in` as a VAR instead of a COMPARATOR. Match on the literal
97
+ // string so the user-facing error stays "Expected iterable" for that
98
+ // shape rather than "Expected in".
99
+ var inTok = consume();
100
+ if (!inTok || inTok.match !== 'in' || (inTok.type !== types.COMPARATOR && inTok.type !== types.VAR)) {
101
+ utils.throwError('Expected "in" in "for" tag', line, opts.filename);
102
+ }
103
+
104
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
105
+
106
+ var iterableTokens = tokens.slice(pos);
107
+ if (!iterableTokens.length) {
108
+ utils.throwError('Expected iterable after "in" in "for" tag', line, opts.filename);
109
+ }
110
+
111
+ token.args = key !== undefined ? [key, val] : [val];
112
+ token.irExpr = parser.parseExpr(iterableTokens);
113
+ return true;
114
+ };
115
+
116
+ /**
117
+ * Emit an IRFor node. The backend's `For` branch owns the loopcache +
118
+ * `_utils.each` scaffolding — this returns only (val, iterable, body,
119
+ * key). Body is the recursively-compiled content wrapped in IRLegacyJS.
120
+ *
121
+ * @return {object} IRFor node.
122
+ */
123
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
124
+ var val, key;
125
+ if (args.length === 2) {
126
+ key = args[0];
127
+ val = args[1];
128
+ } else {
129
+ val = args[0];
130
+ key = '__k';
131
+ }
132
+ var bodyJS = compiler(content, parents, options, blockName);
133
+ return ir.forStmt(val, token.irExpr, [ir.legacyJS(bodyJS)], key);
134
+ };