@rhinostone/swig 2.9.0 → 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,18 @@
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
+
1
16
  [2.9.0](https://github.com/gina-io/swig/tree/v2.9.0) / 2026-10-23
2
17
  -----------------------------------------------------------------
3
18
 
package/README.md CHANGED
@@ -43,14 +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.9.0
46
+ What's new in v2.9.1
47
47
  --------------------
48
48
 
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.
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 — `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.
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`.
54
57
 
55
58
  See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
56
59
 
package/ROADMAP.md CHANGED
@@ -28,6 +28,12 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
28
28
 
29
29
  ## Completed
30
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
+
31
37
  ### v2.9.0 (October 2026)
32
38
 
33
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.
package/dist/swig.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Swig v2.9.0 | 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;
@@ -1183,7 +1227,7 @@
1183
1227
  if (out !== void 0) {
1184
1228
  return out;
1185
1229
  }
1186
- return input.toString().replace(/(<([^>]+)>)/ig, "");
1230
+ return utils.stripTags(input.toString());
1187
1231
  };
1188
1232
  exports.title = function(input) {
1189
1233
  var out = iterateFilter.apply(exports.title, arguments);
@@ -4894,7 +4938,7 @@
4894
4938
  var loaders = require_loaders2();
4895
4939
  var preWalker = require_pre_walker();
4896
4940
  var engine = require_engine();
4897
- exports.version = "2.9.0";
4941
+ exports.version = "2.9.1";
4898
4942
  var defaultOptions = {
4899
4943
  autoescape: true,
4900
4944
  varControls: ["{{", "}}"],
@@ -5120,6 +5164,10 @@
5120
5164
  window.swig = swig;
5121
5165
  }
5122
5166
  })();
5167
+ /*!
5168
+ * What a \s regex atom matches, tested one code unit at a time.
5169
+ * @private
5170
+ */
5123
5171
  /*!
5124
5172
  * Resolve a single path segment against a value, Django-style. A plain
5125
5173
  * property access (`obj[seg]`) covers dictionary, attribute, and array /