@rhinostone/swig-core 2.4.3 → 2.5.1
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 +2 -1
- package/lib/backend.js +72 -35
- package/lib/engine.js +1 -1
- package/lib/filters.js +1 -1
- package/lib/index.js +1 -1
- package/lib/ir.js +15 -15
- package/lib/tokenparser.js +3 -3
- package/lib/utils.js +93 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[](https://www.npmjs.com/package/@rhinostone/swig-core) [](https://socket.dev/npm/package/@rhinostone/swig-core)
|
|
5
5
|
|
|
6
|
-
> **Shared runtime** for the `@rhinostone/swig` family of template engines. Not intended for direct consumption unless you are building a custom frontend. Install [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) for the default Swig (Jinja2/Django-inspired) flavor,
|
|
6
|
+
> **Shared runtime** for the `@rhinostone/swig` family of template engines. Not intended for direct consumption unless you are building a custom frontend. Install [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) for the default Swig (Jinja2/Django-inspired) flavor, [@rhinostone/swig-twig](https://www.npmjs.com/package/@rhinostone/swig-twig) for the Twig flavor, or [@rhinostone/swig-jinja2](https://www.npmjs.com/package/@rhinostone/swig-jinja2) for the Python Jinja2 flavor — they all pull this package in pinned to the matching version.
|
|
7
7
|
|
|
8
8
|
Extracted from `@rhinostone/swig@1.6.0` during the `2.0.0-alpha.1` multi-flavor carve. See [ROADMAP.md](https://github.com/gina-io/swig/blob/develop/ROADMAP.md) for the release narrative.
|
|
9
9
|
|
|
@@ -21,6 +21,7 @@ Consumers
|
|
|
21
21
|
|
|
22
22
|
* [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) — default Swig flavor.
|
|
23
23
|
* [@rhinostone/swig-twig](https://www.npmjs.com/package/@rhinostone/swig-twig) — Twig parity frontend.
|
|
24
|
+
* [@rhinostone/swig-jinja2](https://www.npmjs.com/package/@rhinostone/swig-jinja2) — Python Jinja2 frontend.
|
|
24
25
|
|
|
25
26
|
Versioning
|
|
26
27
|
----------
|
package/lib/backend.js
CHANGED
|
@@ -5,21 +5,21 @@ var utils = require('./utils'),
|
|
|
5
5
|
/**
|
|
6
6
|
* JS-codegen backend shared across @rhinostone/swig-family frontends.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The template-level walker dispatches on IR node shape. At
|
|
9
9
|
* entry, each parse-tree token is lifted into an IR node: string tokens
|
|
10
10
|
* become `IRText` (value carried verbatim, escaped at emit time);
|
|
11
11
|
* VarToken / TagToken entries call `token.compile(...)` and the return
|
|
12
12
|
* value is lifted according to its shape: a JS source string becomes
|
|
13
13
|
* `IRLegacyJS` (userland `setTag` contract), a single IR node is spliced
|
|
14
14
|
* in directly, and an array of IR nodes is flattened. The walker then
|
|
15
|
-
* iterates the IR array and dispatches on node shape.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* iterates the IR array and dispatches on node shape. Real IR emitters
|
|
16
|
+
* (`Autoescape`, `If`, `For`, `Set`, etc.) were introduced alongside
|
|
17
|
+
* their matching tag migrations, and each new shape gets its own
|
|
18
|
+
* dispatch branch here.
|
|
19
19
|
*
|
|
20
20
|
* Userland tag `compile` functions keep returning JS source strings —
|
|
21
21
|
* the `(compiler, args, content, parents, options, blockName)` contract
|
|
22
|
-
* is unchanged. Built-in tags migrate
|
|
22
|
+
* is unchanged. Built-in tags migrate by returning IR nodes
|
|
23
23
|
* directly. The `new Function(...)` wrapper stays with the native
|
|
24
24
|
* frontend (filename-aware error attribution, per the seam rule).
|
|
25
25
|
*/
|
|
@@ -101,9 +101,9 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
if (node.type === 'If') {
|
|
104
|
-
//
|
|
104
|
+
// Multi-branch shape. The native if tag owns
|
|
105
105
|
// content scanning and splits at else/elseif marker tokens so each
|
|
106
|
-
// IRIfBranch carries its own test + body.
|
|
106
|
+
// IRIfBranch carries its own test + body. The IR migration
|
|
107
107
|
// widened `test` to `IRExpr | IRLegacyJS | null`: `IRExpr` for
|
|
108
108
|
// clean expressions, `null` for the trailing else, `IRLegacyJS`
|
|
109
109
|
// for the filter-in-test fallback (`if.lowerExpr` bails on
|
|
@@ -114,8 +114,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
114
114
|
//
|
|
115
115
|
// Emission shape matches the pre-carve `} else if (...) {` /
|
|
116
116
|
// `} else {` fragments that else.js and elseif.js used to return
|
|
117
|
-
// inline — byte-identity held on the
|
|
118
|
-
// Session 14c notes in roadmap).
|
|
117
|
+
// inline — byte-identity held on the baseline.
|
|
119
118
|
var ifOut = '';
|
|
120
119
|
utils.each(node.branches, function (br, bi) {
|
|
121
120
|
var bodyJS = '',
|
|
@@ -148,7 +147,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
148
147
|
return;
|
|
149
148
|
}
|
|
150
149
|
if (node.type === 'Set') {
|
|
151
|
-
//
|
|
150
|
+
// Target is structured IRVarRef
|
|
152
151
|
// for pure-dot LHS shapes (`foo`, `foo.bar.baz`), emitted as a
|
|
153
152
|
// bare `_ctx.<dot.path>` lvalue with a per-segment _dangerousProps
|
|
154
153
|
// guard. Bracket-touched targets (`foo[bar]`, `foo["bar"]`, mixed
|
|
@@ -156,7 +155,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
156
155
|
// bracket-lvalue contract is a cross-flavor design call and is
|
|
157
156
|
// deferred. The frontend's set-tag parse handler retains its own
|
|
158
157
|
// _dangerousProps guards on every LHS path segment.
|
|
159
|
-
// `value` is an IRExpr node
|
|
158
|
+
// `value` is an IRExpr node — backward-compat string
|
|
160
159
|
// fallback preserved for userland setTag tags that may still hand
|
|
161
160
|
// in a raw JS fragment. Emits `<target> <op> <value>;`.
|
|
162
161
|
var setTargetJS;
|
|
@@ -182,10 +181,10 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
182
181
|
return;
|
|
183
182
|
}
|
|
184
183
|
if (node.type === 'For') {
|
|
185
|
-
//
|
|
184
|
+
// The full loopcache + _utils.each IIFE scaffolding is
|
|
186
185
|
// emitted here; the frontend tag surfaces only (value, key,
|
|
187
186
|
// iterable, body) and the backend owns all JS plumbing. `iterable`
|
|
188
|
-
// is an IRExpr node
|
|
187
|
+
// is an IRExpr node — backward-compat string fallback
|
|
189
188
|
// preserved for userland setTag tags that may still hand in a raw
|
|
190
189
|
// JS fragment. The loopcache identifier uses `Math.random()`
|
|
191
190
|
// per-occurrence to keep nested loops from clobbering each other's
|
|
@@ -209,9 +208,24 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
209
208
|
return;
|
|
210
209
|
}
|
|
211
210
|
});
|
|
211
|
+
// `emptyBody` (Twig/Jinja2/Django `{% for … %}{% else %}`) runs when
|
|
212
|
+
// the iterable is absent or has no items. When unset, the early-return
|
|
213
|
+
// guard stays byte-identical to the no-else shape.
|
|
214
|
+
var forEmptyCheck = ' if (!__l) { return; }\n';
|
|
215
|
+
if (node.emptyBody) {
|
|
216
|
+
var forEmptyJS = '';
|
|
217
|
+
utils.each(node.emptyBody, function (b) {
|
|
218
|
+
if (b.type === 'LegacyJS') { forEmptyJS += b.js; return; }
|
|
219
|
+
if (b.type === 'Text' || b.type === 'Raw') {
|
|
220
|
+
forEmptyJS += '_output += "' + escapeTextValue(b.value) + '";\n';
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
forEmptyCheck = ' if (!__l || !__len) {\n' + forEmptyJS + ' return;\n }\n';
|
|
225
|
+
}
|
|
212
226
|
out += '(function () {\n' +
|
|
213
227
|
' var __l = ' + forIterable + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n' +
|
|
214
|
-
|
|
228
|
+
forEmptyCheck +
|
|
215
229
|
' var ' + ctxloopcache + ' = { loop: ' + ctxloop + ', ' + forVal + ': ' + ctx + forVal + ', ' + forKey + ': ' + ctx + forKey + ' };\n' +
|
|
216
230
|
' ' + ctxloop + ' = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n' +
|
|
217
231
|
' _utils.each(__l, function (' + forVal + ', ' + forKey + ') {\n' +
|
|
@@ -231,12 +245,12 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
231
245
|
return;
|
|
232
246
|
}
|
|
233
247
|
if (node.type === 'Macro') {
|
|
234
|
-
//
|
|
248
|
+
// `params` is IRMacroParam[] —
|
|
235
249
|
// structured `{name, default?}` entries. Backend builds the JS
|
|
236
250
|
// function param list via `names.join(', ')` and the _utils.each
|
|
237
251
|
// shadow-delete indexOf list via `names.map(JSON.stringify).join(',')`.
|
|
238
252
|
// A string[] fallback is preserved for userland setTag tags that
|
|
239
|
-
// may still hand in the
|
|
253
|
+
// may still hand in the legacy raw-token slice (including
|
|
240
254
|
// the `, ` separator quirk). The frontend's macro parse handler
|
|
241
255
|
// has already applied the CVE-2023-25345 guard on the macro name
|
|
242
256
|
// (FUNCTION/FUNCTIONEMPTY) and every param name (VAR).
|
|
@@ -250,10 +264,23 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
250
264
|
});
|
|
251
265
|
var macroParams = node.params || [],
|
|
252
266
|
macroSigJS,
|
|
253
|
-
macroIndexOfJS
|
|
267
|
+
macroIndexOfJS,
|
|
268
|
+
macroDefaultsJS = '';
|
|
254
269
|
if (macroParams.length && typeof macroParams[0] === 'object' && macroParams[0] !== null && typeof macroParams[0].name === 'string') {
|
|
255
270
|
var macroNames = [];
|
|
256
|
-
utils.each(macroParams, function (p) {
|
|
271
|
+
utils.each(macroParams, function (p) {
|
|
272
|
+
macroNames.push(p.name);
|
|
273
|
+
// Parameter defaults — `{% macro foo(a, b='x') %}` carries an
|
|
274
|
+
// IRExpr on `p.default`. Emit `if (b === undefined) { b = <expr>; }`
|
|
275
|
+
// at the top of the macro body, before the _ctx shadow-delete, so a
|
|
276
|
+
// default expression reads the incoming context (and earlier
|
|
277
|
+
// parameters) consistently with the macro model. Missing args
|
|
278
|
+
// arrive as `undefined`, so the guard applies the default while
|
|
279
|
+
// leaving an explicit falsy value (`0`, `false`, `''`) alone.
|
|
280
|
+
if (p['default']) {
|
|
281
|
+
macroDefaultsJS += ' if (' + p.name + ' === undefined) { ' + p.name + ' = ' + exports.emitExpr(p['default']) + '; }\n';
|
|
282
|
+
}
|
|
283
|
+
});
|
|
257
284
|
macroSigJS = macroNames.join(', ');
|
|
258
285
|
var macroJsonNames = [];
|
|
259
286
|
utils.each(macroNames, function (n) { macroJsonNames.push(JSON.stringify(n)); });
|
|
@@ -265,6 +292,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
265
292
|
out += '_ctx.' + node.name + ' = function (' + macroSigJS + ') {\n' +
|
|
266
293
|
' var _output = "",\n' +
|
|
267
294
|
' __ctx = _utils.extend({}, _ctx);\n' +
|
|
295
|
+
macroDefaultsJS +
|
|
268
296
|
' _utils.each(_ctx, function (v, k) {\n' +
|
|
269
297
|
' if ([' + macroIndexOfJS + '].indexOf(k) !== -1) { delete _ctx[k]; }\n' +
|
|
270
298
|
' });\n' +
|
|
@@ -273,7 +301,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
273
301
|
' return _output;\n' +
|
|
274
302
|
'};\n' +
|
|
275
303
|
'_ctx.' + node.name + '.safe = true;\n';
|
|
276
|
-
//
|
|
304
|
+
// Async-codegen mirrors the macro into _exports so
|
|
277
305
|
// {% import %} / Twig's {% from %} can bind macros across templates
|
|
278
306
|
// at runtime. The macro's closure still references _ctx (not
|
|
279
307
|
// _exports) for outer-ctx lookups — sync-semantics-preserving.
|
|
@@ -289,7 +317,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
289
317
|
return;
|
|
290
318
|
}
|
|
291
319
|
if (node.type === 'Parent') {
|
|
292
|
-
//
|
|
320
|
+
// The parent tag walks the parents chain at compile time
|
|
293
321
|
// and splices the matched block's pre-resolved body into this node.
|
|
294
322
|
// Emit the body verbatim; no wrapper, no `super()`-style runtime
|
|
295
323
|
// plumbing is needed (the lookup is fully resolved at parse time).
|
|
@@ -303,13 +331,13 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
303
331
|
return;
|
|
304
332
|
}
|
|
305
333
|
if (node.type === 'Block') {
|
|
306
|
-
//
|
|
334
|
+
// Block tokens are resolved at parse time by the engine's
|
|
307
335
|
// remapBlocks / importNonBlocks — by the time the backend walks a
|
|
308
336
|
// block, its body carries whichever generation's content is active.
|
|
309
337
|
// Emit the body verbatim; the block name is carried as metadata for
|
|
310
338
|
// downstream tooling (parent-chain walks happen in the parent tag).
|
|
311
339
|
//
|
|
312
|
-
//
|
|
340
|
+
// Async mode: block remapping happens at runtime
|
|
313
341
|
// instead of parse time. When _blocks is supplied (by an extending
|
|
314
342
|
// child via IRExtendsDeferred-emitted `_parent(_ctx, _mergedBlocks)`)
|
|
315
343
|
// and contains an override for this block name, call the override
|
|
@@ -345,8 +373,8 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
345
373
|
return;
|
|
346
374
|
}
|
|
347
375
|
if (node.type === 'Include') {
|
|
348
|
-
//
|
|
349
|
-
//
|
|
376
|
+
// `path` and `context` are IRExpr nodes — per-slot dispatch on
|
|
377
|
+
// object-with-.type → emitExpr,
|
|
350
378
|
// else verbatim string fallback preserves the userland setTag
|
|
351
379
|
// path (compile functions that still hand in raw JS-source
|
|
352
380
|
// fragments). `resolveFrom` is a plain filesystem path that must
|
|
@@ -384,7 +412,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
384
412
|
return;
|
|
385
413
|
}
|
|
386
414
|
if (node.type === 'IncludeDeferred') {
|
|
387
|
-
//
|
|
415
|
+
// Async-codegen counterpart to Include. Resolves the
|
|
388
416
|
// path via `_swig.getTemplate(...)` at render time (Promise<TemplateFn>),
|
|
389
417
|
// then awaits the resolved template fn's call. The double-await flows
|
|
390
418
|
// correctly whether the loaded tpl is sync (returns string) or async-
|
|
@@ -419,7 +447,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
419
447
|
return;
|
|
420
448
|
}
|
|
421
449
|
if (node.type === 'ImportDeferred') {
|
|
422
|
-
//
|
|
450
|
+
// Async-codegen counterpart to the parse-time
|
|
423
451
|
// import flow. The imported template is loaded via _swig.getTemplate
|
|
424
452
|
// and called with the parent's _ctx so its body's compiled JS
|
|
425
453
|
// closes over outer-ctx vars (sync-semantics-preserving). Only
|
|
@@ -439,7 +467,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
439
467
|
return;
|
|
440
468
|
}
|
|
441
469
|
if (node.type === 'FromImportDeferred') {
|
|
442
|
-
//
|
|
470
|
+
// Async-codegen counterpart to Twig's `{% from
|
|
443
471
|
// "file" import a, b as c %}`. Single getTemplate call, then
|
|
444
472
|
// per-entry bind onto _ctx. Async IIFE keeps `_imp` local so
|
|
445
473
|
// multiple from-imports in the same template don't collide.
|
|
@@ -472,7 +500,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
472
500
|
return;
|
|
473
501
|
}
|
|
474
502
|
if (node.type === 'ExtendsDeferred') {
|
|
475
|
-
//
|
|
503
|
+
// Async-codegen counterpart to engine.getParents +
|
|
476
504
|
// engine.precompile's parse-time block remap. Inverts at runtime
|
|
477
505
|
// via the _blocks parameter contract:
|
|
478
506
|
//
|
|
@@ -535,7 +563,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
535
563
|
return;
|
|
536
564
|
}
|
|
537
565
|
if (node.type === 'With') {
|
|
538
|
-
//
|
|
566
|
+
// Scoped-context region (Twig's `{% with %}`).
|
|
539
567
|
// Emits an IIFE that shadows `_ctx` for the body's lexical scope;
|
|
540
568
|
// `_output` stays in the outer scope and is mutated via closure, so
|
|
541
569
|
// body writes still flow to the compiled template's output.
|
|
@@ -578,8 +606,8 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
578
606
|
return;
|
|
579
607
|
}
|
|
580
608
|
if (node.type === 'Output') {
|
|
581
|
-
//
|
|
582
|
-
//
|
|
609
|
+
// `expr` is typed IRExpr | IRLegacyJS. The frontend's parseVariable
|
|
610
|
+
// falls back to LegacyJS
|
|
583
611
|
// for shapes the flat IROutput.filters chain can't represent
|
|
584
612
|
// (per-operand filter precedence, deep filters, partial consumes,
|
|
585
613
|
// string-valued autoescape). LegacyJS carries the complete
|
|
@@ -602,11 +630,20 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
602
630
|
outExprJS = '_filters["' + fc.name + '"](' + outExprJS + fcArgsJS + ')';
|
|
603
631
|
});
|
|
604
632
|
}
|
|
605
|
-
|
|
633
|
+
// Opt-in null/undefined output coercion. A frontend sets
|
|
634
|
+
// `node.coerce` on outputs whose expression is not a VarRef (VarRef
|
|
635
|
+
// already coerces in emitVarRef), so an expression that evaluates to
|
|
636
|
+
// null / undefined renders as "" instead of the literal word. When
|
|
637
|
+
// the flag is absent the emit is unchanged.
|
|
638
|
+
if (node.coerce) {
|
|
639
|
+
out += '_output += _utils.coerceOutput(' + outExprJS + ');\n';
|
|
640
|
+
} else {
|
|
641
|
+
out += '_output += ' + outExprJS + ';\n';
|
|
642
|
+
}
|
|
606
643
|
return;
|
|
607
644
|
}
|
|
608
645
|
if (node.type === 'Filter') {
|
|
609
|
-
//
|
|
646
|
+
// `args` is IRExpr[] — per-arg
|
|
610
647
|
// dispatch on object-with-.type → emitExpr, else verbatim string
|
|
611
648
|
// fallback preserves the userland setTag path (compile functions
|
|
612
649
|
// that still hand in raw JS-source fragments).
|
|
@@ -641,7 +678,7 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
641
678
|
|
|
642
679
|
/**
|
|
643
680
|
* Emit a JS-source fragment for a single IR expression node. Round-trip
|
|
644
|
-
* target for the TokenParser → IRExpr migration
|
|
681
|
+
* target for the TokenParser → IRExpr migration: once
|
|
645
682
|
* the frontend produces real {@link IRExpr} values, every transitional
|
|
646
683
|
* `IRExpr | string` slot in the statement IR (IRFilter.args,
|
|
647
684
|
* IRIfBranch.test, IRFor.iterable, IRSet.target/value, IRInclude.path/
|
package/lib/engine.js
CHANGED
|
@@ -24,7 +24,7 @@ function efn() { return ''; }
|
|
|
24
24
|
/**
|
|
25
25
|
* Runtime engine plumbing shared across @rhinostone/swig-family frontends.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* Owns the `extends`-chain walker and block-remap helpers.
|
|
28
28
|
* The native Swig constructor in lib/swig.js delegates here so each frontend
|
|
29
29
|
* inherits the loader-walk + circular-extends detection + block merge logic
|
|
30
30
|
* for free.
|
package/lib/filters.js
CHANGED
|
@@ -3,7 +3,7 @@ var utils = require('./utils');
|
|
|
3
3
|
/**
|
|
4
4
|
* Filter infrastructure shared across @rhinostone/swig-family frontends.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* `iterateFilter` and the `.safe` flag convention live
|
|
7
7
|
* here so every flavor's filter catalog (native Swig, Twig, Jinja2,
|
|
8
8
|
* Django) picks up identical recursion + autoescape-bypass semantics.
|
|
9
9
|
* Filter catalogs themselves stay per-flavor.
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @rhinostone/swig-core — shared IR, backend, and runtime for the swig
|
|
3
3
|
* family of template engines.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Initial scaffold. Subsequent commits move security guards, loader
|
|
6
6
|
* contract, filter infra, cache, and the JS-codegen backend in from
|
|
7
7
|
* @rhinostone/swig.
|
|
8
8
|
*/
|
package/lib/ir.js
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
* Output an expression with optional filter chain.
|
|
50
50
|
* `safe: true` bypasses autoescape.
|
|
51
51
|
*
|
|
52
|
-
* `expr` is typed `IRExpr | IRLegacyJS
|
|
53
|
-
*
|
|
52
|
+
* `expr` is typed `IRExpr | IRLegacyJS`. The IR shape can't represent
|
|
53
|
+
* legacy Swig's per-operand
|
|
54
54
|
* filter precedence (filter binds to the last operand across binary
|
|
55
55
|
* ops, e.g. `{{ a + b|upper }}` → `a + _filters["upper"](b)`); for
|
|
56
56
|
* those outputs the frontend falls back to the legacy JS-string
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
* lowers cleanly, or an {@link IRLegacyJS} escape-hatch when the test
|
|
92
92
|
* contains a top-level filter chain mixed with a binary op (e.g.
|
|
93
93
|
* `{% if a === b|upper %}`) — per-operand filter precedence cannot be
|
|
94
|
-
* represented in a flat IR, same widening as {@link IROutput.expr}
|
|
95
|
-
*
|
|
94
|
+
* represented in a flat IR, same widening as {@link IROutput.expr}.
|
|
95
|
+
* The factory is opaque — it accepts any value
|
|
96
96
|
* and stores it — but consumers (backends) dispatch on the shape.
|
|
97
97
|
*
|
|
98
98
|
* @typedef {Object} IRIfBranch
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
/**
|
|
104
104
|
* For-loop. `emptyBody` supports Twig/Django `{% for … %}{% else %}`.
|
|
105
105
|
*
|
|
106
|
-
* `iterable` is typed `IRExpr | string`
|
|
106
|
+
* `iterable` is typed `IRExpr | string` — the native
|
|
107
107
|
* frontend still hands in a raw JS-source string (its `for` tag has
|
|
108
108
|
* not migrated to expression-level IR), while the Twig frontend lowers
|
|
109
109
|
* to a real {@link IRExpr}. Backends MUST tolerate both shapes — same
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
*/
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
*
|
|
185
|
+
* Pure-dot LHS shapes (`foo`, `foo.bar`,
|
|
186
186
|
* `foo.bar.baz`) are now structured {@link IRVarRef} nodes. The
|
|
187
187
|
* bracket-touched path (`foo[bar]`, `foo["bar"]`, mixed dot+bracket)
|
|
188
188
|
* stays on the transitional `string` form — bracket-lvalue semantics
|
|
@@ -213,12 +213,12 @@
|
|
|
213
213
|
/**
|
|
214
214
|
* Emit the parent block's compiled content (super() / block.super).
|
|
215
215
|
*
|
|
216
|
-
*
|
|
216
|
+
* IRParent optionally carries a `body` slot so
|
|
217
217
|
* the native frontend's parent tag can resolve the parent-chain lookup
|
|
218
218
|
* at compile time (emitting the matched block's body) without the
|
|
219
219
|
* backend having to re-walk the parents array. Target shape is a bare
|
|
220
220
|
* marker — the backend resolves parent() by itself — reached once the
|
|
221
|
-
* IR owns the extends/blocks graph directly
|
|
221
|
+
* IR owns the extends/blocks graph directly.
|
|
222
222
|
*
|
|
223
223
|
* @typedef {Object} IRParent
|
|
224
224
|
* @property {'Parent'} type
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
* functions, and built-in tags not yet migrated to real IR nodes. The
|
|
272
272
|
* backend concatenates `js` verbatim into the compiled template body.
|
|
273
273
|
*
|
|
274
|
-
* Transitional per the
|
|
274
|
+
* Transitional per the layering decision (hybrid / option iii).
|
|
275
275
|
*
|
|
276
276
|
* @typedef {Object} IRLegacyJS
|
|
277
277
|
* @property {'LegacyJS'} type
|
|
@@ -626,7 +626,7 @@ exports.ifBranch = function (test, body) {
|
|
|
626
626
|
/**
|
|
627
627
|
* Build an {@link IRFor} node.
|
|
628
628
|
*
|
|
629
|
-
* `iterable` is typed `IRExpr | string`
|
|
629
|
+
* `iterable` is typed `IRExpr | string` — see the IRFor typedef
|
|
630
630
|
* for the transitional shape. The factory stores `iterable` opaquely and
|
|
631
631
|
* does not inspect it.
|
|
632
632
|
*
|
|
@@ -659,7 +659,7 @@ exports.block = function (name, body, loc) {
|
|
|
659
659
|
/**
|
|
660
660
|
* Build an {@link IRInclude} node.
|
|
661
661
|
*
|
|
662
|
-
* `path` and `context` are {@link IRExpr} nodes
|
|
662
|
+
* `path` and `context` are {@link IRExpr} nodes.
|
|
663
663
|
* The factory stores both opaquely and does not inspect them — backward-
|
|
664
664
|
* compat string fallback is handled at backend emit time for userland
|
|
665
665
|
* setTag tags that may still hand in raw JS-source fragments.
|
|
@@ -735,8 +735,8 @@ exports.call = function (callee, args, loc) {
|
|
|
735
735
|
* Build an {@link IRSet} node. `target` MUST pass the dangerousProps
|
|
736
736
|
* guard at every path segment at backend emit time.
|
|
737
737
|
*
|
|
738
|
-
* `target` is typed `IRVarRef | string`
|
|
739
|
-
* shapes are structured {@link IRVarRef}
|
|
738
|
+
* `target` is typed `IRVarRef | string` — pure-dot LHS
|
|
739
|
+
* shapes are structured {@link IRVarRef};
|
|
740
740
|
* bracket-touched LHS stays a string fragment until the cross-flavor
|
|
741
741
|
* bracket-lvalue contract lands. The factory stores `target` and
|
|
742
742
|
* `value` opaquely and does not inspect them. `op` is the JS assignment
|
|
@@ -765,11 +765,11 @@ exports.raw = function (value, loc) {
|
|
|
765
765
|
/**
|
|
766
766
|
* Build an {@link IRParent} super()-equivalent node.
|
|
767
767
|
*
|
|
768
|
-
*
|
|
768
|
+
* Optional `body` slot carries the pre-resolved parent-block
|
|
769
769
|
* content as IR statements. Swig's parent tag walks the parents chain
|
|
770
770
|
* at compile time and drops the matched block's body here; backends
|
|
771
771
|
* emit the body as-is. A bare Parent (no body) is valid and means the
|
|
772
|
-
* backend will resolve the lookup itself
|
|
772
|
+
* backend will resolve the lookup itself.
|
|
773
773
|
*
|
|
774
774
|
* @param {IRStatement[]} [body]
|
|
775
775
|
* @param {IRLoc} [loc]
|
package/lib/tokenparser.js
CHANGED
|
@@ -428,9 +428,9 @@ TokenParser.prototype = {
|
|
|
428
428
|
* `parseExpr` emits structured IR that {@link backend.emitExpr}
|
|
429
429
|
* later lowers into an equivalent JS-source fragment. `.parse()` is
|
|
430
430
|
* unchanged and remains the production path; `parseExpr` is the
|
|
431
|
-
* incoming target shape for
|
|
432
|
-
*
|
|
433
|
-
*
|
|
431
|
+
* incoming target shape for the IR migration, introduced additively
|
|
432
|
+
* so the IR grammar can be proven against real lexer output before
|
|
433
|
+
* consumers are flipped over to it.
|
|
434
434
|
*
|
|
435
435
|
* The CVE-2023-25345 prototype-chain guards (`_dangerousProps` on
|
|
436
436
|
* VAR segments, DOTKEY matches, STRING-inside-BRACKETOPEN values,
|
package/lib/utils.js
CHANGED
|
@@ -218,3 +218,96 @@ exports.range = function (from, to) {
|
|
|
218
218
|
}
|
|
219
219
|
return out;
|
|
220
220
|
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Python-style slice for arrays and strings. Mirrors Jinja2's
|
|
224
|
+
* `seq[start:stop:step]` subscript: negative indices count from the end,
|
|
225
|
+
* omitted bounds (null/undefined) take their step-direction default, and a
|
|
226
|
+
* negative step walks backwards (`seq[::-1]` reverses). Returns a string
|
|
227
|
+
* for string input and an array otherwise. A null/undefined object or a
|
|
228
|
+
* non-indexable value yields an empty result so compiled templates degrade
|
|
229
|
+
* silently rather than throwing at render time.
|
|
230
|
+
*
|
|
231
|
+
* @param {Array|string} obj
|
|
232
|
+
* @param {?number} start
|
|
233
|
+
* @param {?number} stop
|
|
234
|
+
* @param {?number} step
|
|
235
|
+
* @return {Array|string}
|
|
236
|
+
*/
|
|
237
|
+
exports.slice = function (obj, start, stop, step) {
|
|
238
|
+
var isString = (typeof obj === 'string'),
|
|
239
|
+
length,
|
|
240
|
+
lower,
|
|
241
|
+
upper,
|
|
242
|
+
s,
|
|
243
|
+
e,
|
|
244
|
+
result = [],
|
|
245
|
+
i;
|
|
246
|
+
|
|
247
|
+
function toInt(n) {
|
|
248
|
+
n = Number(n);
|
|
249
|
+
if (isNaN(n)) { return NaN; }
|
|
250
|
+
return n < 0 ? Math.ceil(n) : Math.floor(n);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clamp(v, dflt) {
|
|
254
|
+
if (v === null || v === undefined) { return dflt; }
|
|
255
|
+
v = toInt(v);
|
|
256
|
+
if (isNaN(v)) { return dflt; }
|
|
257
|
+
if (v < 0) {
|
|
258
|
+
v += length;
|
|
259
|
+
if (v < lower) { v = lower; }
|
|
260
|
+
} else if (v > upper) {
|
|
261
|
+
v = upper;
|
|
262
|
+
}
|
|
263
|
+
return v;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (obj === null || obj === undefined || typeof obj.length !== 'number') {
|
|
267
|
+
return isString ? '' : [];
|
|
268
|
+
}
|
|
269
|
+
length = obj.length;
|
|
270
|
+
|
|
271
|
+
if (step === null || step === undefined) {
|
|
272
|
+
step = 1;
|
|
273
|
+
} else {
|
|
274
|
+
step = toInt(step);
|
|
275
|
+
if (isNaN(step) || step === 0) { step = 1; }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Lower / upper bounds depend on walk direction (CPython slice rules).
|
|
279
|
+
if (step < 0) {
|
|
280
|
+
lower = -1;
|
|
281
|
+
upper = length - 1;
|
|
282
|
+
} else {
|
|
283
|
+
lower = 0;
|
|
284
|
+
upper = length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
s = clamp(start, (step < 0) ? upper : lower);
|
|
288
|
+
e = clamp(stop, (step < 0) ? lower : upper);
|
|
289
|
+
|
|
290
|
+
if (step > 0) {
|
|
291
|
+
for (i = s; i < e; i += step) { result.push(obj[i]); }
|
|
292
|
+
} else {
|
|
293
|
+
for (i = s; i > e; i += step) { result.push(obj[i]); }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return isString ? result.join('') : result;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Coerce a value for string output: `null` and `undefined` become the
|
|
301
|
+
* empty string, everything else passes through unchanged. Used by the
|
|
302
|
+
* backend's `Output` emit when a frontend opts in via `IROutput.coerce`,
|
|
303
|
+
* so a non-VarRef expression that evaluates to null / undefined renders
|
|
304
|
+
* as "" rather than the literal word "null" / "undefined". (VarRef
|
|
305
|
+
* outputs already coerce in `emitVarRef`, so frontends only flag
|
|
306
|
+
* non-VarRef outputs.)
|
|
307
|
+
*
|
|
308
|
+
* @param {*} v
|
|
309
|
+
* @return {*} `v`, or "" when `v` is null / undefined.
|
|
310
|
+
*/
|
|
311
|
+
exports.coerceOutput = function (v) {
|
|
312
|
+
return (v === null || v === undefined) ? '' : v;
|
|
313
|
+
};
|