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