@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.
- package/README.md +57 -0
- package/lib/async/pre-walker.js +261 -0
- package/lib/filters.js +1241 -0
- package/lib/index.js +345 -0
- package/lib/lexer.js +302 -0
- package/lib/parser.js +739 -0
- package/lib/tags/autoescape.js +76 -0
- package/lib/tags/block.js +92 -0
- package/lib/tags/comment.js +42 -0
- package/lib/tags/cycle.js +104 -0
- package/lib/tags/elif.js +33 -0
- package/lib/tags/else.js +30 -0
- package/lib/tags/empty.js +28 -0
- package/lib/tags/extends.js +95 -0
- package/lib/tags/firstof.js +101 -0
- package/lib/tags/for.js +262 -0
- package/lib/tags/if.js +82 -0
- package/lib/tags/include.js +200 -0
- package/lib/tags/index.js +37 -0
- package/lib/tags/spaceless.js +69 -0
- package/lib/tags/verbatim.js +51 -0
- package/lib/tags/with.js +157 -0
- package/lib/tokentypes.js +87 -0
- package/package.json +30 -0
|
@@ -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
|
+
};
|
package/lib/tags/elif.js
ADDED
|
@@ -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
|
+
};
|
package/lib/tags/else.js
ADDED
|
@@ -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
|
+
};
|