@rhinostone/swig 2.4.2 → 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/HISTORY.md CHANGED
@@ -1,3 +1,13 @@
1
+ [2.5.0](https://github.com/gina-io/swig/tree/v2.5.0) / 2026-05-27
2
+ -----------------------------------------------------------------
3
+
4
+ * **Added** @rhinostone/swig-jinja2 — a Python Jinja2-syntax frontend on the shared swig-core engine (13 tags, 39 filters, 16 is-tests, async loader support, autoescape and CVE-2023-25345 guards inherited from swig-core). Releases in lockstep with @rhinostone/swig, @rhinostone/swig-core, and @rhinostone/swig-twig.
5
+
6
+ [2.4.3](https://github.com/gina-io/swig/tree/v2.4.3) / 2026-05-22
7
+ -----------------------------------------------------------------
8
+
9
+ * **Fixed** Native `import` no longer leaks an imported file's own import aliases into the importing template's context. A file-level `{% import %}` inside an imported file is now re-homed under the importing alias instead of emitted bare, so it cannot clobber a same-named variable in the caller, corrupt a macro when the caller later reassigns that name, or cascade across import depth. Macros still resolve their own file's imports at call time.
10
+
1
11
  [2.4.2](https://github.com/gina-io/swig/tree/v2.4.2) / 2026-05-22
2
12
  -----------------------------------------------------------------
3
13
 
@@ -52,7 +62,7 @@
52
62
  [2.0.0](https://github.com/gina-io/swig/tree/v2.0.0) / 2026-05-06
53
63
  -----------------------------------------------------------------
54
64
 
55
- * **Fixed** Refreshed stale external URL references across documentation surfaces from the 2026-04-13 link-health audit (gina-io/gina#18). `README.md`, `packages/swig-core/README.md`, `packages/swig-twig/README.md` — swapped `www.npmjs.org` for the canonical `www.npmjs.com` in the NPM badges (the old `.org` form now redirects). `README.md` — rewrote the "Swig v0.x → v1.x migration notes" bullet, since the `paularmstrong/swig/wiki/Migrating-from-...` page was deleted upstream (302s to repo root). `HISTORY.md` — dropped the three dead upstream-wiki links around the v1.0.0 / v1.0.0-pre1 entries; the breaking-change prose stays intact. `browser/comments.js` — updated the DateZ `@license` URL from `TomoUniversalis/DateZ` to `ocrybit/DateZ` (the upstream GitHub user was renamed). `packages/swig-core/lib/dateformatter.js` — dropped the dead `http://tomouniversalis.com` parenthetical from the DateZ copyright comment (domain NXDOMAINs). `.changes/v1.0.0.md` and `.changes/v1.0.0-pre1.md` synced to match the rewritten `HISTORY.md` lines so future `changie merge` runs stay idempotent. Documentation and comment-only; no runtime or API change.
65
+ * **Fixed** Refreshed stale external URL references across documentation surfaces from the 2026-04-13 link-health audit (gina-io/gina#18). `README.md`, `packages/swig-core/README.md`, `packages/swig-twig/README.md` — swapped `www.npmjs.org` for the canonical `www.npmjs.com` in the NPM badges (the old `.org` form now redirects). `README.md` — rewrote the "Swig v0.x → v1.x migration notes" bullet, since the `paularmstrong/swig/wiki/Migrating-from-...` page was deleted upstream (302s to repo root). `HISTORY.md` — dropped the three dead upstream-wiki links around the v1.0.0 / v1.0.0-pre1 entries; the breaking-change prose stays intact. `browser/comments.js` — updated the DateZ `@license` URL from `TomoUniversalis/DateZ` to `ocrybit/DateZ` (the upstream GitHub user was renamed). `packages/swig-core/lib/dateformatter.js` — dropped the dead `tomouniversalis.com` parenthetical from the DateZ copyright comment (domain NXDOMAINs). `.changes/v1.0.0.md` and `.changes/v1.0.0-pre1.md` synced to match the rewritten `HISTORY.md` lines so future `changie merge` runs stay idempotent. Documentation and comment-only; no runtime or API change.
56
66
 
57
67
  * **Changed** `2.0.0` stable. Multi-flavor architecture (introduced across `2.0.0-alpha.1` through `2.0.0-alpha.5`) is the production-ready cut. No functional or API changes since `2.0.0-alpha.5`; the `alpha.6`–`alpha.8` cycle was metadata republishes plus removal of the soft-deprecated `exports.parse` Path B wrapper from `@rhinostone/swig-twig`. IR ABI is stable from this release onward; cross-package dependencies pin exact versions and frontends + core release in lockstep. README messaging refreshed across all three packages to reflect production-ready status; package descriptions cleaned of historical internal-tracking references.
58
68
 
package/README.md CHANGED
@@ -64,7 +64,7 @@ For Twig syntax:
64
64
  Documentation
65
65
  -------------
66
66
 
67
- User-facing documentation lives in the Gina Docusaurus site under the [Swig Template Engine](https://gina.io/docs/swig) section, maintained in [gina-io/docs](https://github.com/gina-io/docs) at `docs/swig/`. The JSDoc blocks in `lib/swig.js`, `lib/filters.js`, `lib/tags/`, and `lib/loaders/` remain the canonical source-of-truth for the public API and are mirrored into the Docusaurus pages.
67
+ User-facing documentation lives in the Gina Docusaurus site under the [Swig Template Engine](https://gina.io/docs/swig) section, maintained in [gina-io/docs](https://github.com/gina-io/docs) at `docs/templating/swig/`. The JSDoc blocks in `lib/swig.js`, `lib/filters.js`, `lib/tags/`, and `lib/loaders/` remain the canonical source-of-truth for the public API and are mirrored into the Docusaurus pages.
68
68
 
69
69
  Basic Example
70
70
  -------------
package/ROADMAP.md CHANGED
@@ -15,13 +15,22 @@ _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upco
15
15
  | Status | Item |
16
16
  | --- | --- |
17
17
  | Planned | Async parse path for dynamic targets — full support for `{% extends parent_var %}`, `{% include user_template %}`, and runtime-resolved `import` / `from` paths on the async-codegen branch. Static-target async dispatch shipped in 2.2.0; dynamic-target support is on hold pending consumer demand. |
18
- | Planned | Ship Jinja2 and Django frontends as additional `@rhinostone/swig-*` packages. On demand — when there's concrete user demand. |
18
+ | Planned | Ship a Django frontend as an additional `@rhinostone/swig-*` package. On demand — when there's concrete user demand. (The Jinja2 frontend shipped in `2.5.0`.) |
19
19
  | Planned | Test framework migration. Replace mocha 1.x + expect.js with `node:test` + `node:assert/strict`, swap mocha-phantomjs for a modern browser-test harness, swap blanket for `c8`. (The Node engines bump is upstream-driven by gina and is being treated as done.) |
20
20
 
21
21
  ---
22
22
 
23
23
  ## Completed
24
24
 
25
+ ### v2.5.0 (May 2026)
26
+
27
+ - Added `@rhinostone/swig-jinja2`, a Python Jinja2-syntax frontend on the shared `@rhinostone/swig-core` engine — the third dialect in the multi-flavor family alongside native swig and `@rhinostone/swig-twig`. Ships 13 tags (`set`, `if` / `elif` / `else`, `for` with `else`, `block`, `extends`, `include`, `macro`, `import`, `from`, `raw`, `filter`, `with`, `autoescape`), 39 filters, and 16 `is` tests, plus the `**` / `//` / `~` operators, inline-if, Python slicing, and `{{- … -}}` whitespace control. Async loader support via `renderFileAsync` / `compileFileAsync`. Autoescape and the CVE-2023-25345 guards are inherited from `@rhinostone/swig-core`. Every filter and is-test was cross-checked against Python Jinja2 3.x; the behavioural differences (where the JavaScript runtime diverges from CPython) and the explicit non-goals (no sandboxed rendering, `{% call %}` / `{% do %}` / `{% trans %}`, the `map` / `select` filter family, macro kwargs) are documented in the Jinja2 templating guide.
28
+ - All four packages (`@rhinostone/swig`, `@rhinostone/swig-core`, `@rhinostone/swig-twig`, `@rhinostone/swig-jinja2`) released in lockstep at `2.5.0`. `@rhinostone/swig-core` gained additive `slice` and `coerceOutput` runtime helpers consumed by the Jinja2 frontend; native swig and Twig are functionally unchanged.
29
+
30
+ ### v2.4.3 (May 2026)
31
+
32
+ - Fixed native `import` leaking an imported file's own import aliases into the importing template's scope. The `{% import %}` carry-through added in `2.4.1` emitted those nested imports bare into the caller's context, where the leaked alias could clobber a same-named caller variable, corrupt a macro when the caller later reassigned that name (macros read the live context at call time), or cascade across import depth. The nested imports are now re-homed under the importing alias and kept local to the file that declares them — matching `@rhinostone/swig-twig`'s scoping and the Jinja2/Twig import contract. Macros still resolve their own file's imports at call time.
33
+
25
34
  ### v2.4.2 (May 2026)
26
35
 
27
36
  - Fixed the same empty-render bug in `@rhinostone/swig-twig`: a macro that calls a macro imported at the top of its own defining file — via either `{% import %}` or `{% from %}` — now resolves at call time instead of rendering empty. The imported file's own imports stay local to the template that declares them (they are not leaked bare into the importing template's scope), matching Twig's macro/import scoping rule. Resolves recursively across import depth.
package/dist/swig.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Swig v2.4.2 | https://github.com/gina-io/swig | @license https://github.com/gina-io/swig/blob/master/LICENSE */
1
+ /*! Swig v2.5.0 | https://github.com/gina-io/swig | @license https://github.com/gina-io/swig/blob/master/LICENSE */
2
2
  /*! DateZ (c) 2011 Tomo Universalis | @license https://github.com/ocrybit/DateZ/blob/master/LISENCE */
3
3
  (() => {
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -139,6 +139,68 @@
139
139
  }
140
140
  return out;
141
141
  };
142
+ exports.slice = function(obj, start, stop, step) {
143
+ var isString = typeof obj === "string", length, lower, upper, s, e, result = [], i;
144
+ function toInt(n) {
145
+ n = Number(n);
146
+ if (isNaN(n)) {
147
+ return NaN;
148
+ }
149
+ return n < 0 ? Math.ceil(n) : Math.floor(n);
150
+ }
151
+ function clamp(v, dflt) {
152
+ if (v === null || v === void 0) {
153
+ return dflt;
154
+ }
155
+ v = toInt(v);
156
+ if (isNaN(v)) {
157
+ return dflt;
158
+ }
159
+ if (v < 0) {
160
+ v += length;
161
+ if (v < lower) {
162
+ v = lower;
163
+ }
164
+ } else if (v > upper) {
165
+ v = upper;
166
+ }
167
+ return v;
168
+ }
169
+ if (obj === null || obj === void 0 || typeof obj.length !== "number") {
170
+ return isString ? "" : [];
171
+ }
172
+ length = obj.length;
173
+ if (step === null || step === void 0) {
174
+ step = 1;
175
+ } else {
176
+ step = toInt(step);
177
+ if (isNaN(step) || step === 0) {
178
+ step = 1;
179
+ }
180
+ }
181
+ if (step < 0) {
182
+ lower = -1;
183
+ upper = length - 1;
184
+ } else {
185
+ lower = 0;
186
+ upper = length;
187
+ }
188
+ s = clamp(start, step < 0 ? upper : lower);
189
+ e = clamp(stop, step < 0 ? lower : upper);
190
+ if (step > 0) {
191
+ for (i = s; i < e; i += step) {
192
+ result.push(obj[i]);
193
+ }
194
+ } else {
195
+ for (i = s; i > e; i += step) {
196
+ result.push(obj[i]);
197
+ }
198
+ }
199
+ return isString ? result.join("") : result;
200
+ };
201
+ exports.coerceOutput = function(v) {
202
+ return v === null || v === void 0 ? "" : v;
203
+ };
142
204
  }
143
205
  });
144
206
 
@@ -1366,7 +1428,22 @@
1366
1428
  return;
1367
1429
  }
1368
1430
  });
1369
- out += "(function () {\n var __l = " + forIterable + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n if (!__l) { return; }\n var ' + ctxloopcache + " = { loop: " + ctxloop + ", " + forVal + ": " + ctx + forVal + ", " + forKey + ": " + ctx + forKey + " };\n " + ctxloop + " = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n _utils.each(__l, function (" + forVal + ", " + forKey + ") {\n " + ctx + forVal + " = " + forVal + ";\n " + ctx + forKey + " = " + forKey + ";\n " + ctxloop + ".key = " + forKey + ";\n " + ctxloop + ".first = (" + ctxloop + ".index0 === 0);\n " + ctxloop + ".last = (" + ctxloop + ".revindex0 === 0);\n " + forBodyJS + " " + ctxloop + ".index += 1; " + ctxloop + ".index0 += 1; " + ctxloop + ".revindex -= 1; " + ctxloop + ".revindex0 -= 1;\n });\n " + ctxloop + " = " + ctxloopcache + ".loop;\n " + ctx + forVal + " = " + ctxloopcache + "." + forVal + ";\n " + ctx + forKey + " = " + ctxloopcache + "." + forKey + ";\n " + ctxloopcache + " = undefined;\n})();\n";
1431
+ var forEmptyCheck = " if (!__l) { return; }\n";
1432
+ if (node.emptyBody) {
1433
+ var forEmptyJS = "";
1434
+ utils.each(node.emptyBody, function(b) {
1435
+ if (b.type === "LegacyJS") {
1436
+ forEmptyJS += b.js;
1437
+ return;
1438
+ }
1439
+ if (b.type === "Text" || b.type === "Raw") {
1440
+ forEmptyJS += '_output += "' + escapeTextValue(b.value) + '";\n';
1441
+ return;
1442
+ }
1443
+ });
1444
+ forEmptyCheck = " if (!__l || !__len) {\n" + forEmptyJS + " return;\n }\n";
1445
+ }
1446
+ out += "(function () {\n var __l = " + forIterable + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n' + forEmptyCheck + " var " + ctxloopcache + " = { loop: " + ctxloop + ", " + forVal + ": " + ctx + forVal + ", " + forKey + ": " + ctx + forKey + " };\n " + ctxloop + " = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n _utils.each(__l, function (" + forVal + ", " + forKey + ") {\n " + ctx + forVal + " = " + forVal + ";\n " + ctx + forKey + " = " + forKey + ";\n " + ctxloop + ".key = " + forKey + ";\n " + ctxloop + ".first = (" + ctxloop + ".index0 === 0);\n " + ctxloop + ".last = (" + ctxloop + ".revindex0 === 0);\n " + forBodyJS + " " + ctxloop + ".index += 1; " + ctxloop + ".index0 += 1; " + ctxloop + ".revindex -= 1; " + ctxloop + ".revindex0 -= 1;\n });\n " + ctxloop + " = " + ctxloopcache + ".loop;\n " + ctx + forVal + " = " + ctxloopcache + "." + forVal + ";\n " + ctx + forKey + " = " + ctxloopcache + "." + forKey + ";\n " + ctxloopcache + " = undefined;\n})();\n";
1370
1447
  return;
1371
1448
  }
1372
1449
  if (node.type === "Macro") {
@@ -1381,11 +1458,14 @@
1381
1458
  return;
1382
1459
  }
1383
1460
  });
1384
- var macroParams = node.params || [], macroSigJS, macroIndexOfJS;
1461
+ var macroParams = node.params || [], macroSigJS, macroIndexOfJS, macroDefaultsJS = "";
1385
1462
  if (macroParams.length && typeof macroParams[0] === "object" && macroParams[0] !== null && typeof macroParams[0].name === "string") {
1386
1463
  var macroNames = [];
1387
1464
  utils.each(macroParams, function(p) {
1388
1465
  macroNames.push(p.name);
1466
+ if (p["default"]) {
1467
+ macroDefaultsJS += " if (" + p.name + " === undefined) { " + p.name + " = " + exports.emitExpr(p["default"]) + "; }\n";
1468
+ }
1389
1469
  });
1390
1470
  macroSigJS = macroNames.join(", ");
1391
1471
  var macroJsonNames = [];
@@ -1397,7 +1477,7 @@
1397
1477
  macroSigJS = macroParams.join("");
1398
1478
  macroIndexOfJS = '"' + macroParams.join('","') + '"';
1399
1479
  }
1400
- out += "_ctx." + node.name + " = function (" + macroSigJS + ') {\n var _output = "",\n __ctx = _utils.extend({}, _ctx);\n _utils.each(_ctx, function (v, k) {\n if ([' + macroIndexOfJS + "].indexOf(k) !== -1) { delete _ctx[k]; }\n });\n" + macroBodyJS + "\n _ctx = _utils.extend(_ctx, __ctx);\n return _output;\n};\n_ctx." + node.name + ".safe = true;\n";
1480
+ out += "_ctx." + node.name + " = function (" + macroSigJS + ') {\n var _output = "",\n __ctx = _utils.extend({}, _ctx);\n' + macroDefaultsJS + " _utils.each(_ctx, function (v, k) {\n if ([" + macroIndexOfJS + "].indexOf(k) !== -1) { delete _ctx[k]; }\n });\n" + macroBodyJS + "\n _ctx = _utils.extend(_ctx, __ctx);\n return _output;\n};\n_ctx." + node.name + ".safe = true;\n";
1401
1481
  if (options && options.codegenMode === "async") {
1402
1482
  if (_security.dangerousProps.indexOf(node.name) !== -1) {
1403
1483
  throw new Error('Macro name "' + node.name + '" is reserved.');
@@ -1625,7 +1705,11 @@
1625
1705
  outExprJS = '_filters["' + fc.name + '"](' + outExprJS + fcArgsJS + ")";
1626
1706
  });
1627
1707
  }
1628
- out += "_output += " + outExprJS + ";\n";
1708
+ if (node.coerce) {
1709
+ out += "_output += _utils.coerceOutput(" + outExprJS + ");\n";
1710
+ } else {
1711
+ out += "_output += " + outExprJS + ";\n";
1712
+ }
1629
1713
  return;
1630
1714
  }
1631
1715
  if (node.type === "Filter") {
@@ -2356,9 +2440,9 @@
2356
2440
  * `parseExpr` emits structured IR that {@link backend.emitExpr}
2357
2441
  * later lowers into an equivalent JS-source fragment. `.parse()` is
2358
2442
  * unchanged and remains the production path; `parseExpr` is the
2359
- * incoming target shape for Phase 2 (#T15), introduced additively in
2360
- * Session 14b so the IR grammar can be proven against real lexer
2361
- * output before consumers are flipped in Commits 3-8.
2443
+ * incoming target shape for the IR migration, introduced additively
2444
+ * so the IR grammar can be proven against real lexer output before
2445
+ * consumers are flipped over to it.
2362
2446
  *
2363
2447
  * The CVE-2023-25345 prototype-chain guards (`_dangerousProps` on
2364
2448
  * VAR segments, DOTKEY matches, STRING-inside-BRACKETOPEN values,
@@ -3074,14 +3158,30 @@
3074
3158
  re: "_ctx." + ctx + "." + arg.name + "$1"
3075
3159
  };
3076
3160
  });
3161
+ var innerReplacements = [];
3077
3162
  utils.each(nestedImports, function(arg) {
3078
- out += arg.compiled;
3163
+ utils.each(arg.boundNames, function(nm) {
3164
+ innerReplacements.push({
3165
+ ex: new RegExp("_ctx\\." + nm + "(\\W)", "g"),
3166
+ re: "_ctx." + ctx + "." + nm + "$1"
3167
+ });
3168
+ });
3169
+ });
3170
+ utils.each(nestedImports, function(arg) {
3171
+ var c = arg.compiled;
3172
+ utils.each(innerReplacements, function(re) {
3173
+ c = c.replace(re.ex, re.re);
3174
+ });
3175
+ out += c;
3079
3176
  });
3080
3177
  utils.each(macros, function(arg) {
3081
3178
  var c = arg.compiled;
3082
3179
  utils.each(replacements, function(re) {
3083
3180
  c = c.replace(re.ex, re.re);
3084
3181
  });
3182
+ utils.each(innerReplacements, function(re) {
3183
+ c = c.replace(re.ex, re.re);
3184
+ });
3085
3185
  out += c;
3086
3186
  });
3087
3187
  return out;
@@ -3107,7 +3207,11 @@
3107
3207
  if (token2.name === "import") {
3108
3208
  self.out.push({
3109
3209
  compiled: token2.compile(compiler, token2.args.slice(), token2.content, [], compileOpts) + "\n",
3110
- isImport: true
3210
+ isImport: true,
3211
+ // The nested import binds one namespace alias (the tail of its
3212
+ // parsed args). compile() re-homes it under THIS import's alias so
3213
+ // it never leaks bare into the parent _ctx.
3214
+ boundNames: [token2.args[token2.args.length - 1]]
3111
3215
  });
3112
3216
  return;
3113
3217
  }
@@ -4578,7 +4682,7 @@
4578
4682
  var loaders = require_loaders2();
4579
4683
  var preWalker = require_pre_walker();
4580
4684
  var engine = require_engine();
4581
- exports.version = "2.4.2";
4685
+ exports.version = "2.5.0";
4582
4686
  var defaultOptions = {
4583
4687
  autoescape: true,
4584
4688
  varControls: ["{{", "}}"],