@rhinostone/swig-core 2.4.3 → 2.5.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/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
- * Phase 2 — the template-level walker dispatches on IR node shape. At
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. Subsequent
16
- * sessions introduce further real IR emitters (`Autoescape`, `If`,
17
- * `For`, `Set`, etc.) alongside their matching tag migrations, and each
18
- * new shape gets its own dispatch branch here.
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 per session by returning IR nodes
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
- // Phase 2 Session 14c: multi-branch shape. The native if tag owns
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. Session 14b Commit 11
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 session baseline (see
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
- // Phase 2 Session 14b Commit 10: target is structured IRVarRef
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 (Session 14b) — backward-compat string
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
- // Phase 2: the full loopcache + _utils.each IIFE scaffolding is
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 (Session 14b) — backward-compat string fallback
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
- ' if (!__l) { return; }\n' +
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
- // Phase 2: `params` is IRMacroParam[] (Session 14b Commit 8)
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 pre-Phase-2 raw-token slice (including
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) { macroNames.push(p.name); });
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
- // Phase 3 (#T22): async-codegen mirrors the macro into _exports so
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
- // Phase 2: the parent tag walks the parents chain at compile time
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
- // Phase 2: block tokens are resolved at parse time by the engine's
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
- // Phase 3 (#T22) async mode: block remapping happens at runtime
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
- // Phase 2: `path` and `context` are IRExpr nodes (Session 14b
349
- // Commit 7) — per-slot dispatch on object-with-.type → emitExpr,
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
- // Phase 3 (#T22): async-codegen counterpart to Include. Resolves the
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
- // Phase 3 (#T22): async-codegen counterpart to the parse-time
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
- // Phase 3 (#T22): async-codegen counterpart to Twig's `{% from
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
- // Phase 3 (#T22): async-codegen counterpart to engine.getParents +
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
- // Phase 3 Session 12: scoped-context region (Twig's `{% with %}`).
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
- // Phase 2: `expr` is typed IRExpr | IRLegacyJS (Session 14b
582
- // Commit 9). The frontend's parseVariable falls back to LegacyJS
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
- out += '_output += ' + outExprJS + ';\n';
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
- // Phase 2: `args` is IRExpr[] (Session 14b Commit 6) — per-arg
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 (#T15 Session 14+): once
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
- * Phase 1 carve — owns the `extends`-chain walker and block-remap helpers.
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
- * Phase 1 carve — `iterateFilter` and the `.safe` flag convention live
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
- * Phase 1 scaffold. Subsequent commits move security guards, loader
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` for Phase 2 see Session 14b
53
- * Commit 9. The IR shape can't represent legacy Swig's per-operand
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} in
95
- * Session 14b Commit 9. The factory is opaque — it accepts any value
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` for Phase 2 — the native
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
- * Phase 2 Session 14b Commit 10: pure-dot LHS shapes (`foo`, `foo.bar`,
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
- * During Phase 2 (#T15), IRParent optionally carries a `body` slot so
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 (post-Phase 2).
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 Phase 2 layering decision (hybrid / option iii).
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` for Phase 2 — see the IRFor typedef
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 (Session 14b Commit 7).
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` for Phase 2 — pure-dot LHS
739
- * shapes are structured {@link IRVarRef} (Session 14b Commit 10);
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
- * Phase 2: optional `body` slot carries the pre-resolved parent-block
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 — reached post-Phase 2.
772
+ * backend will resolve the lookup itself.
773
773
  *
774
774
  * @param {IRStatement[]} [body]
775
775
  * @param {IRLoc} [loc]
@@ -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 Phase 2 (#T15), introduced additively in
432
- * Session 14b so the IR grammar can be proven against real lexer
433
- * output before consumers are flipped in Commits 3-8.
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",