@rhinostone/swig 2.8.2 → 2.8.4
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 +12 -0
- package/README.md +4 -16
- package/ROADMAP.md +11 -0
- package/dist/swig.js +37 -10
- package/dist/swig.min.js +15 -2
- package/dist/swig.min.js.map +1 -1
- package/dist/swig.runtime.js +2 -2
- package/dist/swig.runtime.min.js +2 -2
- package/lib/parser.js +30 -2
- package/lib/runtime.js +1 -1
- package/lib/swig.js +2 -2
- package/lib/tags/include.js +1 -1
- package/package.json +2 -2
package/HISTORY.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
[2.8.4](https://github.com/gina-io/swig/tree/v2.8.4) / 2026-10-21
|
|
2
|
+
-----------------------------------------------------------------
|
|
3
|
+
|
|
4
|
+
* **Fixed** A filename option that is not a string, such as a number, compiles again with import, from and extends on the async render path, as it did in 2.8.2, and include, which failed with a number in 2.8.2 as well, now compiles too. 2.8.3 escaped the filename as a string before writing it into the compiled template, so a number failed with value.replace is not a function; the filename is now converted to a string first. All four frontends.
|
|
5
|
+
|
|
6
|
+
* **Fixed** The autoescape option of @rhinostone/swig now takes the name of an escape strategy, as documented: autoescape set to 'js' gives JavaScript escaping and 'html' HTML escaping. The option was written into the compiled template as code, so these values failed with ReferenceError: js is not defined, and an autoescape option built from untrusted input could run as code in the template. A value already wrapped in quotes keeps working, and a value holding a double quote or a line break is now rejected with an error. The Twig, Jinja2 and Django frontends already passed the option to the escape filter as a string.
|
|
7
|
+
|
|
8
|
+
[2.8.3](https://github.com/gina-io/swig/tree/v2.8.3) / 2026-10-21
|
|
9
|
+
-----------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
* **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.
|
|
12
|
+
|
|
1
13
|
[2.8.2](https://github.com/gina-io/swig/tree/v2.8.2) / 2026-10-20
|
|
2
14
|
-----------------------------------------------------------------
|
|
3
15
|
|
package/README.md
CHANGED
|
@@ -43,24 +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.4
|
|
47
47
|
--------------------
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* **Security — swig-jinja2 `wordwrap` no longer hangs on a width below 1.** A width of 0 or less, 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 now breaks one character at a time, as Python does. Every swig-jinja2 release since 2.5.0 has the hang.
|
|
54
|
-
* **Security — swig-twig ranges no longer hang on an infinite bound.** `a..b` with a bound of Infinity 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. Every release since 2.0.0-alpha.5 has the hang.
|
|
55
|
-
* **Fixed — parenthesized sub-expressions keep their grouping, in every flavor.** Since 2.0.0-alpha.2 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, and `{{ -(-x) }}` failed to compile. Groups that JavaScript reads the same way, `not (…)`, `-(…)` and a group followed by a filter were not affected.
|
|
56
|
-
* **Fixed — swig-jinja2 expressions follow Jinja2's precedence.** A unary minus applies before the filters and the `**` that follow it (`-2 ** 2` is 4); `**` reads from the left (`2 ** 3 ** 2` is 64); a test binds to the operand next to it (`{{ 1 + 2 is number }}` tests 2, and `not x is defined` negates the test) and takes one argument without parentheses (`x is divisibleby 3`); and `not` sits below the comparisons and arithmetic (`not x == y` is `not (x == y)`).
|
|
57
|
-
* **Fixed — swig-jinja2 tests.** `lower` and `upper` need a cased letter, `odd` follows Python's `n % 2 == 1`, and `number` accepts NaN, as in Jinja2.
|
|
58
|
-
* **Fixed — swig-jinja2 filters follow Jinja2.** `first`, `last`, `reverse`, `unique`, `min`, `max` and `slice` use a mapping's keys; `batch` starts a row at exactly `size` items and batches a string by its characters; `sort` of a string returns its sorted characters; `replace` converts a number first and handles an empty search string and a negative count as Python does; `format` prints `%e` with at least two exponent digits and `%g` as Python does; and `wordwrap` keeps the separating space when a line is exactly full.
|
|
59
|
-
* **Fixed — swig-django filters follow Django.** `join` joins a string's characters and a mapping's keys; `pluralize` reads a boolean, a string and a mapping as Django does; `truncatechars` and `truncatewords` return the empty string for a count of 0 or less and the value unchanged for a count that Python's `int()` rejects.
|
|
60
|
-
* **Fixed — the shared engine, in all four frontends.** An async-mode compile no longer shares its cache entry with a sync compile of the same file (a sync include of such a file rendered `[object Promise]`); an error raised while a template compiles is reported when no options object is passed, instead of a TypeError, and names its file once; and the `varControls`, `tagControls` and `cmtControls` options reject a control that is not a string, with the open and close controls named the right way round.
|
|
61
|
-
* **Fixed — guard messages and version exports.** The prototype-chain guards' messages end with `(prototype-chain guard)`, and `swig-core`, `swig-twig`, `swig-jinja2` and `swig-django` export their `version`, as `swig` always has.
|
|
62
|
-
* **Fixed — documentation.** SECURITY.md names 2.8.x as the supported line; the issue and pull-request templates no longer open with the original project's "no longer maintained" notice; and the README says that `compileFile` needs `codegenMode: 'async'` for an async loader, that its compiled function then returns a promise of `{ output, exports }`, and how to port `not in` to native swig; the swig-jinja2 and swig-django READMEs lead with `renderFile` against an async loader.
|
|
63
|
-
* **Changed — development only.** The `globals` dev dependency moves to 17.x; `make lint` reports the same clean result.
|
|
49
|
+
* **Fixed — a string `autoescape` option names the escape strategy.** In `@rhinostone/swig`, `autoescape: 'js'` now gives JavaScript escaping and `autoescape: 'html'` HTML escaping, as documented. The option was written into the compiled template as code, so these values failed with `ReferenceError: js is not defined`, and an autoescape option built from untrusted input could run as code in the template. A value already wrapped in quotes keeps working, and a value holding a double quote or a line break is now rejected with an error. The Twig, Jinja2 and Django frontends already passed the option to the escape filter as a string.
|
|
50
|
+
|
|
51
|
+
* **Fixed — a numeric `filename` option compiles again.** A filename that is not a string, such as a number, compiles again with `import`, `from` and `extends` on the async render path, as it did in 2.8.2, and `include`, which failed with a number in 2.8.2 as well, now compiles too. 2.8.3 escaped the filename as a string and failed with `value.replace is not a function`; it is now converted to a string first. All four frontends.
|
|
64
52
|
|
|
65
53
|
See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
|
|
66
54
|
|
package/ROADMAP.md
CHANGED
|
@@ -27,6 +27,17 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
|
|
|
27
27
|
|
|
28
28
|
## Completed
|
|
29
29
|
|
|
30
|
+
### v2.8.4 (October 2026)
|
|
31
|
+
|
|
32
|
+
- Fixed the `autoescape` option of `@rhinostone/swig` set to an escape strategy name: `'js'` and `'html'` work as documented. The value was written into the compiled template as code, so these failed with a ReferenceError and an option built from untrusted input could run as code; a value holding a double quote or a line break is now rejected with an error.
|
|
33
|
+
- Fixed a `filename` option that is not a string, such as a number, which 2.8.3 no longer compiled with `import`, `from` and `extends` on the async render path; `include` now compiles with a number too. All four frontends.
|
|
34
|
+
- All five packages released in lockstep at `2.8.4`.
|
|
35
|
+
|
|
36
|
+
### v2.8.3 (October 2026)
|
|
37
|
+
|
|
38
|
+
- 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`.
|
|
39
|
+
- All five packages released in lockstep at `2.8.3`.
|
|
40
|
+
|
|
30
41
|
### v2.8.2 (October 2026)
|
|
31
42
|
|
|
32
43
|
- 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.
|
package/dist/swig.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Swig v2.8.
|
|
1
|
+
/*! Swig v2.8.4 | 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;
|
|
@@ -1618,7 +1618,7 @@
|
|
|
1618
1618
|
} else {
|
|
1619
1619
|
incSelector = "_utils.extend({}, _ctx, " + incCtxJS + ")";
|
|
1620
1620
|
}
|
|
1621
|
-
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(String(node.resolveFrom || "")) + '"})(' + incSelector + ");\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
|
|
1622
1622
|
return;
|
|
1623
1623
|
}
|
|
1624
1624
|
if (node.type === "IncludeDeferred") {
|
|
@@ -1643,7 +1643,7 @@
|
|
|
1643
1643
|
} else {
|
|
1644
1644
|
incdSelector = "_utils.extend({}, _ctx, " + incdCtxJS + ")";
|
|
1645
1645
|
}
|
|
1646
|
-
var incdOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
|
|
1646
|
+
var incdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
|
|
1647
1647
|
out += (node.ignoreMissing ? " try {\n" : "") + "_output += (await (await _swig.getTemplate(" + incdPathJS + ", " + incdOpts + "))(" + incdSelector + ")).output;\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
|
|
1648
1648
|
return;
|
|
1649
1649
|
}
|
|
@@ -1657,7 +1657,7 @@
|
|
|
1657
1657
|
} else {
|
|
1658
1658
|
impdPathJS = node.path;
|
|
1659
1659
|
}
|
|
1660
|
-
var impdOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
|
|
1660
|
+
var impdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
|
|
1661
1661
|
out += "_ctx." + node.alias + " = (await (await _swig.getTemplate(" + impdPathJS + ", " + impdOpts + "))(_ctx)).exports;\n";
|
|
1662
1662
|
return;
|
|
1663
1663
|
}
|
|
@@ -1678,7 +1678,7 @@
|
|
|
1678
1678
|
} else {
|
|
1679
1679
|
fromdPathJS = node.path;
|
|
1680
1680
|
}
|
|
1681
|
-
var fromdOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
|
|
1681
|
+
var fromdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
|
|
1682
1682
|
var fromdBindings = "";
|
|
1683
1683
|
utils.each(fromdImports, function(entry) {
|
|
1684
1684
|
var bindName = entry.alias || entry.name;
|
|
@@ -1715,7 +1715,7 @@
|
|
|
1715
1715
|
} else {
|
|
1716
1716
|
extPathJS = node.path;
|
|
1717
1717
|
}
|
|
1718
|
-
var extOpts = '{resolveFrom: "' + (node.resolveFrom || "") + '"}';
|
|
1718
|
+
var extOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
|
|
1719
1719
|
out += "var _parentTpl = await _swig.getTemplate(" + extPathJS + ", " + extOpts + ");\n";
|
|
1720
1720
|
out += "var _parentResult = await _parentTpl(_ctx, _mergedBlocks);\n";
|
|
1721
1721
|
out += "_output = _parentResult.output;\n";
|
|
@@ -3058,9 +3058,23 @@
|
|
|
3058
3058
|
function escapeRegExp(str) {
|
|
3059
3059
|
return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, "\\$&");
|
|
3060
3060
|
}
|
|
3061
|
+
function quoteEscapeOption(value) {
|
|
3062
|
+
var quote;
|
|
3063
|
+
if (typeof value !== "string" || value === "") {
|
|
3064
|
+
return value;
|
|
3065
|
+
}
|
|
3066
|
+
quote = value.charAt(0);
|
|
3067
|
+
if (value.length > 1 && (quote === '"' || quote === "'") && value.charAt(value.length - 1) === quote) {
|
|
3068
|
+
value = value.slice(1, -1);
|
|
3069
|
+
}
|
|
3070
|
+
if (/["\n\r\u2028\u2029]/.test(value)) {
|
|
3071
|
+
throw new Error('Option "autoescape" must be true, false or an escape strategy name without double quotes or line breaks.');
|
|
3072
|
+
}
|
|
3073
|
+
return '"' + value + '"';
|
|
3074
|
+
}
|
|
3061
3075
|
exports.parse = function(swig2, source, opts, tags, filters) {
|
|
3062
3076
|
source = source.replace(/\r\n/g, "\n");
|
|
3063
|
-
var escape = opts.autoescape, tagOpen = opts.tagControls[0], tagClose = opts.tagControls[1], varOpen = opts.varControls[0], varClose = opts.varControls[1], escapedTagOpen = escapeRegExp(tagOpen), escapedTagClose = escapeRegExp(tagClose), escapedVarOpen = escapeRegExp(varOpen), escapedVarClose = escapeRegExp(varClose), tagStrip = new RegExp("^" + escapedTagOpen + "-?\\s*|\\s*-?" + escapedTagClose + "$", "g"), tagStripBefore = new RegExp("^" + escapedTagOpen + "-"), tagStripAfter = new RegExp("-" + escapedTagClose + "$"), varStrip = new RegExp("^" + escapedVarOpen + "-?\\s*|\\s*-?" + escapedVarClose + "$", "g"), varStripBefore = new RegExp("^" + escapedVarOpen + "-"), varStripAfter = new RegExp("-" + escapedVarClose + "$"), cmtOpen = opts.cmtControls[0], cmtClose = opts.cmtControls[1], anyChar = "[\\s\\S]*?", splitter = new RegExp(
|
|
3077
|
+
var escape = quoteEscapeOption(opts.autoescape), tagOpen = opts.tagControls[0], tagClose = opts.tagControls[1], varOpen = opts.varControls[0], varClose = opts.varControls[1], escapedTagOpen = escapeRegExp(tagOpen), escapedTagClose = escapeRegExp(tagClose), escapedVarOpen = escapeRegExp(varOpen), escapedVarClose = escapeRegExp(varClose), tagStrip = new RegExp("^" + escapedTagOpen + "-?\\s*|\\s*-?" + escapedTagClose + "$", "g"), tagStripBefore = new RegExp("^" + escapedTagOpen + "-"), tagStripAfter = new RegExp("-" + escapedTagClose + "$"), varStrip = new RegExp("^" + escapedVarOpen + "-?\\s*|\\s*-?" + escapedVarClose + "$", "g"), varStripBefore = new RegExp("^" + escapedVarOpen + "-"), varStripAfter = new RegExp("-" + escapedVarClose + "$"), cmtOpen = opts.cmtControls[0], cmtClose = opts.cmtControls[1], anyChar = "[\\s\\S]*?", splitter = new RegExp(
|
|
3064
3078
|
"(" + escapedTagOpen + anyChar + escapedTagClose + "|" + escapedVarOpen + anyChar + escapedVarClose + "|" + escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) + ")"
|
|
3065
3079
|
), line = 1, stack = [], parent = null, parentExpr = null, tokens = [], blocks = {}, inRaw = false, stripNext;
|
|
3066
3080
|
function parseVariable(str, line2) {
|
|
@@ -3080,7 +3094,7 @@
|
|
|
3080
3094
|
if (last && last.name === str.split(/\s+/)[0].replace(/^end/, "") && last.ends) {
|
|
3081
3095
|
switch (last.name) {
|
|
3082
3096
|
case "autoescape":
|
|
3083
|
-
escape = opts.autoescape;
|
|
3097
|
+
escape = quoteEscapeOption(opts.autoescape);
|
|
3084
3098
|
break;
|
|
3085
3099
|
case "raw":
|
|
3086
3100
|
inRaw = false;
|
|
@@ -3327,7 +3341,7 @@
|
|
|
3327
3341
|
var missing = "missing";
|
|
3328
3342
|
var only = "only";
|
|
3329
3343
|
exports.compile = function(compiler, args, content, parents, options, blockName, token) {
|
|
3330
|
-
var file = args.shift(), onlyIdx = args.indexOf(only), onlyCtx = onlyIdx !== -1 ? args.splice(onlyIdx, 1) : false, parentFile =
|
|
3344
|
+
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("");
|
|
3331
3345
|
if (token && token.irExpr) {
|
|
3332
3346
|
file = token.irExpr.file;
|
|
3333
3347
|
if (token.irExpr.context !== void 0) {
|
|
@@ -4822,7 +4836,7 @@
|
|
|
4822
4836
|
var loaders = require_loaders2();
|
|
4823
4837
|
var preWalker = require_pre_walker();
|
|
4824
4838
|
var engine = require_engine();
|
|
4825
|
-
exports.version = "2.8.
|
|
4839
|
+
exports.version = "2.8.4";
|
|
4826
4840
|
var defaultOptions = {
|
|
4827
4841
|
autoescape: true,
|
|
4828
4842
|
varControls: ["{{", "}}"],
|
|
@@ -5185,6 +5199,19 @@
|
|
|
5185
5199
|
* @return {string}
|
|
5186
5200
|
* @private
|
|
5187
5201
|
*/
|
|
5202
|
+
/*!
|
|
5203
|
+
* Turn a string autoescape option into the string literal that the
|
|
5204
|
+
* autoescape tail writes into the compiled template as the e filter's
|
|
5205
|
+
* strategy argument; the bare option used to be written there as code.
|
|
5206
|
+
* The tail doubles every backslash, so the value is wrapped in double
|
|
5207
|
+
* quotes without escaping it, and a value holding a double quote or a line
|
|
5208
|
+
* break is rejected. One pair of matching outer quotes is removed first,
|
|
5209
|
+
* so a value that was already quoted keeps working. A value that is not a
|
|
5210
|
+
* non-empty string is returned unchanged.
|
|
5211
|
+
* @param {*} value The autoescape option.
|
|
5212
|
+
* @return {*}
|
|
5213
|
+
* @private
|
|
5214
|
+
*/
|
|
5188
5215
|
/*!
|
|
5189
5216
|
* Loop over the source, split via the tag/var/comment regular expression splitter.
|
|
5190
5217
|
* Send each chunk to the appropriate parser.
|