@rhinostone/swig-django 2.9.7 → 2.9.9
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 +1 -1
- package/lib/index.js +1 -1
- package/lib/lexer.js +10 -7
- package/lib/parser.js +57 -9
- package/lib/tags/cycle.js +5 -2
- package/lib/tags/else.js +4 -0
- package/lib/tags/empty.js +3 -0
- package/lib/tags/for.js +31 -34
- package/lib/tags/include.js +1 -1
- package/lib/tags/with.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Explicitly unsupported (parse-time throw or absent)
|
|
|
43
43
|
* `{% url %}`, `{% static %}`, `{% csrf_token %}`, `{% trans %}`, `{% blocktrans %}` — framework infrastructure, out of scope.
|
|
44
44
|
* No whitespace-control (`{%- … -%}`) — not part of the Django Template Language.
|
|
45
45
|
* No `is` tests — Django's `is` is identity comparison, not Jinja2-style tests.
|
|
46
|
-
* `{
|
|
46
|
+
* `{% for … reversed %}` is deferred (a clear parse-time error). `{{ block.super }}` is deferred too, and renders an empty string with no error.
|
|
47
47
|
* Deferred filters — `dictsort` / `dictsortreversed`, `stringformat`, `random`, `urlize` / `urlizetrunc`, `timesince` / `timeuntil`, `unordered_list`, `json_script`, `truncatechars_html` / `truncatewords_html`, `safeseq`, `pprint`.
|
|
48
48
|
|
|
49
49
|
Repository
|
package/lib/index.js
CHANGED
package/lib/lexer.js
CHANGED
|
@@ -35,12 +35,15 @@ var TYPES = require('./tokentypes');
|
|
|
35
35
|
*
|
|
36
36
|
* - FILTEREMPTY / FUNCTION above VAR — `|name` and `name(` must be
|
|
37
37
|
* consumed as filter / function tokens before VAR's `^[a-zA-Z_$]\w*`
|
|
38
|
-
* pattern gobbles the bare identifier.
|
|
38
|
+
* pattern gobbles the bare identifier. FUNCTION and FUNCTIONEMPTY skip
|
|
39
|
+
* `and`, `or`, `not` and `in`, so `not(x)` reads as `not (x)`.
|
|
39
40
|
* - LOGIC (`and` / `or`), NOT (`not`), BOOL (`True` / `False`), NONE
|
|
40
41
|
* (`None`), and the `is` / `is not` keywords above VAR — same reason:
|
|
41
42
|
* bake the keyword sequence into its own token rather than emit an
|
|
42
43
|
* identifier. The `\b` word boundary keeps identifiers like `Truthy`,
|
|
43
|
-
* `Nonexistent`, or `island` from matching
|
|
44
|
+
* `Nonexistent`, or `island` from matching; the `and`, `or`, `not` and
|
|
45
|
+
* `in` rules take whitespace or `(` after the keyword, so an object key
|
|
46
|
+
* such as `{and: 1}` stays a name.
|
|
44
47
|
*
|
|
45
48
|
* Rules are tried in order; first match wins. Patterns are anchored at
|
|
46
49
|
* start-of-string because the consumer slices `str` before each dispatch.
|
|
@@ -71,14 +74,14 @@ var rules = [
|
|
|
71
74
|
{
|
|
72
75
|
type: TYPES.FUNCTIONEMPTY,
|
|
73
76
|
regex: [
|
|
74
|
-
/^\s*(\w+)\(\)/
|
|
77
|
+
/^\s*(?!(?:and|or|not|in)\()(\w+)\(\)/
|
|
75
78
|
],
|
|
76
79
|
idx: 1
|
|
77
80
|
},
|
|
78
81
|
{
|
|
79
82
|
type: TYPES.FUNCTION,
|
|
80
83
|
regex: [
|
|
81
|
-
/^\s*(\w+)\(/
|
|
84
|
+
/^\s*(?!(?:and|or|not|in)\()(\w+)\(/
|
|
82
85
|
],
|
|
83
86
|
idx: 1
|
|
84
87
|
},
|
|
@@ -104,7 +107,7 @@ var rules = [
|
|
|
104
107
|
type: TYPES.LOGIC,
|
|
105
108
|
regex: [
|
|
106
109
|
/^(&&|\|\|)\s*/,
|
|
107
|
-
/^(and|or)\s
|
|
110
|
+
/^(and|or)(?=[\s(])\s*/
|
|
108
111
|
],
|
|
109
112
|
idx: 1,
|
|
110
113
|
replace: {
|
|
@@ -115,7 +118,7 @@ var rules = [
|
|
|
115
118
|
{
|
|
116
119
|
type: TYPES.COMPARATOR,
|
|
117
120
|
regex: [
|
|
118
|
-
/^(===|==|\!==|\!=|<=|<|>=|>|not\s+in\s|in\s)\s*/
|
|
121
|
+
/^(===|==|\!==|\!=|<=|<|>=|>|not\s+in(?=[\s(])|in(?=[\s(]))\s*/
|
|
119
122
|
],
|
|
120
123
|
idx: 1
|
|
121
124
|
},
|
|
@@ -129,7 +132,7 @@ var rules = [
|
|
|
129
132
|
type: TYPES.NOT,
|
|
130
133
|
regex: [
|
|
131
134
|
/^\!\s*/,
|
|
132
|
-
/^not\s
|
|
135
|
+
/^not(?=[\s(])\s*/
|
|
133
136
|
],
|
|
134
137
|
replace: {
|
|
135
138
|
'not': '!'
|
package/lib/parser.js
CHANGED
|
@@ -42,6 +42,24 @@ function membership(left, right, negate) {
|
|
|
42
42
|
return node;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Build the IR of a call to one of the engine's own helpers (an `_ext` test,
|
|
47
|
+
* an `_utils` function, a `Math` function), flagged so swig-core reads the
|
|
48
|
+
* callee from the compiled template's scope and never from the template
|
|
49
|
+
* context, where a locals key such as `_ext`, `_utils` or `Math` would
|
|
50
|
+
* replace it.
|
|
51
|
+
*
|
|
52
|
+
* @param {string[]} path Callee path (`['_utils', 'range']`).
|
|
53
|
+
* @param {object[]} args Argument expressions.
|
|
54
|
+
* @return {object} IRFnCall with `closure === true`.
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
function helperCall(path, args) {
|
|
58
|
+
var node = ir.fnCall(ir.varRef(path), args);
|
|
59
|
+
node.closure = true;
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
/**
|
|
46
64
|
* Build a VarRef IR node flagged for Django-style runtime resolution. The
|
|
47
65
|
* `resolve` flag makes swig-core's emitVarRef emit `_utils.resolve(_ctx,
|
|
@@ -62,6 +80,30 @@ function mkVar(path) {
|
|
|
62
80
|
return n;
|
|
63
81
|
}
|
|
64
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Apply Django's string_if_invalid ("") to the variable an expression starts
|
|
85
|
+
* with: the input of its filter chain, or the expression itself. Django
|
|
86
|
+
* resolves that variable first and gives a failed lookup the value "" before
|
|
87
|
+
* any filter runs, in `{{ }}` outputs and in the values of `with`, `cycle`
|
|
88
|
+
* and include-with; `if`, `for` and `firstof` read a failed lookup as None
|
|
89
|
+
* and do not call this. Sets the VarRef's opt-in `missingAsEmpty` flag (the
|
|
90
|
+
* backend then passes "" as `_utils.resolve`'s third argument); any other
|
|
91
|
+
* start (a literal, a subscript) is left as it is.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} expr IRExpr tree from parseExpr.
|
|
94
|
+
* @return {object} The same tree.
|
|
95
|
+
*/
|
|
96
|
+
exports.missingAsEmpty = function (expr) {
|
|
97
|
+
var head = expr;
|
|
98
|
+
while (head && head.type === 'FilterCall') {
|
|
99
|
+
head = head.input;
|
|
100
|
+
}
|
|
101
|
+
if (head && head.type === 'VarRef') {
|
|
102
|
+
head.missingAsEmpty = true;
|
|
103
|
+
}
|
|
104
|
+
return expr;
|
|
105
|
+
};
|
|
106
|
+
|
|
65
107
|
/**
|
|
66
108
|
* Django expression parser — Pratt-style recursive descent.
|
|
67
109
|
*
|
|
@@ -145,6 +187,10 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
145
187
|
}
|
|
146
188
|
}
|
|
147
189
|
|
|
190
|
+
// Levels follow Django's smartif: `or` 1, `and` 2, then `not` (parsed in
|
|
191
|
+
// parseUnary, with an operand at level 4), `in` / `not in` 4, and the
|
|
192
|
+
// comparisons with `is` / `is not` 5, so `not x == y` is `not (x == y)` and
|
|
193
|
+
// `a == b in c` is `(a == b) in c`. Arithmetic (6, 8) is a swig extension.
|
|
148
194
|
function getBinaryOpInfo(tok) {
|
|
149
195
|
var m;
|
|
150
196
|
if (tok.type === _t.LOGIC) {
|
|
@@ -153,22 +199,22 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
153
199
|
}
|
|
154
200
|
if (tok.type === _t.COMPARATOR) {
|
|
155
201
|
m = tok.match;
|
|
156
|
-
if (m === '===' || m === '!==' || m === '==' || m === '!=') {
|
|
157
|
-
return { op: m, prec: 3 };
|
|
158
|
-
}
|
|
159
202
|
if (/^not\s+in$/.test(m)) {
|
|
160
203
|
return { op: 'not in', prec: 4 };
|
|
161
204
|
}
|
|
162
|
-
|
|
205
|
+
if (m === 'in') {
|
|
206
|
+
return { op: m, prec: 4 };
|
|
207
|
+
}
|
|
208
|
+
return { op: m, prec: 5 };
|
|
163
209
|
}
|
|
164
210
|
// Django `is` / `is not` are *identity* comparisons (not Jinja2 test
|
|
165
211
|
// calls): lowered to `===` / `!==` in parseExpression. Same precedence
|
|
166
212
|
// level as `==` so `x is None` / `x is not None` group naturally.
|
|
167
213
|
if (tok.type === _t.IS) {
|
|
168
|
-
return { op: 'is', prec:
|
|
214
|
+
return { op: 'is', prec: 5 };
|
|
169
215
|
}
|
|
170
216
|
if (tok.type === _t.ISNOT) {
|
|
171
|
-
return { op: 'is not', prec:
|
|
217
|
+
return { op: 'is not', prec: 5 };
|
|
172
218
|
}
|
|
173
219
|
if (tok.type === _t.OPERATOR) {
|
|
174
220
|
m = tok.match;
|
|
@@ -330,7 +376,7 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
330
376
|
}
|
|
331
377
|
}
|
|
332
378
|
expectBracketClose();
|
|
333
|
-
return
|
|
379
|
+
return helperCall(['_utils', 'slice'], [
|
|
334
380
|
obj,
|
|
335
381
|
startExpr || undefinedLiteral(),
|
|
336
382
|
stopExpr || undefinedLiteral(),
|
|
@@ -437,7 +483,9 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
437
483
|
var tok = peek();
|
|
438
484
|
if (tok && tok.type === _t.NOT) {
|
|
439
485
|
consume();
|
|
440
|
-
|
|
486
|
+
// Django's `not` takes in, not in, the comparisons and is into its
|
|
487
|
+
// operand, and stops at `and` / `or`.
|
|
488
|
+
return ir.unaryOp('!', parseExpression(4));
|
|
441
489
|
}
|
|
442
490
|
if (tok && tok.type === _t.OPERATOR && (tok.match === '+' || tok.match === '-')) {
|
|
443
491
|
consume();
|
|
@@ -603,7 +651,7 @@ exports.parse = function (swig, source, opts, tags, filters) {
|
|
|
603
651
|
}
|
|
604
652
|
}
|
|
605
653
|
});
|
|
606
|
-
var expr = exports.parseExpr(lexed, filters);
|
|
654
|
+
var expr = exports.missingAsEmpty(exports.parseExpr(lexed, filters));
|
|
607
655
|
var tail;
|
|
608
656
|
var esc = escapeStack[escapeStack.length - 1];
|
|
609
657
|
if (esc && !sawSafe) {
|
package/lib/tags/cycle.js
CHANGED
|
@@ -75,7 +75,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
75
75
|
while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
|
|
76
76
|
if (pos >= tokens.length) { break; }
|
|
77
77
|
var posOut = {};
|
|
78
|
-
var expr = parser.parseExpr(tokens.slice(pos), {}, posOut);
|
|
78
|
+
var expr = parser.missingAsEmpty(parser.parseExpr(tokens.slice(pos), {}, posOut));
|
|
79
79
|
pos += posOut.pos;
|
|
80
80
|
args.push(expr);
|
|
81
81
|
}
|
|
@@ -96,7 +96,10 @@ exports.compile = function (compiler, args, content, parents, options, blockName
|
|
|
96
96
|
|
|
97
97
|
var key = ('__cycle' + Math.random()).replace(/\./g, '');
|
|
98
98
|
var value = '__cv[_ctx.' + key + ' % __cv.length]';
|
|
99
|
-
|
|
99
|
+
// A string escape strategy (the `autoescape: 'js'` option) goes to the
|
|
100
|
+
// `e` filter as its argument, as firstof and `{{ }}` pass it.
|
|
101
|
+
var strategy = (typeof token.escape === 'string') ? ', ' + backend.emitExpr(ir.literal('string', token.escape)) : '';
|
|
102
|
+
var emit = token.escape ? '_filters["e"](' + value + strategy + ')' : '_utils.coerceOutput(' + value + ')';
|
|
100
103
|
|
|
101
104
|
return ir.legacyJS(
|
|
102
105
|
'(function () {\n' +
|
package/lib/tags/else.js
CHANGED
|
@@ -22,6 +22,10 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
22
22
|
if (!top || top.name !== 'if') {
|
|
23
23
|
utils.throwError('"else" is only valid inside an "if" tag', line, opts.filename);
|
|
24
24
|
}
|
|
25
|
+
// Django rejects anything after else (`{% else if b %}` is not elif).
|
|
26
|
+
if (utils.strip(str || '')) {
|
|
27
|
+
utils.throwError('"else" does not accept arguments (found "' + utils.strip(str) + '")', line, opts.filename);
|
|
28
|
+
}
|
|
25
29
|
return true;
|
|
26
30
|
};
|
|
27
31
|
|
package/lib/tags/empty.js
CHANGED
|
@@ -20,6 +20,9 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
20
20
|
if (!top || top.name !== 'for') {
|
|
21
21
|
utils.throwError('"empty" is only valid inside a "for" tag', line, opts.filename);
|
|
22
22
|
}
|
|
23
|
+
if (utils.strip(str || '')) {
|
|
24
|
+
utils.throwError('"empty" does not accept arguments (found "' + utils.strip(str) + '")', line, opts.filename);
|
|
25
|
+
}
|
|
23
26
|
return true;
|
|
24
27
|
};
|
|
25
28
|
|
package/lib/tags/for.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* mapping's keys). An item that does not hold two values binds what it holds,
|
|
21
21
|
* a missing value rendering empty, where Django raises. Two names over a
|
|
22
22
|
* mapping bind the key and the value, a swig extension (Django iterates the
|
|
23
|
-
* keys and unpacks each one) that
|
|
23
|
+
* keys and unpacks each one) that a two-name `.items` loop relies on.
|
|
24
24
|
*
|
|
25
25
|
* Loop variable names must be bare identifiers — dotted paths are rejected
|
|
26
26
|
* at parse time, and the CVE-2023-25345 `_dangerousProps` guard runs on
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
* compile splits the content at it into the loop body and the empty body.
|
|
32
32
|
*
|
|
33
33
|
* Django's dict-iteration pseudo-methods are supported on the iterable —
|
|
34
|
-
* `{% for k in d.keys %}`, `{% for v in d.values %}`,
|
|
35
|
-
* `{% for key, value in d.items %}
|
|
36
|
-
* `{% for pair in d.items %}
|
|
34
|
+
* `{% for k in d.keys %}`, `{% for v in d.values %}`, the canonical
|
|
35
|
+
* `{% for key, value in d.items %}`, and the one-name
|
|
36
|
+
* `{% for pair in d.items %}`, which binds each [key, value] pair. As in
|
|
37
|
+
* Django, a mapping's own key of that name is read first (see
|
|
38
|
+
* rewriteDictIterable).
|
|
37
39
|
*
|
|
38
40
|
* The `reversed` modifier (`{% for x in items reversed %}`) is not yet
|
|
39
41
|
* supported; it is detected and rejected with a clear message rather than
|
|
@@ -83,30 +85,31 @@ function resolveVar(path) {
|
|
|
83
85
|
* `.keys` / `.values` / `.items` would otherwise resolve to a (usually
|
|
84
86
|
* missing) object property, yielding an empty loop.
|
|
85
87
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
88
|
+
* The iterable becomes the mapping (the path without its last segment), and
|
|
89
|
+
* `token.dictIter` records the name, which compile sets on the IRFor node.
|
|
90
|
+
* At render the backend passes the mapping to `_utils.dictIter`, which reads
|
|
91
|
+
* the mapping's own key of that name first, as Django's dictionary lookup
|
|
92
|
+
* does (so `{% for item in page.data.items %}` iterates the list stored under
|
|
93
|
+
* `items`), and otherwise applies the method:
|
|
94
|
+
*
|
|
95
|
+
* - `.keys` → the keys (a one-name `for` binds each key).
|
|
96
|
+
* - `.values` → the values (a one-name `for` binds each value).
|
|
97
|
+
* - `.items` → the mapping itself, so two names bind key + value per entry
|
|
98
|
+
* exactly as Django's `{% for k, v in d.items %}`; with one
|
|
99
|
+
* name (`pairs`), each [key, value] pair, Django's
|
|
100
|
+
* (key, value) tuple, and nothing for a value that is not a
|
|
101
|
+
* mapping.
|
|
95
102
|
*
|
|
96
103
|
* Anything that is not a resolved user-variable path of length >= 2 ending in
|
|
97
|
-
* one of the three names is returned unchanged.
|
|
98
|
-
* interception is unconditional, so if `obj` has a literal key named
|
|
99
|
-
* `keys` / `values` / `items`, the pseudo-method wins (Django's dict-key
|
|
100
|
-
* precedence is not honored for those three names).
|
|
104
|
+
* one of the three names is returned unchanged.
|
|
101
105
|
*
|
|
102
106
|
* @param {object} it Parsed iterable IRExpr.
|
|
103
107
|
* @param {boolean} hasTwoNames Whether the loop declared `key, val`.
|
|
104
|
-
* @param {
|
|
105
|
-
* @param {string} filename Template filename, for error reporting.
|
|
108
|
+
* @param {object} token In-progress TagToken; gets `dictIter`.
|
|
106
109
|
* @return {object} The (possibly rewritten) iterable IRExpr.
|
|
107
110
|
* @private
|
|
108
111
|
*/
|
|
109
|
-
function rewriteDictIterable(it, hasTwoNames,
|
|
112
|
+
function rewriteDictIterable(it, hasTwoNames, token) {
|
|
110
113
|
if (!it || it.type !== 'VarRef' || !it.resolve || it.path.length < 2) {
|
|
111
114
|
return it;
|
|
112
115
|
}
|
|
@@ -114,21 +117,11 @@ function rewriteDictIterable(it, hasTwoNames, line, filename) {
|
|
|
114
117
|
if (!DICT_PSEUDO_METHODS.hasOwnProperty(lastSeg)) {
|
|
115
118
|
return it;
|
|
116
119
|
}
|
|
117
|
-
|
|
118
|
-
if (lastSeg === 'keys') {
|
|
119
|
-
// The synthetic `_utils.keys` callee is intentionally NOT resolve-flagged
|
|
120
|
-
// (emitFnCall reads a callee path directly, never through emitVarRef).
|
|
121
|
-
return ir.fnCall(ir.varRef(['_utils', 'keys']), [resolveVar(objPath)]);
|
|
122
|
-
}
|
|
123
|
-
if (lastSeg === 'values') {
|
|
124
|
-
return ir.fnCall(ir.varRef(['_utils', 'values']), [resolveVar(objPath)]);
|
|
125
|
-
}
|
|
120
|
+
token.dictIter = { name: lastSeg };
|
|
126
121
|
if (lastSeg === 'items' && !hasTwoNames) {
|
|
127
|
-
|
|
122
|
+
token.dictIter.pairs = true;
|
|
128
123
|
}
|
|
129
|
-
|
|
130
|
-
// and the value.
|
|
131
|
-
return resolveVar(objPath);
|
|
124
|
+
return resolveVar(it.path.slice(0, -1));
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
exports.ends = true;
|
|
@@ -214,7 +207,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
214
207
|
}
|
|
215
208
|
|
|
216
209
|
token.args = key !== undefined ? [key, val] : [val];
|
|
217
|
-
token.irExpr = rewriteDictIterable(parser.parseExpr(iterableTokens), key !== undefined,
|
|
210
|
+
token.irExpr = rewriteDictIterable(parser.parseExpr(iterableTokens), key !== undefined, token);
|
|
218
211
|
return true;
|
|
219
212
|
};
|
|
220
213
|
|
|
@@ -275,5 +268,9 @@ exports.compile = function (compiler, args, content, parents, options, blockName
|
|
|
275
268
|
} else {
|
|
276
269
|
node.mapKeys = true;
|
|
277
270
|
}
|
|
271
|
+
// A dict pseudo-method on the iterable (see rewriteDictIterable).
|
|
272
|
+
if (token.dictIter) {
|
|
273
|
+
node.dictIter = token.dictIter;
|
|
274
|
+
}
|
|
278
275
|
return node;
|
|
279
276
|
};
|
package/lib/tags/include.js
CHANGED
|
@@ -112,7 +112,7 @@ function parsePairs(clause, parser, types, line, opts) {
|
|
|
112
112
|
pos = j + 1;
|
|
113
113
|
skipWS();
|
|
114
114
|
var posOut = {};
|
|
115
|
-
var valExpr = parser.parseExpr(clause.slice(pos), {}, posOut);
|
|
115
|
+
var valExpr = parser.missingAsEmpty(parser.parseExpr(clause.slice(pos), {}, posOut));
|
|
116
116
|
pos += posOut.pos;
|
|
117
117
|
props.push(ir.objectProperty(ir.literal('string', nameTok.match), valExpr));
|
|
118
118
|
}
|
package/lib/tags/with.js
CHANGED
|
@@ -89,7 +89,7 @@ function parsePairs(tokens, parser, types, line, opts) {
|
|
|
89
89
|
pos = j + 1;
|
|
90
90
|
skipWS();
|
|
91
91
|
var posOut = {};
|
|
92
|
-
var valExpr = parser.parseExpr(tokens.slice(pos), {}, posOut);
|
|
92
|
+
var valExpr = parser.missingAsEmpty(parser.parseExpr(tokens.slice(pos), {}, posOut));
|
|
93
93
|
pos += posOut.pos;
|
|
94
94
|
props.push(ir.objectProperty(ir.literal('string', nameTok.match), valExpr));
|
|
95
95
|
}
|
|
@@ -134,7 +134,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
134
134
|
utils.throwError('Expected a single variable name after "as" in "with" tag', line, opts.filename);
|
|
135
135
|
}
|
|
136
136
|
guardName(nameTokens[0].match, line, opts);
|
|
137
|
-
props = [ir.objectProperty(ir.literal('string', nameTokens[0].match), parser.parseExpr(exprTokens))];
|
|
137
|
+
props = [ir.objectProperty(ir.literal('string', nameTokens[0].match), parser.missingAsEmpty(parser.parseExpr(exprTokens)))];
|
|
138
138
|
} else {
|
|
139
139
|
props = parsePairs(tokens, parser, types, line, opts);
|
|
140
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig-django",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.9",
|
|
4
4
|
"description": "Django-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=12"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@rhinostone/swig-core": "2.9.
|
|
25
|
+
"@rhinostone/swig-core": "2.9.9"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|