@rhinostone/swig 2.8.4 → 2.9.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,12 @@
1
+ [2.9.0](https://github.com/gina-io/swig/tree/v2.9.0) / 2026-10-23
2
+ -----------------------------------------------------------------
3
+
4
+ * **Added** An OpenSSF Scorecard badge at the top of the README of each of the five packages. It shows the score that a Scorecard workflow publishes for the repository on every push to develop and every week, and links to the Scorecard report. Documentation only; no runtime or API change.
5
+
6
+ * **Added** swig-twig, swig-jinja2 and swig-django: the `not in` operator, as Twig, Jinja2 and Django have it. `x not in y` negates `x in y` and binds at the same level; where Django cannot run the test it is false, as Django gives it, and where Jinja2 raises swig gives true. Until now `x not in y` failed to parse. Native swig still rejects `not in`.
7
+
8
+ * **Fixed** swig-twig, swig-jinja2 and swig-django: the `in` operator tests membership as Twig, Jinja2 and Django do. In Jinja2 and Django a list holds an equal item, a string a substring and a mapping a key; in Twig a list or a mapping holds a value, compared loosely, and a string holds a string or number as a substring. Until now `in` was the JavaScript operator, which tests keys: `3 in [1, 2, 3]` was false, `0 in [5]` true, and `"b" in "abc"` threw. A test the reference engine cannot run (a number or null container, for instance) is false instead of an error. Native swig keeps the JavaScript `in`.
9
+
1
10
  [2.8.4](https://github.com/gina-io/swig/tree/v2.8.4) / 2026-10-21
2
11
  -----------------------------------------------------------------
3
12
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Swig
2
2
  ====
3
3
 
4
- [![CI](https://github.com/gina-io/swig/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/gina-io/swig/actions/workflows/ci.yml) [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![NPM Downloads](https://img.shields.io/npm/dm/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig)](https://socket.dev/npm/package/@rhinostone/swig)
4
+ [![CI](https://github.com/gina-io/swig/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/gina-io/swig/actions/workflows/ci.yml) [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![NPM Downloads](https://img.shields.io/npm/dm/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig)](https://socket.dev/npm/package/@rhinostone/swig) [![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/gina-io/swig?label=openssf%20scorecard&style=flat)](https://scorecard.dev/viewer/?uri=github.com/gina-io/swig)
5
5
 
6
6
  > **Multi-flavor template engine** for Node.js and browsers — native Swig syntax (Jinja2/Django-inspired), Twig syntax, Python Jinja2 syntax, and Django Template Language syntax via dedicated frontends sharing one IR backend. [gina-io/swig](https://github.com/gina-io/swig) started as a maintained continuation of the abandoned `paularmstrong/swig` (last released 2014) and is now a standalone project. Security and bug fixes ship here.
7
7
 
@@ -32,7 +32,7 @@ Features
32
32
  --------
33
33
 
34
34
  * Available for node.js **and** major web browsers.
35
- * **Strict-CSP browser runtime** — AOT-compile templates ahead of time (`swig compile --recursive --register`) and render them with the runtime-only bundle (`dist/swig.runtime.min.js`, ~22 KB) — no parser, no `new Function`, no `unsafe-eval` required.
35
+ * **Strict-CSP browser runtime** — AOT-compile templates ahead of time (`swig compile --recursive --register`) and render them with the runtime-only bundle (`dist/swig.runtime.min.js`, ~24 KB) — no parser, no `new Function`, no `unsafe-eval` required.
36
36
  * [Express](https://expressjs.com/) compatible.
37
37
  * **Async template loading** — back the loader with S3 / Redis / HTTP / any callback API: set `loader.async = true` and pass a callback to `renderFile`, or to `compileFile` with `codegenMode: 'async'` in its options, whose compiled function returns a promise of `{ output, exports }`; `extends` / `include` / `import` resolve at render time, dynamic `{% extends %}` paths included.
38
38
  * Object-Oriented template inheritance.
@@ -43,12 +43,14 @@ Features
43
43
  * Robust without the bloat — a single runtime dependency ([`@rhinostone/swig-core`](https://www.npmjs.com/package/@rhinostone/swig-core)).
44
44
  * Extendable and customizable — register custom filters, tags, and loaders per-instance.
45
45
 
46
- What's new in v2.8.4
46
+ What's new in v2.9.0
47
47
  --------------------
48
48
 
49
- * **Fixed — a string `autoescape` option names the escape strategy.** In `@rhinostone/swig`, `autoescape: 'js'` now gives JavaScript escaping and `autoescape: 'html'` HTML escaping, as documented. The option was written into the compiled template as code, so these values failed with `ReferenceError: js is not defined`, and an autoescape option built from untrusted input could run as code in the template. A value already wrapped in quotes keeps working, and a value holding a double quote or a line break is now rejected with an error. The Twig, Jinja2 and Django frontends already passed the option to the escape filter as a string.
49
+ > **Behaviour change in `@rhinostone/swig-twig`, `@rhinostone/swig-jinja2` and `@rhinostone/swig-django`.** `x in list` now tests the list's items, not its indices, and `x in "text"` a substring, as Twig, Jinja2 and Django do. In swig-twig, `x in mapping` now tests the mapping's values, as Twig does: write `x in mapping|keys` to test a key. Native `@rhinostone/swig` is unchanged.
50
50
 
51
- * **Fixed — a numeric `filename` option compiles again.** A filename that is not a string, such as a number, compiles again with `import`, `from` and `extends` on the async render path, as it did in 2.8.2, and `include`, which failed with a number in 2.8.2 as well, now compiles too. 2.8.3 escaped the filename as a string and failed with `value.replace is not a function`; it is now converted to a string first. All four frontends.
51
+ * **Fixed — `in` tests membership in the Twig, Jinja2 and Django frontends.** In swig-jinja2 and swig-django a list holds an equal item, a string a substring and a mapping a key; in swig-twig a list or a mapping holds a value, compared loosely, and a string holds a string or number as a substring. Until now `in` was the JavaScript operator, which tests keys: `3 in [1, 2, 3]` was false, `0 in [5]` true, and `"b" in "abc"` threw. A test the reference engine cannot run (a number or null container, for instance) is false instead of an error. Native `@rhinostone/swig` keeps the JavaScript `in`, which tests an object's keys or an array's indices.
52
+ * **Added — `not in` in the Twig, Jinja2 and Django frontends.** `x not in y` negates `x in y` and binds at the same level. Where Django cannot run the test it is false, as in Django; where Jinja2 raises, swig gives true. Until now `x not in y` failed to parse. Native `@rhinostone/swig` still rejects `not in`.
53
+ * **Added — an OpenSSF Scorecard badge** at the top of the README of each of the five packages. It shows the score that a Scorecard workflow publishes for the repository on every push to develop and every week, and links to the Scorecard report. Documentation only; no runtime or API change.
52
54
 
53
55
  See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
54
56
 
package/ROADMAP.md CHANGED
@@ -10,10 +10,11 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
10
10
 
11
11
  | Status | Item |
12
12
  | --- | --- |
13
- | Planned | `in` tests membership in all four flavors: `x in list` tests the elements and `x in "text"` the substring. Today `in` tests an object's keys or an array's indices, so `3 in [1, 2, 3]` is false. |
14
13
  | Planned | swig-jinja2 parity: loop filters (`{% for x in xs if cond %}`), `not(x)` written without a space, the `none` / `True` / `False` constants, for-loops over a mapping and over a list of pairs as Jinja2 iterates them, and the builtin filters `attr`, `filesizeformat`, `forceescape`, `items`, `string` and `urlize`. |
15
14
  | Planned | swig-django: two-variable for-loops over a list of pairs unpack each pair, as Django does. |
16
15
  | Planned | Unknown filter and test names rejected when a Twig, Jinja2 or Django template compiles; today the error comes only at render, or never when the branch does not run. Native swig already rejects an unknown filter. |
16
+ | Planned | swig-django: `not` binds below the comparisons and `in`, as in Django; today `not x == y` reads as `(not x) == y`. |
17
+ | Planned | swig-jinja2 and swig-django: `==` and `!=` compare as Python does; today they are JavaScript's loose operators, so `1 == "1"` is true and `[1] == [1]` false. |
17
18
 
18
19
  ## Future (post-2.0)
19
20
 
@@ -27,6 +28,13 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
27
28
 
28
29
  ## Completed
29
30
 
31
+ ### v2.9.0 (October 2026)
32
+
33
+ - `in` tests membership in swig-twig, swig-jinja2 and swig-django, as Twig, Jinja2 and Django do: a list holds an item, a string a substring, and a mapping a key (Jinja2, Django) or a value (Twig). Native swig keeps the JavaScript `in`, which tests an object's keys or an array's indices.
34
+ - `not in` in swig-twig, swig-jinja2 and swig-django.
35
+ - An OpenSSF Scorecard badge on the README of each package.
36
+ - All five packages released in lockstep at `2.9.0`.
37
+
30
38
  ### v2.8.4 (October 2026)
31
39
 
32
40
  - Fixed the `autoescape` option of `@rhinostone/swig` set to an escape strategy name: `'js'` and `'html'` work as documented. The value was written into the compiled template as code, so these failed with a ReferenceError and an option built from untrusted input could run as code; a value holding a double quote or a line break is now rejected with an error.
package/dist/swig.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Swig v2.8.4 | https://github.com/gina-io/swig | @license https://github.com/gina-io/swig/blob/master/LICENSE */
1
+ /*! Swig v2.9.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/tn0ms/DateZ/blob/master/LISENCE */
3
3
  (() => {
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -250,6 +250,58 @@
250
250
  }
251
251
  return cur;
252
252
  };
253
+ function sameItem(a, b, twig) {
254
+ if (a === b) {
255
+ return true;
256
+ }
257
+ if (!twig) {
258
+ return (a === null || a === void 0) && (b === null || b === void 0);
259
+ }
260
+ if (a !== null && typeof a === "object" || b !== null && typeof b === "object" || typeof a === "function" || typeof b === "function") {
261
+ return false;
262
+ }
263
+ return a == b;
264
+ }
265
+ function membership(needle, haystack, twig) {
266
+ var keys, i;
267
+ if (typeof haystack === "string") {
268
+ if (typeof needle === "string" || twig && typeof needle === "number") {
269
+ return haystack.indexOf(String(needle)) !== -1;
270
+ }
271
+ return twig ? false : null;
272
+ }
273
+ if (haystack === null || typeof haystack !== "object") {
274
+ return twig ? false : null;
275
+ }
276
+ if (exports.isArray(haystack)) {
277
+ for (i = 0; i < haystack.length; i += 1) {
278
+ if (sameItem(needle, haystack[i], twig)) {
279
+ return true;
280
+ }
281
+ }
282
+ return false;
283
+ }
284
+ if (twig) {
285
+ keys = Object.keys(haystack);
286
+ for (i = 0; i < keys.length; i += 1) {
287
+ if (sameItem(needle, haystack[keys[i]], true)) {
288
+ return true;
289
+ }
290
+ }
291
+ return false;
292
+ }
293
+ if (typeof needle === "string" || typeof needle === "number") {
294
+ return Object.prototype.hasOwnProperty.call(haystack, String(needle));
295
+ }
296
+ return needle !== null && typeof needle === "object" ? null : false;
297
+ }
298
+ exports.isIn = function(needle, haystack, mode, negate) {
299
+ var found = membership(needle, haystack, mode === "twig");
300
+ if (found === null) {
301
+ return negate === true && mode !== "django";
302
+ }
303
+ return negate === true ? !found : found;
304
+ };
253
305
  }
254
306
  });
255
307
 
@@ -1923,12 +1975,18 @@
1923
1975
  }
1924
1976
  return js;
1925
1977
  }
1978
+ function membershipMode(flag) {
1979
+ return flag === "twig" || flag === "django" ? flag : "jinja2";
1980
+ }
1926
1981
  function emitBinaryOp(node, d) {
1927
1982
  var left = groupOperand(node.left, emitExpr(node.left, d)), right = groupOperand(node.right, emitExpr(node.right, d));
1928
1983
  if (isArithmeticOp(node.op)) {
1929
1984
  return left + " " + node.op + " " + right;
1930
1985
  }
1931
1986
  if (node.op === "in") {
1987
+ if (node.membership) {
1988
+ return "_utils.isIn(" + left + ", " + right + ', "' + membershipMode(node.membership) + '"' + (node.negate ? ", true" : "") + ")";
1989
+ }
1932
1990
  return left + " in " + right;
1933
1991
  }
1934
1992
  if (node.op === "~") {
@@ -4836,7 +4894,7 @@
4836
4894
  var loaders = require_loaders2();
4837
4895
  var preWalker = require_pre_walker();
4838
4896
  var engine = require_engine();
4839
- exports.version = "2.8.4";
4897
+ exports.version = "2.9.0";
4840
4898
  var defaultOptions = {
4841
4899
  autoescape: true,
4842
4900
  varControls: ["{{", "}}"],
@@ -5076,6 +5134,19 @@
5076
5134
  * `_dangerousProps` segments are rejected at runtime as defense-in-depth.
5077
5135
  * @private
5078
5136
  */
5137
+ /*!
5138
+ * Item equality for `isIn`. Jinja2 and Django compare with `===` and treat
5139
+ * null and a missing value as equal (Django reads both as None). Twig
5140
+ * compares two scalars with `==`, as swig-twig's `==` does, and anything
5141
+ * else by identity. @private
5142
+ */
5143
+ /*!
5144
+ * The membership test of `isIn`: true or false, or null when Jinja2 and
5145
+ * Django cannot run it, where Python raises a TypeError: a container that is
5146
+ * not a list, a string or a mapping, a needle that is not a string tested
5147
+ * against a string, or a list or an object tested against a mapping's keys.
5148
+ * Twig runs every test. @private
5149
+ */
5079
5150
  /*!
5080
5151
  * Attach `loc` to the node if provided, skipping the assignment otherwise
5081
5152
  * so consumers can tell "no source location available" from
@@ -5177,6 +5248,10 @@
5177
5248
  * wraps a unary operand, for the positions where its prefix would bind
5178
5249
  * differently (`-(-x)`, `(-x)[0]`). @private
5179
5250
  */
5251
+ /*!
5252
+ * The flavor name `emitBinaryOp` writes into an `_utils.isIn` call: one of
5253
+ * the three names `isIn` knows, never the flag's own text. @private
5254
+ */
5180
5255
  /*!
5181
5256
  * Emit a function / method invocation. Three callee shapes:
5182
5257
  * 1. Single-segment VarRef (`foo(...)`) — FUNCTION-token pattern with