@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 +66 -0
- package/ISSUE_TEMPLATE.md +11 -3
- package/PULL_REQUEST_TEMPLATE.md +8 -2
- package/README.md +8 -10
- package/ROADMAP.md +22 -2
- package/SECURITY.md +2 -2
- package/dist/swig.js +55 -35
- package/dist/swig.min.js +10 -2
- package/dist/swig.min.js.map +1 -1
- package/dist/swig.runtime.js +5 -2
- package/dist/swig.runtime.min.js +2 -2
- package/dist/swig.runtime.min.js.map +1 -1
- package/lib/runtime.js +1 -1
- package/lib/swig.js +11 -4
- package/lib/tags/for.js +1 -1
- package/lib/tags/import.js +1 -1
- package/lib/tags/include.js +1 -1
- package/lib/tags/macro.js +4 -4
- package/lib/tags/set.js +3 -3
- package/package.json +3 -3
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
|
-
|
|
1
|
+
<!--
|
|
2
|
+
Security problems: do not open a public issue; see SECURITY.md for private reporting.
|
|
3
|
+
-->
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**Package and version** (e.g. @rhinostone/swig 2.8.1, or swig-twig, swig-jinja2, swig-django, swig-core):
|
|
4
6
|
|
|
5
|
-
|
|
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:**
|
package/PULL_REQUEST_TEMPLATE.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
Please target the develop branch. See CONTRIBUTING.md.
|
|
3
|
+
-->
|
|
2
4
|
|
|
3
|
-
|
|
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`
|
|
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.
|
|
46
|
+
What's new in v2.8.3
|
|
47
47
|
--------------------
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* **
|
|
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.
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
18
|
-
| `< 2.
|
|
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
|
+
/*! 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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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.
|
|
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 ? "
|
|
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
|
-
|
|
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
|