@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/parser.js
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
var ir = require('@rhinostone/swig-core/lib/ir'),
|
|
2
|
+
utils = require('@rhinostone/swig-core/lib/utils'),
|
|
3
|
+
_dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
4
|
+
|
|
5
|
+
var lexer = require('./lexer');
|
|
6
|
+
var _t = require('./tokentypes');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Make a string safe for embedding into a regular expression.
|
|
10
|
+
* @param {string} str
|
|
11
|
+
* @return {string}
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
function escapeRegExp(str) {
|
|
15
|
+
return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reserved JS keywords that cannot be used as variable names.
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
var _reserved = ['break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a VarRef IR node flagged for Django-style runtime resolution. The
|
|
26
|
+
* `resolve` flag makes swig-core's emitVarRef emit `_utils.resolve(_ctx,
|
|
27
|
+
* path)` — dictionary / attribute / method lookup, numeric index access
|
|
28
|
+
* (`{{ list.0 }}`), and auto-call of callable leaves — instead of a static
|
|
29
|
+
* dot-path. Used at every USER-variable site (bare lookups, dotted-path
|
|
30
|
+
* rebuilds in parsePostfix, filter-argument variables). Deliberately NOT
|
|
31
|
+
* used for a function-call callee (emitFnCall reads the callee path directly
|
|
32
|
+
* and ignores the flag) or the synthetic `_utils.slice` reference.
|
|
33
|
+
*
|
|
34
|
+
* @param {string[]} path Path segments.
|
|
35
|
+
* @return {object} IRVarRef with `resolve === true`.
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
function mkVar(path) {
|
|
39
|
+
var n = ir.varRef(path);
|
|
40
|
+
n.resolve = true;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Django expression parser — Pratt-style recursive descent.
|
|
46
|
+
*
|
|
47
|
+
* Consumes a flat LexerToken[] (produced by swig-django's lexer) and
|
|
48
|
+
* returns an IRExpr tree using swig-core's IR factories. Mirrors the shape
|
|
49
|
+
* of swig-core's TokenParser.parseExpr so the swig-core backend can emit JS
|
|
50
|
+
* from either frontend's output without changes.
|
|
51
|
+
*
|
|
52
|
+
* This is a *lenient superset* of the Django Template Language expression
|
|
53
|
+
* grammar: it keeps the inherited parenthesis / arithmetic / function-call
|
|
54
|
+
* machinery (so every valid Django expression parses, and a handful of
|
|
55
|
+
* non-Django forms like `{{ a + b }}` also happen to work) and layers the
|
|
56
|
+
* Django-specific bits on top:
|
|
57
|
+
*
|
|
58
|
+
* - `|name` and `|name:arg` colon-filters (Django filters take at most one
|
|
59
|
+
* positional argument; the arg is a single literal or variable lookup,
|
|
60
|
+
* never a chained filter or operator expression).
|
|
61
|
+
* - `True` / `False` / `None` literals.
|
|
62
|
+
* - `is` / `is not` as *identity* comparisons (`===` / `!==`), NOT
|
|
63
|
+
* Jinja2-style test calls.
|
|
64
|
+
*
|
|
65
|
+
* Dropped relative to the Jinja2 sibling: `~` concat, `**` power, `//`
|
|
66
|
+
* floor-division, the inline `if`/`else` conditional, and the `is <test>`
|
|
67
|
+
* test-call machinery — none of which exist in Django.
|
|
68
|
+
*
|
|
69
|
+
* CVE-2023-25345 guards (`_dangerousProps`) fire on VAR path segments,
|
|
70
|
+
* DOTKEY matches, STRING-inside-BRACKETOPEN values, FUNCTION/FUNCTIONEMPTY
|
|
71
|
+
* callee names, and filter-argument variable segments — the same
|
|
72
|
+
* checkpoints as the native and Jinja2 frontends.
|
|
73
|
+
*
|
|
74
|
+
* Binding-power table (higher binds tighter):
|
|
75
|
+
*
|
|
76
|
+
* Level | Tokens | Assoc
|
|
77
|
+
* ------+------------------------------------+------
|
|
78
|
+
* 1 | || / or (LOGIC) | left
|
|
79
|
+
* 2 | && / and (LOGIC) | left
|
|
80
|
+
* 3 | == != === !== is "is not" (COMP) | left
|
|
81
|
+
* 4 | < > <= >= in (COMPARATOR) | left
|
|
82
|
+
* 6 | + - (OPERATOR) | left
|
|
83
|
+
* 8 | * / % (OPERATOR) | left
|
|
84
|
+
* post | DOTKEY BRACKETOPEN PARENOPEN | —
|
|
85
|
+
* | FILTEREMPTY (+ optional :arg) |
|
|
86
|
+
* pfx | NOT, unary +/- | —
|
|
87
|
+
*
|
|
88
|
+
* @param {object[]} tokens LexerToken[] from swig-django's lexer.
|
|
89
|
+
* @param {object} [filters] Filter catalog for name validation.
|
|
90
|
+
* Pass `{}` when no catalog is available.
|
|
91
|
+
* @param {object} [_posOut] Optional out-param; final cursor stored on
|
|
92
|
+
* `_posOut.pos` to let callers detect partial
|
|
93
|
+
* consumption.
|
|
94
|
+
* @return {object} IRExpr tree.
|
|
95
|
+
*/
|
|
96
|
+
exports.parseExpr = function (tokens, filters, _posOut) {
|
|
97
|
+
var pos = 0;
|
|
98
|
+
filters = filters || {};
|
|
99
|
+
|
|
100
|
+
function skipWS() {
|
|
101
|
+
while (pos < tokens.length && tokens[pos].type === _t.WHITESPACE) { pos += 1; }
|
|
102
|
+
}
|
|
103
|
+
function peek() {
|
|
104
|
+
skipWS();
|
|
105
|
+
return pos < tokens.length ? tokens[pos] : null;
|
|
106
|
+
}
|
|
107
|
+
function consume() {
|
|
108
|
+
var t = peek();
|
|
109
|
+
if (t) { pos += 1; }
|
|
110
|
+
return t;
|
|
111
|
+
}
|
|
112
|
+
function bail(msg) {
|
|
113
|
+
utils.throwError(msg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function guardSegment(segment) {
|
|
117
|
+
if (_dangerousProps.indexOf(segment) !== -1) {
|
|
118
|
+
bail('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function guardBracketString(value) {
|
|
122
|
+
if (_dangerousProps.indexOf(value) !== -1) {
|
|
123
|
+
bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (CVE-2023-25345)');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getBinaryOpInfo(tok) {
|
|
128
|
+
var m;
|
|
129
|
+
if (tok.type === _t.LOGIC) {
|
|
130
|
+
if (tok.match === '||') { return { op: '||', prec: 1 }; }
|
|
131
|
+
if (tok.match === '&&') { return { op: '&&', prec: 2 }; }
|
|
132
|
+
}
|
|
133
|
+
if (tok.type === _t.COMPARATOR) {
|
|
134
|
+
m = tok.match;
|
|
135
|
+
if (m === '===' || m === '!==' || m === '==' || m === '!=') {
|
|
136
|
+
return { op: m, prec: 3 };
|
|
137
|
+
}
|
|
138
|
+
return { op: m, prec: 4 };
|
|
139
|
+
}
|
|
140
|
+
// Django `is` / `is not` are *identity* comparisons (not Jinja2 test
|
|
141
|
+
// calls): lowered to `===` / `!==` in parseExpression. Same precedence
|
|
142
|
+
// level as `==` so `x is None` / `x is not None` group naturally.
|
|
143
|
+
if (tok.type === _t.IS) {
|
|
144
|
+
return { op: 'is', prec: 3 };
|
|
145
|
+
}
|
|
146
|
+
if (tok.type === _t.ISNOT) {
|
|
147
|
+
return { op: 'is not', prec: 3 };
|
|
148
|
+
}
|
|
149
|
+
if (tok.type === _t.OPERATOR) {
|
|
150
|
+
m = tok.match;
|
|
151
|
+
if (m === '+' || m === '-') { return { op: m, prec: 6 }; }
|
|
152
|
+
if (m === '*' || m === '/' || m === '%') { return { op: m, prec: 8 }; }
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function unquoteString(match) {
|
|
158
|
+
return match.replace(/^['"]|['"]$/g, '');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseArgList(closeType) {
|
|
162
|
+
var args = [];
|
|
163
|
+
var first = peek();
|
|
164
|
+
if (first && first.type === closeType) {
|
|
165
|
+
consume();
|
|
166
|
+
return args;
|
|
167
|
+
}
|
|
168
|
+
while (true) {
|
|
169
|
+
args.push(parseExpression(0));
|
|
170
|
+
var next = consume();
|
|
171
|
+
if (!next) { bail('Unexpected end of expression'); }
|
|
172
|
+
if (next.type === closeType) { break; }
|
|
173
|
+
if (next.type !== _t.COMMA) { bail('Expected comma or closing delimiter'); }
|
|
174
|
+
}
|
|
175
|
+
return args;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseObjectLiteral() {
|
|
179
|
+
var props = [];
|
|
180
|
+
var first = peek();
|
|
181
|
+
if (first && first.type === _t.CURLYCLOSE) {
|
|
182
|
+
consume();
|
|
183
|
+
return ir.objectLiteral([]);
|
|
184
|
+
}
|
|
185
|
+
while (true) {
|
|
186
|
+
var keyTok = consume();
|
|
187
|
+
if (!keyTok) { bail('Unclosed object literal'); }
|
|
188
|
+
var keyExpr;
|
|
189
|
+
if (keyTok.type === _t.STRING) {
|
|
190
|
+
keyExpr = ir.literal('string', unquoteString(keyTok.match));
|
|
191
|
+
} else if (keyTok.type === _t.VAR) {
|
|
192
|
+
if (keyTok.match.indexOf('.') !== -1) {
|
|
193
|
+
bail('Unexpected dot');
|
|
194
|
+
}
|
|
195
|
+
keyExpr = ir.literal('string', keyTok.match);
|
|
196
|
+
} else if (keyTok.type === _t.NUMBER) {
|
|
197
|
+
keyExpr = ir.literal('number', parseFloat(keyTok.match));
|
|
198
|
+
} else {
|
|
199
|
+
bail('Unexpected object key');
|
|
200
|
+
}
|
|
201
|
+
var colon = consume();
|
|
202
|
+
if (!colon || colon.type !== _t.COLON) { bail('Expected colon in object literal'); }
|
|
203
|
+
var value = parseExpression(0);
|
|
204
|
+
props.push(ir.objectProperty(keyExpr, value));
|
|
205
|
+
var next = consume();
|
|
206
|
+
if (!next) { bail('Unclosed object literal'); }
|
|
207
|
+
if (next.type === _t.CURLYCLOSE) { break; }
|
|
208
|
+
if (next.type !== _t.COMMA) { bail('Expected comma or closing curly brace'); }
|
|
209
|
+
}
|
|
210
|
+
return ir.objectLiteral(props);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Django filter argument — a *single* constant or variable lookup, never a
|
|
214
|
+
// chained filter or operator expression. This deliberately does NOT route
|
|
215
|
+
// through parsePostfix (which would greedily consume a trailing `|next`
|
|
216
|
+
// filter and wrongly bind it to the argument instead of the filtered
|
|
217
|
+
// value). An optional leading sign is honored only before a NUMBER, so
|
|
218
|
+
// `|floatformat:-3` works while `|add:-x` is rejected.
|
|
219
|
+
function parseFilterArg() {
|
|
220
|
+
var sign = '';
|
|
221
|
+
var pk = peek();
|
|
222
|
+
if (pk && pk.type === _t.OPERATOR && (pk.match === '-' || pk.match === '+')) {
|
|
223
|
+
consume();
|
|
224
|
+
sign = pk.match;
|
|
225
|
+
}
|
|
226
|
+
var tok = consume();
|
|
227
|
+
if (!tok) { bail('Expected filter argument after ":"'); }
|
|
228
|
+
switch (tok.type) {
|
|
229
|
+
case _t.STRING:
|
|
230
|
+
if (sign) { bail('Unexpected "' + sign + '" before string filter argument'); }
|
|
231
|
+
return ir.literal('string', unquoteString(tok.match));
|
|
232
|
+
case _t.NUMBER:
|
|
233
|
+
return ir.literal('number', parseFloat(sign + tok.match));
|
|
234
|
+
case _t.BOOL:
|
|
235
|
+
if (sign) { bail('Unexpected "' + sign + '" before boolean filter argument'); }
|
|
236
|
+
return ir.literal('bool', tok.match === 'true');
|
|
237
|
+
case _t.NONE:
|
|
238
|
+
if (sign) { bail('Unexpected "' + sign + '" before None filter argument'); }
|
|
239
|
+
return ir.literal('null', null);
|
|
240
|
+
case _t.VAR:
|
|
241
|
+
if (sign) { bail('Unexpected "' + sign + '" before variable filter argument'); }
|
|
242
|
+
var path = tok.match.split('.');
|
|
243
|
+
if (_reserved.indexOf(path[0]) !== -1) {
|
|
244
|
+
bail('Reserved keyword "' + path[0] + '" attempted to be used as a variable');
|
|
245
|
+
}
|
|
246
|
+
utils.each(path, function (segment) {
|
|
247
|
+
guardSegment(segment);
|
|
248
|
+
});
|
|
249
|
+
return mkVar(path);
|
|
250
|
+
}
|
|
251
|
+
bail('Unexpected filter argument "' + tok.match + '"');
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function expectBracketClose() {
|
|
256
|
+
var close = consume();
|
|
257
|
+
if (!close || close.type !== _t.BRACKETCLOSE) {
|
|
258
|
+
bail('Expected closing square bracket');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function undefinedLiteral() {
|
|
263
|
+
return ir.literal('undefined', undefined);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Called after the opening `[`. Either a single-key access `[expr]` or a
|
|
267
|
+
// Python-style slice `[start:stop:step]` with any part omitted. A leading
|
|
268
|
+
// COLON (omitted start) or a COLON after the first expression signals a
|
|
269
|
+
// slice, which lowers to `_utils.slice(obj, start, stop, step)` with
|
|
270
|
+
// undefined literals for omitted bounds. A plain `[expr]` lowers to an
|
|
271
|
+
// Access (string keys are CVE-guarded).
|
|
272
|
+
function parseSubscript(obj) {
|
|
273
|
+
var startExpr = null,
|
|
274
|
+
stopExpr = null,
|
|
275
|
+
stepExpr = null,
|
|
276
|
+
isSlice = false,
|
|
277
|
+
pk = peek();
|
|
278
|
+
|
|
279
|
+
if (pk && pk.type === _t.COLON) {
|
|
280
|
+
isSlice = true;
|
|
281
|
+
} else {
|
|
282
|
+
startExpr = parseExpression(0);
|
|
283
|
+
pk = peek();
|
|
284
|
+
if (pk && pk.type === _t.COLON) { isSlice = true; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!isSlice) {
|
|
288
|
+
if (startExpr.type === 'Literal' && startExpr.kind === 'string') {
|
|
289
|
+
guardBracketString(startExpr.value);
|
|
290
|
+
}
|
|
291
|
+
expectBracketClose();
|
|
292
|
+
return ir.access(obj, startExpr);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
consume(); // first colon
|
|
296
|
+
pk = peek();
|
|
297
|
+
if (pk && pk.type !== _t.COLON && pk.type !== _t.BRACKETCLOSE) {
|
|
298
|
+
stopExpr = parseExpression(0);
|
|
299
|
+
}
|
|
300
|
+
pk = peek();
|
|
301
|
+
if (pk && pk.type === _t.COLON) {
|
|
302
|
+
consume(); // second colon
|
|
303
|
+
pk = peek();
|
|
304
|
+
if (pk && pk.type !== _t.BRACKETCLOSE) {
|
|
305
|
+
stepExpr = parseExpression(0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
expectBracketClose();
|
|
309
|
+
return ir.fnCall(ir.varRef(['_utils', 'slice']), [
|
|
310
|
+
obj,
|
|
311
|
+
startExpr || undefinedLiteral(),
|
|
312
|
+
stopExpr || undefinedLiteral(),
|
|
313
|
+
stepExpr || undefinedLiteral()
|
|
314
|
+
]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parsePostfix(expr) {
|
|
318
|
+
var tok, fname, fargs, colon;
|
|
319
|
+
while (true) {
|
|
320
|
+
tok = peek();
|
|
321
|
+
if (!tok) { break; }
|
|
322
|
+
if (tok.type === _t.DOTKEY) {
|
|
323
|
+
consume();
|
|
324
|
+
guardSegment(tok.match);
|
|
325
|
+
if (expr.type === 'VarRef') {
|
|
326
|
+
expr = mkVar(expr.path.concat([tok.match]));
|
|
327
|
+
} else {
|
|
328
|
+
expr = ir.access(expr, ir.literal('string', tok.match));
|
|
329
|
+
}
|
|
330
|
+
} else if (tok.type === _t.BRACKETOPEN) {
|
|
331
|
+
consume();
|
|
332
|
+
expr = parseSubscript(expr);
|
|
333
|
+
} else if (tok.type === _t.PARENOPEN) {
|
|
334
|
+
consume();
|
|
335
|
+
expr = ir.fnCall(expr, parseArgList(_t.PARENCLOSE));
|
|
336
|
+
} else if (tok.type === _t.FILTEREMPTY || tok.type === _t.FILTER) {
|
|
337
|
+
consume();
|
|
338
|
+
fname = tok.match;
|
|
339
|
+
if (filters.hasOwnProperty(fname) && typeof filters[fname] !== 'function') {
|
|
340
|
+
bail('Invalid filter "' + fname + '"');
|
|
341
|
+
}
|
|
342
|
+
// Django colon-filter: an optional single positional argument
|
|
343
|
+
// introduced by a colon (`|date:"Y-m-d"`, `|default:user.name`).
|
|
344
|
+
// `fargs` MUST be reset each iteration — `var` is function-scoped, so
|
|
345
|
+
// without this an arg-bearing filter earlier in the chain would leak
|
|
346
|
+
// its args onto a later no-arg filter (e.g. `x|default:y|upper`).
|
|
347
|
+
fargs = undefined;
|
|
348
|
+
colon = peek();
|
|
349
|
+
if (colon && colon.type === _t.COLON) {
|
|
350
|
+
consume();
|
|
351
|
+
fargs = [parseFilterArg()];
|
|
352
|
+
}
|
|
353
|
+
expr = ir.filterCallExpr(fname, expr, fargs);
|
|
354
|
+
} else {
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return expr;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function parsePrimary() {
|
|
362
|
+
var tok = peek();
|
|
363
|
+
if (!tok) { bail('Unexpected end of expression'); }
|
|
364
|
+
|
|
365
|
+
tok = consume();
|
|
366
|
+
var m;
|
|
367
|
+
switch (tok.type) {
|
|
368
|
+
case _t.STRING:
|
|
369
|
+
return parsePostfix(ir.literal('string', unquoteString(tok.match)));
|
|
370
|
+
case _t.NUMBER:
|
|
371
|
+
return parsePostfix(ir.literal('number', parseFloat(tok.match)));
|
|
372
|
+
case _t.BOOL:
|
|
373
|
+
return parsePostfix(ir.literal('bool', tok.match === 'true'));
|
|
374
|
+
case _t.NONE:
|
|
375
|
+
return parsePostfix(ir.literal('null', null));
|
|
376
|
+
case _t.PARENOPEN:
|
|
377
|
+
var grouped = parseExpression(0);
|
|
378
|
+
var close = consume();
|
|
379
|
+
if (!close || close.type !== _t.PARENCLOSE) {
|
|
380
|
+
bail('Mismatched nesting state');
|
|
381
|
+
}
|
|
382
|
+
return parsePostfix(grouped);
|
|
383
|
+
case _t.BRACKETOPEN:
|
|
384
|
+
return parsePostfix(ir.arrayLiteral(parseArgList(_t.BRACKETCLOSE)));
|
|
385
|
+
case _t.CURLYOPEN:
|
|
386
|
+
return parsePostfix(parseObjectLiteral());
|
|
387
|
+
case _t.VAR:
|
|
388
|
+
var path = tok.match.split('.');
|
|
389
|
+
if (_reserved.indexOf(path[0]) !== -1) {
|
|
390
|
+
bail('Reserved keyword "' + path[0] + '" attempted to be used as a variable');
|
|
391
|
+
}
|
|
392
|
+
utils.each(path, function (segment) {
|
|
393
|
+
guardSegment(segment);
|
|
394
|
+
});
|
|
395
|
+
return parsePostfix(mkVar(path));
|
|
396
|
+
case _t.FUNCTION:
|
|
397
|
+
case _t.FUNCTIONEMPTY:
|
|
398
|
+
m = tok.match;
|
|
399
|
+
if (_reserved.indexOf(m) !== -1) {
|
|
400
|
+
bail('Reserved keyword "' + m + '" attempted to be used as a variable');
|
|
401
|
+
}
|
|
402
|
+
guardSegment(m);
|
|
403
|
+
if (tok.type === _t.FUNCTIONEMPTY) {
|
|
404
|
+
return parsePostfix(ir.fnCall(ir.varRef([m]), []));
|
|
405
|
+
}
|
|
406
|
+
return parsePostfix(ir.fnCall(ir.varRef([m]), parseArgList(_t.PARENCLOSE)));
|
|
407
|
+
}
|
|
408
|
+
bail('Unexpected token "' + tok.match + '"');
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function parseUnary() {
|
|
413
|
+
var tok = peek();
|
|
414
|
+
if (tok && tok.type === _t.NOT) {
|
|
415
|
+
consume();
|
|
416
|
+
return ir.unaryOp('!', parseUnary());
|
|
417
|
+
}
|
|
418
|
+
if (tok && tok.type === _t.OPERATOR && (tok.match === '+' || tok.match === '-')) {
|
|
419
|
+
consume();
|
|
420
|
+
return ir.unaryOp(tok.match, parseUnary());
|
|
421
|
+
}
|
|
422
|
+
return parsePrimary();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseExpression(minPrec) {
|
|
426
|
+
var left = parseUnary();
|
|
427
|
+
while (true) {
|
|
428
|
+
var tok = peek();
|
|
429
|
+
if (!tok) { break; }
|
|
430
|
+
var info = getBinaryOpInfo(tok);
|
|
431
|
+
if (!info || info.prec < minPrec) { break; }
|
|
432
|
+
consume();
|
|
433
|
+
// Django `is` / `is not` — identity comparison. The RHS is a full
|
|
434
|
+
// sub-expression (usually a `None` / `True` / `False` literal or a
|
|
435
|
+
// variable), and the operator lowers to `===` / `!==`.
|
|
436
|
+
if (info.op === 'is' || info.op === 'is not') {
|
|
437
|
+
var rightId = parseExpression(info.prec + 1);
|
|
438
|
+
// `x is None` / `x is not None` on a VarRef subject: emitVarRef
|
|
439
|
+
// coerces a missing or null lookup to "" for safe interpolation,
|
|
440
|
+
// which loses the null signal a plain `=== null` needs. Route through
|
|
441
|
+
// IRVarRefExists (defined-and-non-null) instead — the same handling
|
|
442
|
+
// the Jinja2 sibling uses for its `is none` test. `is None` is true
|
|
443
|
+
// when the subject is absent or null (`!exists`); `is not None` is
|
|
444
|
+
// true when it is present (`exists`). `is True` / `is False` /
|
|
445
|
+
// numeric / variable RHS are unaffected — those values are never
|
|
446
|
+
// null, so emitVarRef returns them intact and plain `===` is correct.
|
|
447
|
+
if (rightId.type === 'Literal' && rightId.kind === 'null' && left.type === 'VarRef') {
|
|
448
|
+
var exists = ir.varRefExists(left.path, left.loc);
|
|
449
|
+
left = info.op === 'is' ? ir.unaryOp('!', exists) : exists;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
left = ir.binaryOp(info.op === 'is' ? '===' : '!==', left, rightId);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
var right = parseExpression(info.prec + 1);
|
|
456
|
+
left = ir.binaryOp(info.op, left, right);
|
|
457
|
+
}
|
|
458
|
+
return left;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
var result = parseExpression(0);
|
|
462
|
+
|
|
463
|
+
if (_posOut) {
|
|
464
|
+
_posOut.pos = pos;
|
|
465
|
+
} else {
|
|
466
|
+
skipWS();
|
|
467
|
+
if (pos < tokens.length) {
|
|
468
|
+
bail('Unexpected token "' + tokens[pos].match + '"');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return result;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parse a Django source string into a parse tree of pre-built IR nodes and
|
|
478
|
+
* tag tokens, ready for swig-core's backend walker.
|
|
479
|
+
*
|
|
480
|
+
* Mirrors the shape of the native swig `parser.parse` so the same
|
|
481
|
+
* `engine.install(self, frontend)` plumbing works for every frontend:
|
|
482
|
+
*
|
|
483
|
+
* - Plain text chunks → `IRText` nodes (spliced through by the backend).
|
|
484
|
+
* - `{{ … }}` chunks → `IROutput` nodes built via parseExpr; if
|
|
485
|
+
* autoescape is on, the IROutput.filters slot carries an `e` filterCall
|
|
486
|
+
* tail unless one of the chained filters is `.safe`.
|
|
487
|
+
* - `{% … %}` chunks → TagToken from the registered tag's `parse`.
|
|
488
|
+
* - `{# … #}` chunks → dropped.
|
|
489
|
+
*
|
|
490
|
+
* Django has no whitespace-control markers (`{{-` / `-%}` are not DTL
|
|
491
|
+
* syntax), so the splitter does not strip surrounding whitespace. The
|
|
492
|
+
* verbatim-text tag is `{% verbatim %}` (Django's analog of swig's `raw`);
|
|
493
|
+
* its body is copied through untouched.
|
|
494
|
+
*
|
|
495
|
+
* Django tags own their argument parsing directly via `parseExpr`; there is
|
|
496
|
+
* no `parser.on(types.X, fn)` callback model. The `parser` argument passed
|
|
497
|
+
* to a tag's `parse(str, line, parser, _t, stack, opts, swig, token)` is
|
|
498
|
+
* this module itself (`exports`), which exposes `parseExpr` and `lexer`.
|
|
499
|
+
*
|
|
500
|
+
* @param {object} swig The Swig instance (or undefined when called
|
|
501
|
+
* outside an engine context).
|
|
502
|
+
* @param {string} source Django template source.
|
|
503
|
+
* @param {object} opts Per-call options. Honors `varControls`,
|
|
504
|
+
* `tagControls`, `cmtControls`, `autoescape`,
|
|
505
|
+
* `filename`.
|
|
506
|
+
* @param {object} tags Tag registry (`{ name: { parse, compile, ends,
|
|
507
|
+
* block } }`).
|
|
508
|
+
* @param {object} filters Filter catalog. Only used for `.safe` lookup at
|
|
509
|
+
* autoescape time.
|
|
510
|
+
* @return {object} `{ name, parent, parentExpr, tokens, blocks }` tree
|
|
511
|
+
* consumed by `engine.compile`.
|
|
512
|
+
* @throws {Error} On unknown tag, mismatched end tag, or any parseExpr
|
|
513
|
+
* error inside a `{{ … }}` chunk.
|
|
514
|
+
*/
|
|
515
|
+
exports.parse = function (swig, source, opts, tags, filters) {
|
|
516
|
+
source = String(source).replace(/\r\n/g, '\n');
|
|
517
|
+
opts = opts || {};
|
|
518
|
+
tags = tags || {};
|
|
519
|
+
filters = filters || {};
|
|
520
|
+
|
|
521
|
+
var varControls = opts.varControls || ['{{', '}}'];
|
|
522
|
+
var tagControls = opts.tagControls || ['{%', '%}'];
|
|
523
|
+
var cmtControls = opts.cmtControls || ['{#', '#}'];
|
|
524
|
+
|
|
525
|
+
var escape = opts.autoescape;
|
|
526
|
+
if (typeof escape === 'undefined') { escape = true; }
|
|
527
|
+
// Region-scoped autoescape. `escape` is the template-level default;
|
|
528
|
+
// `{% autoescape on/off %}` pushes a new value for the duration of its
|
|
529
|
+
// body and `{% endautoescape %}` pops it, mirroring the inRaw flag below.
|
|
530
|
+
// parseVariable reads the current top, so the `e` filter tail is baked
|
|
531
|
+
// per-region at parse time (the backend IRAutoescape node is inert).
|
|
532
|
+
var escapeStack = [escape];
|
|
533
|
+
|
|
534
|
+
var tagOpen = tagControls[0];
|
|
535
|
+
var tagClose = tagControls[1];
|
|
536
|
+
var varOpen = varControls[0];
|
|
537
|
+
var varClose = varControls[1];
|
|
538
|
+
var cmtOpen = cmtControls[0];
|
|
539
|
+
var cmtClose = cmtControls[1];
|
|
540
|
+
|
|
541
|
+
var anyChar = '[\\s\\S]*?';
|
|
542
|
+
var splitter = new RegExp(
|
|
543
|
+
'(' +
|
|
544
|
+
escapeRegExp(tagOpen) + anyChar + escapeRegExp(tagClose) + '|' +
|
|
545
|
+
escapeRegExp(varOpen) + anyChar + escapeRegExp(varClose) + '|' +
|
|
546
|
+
escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) +
|
|
547
|
+
')'
|
|
548
|
+
);
|
|
549
|
+
// No whitespace-control: Django has no `{{-` / `-%}` markers, so the strip
|
|
550
|
+
// patterns carry no `-?` and there is no strip-before / strip-after
|
|
551
|
+
// bookkeeping.
|
|
552
|
+
var tagStrip = new RegExp('^' + escapeRegExp(tagOpen) + '\\s*|\\s*' + escapeRegExp(tagClose) + '$', 'g');
|
|
553
|
+
var varStrip = new RegExp('^' + escapeRegExp(varOpen) + '\\s*|\\s*' + escapeRegExp(varClose) + '$', 'g');
|
|
554
|
+
|
|
555
|
+
var line = 1;
|
|
556
|
+
var stack = [];
|
|
557
|
+
var parent = null;
|
|
558
|
+
var parentExpr = null;
|
|
559
|
+
var tokens = [];
|
|
560
|
+
var blocks = {};
|
|
561
|
+
var inRaw = false;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Build an IROutput node for a `{{ … }}` chunk. The autoescape `e` filter
|
|
565
|
+
* tail is appended unless a `.safe` filter appears in the chain.
|
|
566
|
+
*
|
|
567
|
+
* @param {string} str Inner expression text (controls already stripped).
|
|
568
|
+
* @param {number} _line Source line of the opening control.
|
|
569
|
+
* @return {object} IROutput IR node.
|
|
570
|
+
* @private
|
|
571
|
+
*/
|
|
572
|
+
function parseVariable(str, _line) {
|
|
573
|
+
var lexed = lexer.read(utils.strip(str));
|
|
574
|
+
var sawSafe = false;
|
|
575
|
+
utils.each(lexed, function (tok) {
|
|
576
|
+
if (tok.type === _t.FILTEREMPTY || tok.type === _t.FILTER) {
|
|
577
|
+
if (filters.hasOwnProperty(tok.match) && filters[tok.match].safe === true) {
|
|
578
|
+
sawSafe = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
var expr = exports.parseExpr(lexed, filters);
|
|
583
|
+
var tail;
|
|
584
|
+
var esc = escapeStack[escapeStack.length - 1];
|
|
585
|
+
if (esc && !sawSafe) {
|
|
586
|
+
var escapeArgs;
|
|
587
|
+
if (typeof esc === 'string') {
|
|
588
|
+
escapeArgs = [ir.literal('string', esc)];
|
|
589
|
+
}
|
|
590
|
+
tail = [ir.filterCall('e', escapeArgs)];
|
|
591
|
+
}
|
|
592
|
+
var node = ir.output(expr, tail);
|
|
593
|
+
// Coerce null / undefined to "" for any non-VarRef output (function
|
|
594
|
+
// calls, dynamic bracket access, ...). A *plain* VarRef already coerces
|
|
595
|
+
// inside emitVarRef, so the common `{{ name }}` path stays wrapper-free —
|
|
596
|
+
// but a *resolve* VarRef (`{{ obj.method }}`, `{{ list.0 }}`) returns the
|
|
597
|
+
// raw resolved value (including null) so filters such as default_if_none
|
|
598
|
+
// see a real null; that raw null reaches the output drain unescaped (the
|
|
599
|
+
// autoescape `e` tail passes null through), so it needs coerceOutput here.
|
|
600
|
+
if (expr.type !== 'VarRef' || expr.resolve) {
|
|
601
|
+
node.coerce = true;
|
|
602
|
+
}
|
|
603
|
+
return node;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Dispatch a `{% … %}` chunk to its registered tag. Handles `end<name>`
|
|
608
|
+
* close-tag matching against the open-tag stack (filename-aware throws are
|
|
609
|
+
* routed via utils.throwError so the frontend can wrap them via
|
|
610
|
+
* onCompileError).
|
|
611
|
+
*
|
|
612
|
+
* @param {string} str Inner tag text (controls already stripped).
|
|
613
|
+
* @param {number} _line Source line of the opening control.
|
|
614
|
+
* @return {?object} TagToken, or undefined for end-tag close.
|
|
615
|
+
* @private
|
|
616
|
+
*/
|
|
617
|
+
function parseTag(str, _line) {
|
|
618
|
+
var chunks = str.split(/\s+(.+)?/);
|
|
619
|
+
var tagName = chunks.shift();
|
|
620
|
+
var tagArgs = chunks[0] || '';
|
|
621
|
+
var last;
|
|
622
|
+
|
|
623
|
+
if (tagName.indexOf('end') === 0) {
|
|
624
|
+
var openName = tagName.replace(/^end/, '');
|
|
625
|
+
last = stack[stack.length - 1];
|
|
626
|
+
if (last && last.name === openName && last.ends) {
|
|
627
|
+
if (openName === 'verbatim' || openName === 'comment') { inRaw = false; }
|
|
628
|
+
if (openName === 'autoescape') { escapeStack.pop(); }
|
|
629
|
+
stack.pop();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (!inRaw) {
|
|
633
|
+
utils.throwError('Unexpected end of tag "' + openName + '"', _line, opts.filename);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Inside a verbatim block, non-matching tag chunks fall through to the
|
|
638
|
+
// splitter's chunk-as-text path. The `endverbatim` close has already
|
|
639
|
+
// been handled above; everything else returns undefined so the splitter
|
|
640
|
+
// wraps the verbatim chunk via `ir.text`.
|
|
641
|
+
if (inRaw) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!tags.hasOwnProperty(tagName)) {
|
|
646
|
+
utils.throwError('Unexpected tag "' + tagName + '"', _line, opts.filename);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
var tag = tags[tagName];
|
|
650
|
+
var token = {
|
|
651
|
+
block: !!tag.block,
|
|
652
|
+
compile: tag.compile,
|
|
653
|
+
args: [],
|
|
654
|
+
content: [],
|
|
655
|
+
ends: !!tag.ends,
|
|
656
|
+
name: tagName,
|
|
657
|
+
irExpr: undefined,
|
|
658
|
+
// Region autoescape state at this tag's position (the same value the
|
|
659
|
+
// `{{ … }}` output path reads). Output-producing tags (firstof, cycle)
|
|
660
|
+
// consult it to escape their output exactly as a `{{ … }}` would —
|
|
661
|
+
// honoring an enclosing `{% autoescape off %}`. Other tags ignore it.
|
|
662
|
+
escape: escapeStack[escapeStack.length - 1]
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
var ok = tag.parse(tagArgs, _line, exports, _t, stack, opts, swig, token);
|
|
666
|
+
if (!ok) {
|
|
667
|
+
utils.throwError('Unexpected tag "' + tagName + '"', _line, opts.filename);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (tagName === 'verbatim' || tagName === 'comment') {
|
|
671
|
+
// Both bypass parsing of their body: verbatim renders it verbatim,
|
|
672
|
+
// comment discards it (its compile drops the captured text).
|
|
673
|
+
inRaw = true;
|
|
674
|
+
}
|
|
675
|
+
if (tagName === 'autoescape') {
|
|
676
|
+
escapeStack.push(token.escapeValue);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return token;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
utils.each(source.split(splitter), function (chunk) {
|
|
683
|
+
var token, lines;
|
|
684
|
+
|
|
685
|
+
if (!chunk) { return; }
|
|
686
|
+
|
|
687
|
+
if (!inRaw && utils.startsWith(chunk, varOpen) && utils.endsWith(chunk, varClose)) {
|
|
688
|
+
token = parseVariable(chunk.replace(varStrip, ''), line);
|
|
689
|
+
} else if (utils.startsWith(chunk, tagOpen) && utils.endsWith(chunk, tagClose)) {
|
|
690
|
+
token = parseTag(chunk.replace(tagStrip, ''), line);
|
|
691
|
+
if (token) {
|
|
692
|
+
if (token.name === 'extends') {
|
|
693
|
+
parent = token.args.length ? String(token.args[0]) : null;
|
|
694
|
+
parentExpr = token.irExpr && token.irExpr.file;
|
|
695
|
+
} else if (token.block && !stack.length) {
|
|
696
|
+
blocks[token.args.join('')] = token;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// parseTag returns undefined for non-`endverbatim` tag chunks while
|
|
700
|
+
// inRaw is true. Wrap the original chunk as literal text so the
|
|
701
|
+
// content inside `{% verbatim %}` renders verbatim.
|
|
702
|
+
if (inRaw && !token) {
|
|
703
|
+
token = ir.text(chunk);
|
|
704
|
+
}
|
|
705
|
+
} else if (!inRaw && utils.startsWith(chunk, cmtOpen) && utils.endsWith(chunk, cmtClose)) {
|
|
706
|
+
lines = chunk.match(/\n/g);
|
|
707
|
+
line += lines ? lines.length : 0;
|
|
708
|
+
return;
|
|
709
|
+
} else {
|
|
710
|
+
token = ir.text(chunk);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (token) {
|
|
714
|
+
if (stack.length) {
|
|
715
|
+
stack[stack.length - 1].content.push(token);
|
|
716
|
+
} else {
|
|
717
|
+
tokens.push(token);
|
|
718
|
+
}
|
|
719
|
+
if (token.name && token.ends) {
|
|
720
|
+
stack.push(token);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
lines = chunk.match(/\n/g);
|
|
725
|
+
line += lines ? lines.length : 0;
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (stack.length) {
|
|
729
|
+
utils.throwError('Missing end tag for "' + stack[stack.length - 1].name + '"', line, opts.filename);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
name: opts.filename,
|
|
734
|
+
parent: parent,
|
|
735
|
+
parentExpr: parentExpr,
|
|
736
|
+
tokens: tokens,
|
|
737
|
+
blocks: blocks
|
|
738
|
+
};
|
|
739
|
+
};
|