@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,154 @@
1
+ /*!
2
+ * Jinja2 `{% include %}` tag.
3
+ *
4
+ * Jinja2 include syntax:
5
+ *
6
+ * {% include "partial.html" %}
7
+ * {% include "partial.html" with context %} (the default)
8
+ * {% include "partial.html" without context %}
9
+ * {% include "partial.html" ignore missing %}
10
+ * {% include "partial.html" ignore missing without context %}
11
+ * {% include dynamicPath %}
12
+ *
13
+ * Path is lowered through `parser.parseExpr`, so STRING literals, VAR
14
+ * references, member access, inline-ifs, and any other Jinja2 expression
15
+ * all route through the same path.
16
+ *
17
+ * Three keyword markers are recognised via a depth-tracked scan over the
18
+ * lexed token stream, each a two-token VAR sequence at top-level depth:
19
+ * `with context`, `without context`, `ignore missing`. The default is
20
+ * `with context` — the included template sees the caller's locals. Unlike
21
+ * Twig, Jinja2's include has no explicit `with {dict}` context object;
22
+ * `without context` is lowered to an empty-object context so the backend's
23
+ * `Include` selector passes `{}` instead of `_ctx`.
24
+ *
25
+ * The tag emits an `IRInclude` node. The backend's `Include` branch owns
26
+ * the `_swig.compileFile(...)` + `resolveFrom` plumbing and the optional
27
+ * `try { ... } catch {}` wrapper that collapses missing-file errors to the
28
+ * empty string when `ignoreMissing` is set.
29
+ */
30
+
31
+ var ir = require('@rhinostone/swig-core/lib/ir');
32
+ var utils = require('@rhinostone/swig-core/lib/utils');
33
+
34
+ var lexer = require('../lexer');
35
+
36
+ exports.ends = false;
37
+ exports.block = false;
38
+
39
+ /**
40
+ * Strip WHITESPACE tokens from both ends of a slice range.
41
+ *
42
+ * @param {object[]} tokens Token stream.
43
+ * @param {number} start Inclusive start index.
44
+ * @param {number} end Exclusive end index.
45
+ * @param {object} types Lexer token-type enum.
46
+ * @return {object[]} Trimmed slice.
47
+ * @private
48
+ */
49
+ function sliceTrim(tokens, start, end, types) {
50
+ while (start < end && tokens[start].type === types.WHITESPACE) { start += 1; }
51
+ while (end > start && tokens[end - 1].type === types.WHITESPACE) { end -= 1; }
52
+ return tokens.slice(start, end);
53
+ }
54
+
55
+ /**
56
+ * Parse the `{% include %}` tag body — the path expression plus the
57
+ * optional `with context` / `without context` / `ignore missing` markers.
58
+ *
59
+ * @param {string} str Tag body.
60
+ * @param {number} line Source line of the opening `{%`.
61
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`).
62
+ * @param {object} types Jinja2 lexer token-type enum.
63
+ * @param {Array} stack Open-tag stack (unused — include has no body).
64
+ * @param {object} opts Per-call options (honors `opts.filename`).
65
+ * @param {object} swig Swig instance (unused — backend owns load).
66
+ * @param {object} token In-progress TagToken. `token.irExpr` gets the
67
+ * IRInclude node.
68
+ * @return {boolean} Always `true` on success. Throws otherwise.
69
+ */
70
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
71
+ var tokens = lexer.read(utils.strip(str));
72
+
73
+ var depth = 0;
74
+ var keywordIdx = -1;
75
+ var withContext = false;
76
+ var withoutContext = false;
77
+ var ignoreMissing = false;
78
+ var i, tk, nextVar;
79
+
80
+ function nextVarAfter(idx) {
81
+ var j = idx + 1;
82
+ while (j < tokens.length && tokens[j].type === types.WHITESPACE) { j += 1; }
83
+ return j < tokens.length ? tokens[j] : null;
84
+ }
85
+ function markKeyword(idx) {
86
+ if (keywordIdx === -1) { keywordIdx = idx; }
87
+ }
88
+
89
+ for (i = 0; i < tokens.length; i += 1) {
90
+ tk = tokens[i];
91
+ if (tk.type === types.PARENOPEN || tk.type === types.BRACKETOPEN ||
92
+ tk.type === types.CURLYOPEN || tk.type === types.FUNCTION) {
93
+ depth += 1;
94
+ continue;
95
+ }
96
+ if (tk.type === types.PARENCLOSE || tk.type === types.BRACKETCLOSE ||
97
+ tk.type === types.CURLYCLOSE) {
98
+ depth -= 1;
99
+ continue;
100
+ }
101
+ if (depth !== 0 || tk.type !== types.VAR) { continue; }
102
+
103
+ if ((tk.match === 'with' || tk.match === 'without') && !withContext && !withoutContext) {
104
+ nextVar = nextVarAfter(i);
105
+ if (nextVar && nextVar.type === types.VAR && nextVar.match === 'context') {
106
+ if (tk.match === 'with') { withContext = true; } else { withoutContext = true; }
107
+ markKeyword(i);
108
+ }
109
+ } else if (tk.match === 'ignore' && !ignoreMissing) {
110
+ nextVar = nextVarAfter(i);
111
+ if (nextVar && nextVar.type === types.VAR && nextVar.match === 'missing') {
112
+ ignoreMissing = true;
113
+ markKeyword(i);
114
+ }
115
+ }
116
+ }
117
+
118
+ var pathEnd = keywordIdx === -1 ? tokens.length : keywordIdx;
119
+ var pathTokens = sliceTrim(tokens, 0, pathEnd, types);
120
+ if (!pathTokens.length) {
121
+ utils.throwError('Expected template path in "include" tag', line, opts.filename);
122
+ }
123
+ var pathExpr = parser.parseExpr(pathTokens);
124
+
125
+ var resolveFrom = (opts.filename || '').replace(/\\/g, '\\\\');
126
+
127
+ // `without context` -> empty-object context + isolated, so the backend's
128
+ // Include selector emits `{}` rather than `_ctx`. `with context` (and the
129
+ // default) leave context undefined + isolated false -> the selector emits
130
+ // `_ctx`. `withContext` is tracked only to mark the path end.
131
+ var ctxExpr;
132
+ if (withoutContext) {
133
+ ctxExpr = ir.objectLiteral([]);
134
+ }
135
+
136
+ token.irExpr = ir.include(pathExpr, ctxExpr, withoutContext, ignoreMissing, resolveFrom);
137
+ return true;
138
+ };
139
+
140
+ /**
141
+ * Return the pre-built IRInclude node. In async codegen mode, derive an
142
+ * IRIncludeDeferred from the same fields so the backend routes through the
143
+ * `_swig.getTemplate` + `await` path instead of the sync `_swig.compileFile`
144
+ * call.
145
+ *
146
+ * @return {object} IRInclude or IRIncludeDeferred node.
147
+ */
148
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
149
+ if (options && options.codegenMode === 'async') {
150
+ var i = token.irExpr;
151
+ return ir.includeDeferred(i.path, i.context, i.isolated, i.ignoreMissing, i.resolveFrom);
152
+ }
153
+ return token.irExpr;
154
+ };
@@ -0,0 +1,32 @@
1
+ /*!
2
+ * Jinja2 per-flavor tag registry.
3
+ *
4
+ * Each tag exports `{ parse, compile, ends, block }` with a Jinja2-tailored
5
+ * shape:
6
+ *
7
+ * parse(str, line, parser, types, stack, opts, swig, token) → boolean
8
+ *
9
+ * The 8th `token` argument is the in-progress TagToken. Tag implementations
10
+ * call `parser.parseExpr(lexer.read(str), filters)` directly and attach the
11
+ * resulting IRExpr to `token.irExpr`, then return true. This avoids the
12
+ * native-swig `parser.on(types.X, fn)` callback indirection — Jinja2 tags
13
+ * own their own arg-parsing path.
14
+ */
15
+
16
+ module.exports = {
17
+ 'set': require('./set'),
18
+ 'if': require('./if'),
19
+ 'elif': require('./elif'),
20
+ 'else': require('./else'),
21
+ 'for': require('./for'),
22
+ 'block': require('./block'),
23
+ 'extends': require('./extends'),
24
+ 'include': require('./include'),
25
+ 'macro': require('./macro'),
26
+ 'import': require('./import'),
27
+ 'from': require('./from'),
28
+ 'raw': require('./raw'),
29
+ 'filter': require('./filter'),
30
+ 'with': require('./with'),
31
+ 'autoescape': require('./autoescape')
32
+ };
@@ -0,0 +1,174 @@
1
+ /*!
2
+ * Jinja2 `{% macro %}` tag.
3
+ *
4
+ * Jinja2 macro syntax:
5
+ *
6
+ * {% macro name() %}…{% endmacro %}
7
+ * {% macro name(a, b, c) %}…{% endmacro %}
8
+ * {% macro name(a, b='x', c=a) %}…{% endmacro %} (parameter defaults)
9
+ *
10
+ * Defines a reusable function bound to `_ctx.<name>`. The backend emits the
11
+ * full IIFE (`_utils.extend` snapshot, shadow-delete of param names from
12
+ * `_ctx`, default-application, body, restore) — see backend.js Macro branch.
13
+ * The tag ships only semantic IR: name, params (`IRMacroParam[]`), body.
14
+ *
15
+ * A parameter default is any Jinja2 expression and is lowered via
16
+ * `parser.parseExpr`. Defaults are applied at the top of the macro body
17
+ * (before the `_ctx` shadow-delete), so a default may reference an earlier
18
+ * parameter or an incoming context variable. Keyword-style CALLING
19
+ * (`foo(a=1)`) is a non-goal.
20
+ *
21
+ * Param names and the macro name are bare identifiers — dotted paths
22
+ * (`foo.bar`) and CVE-2023-25345 prototype-chain names (`__proto__`,
23
+ * `constructor`, `prototype`) are rejected at parse time. Single-name
24
+ * binding slots reject any `.` in the match before the `_dangerousProps`
25
+ * check.
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
+
34
+ exports.ends = true;
35
+ exports.block = true;
36
+
37
+ /**
38
+ * Parse the `{% macro %}` tag body. Extracts the macro name and the
39
+ * optional comma-separated parameter list (each parameter optionally
40
+ * carrying a `= <default>` expression). Both name and params are validated
41
+ * against the bare-identifier rule and the CVE-2023-25345 `_dangerousProps`
42
+ * blocklist.
43
+ *
44
+ * Accepts both shapes:
45
+ * `name` + FUNCTION/FUNCTIONEMPTY (idiomatic)
46
+ * `name(a, b)` lexed as FUNCTION token whose `match` is the name
47
+ *
48
+ * Stashes `[name, IRMacroParam, IRMacroParam, ...]` on `token.args`. Compile
49
+ * lifts the name off the head and passes the remaining param objects
50
+ * straight to `ir.macro` — the backend handles the IIFE.
51
+ *
52
+ * @param {string} str Tag body.
53
+ * @param {number} line Source line of the opening `{%`.
54
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`,
55
+ * used to lower parameter defaults).
56
+ * @param {object} types Jinja2 lexer token-type enum.
57
+ * @param {Array} stack Open-tag stack (unused — parser.js manages push).
58
+ * @param {object} opts Per-call options (honors `opts.filename`).
59
+ * @param {object} swig Swig instance (unused).
60
+ * @param {object} token In-progress TagToken. `token.args` gets the
61
+ * macro name + IRMacroParam objects.
62
+ * @return {boolean} Always `true` on success. Throws otherwise.
63
+ */
64
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
65
+ var tokens = lexer.read(utils.strip(str));
66
+ var pos = 0;
67
+
68
+ function peek() {
69
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
70
+ return pos < tokens.length ? tokens[pos] : null;
71
+ }
72
+ function consume() {
73
+ var t = peek();
74
+ if (t) { pos += 1; }
75
+ return t;
76
+ }
77
+
78
+ function checkName(name, role) {
79
+ if (name.indexOf('.') !== -1) {
80
+ utils.throwError(role + ' "' + name + '" must be a bare identifier in "macro" tag', line, opts.filename);
81
+ }
82
+ if (_dangerousProps.indexOf(name) !== -1) {
83
+ utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
84
+ }
85
+ }
86
+
87
+ var head = consume();
88
+ if (!head) {
89
+ utils.throwError('Expected macro name in "macro" tag', line, opts.filename);
90
+ }
91
+
92
+ var name;
93
+ var params = [];
94
+
95
+ if (head.type === types.FUNCTIONEMPTY) {
96
+ name = head.match;
97
+ checkName(name, 'Macro name');
98
+ } else if (head.type === types.FUNCTION) {
99
+ name = head.match;
100
+ checkName(name, 'Macro name');
101
+ var first = true;
102
+ while (true) {
103
+ var tk = peek();
104
+ if (!tk) {
105
+ utils.throwError('Unclosed parameter list in "macro" tag', line, opts.filename);
106
+ }
107
+ if (tk.type === types.PARENCLOSE) {
108
+ consume();
109
+ break;
110
+ }
111
+ if (!first) {
112
+ if (tk.type !== types.COMMA) {
113
+ utils.throwError('Expected "," between parameters in "macro" tag', line, opts.filename);
114
+ }
115
+ consume();
116
+ }
117
+ first = false;
118
+ var pTok = consume();
119
+ if (!pTok || pTok.type !== types.VAR) {
120
+ utils.throwError('Expected parameter name in "macro" tag', line, opts.filename);
121
+ }
122
+ checkName(pTok.match, 'Parameter');
123
+ var defaultExpr;
124
+ if (peek() && peek().type === types.ASSIGNMENT) {
125
+ if (peek().match !== '=') {
126
+ utils.throwError('Parameter default must use "=" in "macro" tag', line, opts.filename);
127
+ }
128
+ consume();
129
+ // Parse a single default expression from the cursor. parseExpr stops
130
+ // at the next COMMA / PARENCLOSE (neither is part of an expression);
131
+ // the out-param reports how many slice tokens it consumed so the
132
+ // local cursor can resume at the next parameter.
133
+ var posOut = {};
134
+ defaultExpr = parser.parseExpr(tokens.slice(pos), {}, posOut);
135
+ pos += posOut.pos;
136
+ }
137
+ params.push(ir.macroParam(pTok.match, defaultExpr));
138
+ }
139
+ } else if (head.type === types.VAR) {
140
+ name = head.match;
141
+ checkName(name, 'Macro name');
142
+ } else {
143
+ utils.throwError('Expected macro name in "macro" tag', line, opts.filename);
144
+ }
145
+
146
+ if (peek()) {
147
+ utils.throwError('Unexpected token "' + peek().match + '" after macro signature in "macro" tag', line, opts.filename);
148
+ }
149
+
150
+ token.args = [name].concat(params);
151
+ return true;
152
+ };
153
+
154
+ /**
155
+ * Emit an IRMacro node. The backend's `Macro` branch owns the
156
+ * `_ctx.<name> = function(...) { … }` IIFE, the `_utils.extend` snapshot,
157
+ * the default-application lines, and the shadow-delete of param names from
158
+ * `_ctx`.
159
+ *
160
+ * @param {Function} compiler Backend walker (recurses into `content`).
161
+ * @param {Array} args `[name, IRMacroParam, ...]`.
162
+ * @param {Array} content Child tokens between `{% macro %}` and
163
+ * `{% endmacro %}`.
164
+ * @param {Array} parents Parent template chain (passed through).
165
+ * @param {object} options Compile options (passed through).
166
+ * @param {?string} blockName Enclosing block name (passed through).
167
+ * @return {object} IRMacro node.
168
+ */
169
+ exports.compile = function (compiler, args, content, parents, options, blockName) {
170
+ var name = args[0];
171
+ var params = args.slice(1);
172
+ var bodyJS = compiler(content, parents, options, blockName);
173
+ return ir.macro(name, params, [ir.legacyJS(bodyJS)]);
174
+ };
@@ -0,0 +1,51 @@
1
+ /*!
2
+ * Jinja2 `{% raw %}…{% endraw %}` tag.
3
+ *
4
+ * Preserves arbitrary template-like content as literal output. Inside a
5
+ * raw block, `{{ … }}`, `{% … %}` (other than `{% endraw %}`), and
6
+ * `{# … #}` are NOT parsed — the splitter in `parser.js` flips an `inRaw`
7
+ * flag that bypasses the variable/tag/comment branches and wraps each
8
+ * chunk as `ir.text`, so the content array handed to this tag's compile
9
+ * is already a list of IRText nodes.
10
+ *
11
+ * Takes no arguments. Extra tokens after `raw` are rejected at parse time
12
+ * with a filename-aware throw.
13
+ */
14
+
15
+ var utils = require('@rhinostone/swig-core/lib/utils');
16
+
17
+ /**
18
+ * Reject any tokens after the `raw` keyword. The splitter owns all
19
+ * content-capture behaviour via its `inRaw` flag, so this handler only
20
+ * has to validate the tag's own argument list (which must be empty).
21
+ *
22
+ * @param {string} str Tag body (everything after `raw`).
23
+ * @param {number} line Source line of the opening `{%`.
24
+ * @param {object} parser The Jinja2 parser module (unused).
25
+ * @param {object} types Jinja2 lexer token-type enum (unused).
26
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
27
+ * @param {object} opts Per-call options (honors `opts.filename`).
28
+ * @return {boolean} Always `true` on success. Throws otherwise.
29
+ */
30
+ exports.parse = function (str, line, parser, types, stack, opts) {
31
+ var stripped = utils.strip(str || '');
32
+ if (stripped.length > 0) {
33
+ utils.throwError('Unexpected token "' + stripped + '" after "raw"', line, opts.filename);
34
+ }
35
+ return true;
36
+ };
37
+
38
+ /**
39
+ * Return the captured content array unchanged. Each item is already an
40
+ * IRText node (or another pre-built IR node that the backend will splice
41
+ * through), so the backend's emit loop can iterate and emit without any
42
+ * further wrapping.
43
+ *
44
+ * @return {Array} Content node list.
45
+ */
46
+ exports.compile = function (compiler, args, content) {
47
+ return content;
48
+ };
49
+
50
+ exports.ends = true;
51
+ exports.block = false;
@@ -0,0 +1,164 @@
1
+ /*!
2
+ * Jinja2 `{% set %}` tag (assignment + body-capture forms).
3
+ *
4
+ * Jinja2 `set` has two forms:
5
+ *
6
+ * Inline: {% set <lhs> <op> <rhs> %}
7
+ * Body: {% set <lhs> %}…{% endset %}
8
+ *
9
+ * lhs — a bare identifier or a pure-dot path (`foo`, `foo.bar.baz`).
10
+ * Bracket LHS (`foo[bar]`) is rejected at parse time — the
11
+ * bracket-lvalue contract is a cross-flavor design call and is
12
+ * deferred.
13
+ * op — any valid JS assignment operator (`=`, `+=`, `-=`, `*=`, `/=`).
14
+ * Inline form only.
15
+ * rhs — any Jinja2 expression; parsed via `parser.parseExpr`.
16
+ * Inline form only.
17
+ *
18
+ * Inline form emits an IRSet node on `token.irExpr`:
19
+ *
20
+ * ir.set(ir.varRef(['foo', 'bar']), '=', <IRExpr value>)
21
+ *
22
+ * Body form captures the rendered content as a string via an IIFE and
23
+ * assigns it to the target. No dedicated IR factory — emits an IRLegacyJS
24
+ * fragment because the capture is a JS plumbing shape (IIFE over `_output`)
25
+ * that doesn't need its own IR surface.
26
+ *
27
+ * Static `exports.ends = true` is the default so the token's `ends` slot
28
+ * starts truthy — the body form keeps it, the inline form flips
29
+ * `token.ends = false` at parse time so the splitter does NOT push the
30
+ * inline-form token onto the open-tag stack.
31
+ *
32
+ * CVE-2023-25345 checkpoints apply twice — here on the LHS path segments,
33
+ * and again in the backend `checkDangerousSegment` walk — intentional
34
+ * defense-in-depth.
35
+ */
36
+
37
+ var ir = require('@rhinostone/swig-core/lib/ir');
38
+ var utils = require('@rhinostone/swig-core/lib/utils');
39
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
40
+
41
+ var lexer = require('../lexer');
42
+
43
+ exports.ends = true;
44
+ exports.block = true;
45
+
46
+ /**
47
+ * Parse the `{% set %}` tag body and attach the appropriate IR to the
48
+ * token. Inline form sets `token.irExpr` and flips `token.ends = false`;
49
+ * body form leaves `token.ends = true` and stashes the target path on
50
+ * `token.args` for the compile step to pick up.
51
+ *
52
+ * @param {string} str Tag body (everything between `{%` and `%}`, tag name stripped).
53
+ * @param {number} line Source line of the opening `{%`.
54
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`).
55
+ * @param {object} types Jinja2 lexer token-type enum.
56
+ * @param {Array} stack Open-tag stack (unused — parser.js manages the push).
57
+ * @param {object} opts Per-call options (honors `opts.filename` for filename-aware throws).
58
+ * @param {object} swig Swig instance (unused).
59
+ * @param {object} token In-progress TagToken.
60
+ * @return {boolean} Always `true` on success. Throws otherwise.
61
+ */
62
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
63
+ var tokens = lexer.read(utils.strip(str));
64
+ var pos = 0;
65
+
66
+ function peek() {
67
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
68
+ return pos < tokens.length ? tokens[pos] : null;
69
+ }
70
+ function consume() {
71
+ var t = peek();
72
+ if (t) { pos += 1; }
73
+ return t;
74
+ }
75
+
76
+ var lhsTok = consume();
77
+ if (!lhsTok || lhsTok.type !== types.VAR) {
78
+ utils.throwError('Expected variable name in "set" tag', line, opts.filename);
79
+ }
80
+
81
+ var path = lhsTok.match.split('.');
82
+ utils.each(path, function (segment) {
83
+ if (_dangerousProps.indexOf(segment) !== -1) {
84
+ utils.throwError('Unsafe assignment to "' + segment + '" is not allowed (CVE-2023-25345)', line, opts.filename);
85
+ }
86
+ });
87
+
88
+ // DOTKEY tail — the Jinja2 lexer already folds dotted paths into the VAR
89
+ // match, but a defensive DOTKEY consumer here keeps this tag robust if a
90
+ // future lexer tightening splits `foo.bar` into VAR + DOTKEY.
91
+ while (peek() && peek().type === types.DOTKEY) {
92
+ var dk = consume();
93
+ if (_dangerousProps.indexOf(dk.match) !== -1) {
94
+ utils.throwError('Unsafe assignment to "' + dk.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
95
+ }
96
+ path.push(dk.match);
97
+ }
98
+
99
+ var next = peek();
100
+ if (next && next.type === types.BRACKETOPEN) {
101
+ utils.throwError('Bracket-notation assignment is not supported in "set" (use dot-path notation)', line, opts.filename);
102
+ }
103
+
104
+ if (!next) {
105
+ // Body-capture form — no more tokens after the LHS. Keep
106
+ // `token.ends = true` (the default) so the splitter pushes the token
107
+ // onto the open-tag stack and waits for a matching `{% endset %}`.
108
+ // Stash the path on `token.args` for the compile handler.
109
+ token.args = path;
110
+ return true;
111
+ }
112
+
113
+ // Inline form — flip `token.ends = false` so the splitter does NOT push
114
+ // the token onto the open-tag stack.
115
+ token.ends = false;
116
+
117
+ var opTok = consume();
118
+ if (opTok.type !== types.ASSIGNMENT) {
119
+ utils.throwError('Expected assignment operator in "set" tag', line, opts.filename);
120
+ }
121
+
122
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
123
+
124
+ var rhsTokens = tokens.slice(pos);
125
+ if (!rhsTokens.length) {
126
+ utils.throwError('Expected expression after assignment in "set" tag', line, opts.filename);
127
+ }
128
+
129
+ var value = parser.parseExpr(rhsTokens);
130
+ token.irExpr = ir.set(ir.varRef(path), opTok.match, value);
131
+ return true;
132
+ };
133
+
134
+ /**
135
+ * Emit the IR for either the inline-assignment or body-capture form.
136
+ * Inline form returns the pre-built IRSet via `token.irExpr`. Body form
137
+ * compiles the captured content and wraps it in an IIFE assigned to the
138
+ * target.
139
+ *
140
+ * @param {Function} compiler Backend walker (recurses into `content`).
141
+ * @param {Array} args Target path segments (body form only).
142
+ * @param {Array} content Child tokens captured between `{% set %}`
143
+ * and `{% endset %}` (body form only).
144
+ * @param {Array} parents Parent template chain (passed through).
145
+ * @param {object} options Compile options (passed through).
146
+ * @param {?string} blockName Enclosing block name (passed through).
147
+ * @param {object} token The tag token. `token.irExpr` is set for
148
+ * inline form only.
149
+ * @return {object} IRSet (inline) or IRLegacyJS (body).
150
+ */
151
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
152
+ if (token.irExpr) {
153
+ return token.irExpr;
154
+ }
155
+ var path = args;
156
+ var bodyJS = compiler(content, parents, options, blockName);
157
+ return ir.legacyJS(
158
+ '_ctx.' + path.join('.') + ' = (function () {\n' +
159
+ ' var _output = "";\n' +
160
+ bodyJS +
161
+ ' return _output;\n' +
162
+ '})();\n'
163
+ );
164
+ };