@rhinostone/swig 2.8.1 → 2.8.3

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,69 @@
1
+ [2.8.3](https://github.com/gina-io/swig/tree/v2.8.3) / 2026-10-21
2
+ -----------------------------------------------------------------
3
+
4
+ * **Security** The template compiler now escapes the including template filename (resolveFrom) before writing it into the compiled function body, so a double quote in a filename option can no longer break out of the generated string literal (GHSA-q89c). Escaping is centralized in the code generator, which also fixes a latent broken-JS bug for backslash paths through import, from and extends.
5
+
6
+ [2.8.2](https://github.com/gina-io/swig/tree/v2.8.2) / 2026-10-20
7
+ -----------------------------------------------------------------
8
+
9
+ * **Fixed** SECURITY.md's supported-versions table now names 2.8.x as the supported release line; it still listed 2.7.x.
10
+
11
+ * **Fixed** The issue and pull-request templates no longer open with the original project's "Swig is no longer maintained" notice. The issue template asks for the package and version, the environment, and a template with locals that reproduces the problem, and sends security reports to SECURITY.md; the pull-request template points to the develop branch and the test, lint and bundle checks.
12
+
13
+ * **Fixed** The README no longer says a callback to compileFile is enough to load templates through an async loader: compileFile also needs codegenMode set to async in its options, as its API documentation says, while renderFile switches on loader.async alone. It now also says that, in that mode, the compiled function returns a promise of { output, exports }. The README's documentation section now names packages/swig-core/lib/loaders/, where the loader JSDoc lives, instead of the one-line lib/loaders/ re-exports, and the swig-jinja2 and swig-django READMEs lead with renderFile against an async loader and mark renderFileAsync and compileFileAsync as deprecated.
14
+
15
+ * **Fixed** A template compiled with codegenMode set to async no longer shares its cache entry with a sync compile of the same file. A sync render or include of a file first compiled in async mode now renders its output instead of [object Promise], and an async-mode compile after a sync one now returns an async function instead of the cached sync one. The fix is in the shared engine, so all four frontends get it; the async entry is cached under its own key.
16
+
17
+ * **Fixed** An error raised while a template is compiled is now reported when render, compile or precompile is called without an options object; it used to surface as TypeError: Cannot read properties of undefined (reading 'filename'), hiding the template error. An error whose message already names the template file, such as Multiple "else" branches in "if" tag in file page.html, now names it once instead of twice, and an error with no file name to add is thrown unchanged instead of being wrapped again with an "Error: " prefix and a doubled period. All four frontends.
18
+
19
+ * **Fixed** The varControls, tagControls and cmtControls options are rejected when a control is not a string, with the message Option "<name>" must be an array containing 2 different control strings. A number or a nested array used to pass validation and fail at the first render with str.replace is not a function, and null failed inside the validator with a TypeError. All four frontends.
20
+
21
+ * **Fixed** The error for a control option that is too short now names the right control: a short open control is reported as the open control and a short close control as the close control; the two words were swapped. All four frontends.
22
+
23
+ * **Fixed** Jinja2 first, last, reverse, unique, min, max and slice now use the keys of a mapping, as Jinja2 iterates a mapping. first and last give the first and last key, reverse gives the keys in reverse order, unique the unique keys, min and max compare the keys, and slice distributes them; they used the values before. sum and random keep their results on a mapping, where Jinja2 raises.
24
+
25
+ * **Fixed** Jinja2 batch now follows Jinja2. A new row starts when the current one holds exactly size items, so a size of 0 gives an empty first row and a size that no row length can equal (negative, fractional or not a number) gives a single row; batch returned an empty list for a size of 0, a negative size or one that is not a number, rounded a fractional size up, and read a numeric string such as "2" as a number. A string is now batched by its characters and a mapping by its keys; batch returned an empty list for a string and used the values of a mapping. The fill pads the last row only up to a finite numeric size.
26
+
27
+ * **Fixed** Jinja2 sort of a string now returns its characters as a sorted list, as Jinja2 does; it joined them back into a string, so "cba" gave "abc" where Jinja2 gives ["a", "b", "c"].
28
+
29
+ * **Fixed** Jinja2 replace now follows Python. A number input is converted to a string first, so 5|replace("5", "X") gives "X" where it returned the number 5; an empty search string inserts the replacement before each character and at the end, honouring the count, so "abc"|replace("", "X") gives "XaXbXcX" where it returned "abc"; and a negative count replaces every occurrence, where it replaced none.
30
+
31
+ * **Fixed** Jinja2 format now prints %e and %E with at least two exponent digits, as Python does, so 12345.678 gives "1.234568e+04" where it gave "1.234568e+4". %g and %G now follow Python too: the precision (6 by default) counts significant digits, the number is printed in fixed notation when its decimal exponent is at least -4 and below the precision and in exponential notation otherwise, and trailing zeros are removed; they printed the number as JavaScript does, so 1234567 gave "1234567" where Python gives "1.23457e+06".
32
+
33
+ * **Fixed** swig-jinja2 wordwrap keeps the separating space when a line is exactly full before a long word, as Python textwrap does, so the output is "abcd |xxxxx|xxxxx" where it was "abcd|xxxxx|xxxxx".
34
+
35
+ * **Fixed** Django join now joins the characters of a string and the keys of a mapping, as Django does. It returned a string unchanged, so "abc"|join:"," gave "abc" where Django gives "a,b,c", and it returned a mapping itself, which rendered as [object Object].
36
+
37
+ * **Fixed** Django pluralize now reads its input as Django does. A boolean is compared with 1, so true gives the singular and false the plural, where both gave the empty string; a string must parse as a Python float, so "abc" and the empty string give the empty string where they gave the plural, and "1_000" gives the plural where it gave the singular; a mapping counts its keys, so an empty mapping gives the plural where it gave the empty string.
38
+
39
+ * **Fixed** Django truncatechars and truncatewords now return the empty string for a count of 0 or less, as Django does, where truncatechars gave the ellipsis alone and truncatewords kept a word or added an ellipsis ("a"|truncatewords:0 gave " …"). They also read the count as Python int() does, so a count string int() rejects, such as "3abc" or "2.5", returns the value unchanged where swig truncated on its leading digits.
40
+
41
+ * **Fixed** The Jinja2 lower and upper tests now need a cased letter, as Python's islower and isupper do: an empty string, digits, punctuation, whitespace or an uncased script such as Chinese is neither lower nor upper, and so is a null or missing variable; they used to be both.
42
+
43
+ * **Fixed** The Jinja2 odd test now follows Python's n % 2 == 1, so a non-integer such as 1.5, or an infinity, is not odd, as in Jinja2; it used to call them odd. A negative odd integer is still odd.
44
+
45
+ * **Fixed** The Jinja2 number test now accepts NaN, as Python's numbers.Number does; it used to reject it.
46
+
47
+ * **Fixed** Parenthesized sub-expressions keep their grouping when a template compiles, in every flavor. Since 2.0.0-alpha.2 the emitted JavaScript could lose the parentheses of a group, so a group whose operator binds more loosely than the operator outside it was evaluated wrongly ({{ (1 + 2) * 3 }} rendered 7, and {% if (x or y) and z %} took its branch with x true and z false), as were a comparison grouped inside a comparison and a subscript on a + group (("a" + "b")[1] rendered aundefined), and {{ -(-x) }} failed to compile. A group that JavaScript reads the same way without the parentheses, not (...), -(...) and a group followed by a filter rendered correctly.
48
+
49
+ * **Fixed** swig-jinja2 applies a unary minus or plus before the filters and the ** that follow it, as Jinja2 does: {{ -x|abs }} is the absolute value of -x, so it gives 5 for x 5 where it gave -5, and -2 ** 2 is (-2) ** 2, so it gives 4 where it gave -4.
50
+
51
+ * **Fixed** swig-jinja2 reads a chain of ** from the left, as Jinja2 does: 2 ** 3 ** 2 is (2 ** 3) ** 2, so it gives 64 where it gave 512.
52
+
53
+ * **Fixed** swig-jinja2 binds a test to the operand next to it, as Jinja2 does: {{ 1 + 2 is number }} tests 2, so it gives 2 where it gave true, and {% if not x is defined %} negates the test, so it is false for a defined x where it was true. A test also takes one argument without parentheses, as in Jinja2 (x is divisibleby 3, x is sameas false), where that threw an Unexpected token error.
54
+
55
+ * **Fixed** swig-jinja2 gives not its Jinja2 level, below the comparisons and arithmetic: {% if not x == y %} is not (x == y), so it is true for x 1 and y 2 where it was false, and {% if not x + y %} negates the sum, so it is false for x 0 and y 1 where it was true. A not in another operand position (x == not y) still binds its operand.
56
+
57
+ * **Fixed** The error messages of the prototype-chain guards, in every package, end with (prototype-chain guard) instead of (CVE-2023-25345). That CVE is the directory traversal through include and extends that 2.7.1 fixed in the filesystem loader, whose message keeps the label; the guards block access to __proto__, constructor and prototype. Code that matched the old text has to match the new one.
58
+
59
+ * **Fixed** swig-core, swig-twig, swig-jinja2 and swig-django export their version as a string, as swig always has; the property was undefined.
60
+
61
+ * **Changed** Upgraded the globals dev dependency, which the ESLint config reads, from 16.x to 17.x. Its node global set drops DisposableStack, AsyncDisposableStack and SuppressedError, which no linted file uses, and its mocha set is unchanged, so make lint reports the same clean result.
62
+
63
+ * **Security** swig-jinja2 wordwrap no longer hangs on a width below 1. A width of 0 or less, given as a number or as a string such as "0", ran for four to six seconds and took more than 2 GB of memory before a RangeError; it now returns the input unchanged (Jinja2 raises). A width between 0 and 1, which hung the same way, now breaks one character at a time, as Python does. Every swig-jinja2 release since 2.5.0 has the hang; reaching it takes an application that passes a width from the request into the filter.
64
+
65
+ * **Security** swig-twig ranges no longer hang on an infinite bound. With a bound of Infinity, which plain JSON produces for a number such as 1e999, a..b looped until the array overflowed, taking up to about three seconds and more than 2 GB of memory before a RangeError. The range is now empty, as it already was for NaN (PHP 8.3 rejects such a bound). Every release since 2.0.0-alpha.5 has the hang; reaching it takes an application that passes a number from the request into a range bound.
66
+
1
67
  [2.8.1](https://github.com/gina-io/swig/tree/v2.8.1) / 2026-10-20
2
68
  -----------------------------------------------------------------
3
69
 
package/ISSUE_TEMPLATE.md CHANGED
@@ -1,5 +1,13 @@
1
- # Swig is no longer maintained
1
+ <!--
2
+ Security problems: do not open a public issue; see SECURITY.md for private reporting.
3
+ -->
2
4
 
3
- Feel free to file an issue. Someone in the community may be able to help.
5
+ **Package and version** (e.g. @rhinostone/swig 2.8.1, or swig-twig, swig-jinja2, swig-django, swig-core):
4
6
 
5
- However, no new releases are being made to this repository.
7
+ **Node version, or browser and bundle:**
8
+
9
+ **Template and locals that reproduce it:**
10
+
11
+ **Expected output:**
12
+
13
+ **Actual output or error:**
@@ -1,3 +1,9 @@
1
- # Swig is no longer maintained
1
+ <!--
2
+ Please target the develop branch. See CONTRIBUTING.md.
3
+ -->
2
4
 
3
- If you're in need of a fix or a new feature, you should maintain your own fork of this repository.
5
+ **What this changes and why:**
6
+
7
+ - [ ] Tests added or updated (`make test`)
8
+ - [ ] `make lint` passes
9
+ - [ ] `make test-bundles` passes, if `lib/`, `packages/` or the build changed
package/README.md CHANGED
@@ -34,7 +34,7 @@ Features
34
34
  * Available for node.js **and** major web browsers.
35
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.
36
36
  * [Express](https://expressjs.com/) compatible.
37
- * **Async template loading** — back the loader with S3 / Redis / HTTP / any callback API: set `loader.async = true` and pass a callback to `renderFile` / `compileFile`; `extends` / `include` / `import` resolve at render time, dynamic `{% extends %}` paths included.
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.
39
39
  * Apply filters and transformations to output in your templates.
40
40
  * **Security-hardened** — prototype-pollution vectors (`__proto__` / `constructor` / `prototype`) blocked at parser, tag-side, and IR-emission layers; template-driven file reads outside the loader root rejected (CVE-2023-25345 directory traversal, patched in 2.7.1). Dedicated CVE regression suite in [`tests/regressions.test.js`](./tests/regressions.test.js).
@@ -43,14 +43,12 @@ 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.1
46
+ What's new in v2.8.3
47
47
  --------------------
48
48
 
49
- * **Fixed — README corrections now on npm.** The 2.8.0 README listed CVE-2023-25345 under the prototype-chain hardening; it is the directory-traversal fix, and the security bullet now says so without pinning an exact CVE regression-test count. The benchmark summary reflects the latest measured run (swig leads on all five workloads; the previous table predated the 2.2.0 escape-filter rewrite and showed nunjucks ahead on simple var output). The Jinja2/Django migration notes no longer list the `default` filter as absent — it has been built into native swig since upstream 2014.
50
- * **Fixed — README completeness for the 2.x line.** The feature list now covers the async-loader render path (callback dispatch with `loader.async`, dynamic `extends` included) and the single-runtime-dependency posture, and "How it works" explains the runtime-only strict-CSP path.
51
- * **Added — a "What's new" section.** The README now opens with a section like this one, regenerated from the release notes at every cut.
52
- * **Fixed — attribution and changelog links.** The bundled DateZ attribution banner points at `https://github.com/tn0ms/DateZ` after the upstream account's rename, and the v0.9.0 changelog's PHP date-format citation links to the canonical HTTPS `php.net` page. Both old links still resolved; documentation only, no runtime or API change.
53
- * **Changed — ESLint 10 for development.** ESLint 9 is no longer supported by its maintainers, so the lint toolchain moves to ESLint 10 with the flat config unchanged. Running `make lint` now needs Node ^20.19.0, ^22.13.0 or >=24; the published package and its runtime Node floor are unaffected.
49
+ > **Security.** If your application passes untrusted data as the `filename` option to `render`, `compile`, `precompile`, `renderFile` or `compileFile` (or a flavor equivalent), upgrade to `2.8.3`.
50
+
51
+ * **Security — the compiled template escapes the including filename (GHSA-q89c).** The code generator now escapes a template's filename (`resolveFrom`) before writing it into the compiled function body, so a double quote in a `filename` option can no longer break out of the generated string literal. The escape is centralized in the generator, which also fixes a latent broken-JS bug for backslash paths through `import`, `from` and `extends`.
54
52
 
55
53
  See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
56
54
 
@@ -63,7 +61,7 @@ Benchmarks
63
61
  cd benchmarks && npm install && node render.js
64
62
  ```
65
63
 
66
- In production-typical settings (autoescape on), `@rhinostone/swig` leads Nunjucks on all five workload shapes — from ~1.8× on simple variable output to 3.5–5.5× on iteration-heavy templates (median of 3 runs, 2026-08). See [`benchmarks/README.md`](./benchmarks/README.md) for the methodology, the full result table, and how to reproduce on your own hardware.
64
+ In production-typical settings (autoescape on), `@rhinostone/swig` leads Nunjucks on all five workload shapes — from ~1.3× on simple variable output to 5.3–5.9× on iteration-heavy templates (median of 5 runs, 2026-10). See [`benchmarks/README.md`](./benchmarks/README.md) for the methodology, the full result table, and how to reproduce on your own hardware.
67
65
 
68
66
  Need Help? Have Questions? Comments?
69
67
  ------------------------------------
@@ -91,7 +89,7 @@ For Django syntax:
91
89
  Documentation
92
90
  -------------
93
91
 
94
- User-facing documentation lives in the Gina Docusaurus site under the [Swig Template Engine](https://gina.io/docs/templating/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.
92
+ User-facing documentation lives in the Gina Docusaurus site under the [Swig Template Engine](https://gina.io/docs/templating/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 `packages/swig-core/lib/loaders/` remain the canonical source-of-truth for the public API and are mirrored into the Docusaurus pages.
95
93
 
96
94
  Basic Example
97
95
  -------------
@@ -143,7 +141,7 @@ Migrating from Jinja2 or Django
143
141
 
144
142
  Swig is *inspired by* Jinja2 and Django, not a drop-in replacement. Common pitfalls when porting existing templates:
145
143
 
146
- * **No `is` / `is not` / `not in` operators** — rewrite `{% if x is defined %}` as `{% if x !== undefined %}`, `{% if x not in xs %}` as `{% if not (x in xs) %}`.
144
+ * **No `is` / `is not` / `not in` operators** — rewrite `{% if x is defined %}` as `{% if x !== undefined %}`, and `{% if x not in xs %}` for a list as `{% if xs.indexOf(x) === -1 %}` (`in` tests an object's keys or an array's indices, not membership).
147
145
  * **Django `forloop.counter` → Swig `loop.index`** (Swig follows Jinja2 loop-variable naming).
148
146
  * **`{{ super() }}` / `{{ block.super }}` → `{% parent %}`** — Swig uses a dedicated tag inside the overriding block.
149
147
  * **Django filter args use a colon (`|date:"Y-m-d"`) — Swig uses parens (`|date("Y-m-d")`)**.
package/ROADMAP.md CHANGED
@@ -8,20 +8,40 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
8
8
 
9
9
  ## Next
10
10
 
11
- _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upcoming work._
11
+ | Status | Item |
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
+ | 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
+ | Planned | swig-django: two-variable for-loops over a list of pairs unpack each pair, as Django does. |
16
+ | 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. |
12
17
 
13
18
  ## Future (post-2.0)
14
19
 
15
20
  | Status | Item |
16
21
  | --- | --- |
17
22
  | Planned | Async parse path for the remaining dynamic targets — runtime-resolved `{% import %}` / `{% from %}` paths on the async-codegen branch. Static-target async dispatch shipped in 2.2.0; dynamic `{% extends %}` shipped in 2.5.2 (native + Twig + Jinja2) and dynamic `{% include %}` paths already resolve (the include path has always been an expression). Dynamic `import` / `from` are on hold pending consumer demand. |
18
- | Planned | Modern browser-test harness. The legacy phantomjs runner was removed; a replacement (e.g. Playwright or JSDOM) has not yet landed, so browser parity is verified in the interim via the production build plus a symbol grep. (The `mocha` → `node:test` runner migration, the `expect.js` → in-repo assertion-shim swap, and the `blanket` → `node:test` built-in coverage migration have all shipped.) |
23
+ | Planned | Modern browser-test harness. The legacy phantomjs runner was removed; a replacement (e.g. Playwright or JSDOM) has not yet landed. In the interim, CI builds the four browser bundles and renders a template set through each in a Node vm context, comparing the output with the Node library. (The `mocha` → `node:test` runner migration, the `expect.js` → in-repo assertion-shim swap, and the `blanket` → `node:test` built-in coverage migration have all shipped.) |
19
24
  | On demand | Ahead-of-time browser support for the Twig, Jinja2, and Django frontends, in two parts that ship together. Runtime-only bundles: the shared runtime factory already supports them, and each flavor needs its own bundle entry and build target, since filter catalogs are per-flavor. A flavor-aware compile step: `swig compile --register` compiles with the native parser only, so flavor templates are precompiled programmatically until it lands. The native `dist/swig.runtime.min.js` shipped in 2.8.0; the flavor pieces land when a consumer needs them. |
20
25
 
21
26
  ---
22
27
 
23
28
  ## Completed
24
29
 
30
+ ### v2.8.3 (October 2026)
31
+
32
+ - Security: the template compiler escapes the including template's filename before writing it into the compiled function body, so a double quote in a `filename` option can no longer break out of the generated string literal (GHSA-q89c). Escaping is centralized in the code generator, which also fixes a latent broken-JS bug for backslash paths through `import`, `from` and `extends`.
33
+ - All five packages released in lockstep at `2.8.3`.
34
+
35
+ ### v2.8.2 (October 2026)
36
+
37
+ - Fixed two hangs that request data can reach. swig-jinja2's `wordwrap` with a width below 1, as a number or as a string such as `"0"`, ran for four to six seconds and took more than 2 GB of memory before a RangeError; it now returns the input unchanged, and a width between 0 and 1 breaks one character at a time. swig-twig's `a..b` with an infinite bound, which plain JSON produces for a number such as `1e999`, looped until the array overflowed; the range is now empty.
38
+ - Fixed parenthesized sub-expressions in every flavor. Since 2.0.0-alpha.2 a group whose operator binds more loosely than the operator outside it, a comparison grouped inside a comparison and a subscript on a `+` group were evaluated without their parentheses, and `-(-x)` failed to compile.
39
+ - Fixed swig-jinja2 expression precedence to follow Jinja2 (a unary minus before the filters and `**`, `**` read from the left, tests bound at the filter level and taking a bare argument, `not` below the comparisons) and its `lower`, `upper`, `odd` and `number` tests.
40
+ - Fixed swig-jinja2 and swig-django filters that diverged from the reference engines: Jinja2 `first`, `last`, `reverse`, `unique`, `min`, `max`, `slice`, `batch`, `sort`, `replace`, `format` and `wordwrap`, and Django `join`, `pluralize`, `truncatechars` and `truncatewords`.
41
+ - Fixed the shared engine in all four frontends: an async-mode compile has its own cache entry, a compile error is reported when no options object is passed, and the control options must be strings.
42
+ - The prototype-chain guards' error messages end with `(prototype-chain guard)` instead of `(CVE-2023-25345)`, which is the loader's directory-traversal CVE, and every package exports its `version`.
43
+ - All five packages released in lockstep at `2.8.2`.
44
+
25
45
  ### v2.8.0 (August 2026)
26
46
 
27
47
  - Added ahead-of-time template registration: `swig.register(path, fn)` and `swig.registerBundle(map)` place pre-compiled template functions into the template cache under loader-normalized keys, so `include`, `compileFile`, and `renderFile` resolve them without a compiler. Available on all four frontends (native, Twig, Jinja2, Django). `swig compile --recursive --register` emits a bundle that registers itself on load and also exports the template map, so a page needs no per-template wiring.
package/SECURITY.md CHANGED
@@ -14,8 +14,8 @@ security fix ships across the family at the same version.
14
14
 
15
15
  | Version | Supported |
16
16
  | ------------------ | ------------------ |
17
- | `2.7.x` (latest) | :white_check_mark: |
18
- | `< 2.7.0` | :x: — upgrade |
17
+ | `2.8.x` (latest) | :white_check_mark: |
18
+ | `< 2.8.0` | :x: — upgrade |
19
19
 
20
20
  ## Reporting a vulnerability
21
21
 
package/dist/swig.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Swig v2.8.1 | https://github.com/gina-io/swig | @license https://github.com/gina-io/swig/blob/master/LICENSE */
1
+ /*! Swig v2.8.3 | 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;
@@ -124,6 +124,9 @@
124
124
  exports.range = function(from, to) {
125
125
  var out = [], i, fc, tc;
126
126
  if (typeof from === "number" && typeof to === "number") {
127
+ if (!isFinite(from) || !isFinite(to)) {
128
+ return out;
129
+ }
127
130
  if (from <= to) {
128
131
  for (i = from; i <= to; i += 1) {
129
132
  out.push(i);
@@ -1328,7 +1331,7 @@
1328
1331
  throw new Error('Loop variable "' + token.match + '" must be a bare identifier in "for" tag on line ' + line + ".");
1329
1332
  }
1330
1333
  if (_dangerousProps.indexOf(token.match) !== -1) {
1331
- throw new Error('Unsafe loop variable "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
1334
+ throw new Error('Unsafe loop variable "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
1332
1335
  }
1333
1336
  if (!this.out.length) {
1334
1337
  firstVar = true;
@@ -1615,7 +1618,7 @@
1615
1618
  } else {
1616
1619
  incSelector = "_utils.extend({}, _ctx, " + incCtxJS + ")";
1617
1620
  }
1618
- out += (node.ignoreMissing ? " try {\n" : "") + "_output += _swig.compileFile(" + incPathJS + ', {resolveFrom: "' + node.resolveFrom + '"})(' + incSelector + ");\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
1621
+ out += (node.ignoreMissing ? " try {\n" : "") + "_output += _swig.compileFile(" + incPathJS + ', {resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"})(' + incSelector + ");\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
1619
1622
  return;
1620
1623
  }
1621
1624
  if (node.type === "IncludeDeferred") {
@@ -1640,7 +1643,7 @@
1640
1643
  } else {
1641
1644
  incdSelector = "_utils.extend({}, _ctx, " + incdCtxJS + ")";
1642
1645
  }
1643
- var incdOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
1646
+ var incdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1644
1647
  out += (node.ignoreMissing ? " try {\n" : "") + "_output += (await (await _swig.getTemplate(" + incdPathJS + ", " + incdOpts + "))(" + incdSelector + ")).output;\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
1645
1648
  return;
1646
1649
  }
@@ -1654,7 +1657,7 @@
1654
1657
  } else {
1655
1658
  impdPathJS = node.path;
1656
1659
  }
1657
- var impdOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
1660
+ var impdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1658
1661
  out += "_ctx." + node.alias + " = (await (await _swig.getTemplate(" + impdPathJS + ", " + impdOpts + "))(_ctx)).exports;\n";
1659
1662
  return;
1660
1663
  }
@@ -1675,7 +1678,7 @@
1675
1678
  } else {
1676
1679
  fromdPathJS = node.path;
1677
1680
  }
1678
- var fromdOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
1681
+ var fromdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1679
1682
  var fromdBindings = "";
1680
1683
  utils.each(fromdImports, function(entry) {
1681
1684
  var bindName = entry.alias || entry.name;
@@ -1712,7 +1715,7 @@
1712
1715
  } else {
1713
1716
  extPathJS = node.path;
1714
1717
  }
1715
- var extOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
1718
+ var extOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1716
1719
  out += "var _parentTpl = await _swig.getTemplate(" + extPathJS + ", " + extOpts + ");\n";
1717
1720
  out += "var _parentResult = await _parentTpl(_ctx, _mergedBlocks);\n";
1718
1721
  out += "_output = _parentResult.output;\n";
@@ -1849,7 +1852,7 @@
1849
1852
  if (d.dangerousProps.indexOf(segment) !== -1) {
1850
1853
  var line = node && node.loc && node.loc.line || void 0;
1851
1854
  var filename = node && node.loc && node.loc.filename || void 0;
1852
- d.throwError('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)', line, filename);
1855
+ d.throwError('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)', line, filename);
1853
1856
  }
1854
1857
  }
1855
1858
  function emitLiteral(node, d) {
@@ -1909,13 +1912,19 @@
1909
1912
  if (node.key && node.key.type === "Literal" && node.key.kind === "string") {
1910
1913
  checkDangerousSegment(node.key.value, d, node);
1911
1914
  }
1912
- return emitExpr(node.object, d) + "[" + emitExpr(node.key, d) + "]";
1915
+ return groupOperand(node.object, emitExpr(node.object, d), true) + "[" + emitExpr(node.key, d) + "]";
1913
1916
  }
1914
1917
  function isArithmeticOp(op) {
1915
1918
  return op === "+" || op === "-" || op === "*" || op === "/" || op === "%";
1916
1919
  }
1920
+ function groupOperand(node, js, unary) {
1921
+ if (node && (node.type === "BinaryOp" || unary && node.type === "UnaryOp")) {
1922
+ return "(" + js + ")";
1923
+ }
1924
+ return js;
1925
+ }
1917
1926
  function emitBinaryOp(node, d) {
1918
- var left = emitExpr(node.left, d), right = emitExpr(node.right, d);
1927
+ var left = groupOperand(node.left, emitExpr(node.left, d)), right = groupOperand(node.right, emitExpr(node.right, d));
1919
1928
  if (isArithmeticOp(node.op)) {
1920
1929
  return left + " " + node.op + " " + right;
1921
1930
  }
@@ -1932,11 +1941,7 @@
1932
1941
  return left + node.op + right;
1933
1942
  }
1934
1943
  function emitUnaryOp(node, d) {
1935
- var operandJS = emitExpr(node.operand, d);
1936
- if (node.operand && node.operand.type === "BinaryOp") {
1937
- operandJS = "(" + operandJS + ")";
1938
- }
1939
- return node.op + operandJS;
1944
+ return node.op + groupOperand(node.operand, emitExpr(node.operand, d), true);
1940
1945
  }
1941
1946
  function emitConditional(node, d) {
1942
1947
  return "(" + emitExpr(node.test, d) + " ? " + emitExpr(node.then, d) + " : " + emitExpr(node["else"], d) + ")";
@@ -2311,7 +2316,7 @@
2311
2316
  if (lastState === _t.BRACKETOPEN) {
2312
2317
  var strippedMatch = match.replace(/^['"]|['"]$/g, "");
2313
2318
  if (_dangerousProps.indexOf(strippedMatch) !== -1) {
2314
- utils.throwError('Unsafe access to "' + strippedMatch + '" via bracket notation is not allowed in templates (CVE-2023-25345)', self.line, self.filename);
2319
+ utils.throwError('Unsafe access to "' + strippedMatch + '" via bracket notation is not allowed in templates (prototype-chain guard)', self.line, self.filename);
2315
2320
  }
2316
2321
  }
2317
2322
  self.filterApplyIdx.push(self.out.length);
@@ -2459,7 +2464,7 @@
2459
2464
  utils.throwError('Unexpected key "' + match + '"', self.line, self.filename);
2460
2465
  }
2461
2466
  if (_dangerousProps.indexOf(match) !== -1) {
2462
- utils.throwError('Unsafe access to "' + match + '" is not allowed in templates (CVE-2023-25345)', self.line, self.filename);
2467
+ utils.throwError('Unsafe access to "' + match + '" is not allowed in templates (prototype-chain guard)', self.line, self.filename);
2463
2468
  }
2464
2469
  self.out.push("." + match);
2465
2470
  break;
@@ -2485,7 +2490,7 @@
2485
2490
  }
2486
2491
  utils.each(match, function(segment) {
2487
2492
  if (_dangerousProps.indexOf(segment) !== -1) {
2488
- utils.throwError('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)', self.line, self.filename);
2493
+ utils.throwError('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)', self.line, self.filename);
2489
2494
  }
2490
2495
  });
2491
2496
  self.filterApplyIdx.push(self.out.length);
@@ -2553,12 +2558,12 @@
2553
2558
  }
2554
2559
  function guardSegment(segment) {
2555
2560
  if (_dangerousProps.indexOf(segment) !== -1) {
2556
- bail('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)');
2561
+ bail('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)');
2557
2562
  }
2558
2563
  }
2559
2564
  function guardBracketString(value) {
2560
2565
  if (_dangerousProps.indexOf(value) !== -1) {
2561
- bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (CVE-2023-25345)');
2566
+ bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (prototype-chain guard)');
2562
2567
  }
2563
2568
  }
2564
2569
  function getBinaryOpInfo(tok) {
@@ -3301,7 +3306,7 @@
3301
3306
  return;
3302
3307
  }
3303
3308
  if (_dangerousProps.indexOf(token.match) !== -1) {
3304
- throw new Error('Unsafe import alias "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3309
+ throw new Error('Unsafe import alias "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3305
3310
  }
3306
3311
  ctx = token.match;
3307
3312
  self.out.push(ctx);
@@ -3322,7 +3327,7 @@
3322
3327
  var missing = "missing";
3323
3328
  var only = "only";
3324
3329
  exports.compile = function(compiler, args, content, parents, options, blockName, token) {
3325
- var file = args.shift(), onlyIdx = args.indexOf(only), onlyCtx = onlyIdx !== -1 ? args.splice(onlyIdx, 1) : false, parentFile = (args.pop() || "").replace(/\\/g, "\\\\"), ignoreMissing = args[args.length - 1] === missing ? args.pop() : false, w = args.join("");
3330
+ var file = args.shift(), onlyIdx = args.indexOf(only), onlyCtx = onlyIdx !== -1 ? args.splice(onlyIdx, 1) : false, parentFile = args.pop() || "", ignoreMissing = args[args.length - 1] === missing ? args.pop() : false, w = args.join("");
3326
3331
  if (token && token.irExpr) {
3327
3332
  file = token.irExpr.file;
3328
3333
  if (token.irExpr.context !== void 0) {
@@ -3452,21 +3457,21 @@
3452
3457
  }
3453
3458
  if (!name) {
3454
3459
  if (_dangerousProps.indexOf(token.match) !== -1) {
3455
- throw new Error('Unsafe macro name "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3460
+ throw new Error('Unsafe macro name "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3456
3461
  }
3457
3462
  name = token.match;
3458
3463
  this.out.push(name);
3459
3464
  return;
3460
3465
  }
3461
3466
  if (_dangerousProps.indexOf(token.match) !== -1) {
3462
- throw new Error('Unsafe macro argument "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3467
+ throw new Error('Unsafe macro argument "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3463
3468
  }
3464
3469
  this.out.push({ name: token.match });
3465
3470
  });
3466
3471
  parser.on(types.FUNCTION, function(token) {
3467
3472
  if (!name) {
3468
3473
  if (_dangerousProps.indexOf(token.match) !== -1) {
3469
- throw new Error('Unsafe macro name "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3474
+ throw new Error('Unsafe macro name "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3470
3475
  }
3471
3476
  name = token.match;
3472
3477
  this.out.push(name);
@@ -3476,7 +3481,7 @@
3476
3481
  parser.on(types.FUNCTIONEMPTY, function(token) {
3477
3482
  if (!name) {
3478
3483
  if (_dangerousProps.indexOf(token.match) !== -1) {
3479
- throw new Error('Unsafe macro name "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3484
+ throw new Error('Unsafe macro name "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3480
3485
  }
3481
3486
  name = token.match;
3482
3487
  this.out.push(name);
@@ -3613,7 +3618,7 @@
3613
3618
  var segments = token.match.split(".");
3614
3619
  for (var i = 0; i < segments.length; i++) {
3615
3620
  if (_dangerousProps.indexOf(segments[i]) !== -1) {
3616
- throw new Error('Unsafe assignment to "' + segments[i] + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3621
+ throw new Error('Unsafe assignment to "' + segments[i] + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3617
3622
  }
3618
3623
  }
3619
3624
  nameSet += token.match;
@@ -3632,7 +3637,7 @@
3632
3637
  if (propertyName && !this.out.length) {
3633
3638
  var stripped = token.match.replace(/^['"]|['"]$/g, "");
3634
3639
  if (_dangerousProps.indexOf(stripped) !== -1) {
3635
- throw new Error('Unsafe assignment to "' + stripped + '" via bracket notation is not allowed (CVE-2023-25345) on line ' + line + ".");
3640
+ throw new Error('Unsafe assignment to "' + stripped + '" via bracket notation is not allowed (prototype-chain guard) on line ' + line + ".");
3636
3641
  }
3637
3642
  propertyName += token.match;
3638
3643
  return;
@@ -3652,7 +3657,7 @@
3652
3657
  return true;
3653
3658
  }
3654
3659
  if (_dangerousProps.indexOf(token.match) !== -1) {
3655
- throw new Error('Unsafe assignment to "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + ".");
3660
+ throw new Error('Unsafe assignment to "' + token.match + '" is not allowed (prototype-chain guard) on line ' + line + ".");
3656
3661
  }
3657
3662
  nameSet += "." + token.match;
3658
3663
  return;
@@ -4412,6 +4417,9 @@
4412
4417
  function efn() {
4413
4418
  return "";
4414
4419
  }
4420
+ function compileCacheKey(filename, options) {
4421
+ return options && options.codegenMode === "async" ? filename + "#async" : filename;
4422
+ }
4415
4423
  exports.remapBlocks = function remapBlocks(blocks, tokens) {
4416
4424
  return utils.map(tokens, function(token) {
4417
4425
  var args = token.args ? token.args.join("") : "";
@@ -4652,7 +4660,7 @@
4652
4660
  return { tpl, tokens };
4653
4661
  };
4654
4662
  self.compile = function(source, options) {
4655
- var key = options ? options.filename : null, cached = key ? cacheGet(key, options) : null, context, contextLength, pre;
4663
+ var key = options && options.filename ? compileCacheKey(options.filename, options) : null, cached = key ? cacheGet(key, options) : null, context, contextLength, pre;
4656
4664
  if (cached) {
4657
4665
  return cached;
4658
4666
  }
@@ -4687,7 +4695,7 @@
4687
4695
  if (!options.filename) {
4688
4696
  options = utils.extend({ filename: pathname }, options);
4689
4697
  }
4690
- cached = cacheGet(pathname, options);
4698
+ cached = cacheGet(compileCacheKey(pathname, options), options);
4691
4699
  if (cached) {
4692
4700
  if (cb) {
4693
4701
  cb(null, cached);
@@ -4814,7 +4822,7 @@
4814
4822
  var loaders = require_loaders2();
4815
4823
  var preWalker = require_pre_walker();
4816
4824
  var engine = require_engine();
4817
- exports.version = "2.8.1";
4825
+ exports.version = "2.8.3";
4818
4826
  var defaultOptions = {
4819
4827
  autoescape: true,
4820
4828
  varControls: ["{{", "}}"],
@@ -4869,7 +4877,7 @@
4869
4877
  if (!options.hasOwnProperty(key)) {
4870
4878
  return;
4871
4879
  }
4872
- if (!utils.isArray(options[key]) || options[key].length !== 2) {
4880
+ if (!utils.isArray(options[key]) || options[key].length !== 2 || typeof options[key][0] !== "string" || typeof options[key][1] !== "string") {
4873
4881
  throw new Error('Option "' + key + '" must be an array containing 2 different control strings.');
4874
4882
  }
4875
4883
  if (options[key][0] === options[key][1]) {
@@ -4877,7 +4885,7 @@
4877
4885
  }
4878
4886
  utils.each(options[key], function(a, i) {
4879
4887
  if (a.length < 2) {
4880
- throw new Error('Option "' + key + '" ' + (i ? "open " : "close ") + 'control must be at least 2 characters. Saw "' + a + '" instead.');
4888
+ throw new Error('Option "' + key + '" ' + (i ? "close " : "open ") + 'control must be at least 2 characters. Saw "' + a + '" instead.');
4881
4889
  }
4882
4890
  });
4883
4891
  });
@@ -4914,7 +4922,11 @@
4914
4922
  filters: _filters,
4915
4923
  validateOptions,
4916
4924
  onCompileError: function(err, options) {
4917
- utils.throwError(err, null, options.filename);
4925
+ var filename = options && options.filename;
4926
+ if (err instanceof Error && (!filename || err.message.indexOf(" in file " + filename) !== -1)) {
4927
+ throw err;
4928
+ }
4929
+ utils.throwError(err, null, filename);
4918
4930
  }
4919
4931
  });
4920
4932
  var self = this;
@@ -5143,6 +5155,14 @@
5143
5155
  * COMPARATOR output shape. `in` needs trailing space so the keyword
5144
5156
  * detokenises — `(a)in(b)` parses but `ain(b)` does not. @private
5145
5157
  */
5158
+ /*!
5159
+ * Parenthesize an operand whose emitted JS could regroup. The IR tree
5160
+ * carries the grouping (`(a + b) * c` parses to a `*` node whose left
5161
+ * operand is the `+` node), so a nested binary node is wrapped or JS
5162
+ * precedence and associativity regroup it (`a + b * c`). `unary` also
5163
+ * wraps a unary operand, for the positions where its prefix would bind
5164
+ * differently (`-(-x)`, `(-x)[0]`). @private
5165
+ */
5146
5166
  /*!
5147
5167
  * Emit a function / method invocation. Three callee shapes:
5148
5168
  * 1. Single-segment VarRef (`foo(...)`) — FUNCTION-token pattern with