@rhinostone/swig 2.8.4 → 2.9.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/HISTORY.md CHANGED
@@ -1,3 +1,27 @@
1
+ [2.9.1](https://github.com/gina-io/swig/tree/v2.9.1) / 2026-10-23
2
+ -----------------------------------------------------------------
3
+
4
+ * **Fixed** The Express example (examples/express) no longer has its catch-all route, which rendered any .html file a URL named as a template, including files outside the views folder. Its pages link only to the explicit routes, which are unchanged. The example is not part of any package; an application built from it should remove or confine that route.
5
+
6
+ * **Changed** The test suite gains property-based tests, which check each property on many generated inputs: in every flavor the escape filter is idempotent and leaves no raw markup character, `{{ x }}` renders the escaped string, text with no tag renders unchanged apart from line endings, and any template filename reaches the loader unchanged through an include; the membership helper behind the `in` operator matches its definition on lists. They run with the new fast-check dev dependency (MIT, one dependency, pure-rand) under a fixed seed. Development and CI only; the published package is unaffected and its only runtime dependency remains @rhinostone/swig-core.
7
+
8
+ * **Security** swig striptags no longer takes time quadratic in the length of its input. On text with many < and no > after them it ran for about five seconds per 100 000 characters, so a template that applied it to request data could be made to block the event loop. It now runs in linear time with the same output. The regex dates from the original project, so every earlier release has it.
9
+
10
+ * **Security** swig-twig striptags and trim no longer take time quadratic in the length of their input. striptags ran for about five seconds per 100 000 characters on text with many < and no > after them, and trim took about as long on a long run of the trimmed characters inside the text, so a template that applied either to request data could be made to block the event loop. Both now run in linear time with the same output. Every swig-twig release since 2.0.0-alpha.3 has them.
11
+
12
+ * **Security** swig-jinja2 striptags, trim and format no longer take time quadratic in the length of their input. striptags ran for about five seconds per 100 000 characters on text with many < or <!-- and nothing closing them, trim took as long on a long run of the trimmed characters inside the text, and format longer still on a % followed by a long run of zeros. A template that applied one of them to request data could be made to block the event loop. All three now run in linear time with the same output (Jinja2 3.1.6 strips tags and trims these inputs in linear time). Every swig-jinja2 release since 2.5.0 has them.
13
+
14
+ * **Security** swig-django striptags, truncatewords, pluralize, slugify and floatformat no longer take time quadratic in the length of their input. striptags ran for about five seconds per 100 000 characters on text with many < and no > after them, truncatewords and pluralize took about as long on a long run of whitespace inside the value, slugify on a long run of underscores, and floatformat on an argument with a long run of g or u not at its end. A template that applied one of them to request data could be made to block the event loop. All five now run in linear time with the same output (Django 5.2.18 handles these inputs in linear time). Every swig-django release since 2.7.0 has them, except pluralize, which has had it since 2.8.2.
15
+
16
+ [2.9.0](https://github.com/gina-io/swig/tree/v2.9.0) / 2026-10-23
17
+ -----------------------------------------------------------------
18
+
19
+ * **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.
20
+
21
+ * **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`.
22
+
23
+ * **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`.
24
+
1
25
  [2.8.4](https://github.com/gina-io/swig/tree/v2.8.4) / 2026-10-21
2
26
  -----------------------------------------------------------------
3
27
 
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,17 @@ 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.1
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
+ > **Security release: upgrade all five packages to 2.9.1.** `striptags` in every flavor, `trim` in swig-twig and swig-jinja2, swig-jinja2 `format`, and swig-django `truncatewords`, `pluralize`, `slugify` and `floatformat` took time quadratic in the length of their input on some inputs, several seconds for 100 000 characters. A template that applied one of them to request data could be made to block the event loop. They now run in linear time with the same output.
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
+ * **Security — swig `striptags` runs in linear time.** On text with many `<` and no `>` after them it ran for about five seconds per 100 000 characters. The regex dates from the original project, so every earlier release has it.
52
+ * **Security — swig-twig `striptags` and `trim` run in linear time.** `striptags` was slow on text with many `<` and no `>` after them, and `trim` on a long run of the trimmed characters inside the text. Every swig-twig release since 2.0.0-alpha.3 has them.
53
+ * **Security — swig-jinja2 `striptags`, `trim` and `format` run in linear time.** `striptags` was slow on text with many `<` or `<!--` and nothing closing them, `trim` on a long run of the trimmed characters inside the text, and `format` slower still on a `%` followed by a long run of zeros. Every swig-jinja2 release since 2.5.0 has them.
54
+ * **Security — swig-django `striptags`, `truncatewords`, `pluralize`, `slugify` and `floatformat` run in linear time.** `striptags` was slow on text with many `<` and no `>` after them, `truncatewords` and `pluralize` on a long run of whitespace inside the value, `slugify` on a long run of underscores, and `floatformat` on an argument with a long run of `g` or `u` not at its end. Every swig-django release since 2.7.0 has them, except `pluralize`, which has had it since 2.8.2.
55
+ * **Fixed — the Express example no longer has its catch-all route.** The route rendered any `.html` file a URL named as a template, including files outside the views folder. The example is not part of any package; an application built from it should remove or confine that route.
56
+ * **Changed — property-based tests.** The test suite checks properties on many generated inputs, with the new fast-check dev dependency under a fixed seed. Development and CI only; the only runtime dependency remains `@rhinostone/swig-core`.
52
57
 
53
58
  See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
54
59
 
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,19 @@ 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.1 (October 2026)
32
+
33
+ - Security: `striptags` in every flavor, `trim` in swig-twig and swig-jinja2, swig-jinja2 `format`, and swig-django `truncatewords`, `pluralize`, `slugify` and `floatformat` run in linear time with the same output. Their regexes took time quadratic in the input length on some inputs, so a template that applied one of them to request data could be made to block the event loop.
34
+ - The Express example no longer has its catch-all route, which rendered any `.html` file a URL named as a template.
35
+ - All five packages released in lockstep at `2.9.1`.
36
+
37
+ ### v2.9.0 (October 2026)
38
+
39
+ - `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.
40
+ - `not in` in swig-twig, swig-jinja2 and swig-django.
41
+ - An OpenSSF Scorecard badge on the README of each package.
42
+ - All five packages released in lockstep at `2.9.0`.
43
+
30
44
  ### v2.8.4 (October 2026)
31
45
 
32
46
  - 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.1 | 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;
@@ -22,8 +22,52 @@
22
22
  "packages/swig-core/lib/utils.js"(exports) {
23
23
  var security = require_security();
24
24
  var isArray;
25
+ var whitespace = /\s/;
25
26
  exports.strip = function(input) {
26
- return input.replace(/^\s+|\s+$/g, "");
27
+ return exports.trimEnd(input.replace(/^\s+/, ""));
28
+ };
29
+ exports.trimEnd = function(input, chars) {
30
+ var end = input.length;
31
+ if (chars === void 0) {
32
+ while (end > 0 && whitespace.test(input.charAt(end - 1))) {
33
+ end -= 1;
34
+ }
35
+ } else {
36
+ while (end > 0 && chars.indexOf(input.charAt(end - 1)) !== -1) {
37
+ end -= 1;
38
+ }
39
+ }
40
+ return input.slice(0, end);
41
+ };
42
+ exports.stripTags = function(input, allowEmpty) {
43
+ var out = "", from = 0, lt = input.indexOf("<"), gt;
44
+ while (lt !== -1) {
45
+ gt = input.indexOf(">", lt + 1);
46
+ if (gt === -1) {
47
+ break;
48
+ }
49
+ if (gt === lt + 1 && !allowEmpty) {
50
+ lt = input.indexOf("<", lt + 1);
51
+ continue;
52
+ }
53
+ out += input.slice(from, lt);
54
+ from = gt + 1;
55
+ lt = input.indexOf("<", from);
56
+ }
57
+ return out + input.slice(from);
58
+ };
59
+ exports.stripComments = function(input) {
60
+ var out = "", from = 0, open = input.indexOf("<!--"), close;
61
+ while (open !== -1) {
62
+ close = input.indexOf("-->", open + 4);
63
+ if (close === -1) {
64
+ break;
65
+ }
66
+ out += input.slice(from, open);
67
+ from = close + 3;
68
+ open = input.indexOf("<!--", from);
69
+ }
70
+ return out + input.slice(from);
27
71
  };
28
72
  exports.startsWith = function(str, prefix) {
29
73
  return str.indexOf(prefix) === 0;
@@ -250,6 +294,58 @@
250
294
  }
251
295
  return cur;
252
296
  };
297
+ function sameItem(a, b, twig) {
298
+ if (a === b) {
299
+ return true;
300
+ }
301
+ if (!twig) {
302
+ return (a === null || a === void 0) && (b === null || b === void 0);
303
+ }
304
+ if (a !== null && typeof a === "object" || b !== null && typeof b === "object" || typeof a === "function" || typeof b === "function") {
305
+ return false;
306
+ }
307
+ return a == b;
308
+ }
309
+ function membership(needle, haystack, twig) {
310
+ var keys, i;
311
+ if (typeof haystack === "string") {
312
+ if (typeof needle === "string" || twig && typeof needle === "number") {
313
+ return haystack.indexOf(String(needle)) !== -1;
314
+ }
315
+ return twig ? false : null;
316
+ }
317
+ if (haystack === null || typeof haystack !== "object") {
318
+ return twig ? false : null;
319
+ }
320
+ if (exports.isArray(haystack)) {
321
+ for (i = 0; i < haystack.length; i += 1) {
322
+ if (sameItem(needle, haystack[i], twig)) {
323
+ return true;
324
+ }
325
+ }
326
+ return false;
327
+ }
328
+ if (twig) {
329
+ keys = Object.keys(haystack);
330
+ for (i = 0; i < keys.length; i += 1) {
331
+ if (sameItem(needle, haystack[keys[i]], true)) {
332
+ return true;
333
+ }
334
+ }
335
+ return false;
336
+ }
337
+ if (typeof needle === "string" || typeof needle === "number") {
338
+ return Object.prototype.hasOwnProperty.call(haystack, String(needle));
339
+ }
340
+ return needle !== null && typeof needle === "object" ? null : false;
341
+ }
342
+ exports.isIn = function(needle, haystack, mode, negate) {
343
+ var found = membership(needle, haystack, mode === "twig");
344
+ if (found === null) {
345
+ return negate === true && mode !== "django";
346
+ }
347
+ return negate === true ? !found : found;
348
+ };
253
349
  }
254
350
  });
255
351
 
@@ -1131,7 +1227,7 @@
1131
1227
  if (out !== void 0) {
1132
1228
  return out;
1133
1229
  }
1134
- return input.toString().replace(/(<([^>]+)>)/ig, "");
1230
+ return utils.stripTags(input.toString());
1135
1231
  };
1136
1232
  exports.title = function(input) {
1137
1233
  var out = iterateFilter.apply(exports.title, arguments);
@@ -1923,12 +2019,18 @@
1923
2019
  }
1924
2020
  return js;
1925
2021
  }
2022
+ function membershipMode(flag) {
2023
+ return flag === "twig" || flag === "django" ? flag : "jinja2";
2024
+ }
1926
2025
  function emitBinaryOp(node, d) {
1927
2026
  var left = groupOperand(node.left, emitExpr(node.left, d)), right = groupOperand(node.right, emitExpr(node.right, d));
1928
2027
  if (isArithmeticOp(node.op)) {
1929
2028
  return left + " " + node.op + " " + right;
1930
2029
  }
1931
2030
  if (node.op === "in") {
2031
+ if (node.membership) {
2032
+ return "_utils.isIn(" + left + ", " + right + ', "' + membershipMode(node.membership) + '"' + (node.negate ? ", true" : "") + ")";
2033
+ }
1932
2034
  return left + " in " + right;
1933
2035
  }
1934
2036
  if (node.op === "~") {
@@ -4836,7 +4938,7 @@
4836
4938
  var loaders = require_loaders2();
4837
4939
  var preWalker = require_pre_walker();
4838
4940
  var engine = require_engine();
4839
- exports.version = "2.8.4";
4941
+ exports.version = "2.9.1";
4840
4942
  var defaultOptions = {
4841
4943
  autoescape: true,
4842
4944
  varControls: ["{{", "}}"],
@@ -5062,6 +5164,10 @@
5062
5164
  window.swig = swig;
5063
5165
  }
5064
5166
  })();
5167
+ /*!
5168
+ * What a \s regex atom matches, tested one code unit at a time.
5169
+ * @private
5170
+ */
5065
5171
  /*!
5066
5172
  * Resolve a single path segment against a value, Django-style. A plain
5067
5173
  * property access (`obj[seg]`) covers dictionary, attribute, and array /
@@ -5076,6 +5182,19 @@
5076
5182
  * `_dangerousProps` segments are rejected at runtime as defense-in-depth.
5077
5183
  * @private
5078
5184
  */
5185
+ /*!
5186
+ * Item equality for `isIn`. Jinja2 and Django compare with `===` and treat
5187
+ * null and a missing value as equal (Django reads both as None). Twig
5188
+ * compares two scalars with `==`, as swig-twig's `==` does, and anything
5189
+ * else by identity. @private
5190
+ */
5191
+ /*!
5192
+ * The membership test of `isIn`: true or false, or null when Jinja2 and
5193
+ * Django cannot run it, where Python raises a TypeError: a container that is
5194
+ * not a list, a string or a mapping, a needle that is not a string tested
5195
+ * against a string, or a list or an object tested against a mapping's keys.
5196
+ * Twig runs every test. @private
5197
+ */
5079
5198
  /*!
5080
5199
  * Attach `loc` to the node if provided, skipping the assignment otherwise
5081
5200
  * so consumers can tell "no source location available" from
@@ -5177,6 +5296,10 @@
5177
5296
  * wraps a unary operand, for the positions where its prefix would bind
5178
5297
  * differently (`-(-x)`, `(-x)[0]`). @private
5179
5298
  */
5299
+ /*!
5300
+ * The flavor name `emitBinaryOp` writes into an `_utils.isIn` call: one of
5301
+ * the three names `isIn` knows, never the flag's own text. @private
5302
+ */
5180
5303
  /*!
5181
5304
  * Emit a function / method invocation. Three callee shapes:
5182
5305
  * 1. Single-segment VarRef (`foo(...)`) — FUNCTION-token pattern with