@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,51 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django `{% verbatim %}…{% endverbatim %}` tag.
|
|
3
|
+
*
|
|
4
|
+
* Preserves arbitrary template-like content as literal output. Inside a
|
|
5
|
+
* verbatim block, `{{ … }}`, `{% … %}` (other than `{% endverbatim %}`), and
|
|
6
|
+
* `{# … #}` are NOT parsed — the splitter in `parser.js` flips an `inRaw`
|
|
7
|
+
* flag (set on the `verbatim` tag name, cleared on `{% endverbatim %}`) that
|
|
8
|
+
* bypasses the variable / tag / comment branches and wraps each chunk as
|
|
9
|
+
* `ir.text`, so the content array handed to this tag's compile is already a
|
|
10
|
+
* list of IRText nodes.
|
|
11
|
+
*
|
|
12
|
+
* Takes no arguments. Extra tokens after `verbatim` are rejected at parse
|
|
13
|
+
* time with a filename-aware throw.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
17
|
+
|
|
18
|
+
exports.ends = true;
|
|
19
|
+
exports.block = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reject any tokens after the `verbatim` keyword. The splitter owns all
|
|
23
|
+
* content-capture behaviour via its `inRaw` flag, so this handler only has
|
|
24
|
+
* to validate the tag's own argument list (which must be empty).
|
|
25
|
+
*
|
|
26
|
+
* @param {string} str Tag body (everything after `verbatim`).
|
|
27
|
+
* @param {number} line Source line of the opening `{%`.
|
|
28
|
+
* @param {object} parser The Django parser module (unused).
|
|
29
|
+
* @param {object} types Django lexer token-type enum (unused).
|
|
30
|
+
* @param {Array} stack Open-tag stack (parser.js manages the push).
|
|
31
|
+
* @param {object} opts Per-call options (honors `opts.filename`).
|
|
32
|
+
* @return {boolean} Always `true` on success. Throws otherwise.
|
|
33
|
+
*/
|
|
34
|
+
exports.parse = function (str, line, parser, types, stack, opts) {
|
|
35
|
+
var stripped = utils.strip(str || '');
|
|
36
|
+
if (stripped.length > 0) {
|
|
37
|
+
utils.throwError('Unexpected token "' + stripped + '" after "verbatim"', line, opts.filename);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the captured content array unchanged. Each item is already an
|
|
44
|
+
* IRText node (the splitter wrapped it while `inRaw` was set), so the
|
|
45
|
+
* backend's emit loop can iterate and emit without any further wrapping.
|
|
46
|
+
*
|
|
47
|
+
* @return {Array} Content node list.
|
|
48
|
+
*/
|
|
49
|
+
exports.compile = function (compiler, args, content) {
|
|
50
|
+
return content;
|
|
51
|
+
};
|
package/lib/tags/with.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django `{% with %}` tag.
|
|
3
|
+
*
|
|
4
|
+
* {% with a=1 b=total %}…{% endwith %} (modern — space-separated pairs)
|
|
5
|
+
* {% with business.total as t %}…{% endwith %} (legacy single-binding form)
|
|
6
|
+
*
|
|
7
|
+
* Binds one or more names in a new inner scope for the body. Each value is
|
|
8
|
+
* lowered through `parser.parseExpr` and collected into an object literal
|
|
9
|
+
* that the backend's `With` branch merges over a shallow copy of the outer
|
|
10
|
+
* context — so an assignment's value sees the enclosing scope (not its
|
|
11
|
+
* sibling assignments), and the inner bindings do not leak past
|
|
12
|
+
* `{% endwith %}`. The region is never isolated (Django has no `only` here).
|
|
13
|
+
*
|
|
14
|
+
* The modern form uses space-separated `name=value` pairs (Django's shape,
|
|
15
|
+
* NOT Jinja2's comma-separated list). The legacy form is a single
|
|
16
|
+
* `<expr> as <name>`. Each assignment target is a bare identifier — dotted
|
|
17
|
+
* paths and CVE-2023-25345 prototype-chain names are rejected before the
|
|
18
|
+
* binding lands.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
22
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
23
|
+
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
24
|
+
|
|
25
|
+
var lexer = require('../lexer');
|
|
26
|
+
|
|
27
|
+
/*!
|
|
28
|
+
* Strip WHITESPACE tokens from both ends of a slice range. @private
|
|
29
|
+
*/
|
|
30
|
+
function sliceTrim(tokens, start, end, types) {
|
|
31
|
+
while (start < end && tokens[start].type === types.WHITESPACE) { start += 1; }
|
|
32
|
+
while (end > start && tokens[end - 1].type === types.WHITESPACE) { end -= 1; }
|
|
33
|
+
return tokens.slice(start, end);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*!
|
|
37
|
+
* Find the first depth-0 VAR token whose match equals `word`. @private
|
|
38
|
+
*/
|
|
39
|
+
function findKeyword(tokens, types, word) {
|
|
40
|
+
var depth = 0, i, tk;
|
|
41
|
+
for (i = 0; i < tokens.length; i += 1) {
|
|
42
|
+
tk = tokens[i];
|
|
43
|
+
if (tk.type === types.PARENOPEN || tk.type === types.BRACKETOPEN ||
|
|
44
|
+
tk.type === types.CURLYOPEN || tk.type === types.FUNCTION) { depth += 1; continue; }
|
|
45
|
+
if (tk.type === types.PARENCLOSE || tk.type === types.BRACKETCLOSE ||
|
|
46
|
+
tk.type === types.CURLYCLOSE) { depth -= 1; continue; }
|
|
47
|
+
if (depth !== 0 || tk.type !== types.VAR) { continue; }
|
|
48
|
+
if (tk.match === word) { return i; }
|
|
49
|
+
}
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*!
|
|
54
|
+
* Validate an assignment target name (bare identifier, not a dangerous
|
|
55
|
+
* prototype-chain property). @private
|
|
56
|
+
*/
|
|
57
|
+
function guardName(name, line, opts) {
|
|
58
|
+
if (name.indexOf('.') !== -1) {
|
|
59
|
+
utils.throwError('"with" assignment target "' + name + '" must be a bare identifier', line, opts.filename);
|
|
60
|
+
}
|
|
61
|
+
if (_dangerousProps.indexOf(name) !== -1) {
|
|
62
|
+
utils.throwError('Unsafe "with" assignment to "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/*!
|
|
67
|
+
* Parse space-separated `name = value` pairs from a token slice into IR
|
|
68
|
+
* object properties. Mirrors the include tag's k=v parsing: each value is
|
|
69
|
+
* one expression (parseExpr stops at the next bare name). @private
|
|
70
|
+
*/
|
|
71
|
+
function parsePairs(tokens, parser, types, line, opts) {
|
|
72
|
+
var props = [];
|
|
73
|
+
var pos = 0;
|
|
74
|
+
function skipWS() { while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; } }
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
skipWS();
|
|
78
|
+
if (pos >= tokens.length) { break; }
|
|
79
|
+
var nameTok = tokens[pos];
|
|
80
|
+
if (nameTok.type !== types.VAR) {
|
|
81
|
+
utils.throwError('Expected an assignment name in "with" tag', line, opts.filename);
|
|
82
|
+
}
|
|
83
|
+
var j = pos + 1;
|
|
84
|
+
while (j < tokens.length && tokens[j].type === types.WHITESPACE) { j += 1; }
|
|
85
|
+
if (!(j < tokens.length && tokens[j].type === types.ASSIGNMENT && tokens[j].match === '=')) {
|
|
86
|
+
utils.throwError('Expected "=" after "' + nameTok.match + '" in "with" tag', line, opts.filename);
|
|
87
|
+
}
|
|
88
|
+
guardName(nameTok.match, line, opts);
|
|
89
|
+
pos = j + 1;
|
|
90
|
+
skipWS();
|
|
91
|
+
var posOut = {};
|
|
92
|
+
var valExpr = parser.parseExpr(tokens.slice(pos), {}, posOut);
|
|
93
|
+
pos += posOut.pos;
|
|
94
|
+
props.push(ir.objectProperty(ir.literal('string', nameTok.match), valExpr));
|
|
95
|
+
}
|
|
96
|
+
return props;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
exports.ends = true;
|
|
100
|
+
exports.block = false;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse the `{% with %}` tag body into an object literal on `token.irExpr`.
|
|
104
|
+
* Supports both the modern space-separated `name=value` form and the legacy
|
|
105
|
+
* `<expr> as <name>` form.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} str Tag body.
|
|
108
|
+
* @param {number} line Source line of the opening `{%`.
|
|
109
|
+
* @param {object} parser The Django parser module (exposes `parseExpr`).
|
|
110
|
+
* @param {object} types Django lexer token-type enum.
|
|
111
|
+
* @param {Array} stack Open-tag stack (parser.js manages the push).
|
|
112
|
+
* @param {object} opts Per-call options (honors `opts.filename`).
|
|
113
|
+
* @param {object} swig Swig instance (unused).
|
|
114
|
+
* @param {object} token In-progress TagToken. Gets `token.irExpr`.
|
|
115
|
+
* @return {boolean} Always `true` on success. Throws otherwise.
|
|
116
|
+
*/
|
|
117
|
+
exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
118
|
+
var tokens = lexer.read(utils.strip(str));
|
|
119
|
+
if (!tokens.length) {
|
|
120
|
+
utils.throwError('Expected at least one assignment in "with" tag', line, opts.filename);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var props;
|
|
124
|
+
var asIdx = findKeyword(tokens, types, 'as');
|
|
125
|
+
|
|
126
|
+
if (asIdx !== -1) {
|
|
127
|
+
// Legacy `<expr> as <name>` form.
|
|
128
|
+
var exprTokens = sliceTrim(tokens, 0, asIdx, types);
|
|
129
|
+
var nameTokens = sliceTrim(tokens, asIdx + 1, tokens.length, types);
|
|
130
|
+
if (!exprTokens.length) {
|
|
131
|
+
utils.throwError('Expected an expression before "as" in "with" tag', line, opts.filename);
|
|
132
|
+
}
|
|
133
|
+
if (nameTokens.length !== 1 || nameTokens[0].type !== types.VAR) {
|
|
134
|
+
utils.throwError('Expected a single variable name after "as" in "with" tag', line, opts.filename);
|
|
135
|
+
}
|
|
136
|
+
guardName(nameTokens[0].match, line, opts);
|
|
137
|
+
props = [ir.objectProperty(ir.literal('string', nameTokens[0].match), parser.parseExpr(exprTokens))];
|
|
138
|
+
} else {
|
|
139
|
+
props = parsePairs(tokens, parser, types, line, opts);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
token.irExpr = ir.objectLiteral(props);
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Emit an IRWith node carrying the assignment object literal and the
|
|
148
|
+
* recursively-compiled body wrapped in IRLegacyJS. The region is never
|
|
149
|
+
* isolated; the backend's `With` branch merges the context over a shallow
|
|
150
|
+
* copy of `_ctx` for the body's lexical scope.
|
|
151
|
+
*
|
|
152
|
+
* @return {object} IRWith node.
|
|
153
|
+
*/
|
|
154
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
155
|
+
var bodyJS = compiler(content, parents, options, blockName);
|
|
156
|
+
return ir.withStmt(token.irExpr, false, [ir.legacyJS(bodyJS)]);
|
|
157
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Django lexer token type enum — the contract between the Django lexer and
|
|
3
|
+
* the Django parser in @rhinostone/swig-django.
|
|
4
|
+
*
|
|
5
|
+
* Numeric IDs in the shared range (0–25, 100) mirror
|
|
6
|
+
* @rhinostone/swig-core/lib/tokentypes by design: Django and native Swig
|
|
7
|
+
* lower to the same swig-core IR, and aligning the IDs keeps shared
|
|
8
|
+
* consumers (e.g. CVE-2023-25345 `_dangerousProps` enforcement) flavor-
|
|
9
|
+
* agnostic. The Django parser is its own module — it does not inherit from
|
|
10
|
+
* swig-core's TokenParser — but the cognitive overhead of re-mapping IDs
|
|
11
|
+
* across flavors is not worth the freedom.
|
|
12
|
+
*
|
|
13
|
+
* Django-only IDs (30+) are reserved here. The Django Template Language has
|
|
14
|
+
* none of Jinja2's `~` concat, `**` power, or `//` floor-division operators
|
|
15
|
+
* (so those Jinja2-only IDs are absent), but it does have `is` / `is not` —
|
|
16
|
+
* as an *identity* comparison, not Jinja2's test calls — so IS / ISNOT keep
|
|
17
|
+
* the same IDs as the Jinja2 sibling for consistency. `None` lexes to its
|
|
18
|
+
* own NONE token (Django's null literal), distinct from BOOL (`True` /
|
|
19
|
+
* `False`).
|
|
20
|
+
*
|
|
21
|
+
* @readonly
|
|
22
|
+
* @enum {number}
|
|
23
|
+
*/
|
|
24
|
+
module.exports = {
|
|
25
|
+
/** Whitespace */
|
|
26
|
+
WHITESPACE: 0,
|
|
27
|
+
/** Plain string literal */
|
|
28
|
+
STRING: 1,
|
|
29
|
+
/** Variable filter call with arguments — `|name(...)` (unused by Django; kept for ID alignment) */
|
|
30
|
+
FILTER: 2,
|
|
31
|
+
/** Variable filter call — `|name` (Django filters grab an optional `:arg` in the parser) */
|
|
32
|
+
FILTEREMPTY: 3,
|
|
33
|
+
/** Function call with arguments — `name(...)` */
|
|
34
|
+
FUNCTION: 4,
|
|
35
|
+
/** Function call with no arguments — `name()` */
|
|
36
|
+
FUNCTIONEMPTY: 5,
|
|
37
|
+
/** Open parenthesis */
|
|
38
|
+
PARENOPEN: 6,
|
|
39
|
+
/** Close parenthesis */
|
|
40
|
+
PARENCLOSE: 7,
|
|
41
|
+
/** Comma */
|
|
42
|
+
COMMA: 8,
|
|
43
|
+
/** Variable identifier */
|
|
44
|
+
VAR: 9,
|
|
45
|
+
/** Numeric literal */
|
|
46
|
+
NUMBER: 10,
|
|
47
|
+
/** Math operator (+, -, *, /, %) */
|
|
48
|
+
OPERATOR: 11,
|
|
49
|
+
/** Open square bracket */
|
|
50
|
+
BRACKETOPEN: 12,
|
|
51
|
+
/** Close square bracket */
|
|
52
|
+
BRACKETCLOSE: 13,
|
|
53
|
+
/** Dot-key accessor — `.key` */
|
|
54
|
+
DOTKEY: 14,
|
|
55
|
+
/** Open square bracket at the start of an array literal */
|
|
56
|
+
ARRAYOPEN: 15,
|
|
57
|
+
/** Open curly brace */
|
|
58
|
+
CURLYOPEN: 17,
|
|
59
|
+
/** Close curly brace */
|
|
60
|
+
CURLYCLOSE: 18,
|
|
61
|
+
/** Colon — object-literal key/value separator, slice subscript, and Django filter argument separator (`|date:"Y"`) */
|
|
62
|
+
COLON: 19,
|
|
63
|
+
/** JavaScript-valid comparator (==, !=, <=, etc.) */
|
|
64
|
+
COMPARATOR: 20,
|
|
65
|
+
/** Boolean logic (`and`, `or`, `&&`, `||`) */
|
|
66
|
+
LOGIC: 21,
|
|
67
|
+
/** Boolean negation (`not`, `!`) */
|
|
68
|
+
NOT: 22,
|
|
69
|
+
/** Boolean literal (`True`, `False`) */
|
|
70
|
+
BOOL: 23,
|
|
71
|
+
/** Variable assignment (`=`, `+=`, `-=`, `*=`, `/=`) */
|
|
72
|
+
ASSIGNMENT: 24,
|
|
73
|
+
/** Method call open — internal */
|
|
74
|
+
METHODOPEN: 25,
|
|
75
|
+
|
|
76
|
+
/* ---- Django-only token IDs ---- */
|
|
77
|
+
|
|
78
|
+
/** Django identity operator — `is` (lowers to `===`, NOT a Jinja2-style test call) */
|
|
79
|
+
IS: 33,
|
|
80
|
+
/** Django negated identity operator — `is not` (lowers to `!==`) */
|
|
81
|
+
ISNOT: 34,
|
|
82
|
+
/** Django null literal — `None` (lowers to a JS `null` literal) */
|
|
83
|
+
NONE: 35,
|
|
84
|
+
|
|
85
|
+
/** Unknown token */
|
|
86
|
+
UNKNOWN: 100
|
|
87
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rhinostone/swig-django",
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "Django-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"template",
|
|
7
|
+
"templating",
|
|
8
|
+
"django",
|
|
9
|
+
"swig",
|
|
10
|
+
"swig-django",
|
|
11
|
+
"frontend"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/gina-io/swig.git",
|
|
16
|
+
"directory": "packages/swig-django"
|
|
17
|
+
},
|
|
18
|
+
"author": "Rhinostone <contact@gina.io>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"main": "lib/index.js",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=12"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@rhinostone/swig-core": "2.7.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|