@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
package/lib/tags/for.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django `{% for %}` tag.
|
|
3
|
+
*
|
|
4
|
+
* {% for <val> in <iterable> %}…{% endfor %}
|
|
5
|
+
* {% for <key>, <val> in <iterable> %}…{% endfor %}
|
|
6
|
+
* {% for <val> in <iterable> %}…{% empty %}…{% endfor %}
|
|
7
|
+
*
|
|
8
|
+
* Surfaces Django's `forloop` context object — `forloop.counter` /
|
|
9
|
+
* `counter0` / `revcounter` / `revcounter0` / `first` / `last` /
|
|
10
|
+
* `parentloop` — by setting the opt-in `loopName` / `loopFields` /
|
|
11
|
+
* `loopParent` flags on the IRFor node. The swig-core backend owns the
|
|
12
|
+
* loopcache + `_utils.each` scaffolding (the same IIFE every flavor's `for`
|
|
13
|
+
* loop compiles to); the flags only rename the emitted loop var + counter
|
|
14
|
+
* fields and expose the enclosing loop, so nested-loop safety and the
|
|
15
|
+
* for-empty handling come for free.
|
|
16
|
+
*
|
|
17
|
+
* Loop variable names must be bare identifiers — dotted paths are rejected
|
|
18
|
+
* at parse time, and the CVE-2023-25345 `_dangerousProps` guard runs on
|
|
19
|
+
* every bound name (key and val).
|
|
20
|
+
*
|
|
21
|
+
* Django's empty-iterable branch is `{% empty %}` (NOT `{% else %}`, which
|
|
22
|
+
* is `if`-only here); it lexes as its own marker token (tags/empty.js) and
|
|
23
|
+
* compile splits the content at it into the loop body and the empty body.
|
|
24
|
+
*
|
|
25
|
+
* Django's dict-iteration pseudo-methods are supported on the iterable —
|
|
26
|
+
* `{% for k in d.keys %}`, `{% for v in d.values %}`, and the canonical
|
|
27
|
+
* `{% for key, value in d.items %}` (see rewriteDictIterable). The one-name
|
|
28
|
+
* `{% for pair in d.items %}` tuple form is rejected with a clear message.
|
|
29
|
+
*
|
|
30
|
+
* The `reversed` modifier (`{% for x in items reversed %}`) is not yet
|
|
31
|
+
* supported; it is detected and rejected with a clear message rather than
|
|
32
|
+
* the cryptic `Unexpected token "reversed"` that parseExpr's
|
|
33
|
+
* full-consumption check would otherwise raise.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
37
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
38
|
+
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
39
|
+
|
|
40
|
+
var lexer = require('../lexer');
|
|
41
|
+
|
|
42
|
+
/*!
|
|
43
|
+
* Django `forloop` ↔ swig `loop` counter-field map. `first` / `last` are
|
|
44
|
+
* identical in both, so only the four counters are renamed; `parentloop`
|
|
45
|
+
* is surfaced separately via the `loopParent` flag. @private
|
|
46
|
+
*/
|
|
47
|
+
var DJANGO_LOOP_FIELDS = {
|
|
48
|
+
index: 'counter',
|
|
49
|
+
index0: 'counter0',
|
|
50
|
+
revindex: 'revcounter',
|
|
51
|
+
revindex0: 'revcounter0'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/*!
|
|
55
|
+
* Django dict-iteration pseudo-methods recognized on a `for` iterable.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
var DICT_PSEUDO_METHODS = { keys: true, values: true, items: true };
|
|
59
|
+
|
|
60
|
+
/*!
|
|
61
|
+
* Build a resolve-flagged VarRef for an object path (the iterable with its
|
|
62
|
+
* trailing pseudo-method segment removed) so the object itself is resolved
|
|
63
|
+
* Django-style by the swig-core variable resolver. @private
|
|
64
|
+
*/
|
|
65
|
+
function resolveVar(path) {
|
|
66
|
+
var n = ir.varRef(path);
|
|
67
|
+
n.resolve = true;
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*!
|
|
72
|
+
* Rewrite a `for` iterable that ends in a Django dict-iteration
|
|
73
|
+
* pseudo-method (`.keys` / `.values` / `.items`), so the loop walks the
|
|
74
|
+
* mapping the way Django does. With the variable resolver in place a trailing
|
|
75
|
+
* `.keys` / `.values` / `.items` would otherwise resolve to a (usually
|
|
76
|
+
* missing) object property, yielding an empty loop.
|
|
77
|
+
*
|
|
78
|
+
* - `.keys` → `_utils.keys(obj)` (a plain key array; a one-name `for`
|
|
79
|
+
* binds each key).
|
|
80
|
+
* - `.values` → the object itself; swig's one-name object iteration binds
|
|
81
|
+
* each VALUE (`_utils.each(obj, fn(value, key))`), matching
|
|
82
|
+
* Django.
|
|
83
|
+
* - `.items` → the object itself, walked with the two-name form, so swig
|
|
84
|
+
* binds key + value per entry exactly as Django's two-name
|
|
85
|
+
* `{% for k, v in d.items %}`. The one-name tuple form is
|
|
86
|
+
* rejected (there is no `_utils.entries` to synthesize a
|
|
87
|
+
* (key, value) tuple sequence flavor-locally).
|
|
88
|
+
*
|
|
89
|
+
* Anything that is not a resolved user-variable path of length >= 2 ending in
|
|
90
|
+
* one of the three names is returned unchanged. Documented divergence: the
|
|
91
|
+
* interception is unconditional, so if `obj` has a literal key named
|
|
92
|
+
* `keys` / `values` / `items`, the pseudo-method wins (Django's dict-key
|
|
93
|
+
* precedence is not honored for those three names).
|
|
94
|
+
*
|
|
95
|
+
* @param {object} it Parsed iterable IRExpr.
|
|
96
|
+
* @param {boolean} hasTwoNames Whether the loop declared `key, val`.
|
|
97
|
+
* @param {number} line Source line, for error reporting.
|
|
98
|
+
* @param {string} filename Template filename, for error reporting.
|
|
99
|
+
* @return {object} The (possibly rewritten) iterable IRExpr.
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
function rewriteDictIterable(it, hasTwoNames, line, filename) {
|
|
103
|
+
if (!it || it.type !== 'VarRef' || !it.resolve || it.path.length < 2) {
|
|
104
|
+
return it;
|
|
105
|
+
}
|
|
106
|
+
var lastSeg = it.path[it.path.length - 1];
|
|
107
|
+
if (!DICT_PSEUDO_METHODS.hasOwnProperty(lastSeg)) {
|
|
108
|
+
return it;
|
|
109
|
+
}
|
|
110
|
+
var objPath = it.path.slice(0, -1);
|
|
111
|
+
if (lastSeg === 'keys') {
|
|
112
|
+
// The synthetic `_utils.keys` callee is intentionally NOT resolve-flagged
|
|
113
|
+
// (emitFnCall reads a callee path directly, never through emitVarRef).
|
|
114
|
+
return ir.fnCall(ir.varRef(['_utils', 'keys']), [resolveVar(objPath)]);
|
|
115
|
+
}
|
|
116
|
+
if (lastSeg === 'items' && !hasTwoNames) {
|
|
117
|
+
utils.throwError('The "items" pseudo-method in a "for" tag requires two loop variables — use {% for key, value in ' + objPath.join('.') + '.items %}', line, filename);
|
|
118
|
+
}
|
|
119
|
+
// `.values` (one-name → value) and `.items` (two-name → key, value) both
|
|
120
|
+
// walk the object directly through swig's object iteration.
|
|
121
|
+
return resolveVar(objPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
exports.ends = true;
|
|
125
|
+
exports.block = false;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse the `{% for %}` tag body. Extracts the binding names (val or
|
|
129
|
+
* key+val), validates them, then lowers the iterable expression. Names go
|
|
130
|
+
* on `token.args` (`[val]` or `[key, val]`); the iterable IR on
|
|
131
|
+
* `token.irExpr`.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} str Tag body.
|
|
134
|
+
* @param {number} line Source line of the opening `{%`.
|
|
135
|
+
* @param {object} parser The Django parser module (exposes `parseExpr`).
|
|
136
|
+
* @param {object} types Django lexer token-type enum.
|
|
137
|
+
* @param {Array} stack Open-tag stack (parser.js manages the push).
|
|
138
|
+
* @param {object} opts Per-call options (honors `opts.filename`).
|
|
139
|
+
* @param {object} swig Swig instance (unused).
|
|
140
|
+
* @param {object} token In-progress TagToken.
|
|
141
|
+
* @return {boolean} Always `true` on success. Throws otherwise.
|
|
142
|
+
*/
|
|
143
|
+
exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
144
|
+
var tokens = lexer.read(utils.strip(str));
|
|
145
|
+
var pos = 0;
|
|
146
|
+
|
|
147
|
+
function peek() {
|
|
148
|
+
while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
|
|
149
|
+
return pos < tokens.length ? tokens[pos] : null;
|
|
150
|
+
}
|
|
151
|
+
function consume() {
|
|
152
|
+
var t = peek();
|
|
153
|
+
if (t) { pos += 1; }
|
|
154
|
+
return t;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function takeName() {
|
|
158
|
+
var tok = consume();
|
|
159
|
+
if (!tok || tok.type !== types.VAR) {
|
|
160
|
+
utils.throwError('Expected loop variable in "for" tag', line, opts.filename);
|
|
161
|
+
}
|
|
162
|
+
if (tok.match.indexOf('.') !== -1) {
|
|
163
|
+
utils.throwError('Loop variable "' + tok.match + '" must be a bare identifier in "for" tag', line, opts.filename);
|
|
164
|
+
}
|
|
165
|
+
if (_dangerousProps.indexOf(tok.match) !== -1) {
|
|
166
|
+
utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
|
|
167
|
+
}
|
|
168
|
+
return tok.match;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var first = takeName();
|
|
172
|
+
var val = first;
|
|
173
|
+
var key;
|
|
174
|
+
|
|
175
|
+
if (peek() && peek().type === types.COMMA) {
|
|
176
|
+
consume();
|
|
177
|
+
key = first;
|
|
178
|
+
val = takeName();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The lexer's COMPARATOR rule requires a trailing `\s` on `in`, so
|
|
182
|
+
// `{% for x in %}` (nothing after `in`) lexes `in` as a VAR. Match on the
|
|
183
|
+
// literal string so the error stays "Expected iterable" for that shape.
|
|
184
|
+
var inTok = consume();
|
|
185
|
+
if (!inTok || inTok.match !== 'in' || (inTok.type !== types.COMPARATOR && inTok.type !== types.VAR)) {
|
|
186
|
+
utils.throwError('Expected "in" in "for" tag', line, opts.filename);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
|
|
190
|
+
|
|
191
|
+
var iterableTokens = tokens.slice(pos);
|
|
192
|
+
if (!iterableTokens.length) {
|
|
193
|
+
utils.throwError('Expected iterable after "in" in "for" tag', line, opts.filename);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// `{% for x in items reversed %}` — trailing `reversed` modifier (a real
|
|
197
|
+
// Django feature, not yet supported). Detect a trailing bare `reversed`
|
|
198
|
+
// VAR with an iterable before it and reject clearly. A lone `reversed`
|
|
199
|
+
// (`{% for x in reversed %}`) is a legitimate variable name and is left
|
|
200
|
+
// to parseExpr.
|
|
201
|
+
var lastTok = iterableTokens[iterableTokens.length - 1];
|
|
202
|
+
if (iterableTokens.length > 1 && lastTok.type === types.VAR && lastTok.match === 'reversed') {
|
|
203
|
+
utils.throwError('The "reversed" modifier in the "for" tag is not yet supported', line, opts.filename);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
token.args = key !== undefined ? [key, val] : [val];
|
|
207
|
+
token.irExpr = rewriteDictIterable(parser.parseExpr(iterableTokens), key !== undefined, line, opts.filename);
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Emit an IRFor node, splitting `content` at an `{% empty %}` marker into
|
|
213
|
+
* the loop body and the empty body, and setting the Django `forloop`
|
|
214
|
+
* opt-in flags. The backend's `For` branch owns the loopcache +
|
|
215
|
+
* `_utils.each` scaffolding.
|
|
216
|
+
*
|
|
217
|
+
* @return {object} IRFor node.
|
|
218
|
+
*/
|
|
219
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
220
|
+
var val, key;
|
|
221
|
+
if (args.length === 2) {
|
|
222
|
+
key = args[0];
|
|
223
|
+
val = args[1];
|
|
224
|
+
} else {
|
|
225
|
+
val = args[0];
|
|
226
|
+
key = '__k';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
var loopContent = [],
|
|
230
|
+
emptyContent = null,
|
|
231
|
+
filename = options && options.filename;
|
|
232
|
+
|
|
233
|
+
utils.each(content, function (child) {
|
|
234
|
+
if (child && child.name === 'empty') {
|
|
235
|
+
if (emptyContent !== null) {
|
|
236
|
+
utils.throwError('Multiple "empty" branches in "for" tag', null, filename);
|
|
237
|
+
}
|
|
238
|
+
emptyContent = [];
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (emptyContent !== null) {
|
|
242
|
+
emptyContent.push(child);
|
|
243
|
+
} else {
|
|
244
|
+
loopContent.push(child);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
var bodyJS = compiler(loopContent, parents, options, blockName);
|
|
249
|
+
var emptyBody;
|
|
250
|
+
if (emptyContent !== null) {
|
|
251
|
+
emptyBody = [ir.legacyJS(compiler(emptyContent, parents, options, blockName))];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
var node = ir.forStmt(val, token.irExpr, [ir.legacyJS(bodyJS)], key, emptyBody);
|
|
255
|
+
// Opt-in loop-context flags → Django `forloop` (see ir.js IRFor typedef +
|
|
256
|
+
// the backend `For` emit). Absent on every other flavor, so their emit
|
|
257
|
+
// stays byte-identical.
|
|
258
|
+
node.loopName = 'forloop';
|
|
259
|
+
node.loopFields = DJANGO_LOOP_FIELDS;
|
|
260
|
+
node.loopParent = true;
|
|
261
|
+
return node;
|
|
262
|
+
};
|
package/lib/tags/if.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django `{% if %}` / `{% elif %}` / `{% else %}` tag.
|
|
3
|
+
*
|
|
4
|
+
* Conditional: `{% if <expr> %}…{% elif <expr> %}…{% else %}…{% endif %}`.
|
|
5
|
+
* The test expression is parsed via `parser.parseExpr` and attached to
|
|
6
|
+
* `token.irExpr`. The body content is captured via the parser's open-tag
|
|
7
|
+
* stack (this tag sets `ends: true`).
|
|
8
|
+
*
|
|
9
|
+
* `{% elif %}` and `{% else %}` lex as their own tags (`ends: false`,
|
|
10
|
+
* registered in tags/index.js) and so appear as marker tokens inside this
|
|
11
|
+
* tag's `content`. The compile path walks `content`, splitting at those
|
|
12
|
+
* markers into one IRIfBranch per segment — the backend's `If` walker then
|
|
13
|
+
* emits the `if (…) { … } else if (…) { … } else { … }` envelope.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
17
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
18
|
+
|
|
19
|
+
var lexer = require('../lexer');
|
|
20
|
+
|
|
21
|
+
exports.ends = true;
|
|
22
|
+
exports.block = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse the `{% if %}` test expression onto `token.irExpr`.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} str Tag body (tag name stripped).
|
|
28
|
+
* @param {number} line Source line of the opening `{%`.
|
|
29
|
+
* @param {object} parser The Django parser module (exposes `parseExpr`).
|
|
30
|
+
* @param {object} types Django lexer token-type enum.
|
|
31
|
+
* @param {Array} stack Open-tag stack (managed by parser.js).
|
|
32
|
+
* @param {object} opts Per-call options (honors `opts.filename`).
|
|
33
|
+
* @param {object} swig Swig instance (unused).
|
|
34
|
+
* @param {object} token In-progress TagToken. `token.irExpr` is set.
|
|
35
|
+
* @return {boolean} Always `true` on success. Throws otherwise.
|
|
36
|
+
*/
|
|
37
|
+
exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
38
|
+
var tokens = lexer.read(utils.strip(str));
|
|
39
|
+
if (!tokens.length) {
|
|
40
|
+
utils.throwError('Expected conditional expression in "if" tag', line, opts.filename);
|
|
41
|
+
}
|
|
42
|
+
token.irExpr = parser.parseExpr(tokens);
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Split `content` at `elif` / `else` marker tokens and emit a multi-branch
|
|
48
|
+
* IRIf node.
|
|
49
|
+
*
|
|
50
|
+
* @return {object} IRIf node.
|
|
51
|
+
*/
|
|
52
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
53
|
+
var branches = [],
|
|
54
|
+
currentTest = token.irExpr,
|
|
55
|
+
currentBody = [],
|
|
56
|
+
sawElse = false,
|
|
57
|
+
filename = options && options.filename;
|
|
58
|
+
|
|
59
|
+
function flush() {
|
|
60
|
+
branches.push(ir.ifBranch(currentTest, [ir.legacyJS(compiler(currentBody, parents, options, blockName))]));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
utils.each(content, function (child) {
|
|
64
|
+
if (child && child.name === 'elif') {
|
|
65
|
+
if (sawElse) { utils.throwError('"elif" after "else" in "if" tag', null, filename); }
|
|
66
|
+
flush();
|
|
67
|
+
currentTest = child.irExpr;
|
|
68
|
+
currentBody = [];
|
|
69
|
+
} else if (child && child.name === 'else') {
|
|
70
|
+
if (sawElse) { utils.throwError('Multiple "else" branches in "if" tag', null, filename); }
|
|
71
|
+
flush();
|
|
72
|
+
currentTest = null;
|
|
73
|
+
currentBody = [];
|
|
74
|
+
sawElse = true;
|
|
75
|
+
} else {
|
|
76
|
+
currentBody.push(child);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
flush();
|
|
80
|
+
|
|
81
|
+
return ir.ifStmt(branches);
|
|
82
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django `{% include %}` tag.
|
|
3
|
+
*
|
|
4
|
+
* Django include syntax:
|
|
5
|
+
*
|
|
6
|
+
* {% include "partial.html" %} (full caller context)
|
|
7
|
+
* {% include "partial.html" with foo=1 bar=baz %} (context + extra vars)
|
|
8
|
+
* {% include "partial.html" with foo=1 only %} (ONLY the extra vars)
|
|
9
|
+
* {% include "partial.html" only %} (empty isolated context)
|
|
10
|
+
* {% include dynamicPath %}
|
|
11
|
+
*
|
|
12
|
+
* Path is lowered through `parser.parseExpr`, so STRING literals, VAR
|
|
13
|
+
* references, member access, and any other Django expression all route
|
|
14
|
+
* through the same path.
|
|
15
|
+
*
|
|
16
|
+
* The `with` clause is space-separated `name=value` assignments (Django's
|
|
17
|
+
* shape, NOT Twig's single `{dict}` expression) — each value is lowered
|
|
18
|
+
* through `parser.parseExpr` and collected into an object literal. The
|
|
19
|
+
* trailing `only` keyword isolates the included template to just that
|
|
20
|
+
* object (the backend's `Include` selector emits the object instead of
|
|
21
|
+
* `_ctx`); without `only`, the object is merged over the caller's context.
|
|
22
|
+
* `only` may also appear without a `with`, giving an empty isolated context.
|
|
23
|
+
*
|
|
24
|
+
* Django has no `ignore missing` modifier (a missing template raises), so
|
|
25
|
+
* the IRInclude's `ignoreMissing` flag is always false here.
|
|
26
|
+
*
|
|
27
|
+
* Emits an `IRInclude` node. The backend's `Include` branch owns the
|
|
28
|
+
* `_swig.compileFile(...)` + `resolveFrom` plumbing and the
|
|
29
|
+
* isolated-vs-merged context selector.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
33
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
34
|
+
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
35
|
+
|
|
36
|
+
var lexer = require('../lexer');
|
|
37
|
+
|
|
38
|
+
exports.ends = false;
|
|
39
|
+
exports.block = false;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Strip WHITESPACE tokens from both ends of a slice range.
|
|
43
|
+
*
|
|
44
|
+
* @param {object[]} tokens Token stream.
|
|
45
|
+
* @param {number} start Inclusive start index.
|
|
46
|
+
* @param {number} end Exclusive end index.
|
|
47
|
+
* @param {object} types Lexer token-type enum.
|
|
48
|
+
* @return {object[]} Trimmed slice.
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
function sliceTrim(tokens, start, end, types) {
|
|
52
|
+
while (start < end && tokens[start].type === types.WHITESPACE) { start += 1; }
|
|
53
|
+
while (end > start && tokens[end - 1].type === types.WHITESPACE) { end -= 1; }
|
|
54
|
+
return tokens.slice(start, end);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find the first depth-0 VAR token whose match equals `word`. Depth tracks
|
|
59
|
+
* paren / bracket / curly / function nesting so a keyword inside the path
|
|
60
|
+
* expression is left alone.
|
|
61
|
+
*
|
|
62
|
+
* @return {number} Token index, or -1 if not found.
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
function findKeyword(tokens, types, word) {
|
|
66
|
+
var depth = 0, i, tk;
|
|
67
|
+
for (i = 0; i < tokens.length; i += 1) {
|
|
68
|
+
tk = tokens[i];
|
|
69
|
+
if (tk.type === types.PARENOPEN || tk.type === types.BRACKETOPEN ||
|
|
70
|
+
tk.type === types.CURLYOPEN || tk.type === types.FUNCTION) { depth += 1; continue; }
|
|
71
|
+
if (tk.type === types.PARENCLOSE || tk.type === types.BRACKETCLOSE ||
|
|
72
|
+
tk.type === types.CURLYCLOSE) { depth -= 1; continue; }
|
|
73
|
+
if (depth !== 0 || tk.type !== types.VAR) { continue; }
|
|
74
|
+
if (tk.match === word) { return i; }
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse space-separated `name = value` pairs from a token slice. Each value
|
|
81
|
+
* is one expression (parseExpr stops at the next bare name, since
|
|
82
|
+
* `<value> <name>` has no operator between them). Stops when the next token
|
|
83
|
+
* is not a `name =` start, returning the unconsumed remainder as `leftover`
|
|
84
|
+
* (e.g. a trailing `only` keyword). Django parses the kwargs greedily first,
|
|
85
|
+
* so `with x=only` binds x to the variable `only` while `with x=1 only`
|
|
86
|
+
* leaves a trailing `only`.
|
|
87
|
+
*
|
|
88
|
+
* @return {{props: object[], leftover: object[]}}
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
function parsePairs(clause, parser, types, line, opts) {
|
|
92
|
+
var props = [];
|
|
93
|
+
var pos = 0;
|
|
94
|
+
function skipWS() { while (pos < clause.length && clause[pos].type === types.WHITESPACE) { pos += 1; } }
|
|
95
|
+
|
|
96
|
+
while (true) {
|
|
97
|
+
skipWS();
|
|
98
|
+
if (pos >= clause.length) { break; }
|
|
99
|
+
var nameTok = clause[pos];
|
|
100
|
+
if (nameTok.type !== types.VAR) { break; }
|
|
101
|
+
var j = pos + 1;
|
|
102
|
+
while (j < clause.length && clause[j].type === types.WHITESPACE) { j += 1; }
|
|
103
|
+
if (!(j < clause.length && clause[j].type === types.ASSIGNMENT && clause[j].match === '=')) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (nameTok.match.indexOf('.') !== -1) {
|
|
107
|
+
utils.throwError('"with" assignment target "' + nameTok.match + '" must be a bare identifier in "include" tag', line, opts.filename);
|
|
108
|
+
}
|
|
109
|
+
if (_dangerousProps.indexOf(nameTok.match) !== -1) {
|
|
110
|
+
utils.throwError('Unsafe "with" assignment to "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
|
|
111
|
+
}
|
|
112
|
+
pos = j + 1;
|
|
113
|
+
skipWS();
|
|
114
|
+
var posOut = {};
|
|
115
|
+
var valExpr = parser.parseExpr(clause.slice(pos), {}, posOut);
|
|
116
|
+
pos += posOut.pos;
|
|
117
|
+
props.push(ir.objectProperty(ir.literal('string', nameTok.match), valExpr));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
skipWS();
|
|
121
|
+
return { props: props, leftover: clause.slice(pos) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse the `{% include %}` tag body — the path expression plus the optional
|
|
126
|
+
* `with name=value …` assignments and trailing `only` keyword.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} str Tag body.
|
|
129
|
+
* @param {number} line Source line of the opening `{%`.
|
|
130
|
+
* @param {object} parser The Django parser module (exposes `parseExpr`).
|
|
131
|
+
* @param {object} types Django lexer token-type enum.
|
|
132
|
+
* @param {Array} stack Open-tag stack (unused — include has no body).
|
|
133
|
+
* @param {object} opts Per-call options (honors `opts.filename`).
|
|
134
|
+
* @param {object} swig Swig instance (unused — backend owns load).
|
|
135
|
+
* @param {object} token In-progress TagToken. `token.irExpr` gets the
|
|
136
|
+
* IRInclude node.
|
|
137
|
+
* @return {boolean} Always `true` on success. Throws otherwise.
|
|
138
|
+
*/
|
|
139
|
+
exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
140
|
+
var tokens = lexer.read(utils.strip(str));
|
|
141
|
+
|
|
142
|
+
var isolated = false;
|
|
143
|
+
var ctxExpr;
|
|
144
|
+
var pathTokens;
|
|
145
|
+
|
|
146
|
+
var withIdx = findKeyword(tokens, types, 'with');
|
|
147
|
+
|
|
148
|
+
if (withIdx !== -1) {
|
|
149
|
+
pathTokens = sliceTrim(tokens, 0, withIdx, types);
|
|
150
|
+
var clause = sliceTrim(tokens, withIdx + 1, tokens.length, types);
|
|
151
|
+
var parsed = parsePairs(clause, parser, types, line, opts);
|
|
152
|
+
if (!parsed.props.length) {
|
|
153
|
+
utils.throwError('Expected at least one "name=value" assignment after "with" in "include" tag', line, opts.filename);
|
|
154
|
+
}
|
|
155
|
+
if (parsed.leftover.length === 1 && parsed.leftover[0].type === types.VAR && parsed.leftover[0].match === 'only') {
|
|
156
|
+
isolated = true;
|
|
157
|
+
} else if (parsed.leftover.length) {
|
|
158
|
+
utils.throwError('Unexpected token "' + parsed.leftover[0].match + '" after "with" assignments in "include" tag', line, opts.filename);
|
|
159
|
+
}
|
|
160
|
+
ctxExpr = ir.objectLiteral(parsed.props);
|
|
161
|
+
} else {
|
|
162
|
+
// No `with`. A trailing bare `only` (with a path before it) isolates the
|
|
163
|
+
// included template to an empty context. `{% include only %}` with no
|
|
164
|
+
// path before it is a path named `only`, not the keyword.
|
|
165
|
+
var trimmed = sliceTrim(tokens, 0, tokens.length, types);
|
|
166
|
+
var lastTok = trimmed.length ? trimmed[trimmed.length - 1] : null;
|
|
167
|
+
if (trimmed.length >= 2 && lastTok.type === types.VAR && lastTok.match === 'only') {
|
|
168
|
+
isolated = true;
|
|
169
|
+
ctxExpr = ir.objectLiteral([]);
|
|
170
|
+
pathTokens = trimmed.slice(0, trimmed.length - 1);
|
|
171
|
+
} else {
|
|
172
|
+
pathTokens = trimmed;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!pathTokens.length) {
|
|
177
|
+
utils.throwError('Expected template path in "include" tag', line, opts.filename);
|
|
178
|
+
}
|
|
179
|
+
var pathExpr = parser.parseExpr(pathTokens);
|
|
180
|
+
|
|
181
|
+
var resolveFrom = (opts.filename || '').replace(/\\/g, '\\\\');
|
|
182
|
+
token.irExpr = ir.include(pathExpr, ctxExpr, isolated, false, resolveFrom);
|
|
183
|
+
return true;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Return the pre-built IRInclude node. In async codegen mode, derive an
|
|
188
|
+
* IRIncludeDeferred from the same fields so the backend routes through the
|
|
189
|
+
* `_swig.getTemplate` + `await` path instead of the sync `_swig.compileFile`
|
|
190
|
+
* call.
|
|
191
|
+
*
|
|
192
|
+
* @return {object} IRInclude or IRIncludeDeferred node.
|
|
193
|
+
*/
|
|
194
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
195
|
+
if (options && options.codegenMode === 'async') {
|
|
196
|
+
var i = token.irExpr;
|
|
197
|
+
return ir.includeDeferred(i.path, i.context, i.isolated, i.ignoreMissing, i.resolveFrom);
|
|
198
|
+
}
|
|
199
|
+
return token.irExpr;
|
|
200
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django per-flavor tag registry.
|
|
3
|
+
*
|
|
4
|
+
* Each tag exports `{ parse, compile, ends, block }` with a Django-tailored
|
|
5
|
+
* shape:
|
|
6
|
+
*
|
|
7
|
+
* parse(str, line, parser, types, stack, opts, swig, token) → boolean
|
|
8
|
+
*
|
|
9
|
+
* The 8th `token` argument is the in-progress TagToken. Tag implementations
|
|
10
|
+
* call `parser.parseExpr(lexer.read(str), filters)` directly and attach the
|
|
11
|
+
* resulting IRExpr to `token.irExpr`, then return true. This avoids the
|
|
12
|
+
* native-swig `parser.on(types.X, fn)` callback indirection — Django tags
|
|
13
|
+
* own their own arg-parsing path, like the Twig and Jinja2 siblings.
|
|
14
|
+
*
|
|
15
|
+
* Conditionals (if / elif / else), looping (for / empty), template
|
|
16
|
+
* inheritance (block / extends / include), the region tags (with /
|
|
17
|
+
* autoescape / spaceless / comment / verbatim), and the Django-only output
|
|
18
|
+
* tags (cycle / firstof) are registered — the full S3 tag set.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
'if': require('./if'),
|
|
23
|
+
'elif': require('./elif'),
|
|
24
|
+
'else': require('./else'),
|
|
25
|
+
'for': require('./for'),
|
|
26
|
+
'empty': require('./empty'),
|
|
27
|
+
'block': require('./block'),
|
|
28
|
+
'extends': require('./extends'),
|
|
29
|
+
'include': require('./include'),
|
|
30
|
+
'with': require('./with'),
|
|
31
|
+
'autoescape': require('./autoescape'),
|
|
32
|
+
'spaceless': require('./spaceless'),
|
|
33
|
+
'comment': require('./comment'),
|
|
34
|
+
'verbatim': require('./verbatim'),
|
|
35
|
+
'cycle': require('./cycle'),
|
|
36
|
+
'firstof': require('./firstof')
|
|
37
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Django `{% spaceless %}…{% endspaceless %}` tag.
|
|
3
|
+
*
|
|
4
|
+
* Removes whitespace between HTML tags within the block — every run of
|
|
5
|
+
* whitespace that sits directly between a `>` and a `<` collapses away:
|
|
6
|
+
*
|
|
7
|
+
* {% spaceless %}
|
|
8
|
+
* <p>
|
|
9
|
+
* <a href="foo/">Foo</a>
|
|
10
|
+
* </p>
|
|
11
|
+
* {% endspaceless %}
|
|
12
|
+
* // => <p><a href="foo/">Foo</a></p>
|
|
13
|
+
*
|
|
14
|
+
* Django's rule is strictly `>\s+<` → `><` — it does NOT strip leading /
|
|
15
|
+
* trailing whitespace of the block or whitespace inside a tag or inside
|
|
16
|
+
* text. (This is narrower than the native swig `spaceless`, which also
|
|
17
|
+
* strips the leading/trailing whitespace of the WHOLE output — a quirk this
|
|
18
|
+
* faithful port deliberately avoids.)
|
|
19
|
+
*
|
|
20
|
+
* The body is compiled into a shadowed `_output` local (the same shape the
|
|
21
|
+
* backend uses for `{% filter %}`), so the collapse is scoped to just this
|
|
22
|
+
* region's content — content emitted before / after the block is untouched.
|
|
23
|
+
*
|
|
24
|
+
* Takes no arguments. Extra tokens after `spaceless` are rejected at parse
|
|
25
|
+
* time.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
29
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
30
|
+
|
|
31
|
+
exports.ends = true;
|
|
32
|
+
exports.block = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reject any tokens after the `spaceless` keyword.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} str Tag body (everything after `spaceless`).
|
|
38
|
+
* @param {number} line Source line of the opening `{%`.
|
|
39
|
+
* @param {object} parser The Django parser module (unused).
|
|
40
|
+
* @param {object} types Django lexer token-type enum (unused).
|
|
41
|
+
* @param {Array} stack Open-tag stack (parser.js manages the push).
|
|
42
|
+
* @param {object} opts Per-call options (honors `opts.filename`).
|
|
43
|
+
* @return {boolean} Always `true` on success. Throws otherwise.
|
|
44
|
+
*/
|
|
45
|
+
exports.parse = function (str, line, parser, types, stack, opts) {
|
|
46
|
+
var stripped = utils.strip(str || '');
|
|
47
|
+
if (stripped.length > 0) {
|
|
48
|
+
utils.throwError('Unexpected token "' + stripped + '" after "spaceless"', line, opts.filename);
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emit the body into a shadowed `_output` local, then append the
|
|
55
|
+
* whitespace-collapsed result to the real output. Returns an IRLegacyJS
|
|
56
|
+
* node carrying the generated JS.
|
|
57
|
+
*
|
|
58
|
+
* @return {object} IRLegacyJS node.
|
|
59
|
+
*/
|
|
60
|
+
exports.compile = function (compiler, args, content, parents, options, blockName) {
|
|
61
|
+
var bodyJS = compiler(content, parents, options, blockName);
|
|
62
|
+
return ir.legacyJS(
|
|
63
|
+
'_output += (function () {\n' +
|
|
64
|
+
' var _output = "";\n' +
|
|
65
|
+
bodyJS +
|
|
66
|
+
' return _output.replace(/>\\s+</g, "><");\n' +
|
|
67
|
+
'})();\n'
|
|
68
|
+
);
|
|
69
|
+
};
|