@rhinostone/swig 2.8.3 → 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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  [2.8.3](https://github.com/gina-io/swig/tree/v2.8.3) / 2026-10-21
2
9
  -----------------------------------------------------------------
3
10
 
package/README.md CHANGED
@@ -43,12 +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.3
46
+ What's new in v2.8.4
47
47
  --------------------
48
48
 
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`.
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
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`.
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.
52
52
 
53
53
  See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
54
54
 
package/ROADMAP.md CHANGED
@@ -27,6 +27,12 @@ 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
+
30
36
  ### v2.8.3 (October 2026)
31
37
 
32
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`.
package/dist/swig.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Swig v2.8.3 | https://github.com/gina-io/swig | @license https://github.com/gina-io/swig/blob/master/LICENSE */
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: "' + escapeTextValue(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: "' + escapeTextValue(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: "' + escapeTextValue(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: "' + escapeTextValue(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: "' + escapeTextValue(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;
@@ -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.3";
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.