@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.
@@ -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
+ };