@rhinostone/swig-django 2.7.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,76 @@
1
+ /*!
2
+ * Django `{% autoescape %}` tag.
3
+ *
4
+ * {% autoescape on %}{{ html }}{% endautoescape %} (escape the region)
5
+ * {% autoescape off %}{{ 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, so
10
+ * 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 `on` and `off` are accepted (Django's spelling;
15
+ * the other flavors use `true` / `false`). `on` / `off` lex as VAR tokens,
16
+ * not BOOL. A runtime expression (`{% autoescape some_var %}`) is rejected
17
+ * at parse time — the parse-time escape model can only resolve a literal
18
+ * strategy.
19
+ */
20
+
21
+ var ir = require('@rhinostone/swig-core/lib/ir');
22
+ var utils = require('@rhinostone/swig-core/lib/utils');
23
+
24
+ var lexer = require('../lexer');
25
+
26
+ exports.ends = true;
27
+ exports.block = false;
28
+
29
+ /**
30
+ * Parse the `{% autoescape %}` tag body. Requires a single `on` / `off`
31
+ * VAR token and stashes the resolved boolean on `token.escapeValue` — the
32
+ * parser reads it to push onto the escape-value stack for the region, and
33
+ * compile reads it to build the IRAutoescape node.
34
+ *
35
+ * @param {string} str Tag body.
36
+ * @param {number} line Source line of the opening `{%`.
37
+ * @param {object} parser The Django parser module (unused).
38
+ * @param {object} types Django 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. Gets `token.escapeValue`.
43
+ * @return {boolean} Always `true` on success. Throws otherwise.
44
+ */
45
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
46
+ var tokens = lexer.read(utils.strip(str));
47
+ var pos = 0;
48
+
49
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
50
+ var tok = pos < tokens.length ? tokens[pos] : null;
51
+ if (!tok || tok.type !== types.VAR || (tok.match !== 'on' && tok.match !== 'off')) {
52
+ utils.throwError('Expected "on" or "off" in "autoescape" tag', line, opts.filename);
53
+ }
54
+ pos += 1;
55
+
56
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
57
+ if (pos < tokens.length) {
58
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" in "autoescape" tag', line, opts.filename);
59
+ }
60
+
61
+ token.escapeValue = (tok.match === 'on');
62
+ return true;
63
+ };
64
+
65
+ /**
66
+ * Emit an IRAutoescape node wrapping the recursively-compiled body. The
67
+ * body's `{{ … }}` outputs already carry (or omit) their `e` tails from
68
+ * parse time, so the backend emits the body verbatim; the strategy is
69
+ * carried for IR fidelity.
70
+ *
71
+ * @return {object} IRAutoescape node.
72
+ */
73
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
74
+ var bodyJS = compiler(content, parents, options, blockName);
75
+ return ir.autoescape(token.escapeValue, [ir.legacyJS(bodyJS)]);
76
+ };
@@ -0,0 +1,92 @@
1
+ /*!
2
+ * Django `{% block %}` tag.
3
+ *
4
+ * Named override point for template inheritance:
5
+ *
6
+ * {% block <name> %}…{% endblock %}
7
+ * {% block <name> %}…{% endblock <name> %} (optional name on endblock)
8
+ *
9
+ * The block name is a bare identifier (dotted paths rejected) and passes
10
+ * the CVE-2023-25345 `_dangerousProps` guard. The parser captures the block
11
+ * in `template.blocks[name]` when it appears at the top level (the
12
+ * block-keying branch fires on `token.block && !stack.length`). The
13
+ * optional name after `{% endblock %}` is accepted for free: the parser's
14
+ * end-tag matcher keys on the first word (`endblock`) and ignores any
15
+ * trailing argument.
16
+ *
17
+ * Compile emits an `IRBlock` node with the body wrapped in IRLegacyJS. The
18
+ * backend's `Block` branch emits the body verbatim — block-override
19
+ * resolution happens at parse time via `engine.remapBlocks` /
20
+ * `importNonBlocks`, which substitutes the child's block content into the
21
+ * parent's token tree before backend emission.
22
+ *
23
+ * `{{ block.super }}` (Django's parent-block content access) is NOT yet
24
+ * supported — consistent with the Jinja2 sibling, which ships without
25
+ * `{{ super() }}`. The structural override (child block replaces parent
26
+ * block) works; rendering the parent's content inside the override is a
27
+ * deferred follow-up.
28
+ */
29
+
30
+ var ir = require('@rhinostone/swig-core/lib/ir');
31
+ var utils = require('@rhinostone/swig-core/lib/utils');
32
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
33
+
34
+ var lexer = require('../lexer');
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, and stashes it on `token.args` so the parser's top-level
42
+ * block-keying branch can pick it up via `token.args.join('')`.
43
+ *
44
+ * @param {string} str Tag body.
45
+ * @param {number} line Source line of the opening `{%`.
46
+ * @param {object} parser The Django parser module (unused — names are
47
+ * plain identifiers).
48
+ * @param {object} types Django lexer token-type enum.
49
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
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
+ * block name 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 nameTok = pos < tokens.length ? tokens[pos] : null;
62
+ if (!nameTok || nameTok.type !== types.VAR) {
63
+ utils.throwError('Expected block name in "block" tag', line, opts.filename);
64
+ }
65
+ if (nameTok.match.indexOf('.') !== -1) {
66
+ utils.throwError('Block name "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
67
+ }
68
+ if (_dangerousProps.indexOf(nameTok.match) !== -1) {
69
+ utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
70
+ }
71
+
72
+ pos += 1;
73
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
74
+ if (pos < tokens.length) {
75
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" after block name', line, opts.filename);
76
+ }
77
+
78
+ token.args = [nameTok.match];
79
+ return true;
80
+ };
81
+
82
+ /**
83
+ * Emit an IRBlock node. Body is the recursively-compiled content wrapped in
84
+ * IRLegacyJS. Mirrors the native / Jinja2 `block` compile shape so the
85
+ * backend's `Block` branch treats every frontend the same.
86
+ *
87
+ * @return {object} IRBlock node.
88
+ */
89
+ exports.compile = function (compiler, args, content, parents, options) {
90
+ var name = args.join('');
91
+ return ir.block(name, [ir.legacyJS(compiler(content, parents, options, name))]);
92
+ };
@@ -0,0 +1,42 @@
1
+ /*!
2
+ * Django `{% comment %}…{% endcomment %}` tag.
3
+ *
4
+ * {% comment %}…{% endcomment %}
5
+ * {% comment "optional note" %}…{% endcomment %}
6
+ *
7
+ * Discards everything between the tags. The content is NOT parsed — like
8
+ * `{% verbatim %}`, the splitter flips its `inRaw` flag on the `comment` tag
9
+ * name (cleared on `{% endcomment %}`), so `{{ … }}` / `{% … %}` inside a
10
+ * comment are captured as raw text rather than parsed. That makes
11
+ * `{% comment %}{% some_broken_tag %}{% endcomment %}` safe — the body never
12
+ * reaches the parser. compile then throws the captured content away, so the
13
+ * comment renders nothing.
14
+ *
15
+ * An optional note after `comment` (Django allows `{% comment "why" %}`) is
16
+ * accepted and ignored.
17
+ */
18
+
19
+ var ir = require('@rhinostone/swig-core/lib/ir');
20
+
21
+ exports.ends = true;
22
+ exports.block = false;
23
+
24
+ /**
25
+ * Accept the comment tag. Any argument (the optional note) is ignored —
26
+ * Django places no constraint on it.
27
+ *
28
+ * @return {boolean} Always `true`.
29
+ */
30
+ exports.parse = function () {
31
+ return true;
32
+ };
33
+
34
+ /**
35
+ * Emit nothing. The captured content (raw text, since the body was in the
36
+ * splitter's `inRaw` mode) is intentionally discarded.
37
+ *
38
+ * @return {object} An empty IRLegacyJS node.
39
+ */
40
+ exports.compile = function () {
41
+ return ir.legacyJS('');
42
+ };
@@ -0,0 +1,104 @@
1
+ /*!
2
+ * Django `{% cycle %}` tag (anonymous form).
3
+ *
4
+ * {% cycle "row1" "row2" %} (alternates on each invocation)
5
+ * {% cycle a b c %} (values may be expressions)
6
+ *
7
+ * Cycles through its values, emitting the next one on each invocation — so
8
+ * inside a `{% for %}` loop it alternates per iteration. State is a
9
+ * per-occurrence counter stored on `_ctx` under a unique key (the same
10
+ * `Math.random()` scheme the for-loop uses for its loopcache), so it
11
+ * persists across loop iterations within a render and resets on the next
12
+ * render (fresh `_ctx`). Two distinct `{% cycle %}` tags never share state.
13
+ *
14
+ * Values are evaluated fresh on each invocation (Django re-evaluates), and
15
+ * the emitted value is autoescaped per the enclosing region (`token.escape`).
16
+ *
17
+ * The named / reusable forms — `{% cycle a b as name %}`, `{% cycle name %}`,
18
+ * and the `silent` modifier — are not yet supported (they need cross-tag
19
+ * cycle-name tracking) and are rejected with a clear message.
20
+ */
21
+
22
+ var ir = require('@rhinostone/swig-core/lib/ir');
23
+ var utils = require('@rhinostone/swig-core/lib/utils');
24
+ var backend = require('@rhinostone/swig-core/lib/backend');
25
+
26
+ var lexer = require('../lexer');
27
+
28
+ exports.ends = false;
29
+ exports.block = false;
30
+
31
+ /*!
32
+ * True if a bare VAR token matching `word` appears anywhere in `tokens`.
33
+ * @private
34
+ */
35
+ function hasKeyword(tokens, types, word) {
36
+ var i;
37
+ for (i = 0; i < tokens.length; i += 1) {
38
+ if (tokens[i].type === types.VAR && tokens[i].match === word) { return true; }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * Parse the space-separated value list onto `token.irArgs` (an IRExpr[]).
45
+ *
46
+ * @param {string} str Tag body.
47
+ * @param {number} line Source line of the opening `{%`.
48
+ * @param {object} parser The Django parser module (exposes `parseExpr`).
49
+ * @param {object} types Django lexer token-type enum.
50
+ * @param {Array} stack Open-tag stack (unused — cycle has no body).
51
+ * @param {object} opts Per-call options (honors `opts.filename`).
52
+ * @param {object} swig Swig instance (unused).
53
+ * @param {object} token In-progress TagToken. Gets `token.irArgs`.
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
+ if (!tokens.length) {
59
+ utils.throwError('Expected at least one value in "cycle" tag', line, opts.filename);
60
+ }
61
+ if (hasKeyword(tokens, types, 'as')) {
62
+ utils.throwError('Named cycles (the "as name" form of "cycle") are not yet supported', line, opts.filename);
63
+ }
64
+ if (hasKeyword(tokens, types, 'silent')) {
65
+ utils.throwError('The "silent" modifier on "cycle" is not yet supported', line, opts.filename);
66
+ }
67
+
68
+ var args = [];
69
+ var pos = 0;
70
+ while (true) {
71
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
72
+ if (pos >= tokens.length) { break; }
73
+ var posOut = {};
74
+ var expr = parser.parseExpr(tokens.slice(pos), {}, posOut);
75
+ pos += posOut.pos;
76
+ args.push(expr);
77
+ }
78
+
79
+ token.irArgs = args;
80
+ return true;
81
+ };
82
+
83
+ /**
84
+ * Emit the stateful cycle: build the value array, advance a per-occurrence
85
+ * counter on `_ctx`, and append the current value (escaped per the region).
86
+ *
87
+ * @return {object} IRLegacyJS node.
88
+ */
89
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
90
+ var parts = [];
91
+ utils.each(token.irArgs, function (a) { parts.push(backend.emitExpr(a)); });
92
+
93
+ var key = ('__cycle' + Math.random()).replace(/\./g, '');
94
+ var value = '__cv[_ctx.' + key + ' % __cv.length]';
95
+ var emit = token.escape ? '_filters["e"](' + value + ')' : '_utils.coerceOutput(' + value + ')';
96
+
97
+ return ir.legacyJS(
98
+ '(function () {\n' +
99
+ ' var __cv = [' + parts.join(', ') + '];\n' +
100
+ ' _ctx.' + key + ' = (_ctx.' + key + ' === undefined) ? 0 : (_ctx.' + key + ' + 1);\n' +
101
+ ' _output += ' + emit + ';\n' +
102
+ '})();\n'
103
+ );
104
+ };
@@ -0,0 +1,33 @@
1
+ /*!
2
+ * Django `{% 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 the
8
+ * 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,30 @@
1
+ /*!
2
+ * Django `{% else %}` branch marker.
3
+ *
4
+ * Valid inside an `{% if %}` body (the final fallback branch). It carries no
5
+ * expression; the enclosing `if` tag's compile consumes the marker and
6
+ * splits its body at it. The marker never reaches the backend on its own —
7
+ * `compile` only fires if an `else` escapes its enclosing tag, which the
8
+ * parse-time stack check already prevents.
9
+ *
10
+ * Unlike the Jinja2 sibling, Django's `else` is NOT valid inside a `{% for %}`
11
+ * tag — Django's empty-iterable branch is the dedicated `{% empty %}` marker
12
+ * (added with the `for` tag in a later commit), not `{% else %}`.
13
+ */
14
+
15
+ var utils = require('@rhinostone/swig-core/lib/utils');
16
+
17
+ exports.ends = false;
18
+ exports.block = false;
19
+
20
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
21
+ var top = stack[stack.length - 1];
22
+ if (!top || top.name !== 'if') {
23
+ utils.throwError('"else" is only valid inside an "if" tag', line, opts.filename);
24
+ }
25
+ return true;
26
+ };
27
+
28
+ exports.compile = function () {
29
+ throw new Error('"else" used outside an "if" tag.');
30
+ };
@@ -0,0 +1,28 @@
1
+ /*!
2
+ * Django `{% empty %}` branch marker.
3
+ *
4
+ * Valid only inside a `{% for %}` body — Django's empty-iterable fallback
5
+ * (the equivalent of Jinja2's `{% for … %}{% else %}`). It carries no
6
+ * expression; the enclosing `for` tag's compile consumes the marker and
7
+ * splits its body at it into the loop body and the empty body. The marker
8
+ * never reaches the backend on its own — `compile` only fires if an
9
+ * `empty` escapes its enclosing `for`, which the parse-time stack check
10
+ * already prevents.
11
+ */
12
+
13
+ var utils = require('@rhinostone/swig-core/lib/utils');
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 !== 'for') {
21
+ utils.throwError('"empty" is only valid inside a "for" tag', line, opts.filename);
22
+ }
23
+ return true;
24
+ };
25
+
26
+ exports.compile = function () {
27
+ throw new Error('"empty" used outside a "for" tag.');
28
+ };
@@ -0,0 +1,95 @@
1
+ /*!
2
+ * Django `{% extends %}` tag.
3
+ *
4
+ * Declares a parent template for inheritance:
5
+ *
6
+ * {% extends "base.html" %} (static — compile-time)
7
+ * {% extends parent_var %} (dynamic — render-time, async path only)
8
+ *
9
+ * A static string-literal path is pushed as the single `token.args`
10
+ * element; the parser's splitter lifts it onto `template.parent`, and the
11
+ * engine's parent-chain resolution (`engine.getParents` + `remapBlocks` +
12
+ * `importNonBlocks`) walks the chain statically at compile time.
13
+ *
14
+ * A dynamic path is lowered through `parser.parseExpr` into an IRExpr and
15
+ * stashed on `token.irExpr.file`; the splitter lifts it onto the sibling
16
+ * `template.parentExpr` slot, which the async codegen path
17
+ * (`buildExtendsDeferred`) prefers over the string literal and resolves at
18
+ * render time via `_swig.getTemplate`. Dynamic extends therefore requires
19
+ * the async render path (`renderFile` with a callback against an async
20
+ * loader); on the synchronous render path a runtime-valued parent throws a
21
+ * clear "requires the async render path" error. Mirrors the cross-flavor
22
+ * dynamic-extends handling in native / Twig / Jinja2.
23
+ *
24
+ * Compile emits nothing — `extends` is a parse-time declaration carried via
25
+ * `template.parent` / `template.parentExpr` metadata; no runtime code is
26
+ * generated for the tag.
27
+ */
28
+
29
+ var utils = require('@rhinostone/swig-core/lib/utils');
30
+
31
+ var lexer = require('../lexer');
32
+
33
+ exports.ends = false;
34
+ exports.block = true;
35
+
36
+ /**
37
+ * Parse the `{% extends %}` tag body.
38
+ *
39
+ * A single STRING literal is stashed (quotes stripped) as `token.args[0]`
40
+ * for the splitter to lift onto `template.parent`. Any other expression
41
+ * (VAR, member access, ...) is a dynamic path: it is lowered through
42
+ * `parser.parseExpr` into an IRExpr on `token.irExpr.file` (the splitter
43
+ * lifts it onto `template.parentExpr`) and the raw source text is kept on
44
+ * `token.args[0]` so the sync `template.parent` string stays truthy (the
45
+ * async dispatch guard keys on it).
46
+ *
47
+ * @param {string} str Tag body.
48
+ * @param {number} line Source line of the opening `{%`.
49
+ * @param {object} parser The Django parser module (exposes `parseExpr`).
50
+ * @param {object} types Django lexer token-type enum.
51
+ * @param {Array} stack Open-tag stack (unused — extends has no body).
52
+ * @param {object} opts Per-call options (honors `opts.filename`).
53
+ * @param {object} swig Swig instance (unused).
54
+ * @param {object} token In-progress TagToken. Gets `token.args` (raw or
55
+ * unquoted path) and, for a dynamic path,
56
+ * `token.irExpr.file` (the lowered IRExpr).
57
+ * @return {boolean} Always `true` on success. Throws otherwise.
58
+ */
59
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
60
+ var tokens = lexer.read(utils.strip(str));
61
+ var pos = 0;
62
+
63
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
64
+ var pathTok = pos < tokens.length ? tokens[pos] : null;
65
+ if (!pathTok) {
66
+ utils.throwError('Expected parent template path in "extends" tag', line, opts.filename);
67
+ }
68
+
69
+ if (pathTok.type === types.STRING) {
70
+ pos += 1;
71
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
72
+ if (pos < tokens.length) {
73
+ utils.throwError('Unexpected token "' + tokens[pos].match + '" after parent path in "extends" tag', line, opts.filename);
74
+ }
75
+ token.args = [pathTok.match.replace(/^['"]|['"]$/g, '')];
76
+ return true;
77
+ }
78
+
79
+ // Dynamic path: lower to an IRExpr so it resolves at render time on the
80
+ // async codegen path. The raw source is kept on token.args[0] so the sync
81
+ // template.parent string stays truthy (the async dispatch guard keys on
82
+ // it); buildExtendsDeferred prefers template.parentExpr.
83
+ token.irExpr = { file: parser.parseExpr(tokens) };
84
+ token.args = [utils.strip(str)];
85
+ return true;
86
+ };
87
+
88
+ /**
89
+ * No-op compile. Extends is a parse-time declaration — the parent path
90
+ * lives on `template.parent`, which the engine's `getParents` reads during
91
+ * compile. The `{% extends %}` tag itself emits no runtime code.
92
+ *
93
+ * @return {undefined}
94
+ */
95
+ exports.compile = function () {};
@@ -0,0 +1,101 @@
1
+ /*!
2
+ * Django `{% firstof %}` tag.
3
+ *
4
+ * {% firstof a b c %} (output the first "truthy" value)
5
+ * {% firstof a b "fallback" %} (a trailing literal is just another value)
6
+ *
7
+ * Outputs the first argument that evaluates truthy, or the empty string if
8
+ * none do. Each argument is lowered through `parser.parseExpr`, so variables,
9
+ * literals, and member access all work. A missing variable resolves to `""`
10
+ * (via emitVarRef) and so counts as falsy — exactly like Django.
11
+ *
12
+ * The output is autoescaped according to the enclosing region's autoescape
13
+ * state (the same `e`-filter tail a `{{ … }}` would get), read from
14
+ * `token.escape`. There is no user filter chain on a firstof value.
15
+ *
16
+ * The `{% firstof a b as var %}` assign-to-variable form (Django 1.9+) is not
17
+ * yet supported and is rejected with a clear message.
18
+ */
19
+
20
+ var ir = require('@rhinostone/swig-core/lib/ir');
21
+ var utils = require('@rhinostone/swig-core/lib/utils');
22
+
23
+ var lexer = require('../lexer');
24
+
25
+ exports.ends = false;
26
+ exports.block = false;
27
+
28
+ /*!
29
+ * True if a bare VAR token matching `word` appears anywhere in `tokens`.
30
+ * firstof / cycle arguments are simple values, so no paren-depth tracking is
31
+ * needed to spot a trailing keyword. @private
32
+ */
33
+ function hasKeyword(tokens, types, word) {
34
+ var i;
35
+ for (i = 0; i < tokens.length; i += 1) {
36
+ if (tokens[i].type === types.VAR && tokens[i].match === word) { return true; }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Parse the space-separated value list onto `token.irArgs` (an IRExpr[]).
43
+ *
44
+ * @param {string} str Tag body.
45
+ * @param {number} line Source line of the opening `{%`.
46
+ * @param {object} parser The Django parser module (exposes `parseExpr`).
47
+ * @param {object} types Django lexer token-type enum.
48
+ * @param {Array} stack Open-tag stack (unused — firstof has no body).
49
+ * @param {object} opts Per-call options (honors `opts.filename`).
50
+ * @param {object} swig Swig instance (unused).
51
+ * @param {object} token In-progress TagToken. Gets `token.irArgs`.
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
+ if (!tokens.length) {
57
+ utils.throwError('Expected at least one value in "firstof" tag', line, opts.filename);
58
+ }
59
+ if (hasKeyword(tokens, types, 'as')) {
60
+ utils.throwError('The "as" (assign-to-variable) form of "firstof" is not yet supported', line, opts.filename);
61
+ }
62
+
63
+ var args = [];
64
+ var pos = 0;
65
+ while (true) {
66
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
67
+ if (pos >= tokens.length) { break; }
68
+ var posOut = {};
69
+ var expr = parser.parseExpr(tokens.slice(pos), {}, posOut);
70
+ pos += posOut.pos;
71
+ args.push(expr);
72
+ }
73
+
74
+ token.irArgs = args;
75
+ return true;
76
+ };
77
+
78
+ /**
79
+ * Emit an IROutput whose expression is the values chained with `||` and
80
+ * terminated by an empty-string literal (`a || b || c || ""`), so the result
81
+ * is the first truthy value or `""`. The `e` filter tail is appended when the
82
+ * region is autoescaping.
83
+ *
84
+ * @return {object} IROutput node.
85
+ */
86
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
87
+ var exprs = token.irArgs.slice();
88
+ exprs.push(ir.literal('string', ''));
89
+
90
+ var acc = exprs[exprs.length - 1];
91
+ var i;
92
+ for (i = exprs.length - 2; i >= 0; i -= 1) {
93
+ acc = ir.binaryOp('||', exprs[i], acc);
94
+ }
95
+
96
+ var tail;
97
+ if (token.escape) {
98
+ tail = [ir.filterCall('e', (typeof token.escape === 'string') ? [ir.literal('string', token.escape)] : undefined)];
99
+ }
100
+ return ir.output(acc, tail);
101
+ };