@rhinostone/swig 2.8.3 → 2.9.0

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,19 @@
1
+ [2.9.0](https://github.com/gina-io/swig/tree/v2.9.0) / 2026-10-23
2
+ -----------------------------------------------------------------
3
+
4
+ * **Added** An OpenSSF Scorecard badge at the top of the README of each of the five packages. It shows the score that a Scorecard workflow publishes for the repository on every push to develop and every week, and links to the Scorecard report. Documentation only; no runtime or API change.
5
+
6
+ * **Added** swig-twig, swig-jinja2 and swig-django: the `not in` operator, as Twig, Jinja2 and Django have it. `x not in y` negates `x in y` and binds at the same level; where Django cannot run the test it is false, as Django gives it, and where Jinja2 raises swig gives true. Until now `x not in y` failed to parse. Native swig still rejects `not in`.
7
+
8
+ * **Fixed** swig-twig, swig-jinja2 and swig-django: the `in` operator tests membership as Twig, Jinja2 and Django do. In Jinja2 and Django a list holds an equal item, a string a substring and a mapping a key; in Twig a list or a mapping holds a value, compared loosely, and a string holds a string or number as a substring. Until now `in` was the JavaScript operator, which tests keys: `3 in [1, 2, 3]` was false, `0 in [5]` true, and `"b" in "abc"` threw. A test the reference engine cannot run (a number or null container, for instance) is false instead of an error. Native swig keeps the JavaScript `in`.
9
+
10
+ [2.8.4](https://github.com/gina-io/swig/tree/v2.8.4) / 2026-10-21
11
+ -----------------------------------------------------------------
12
+
13
+ * **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.
14
+
15
+ * **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.
16
+
1
17
  [2.8.3](https://github.com/gina-io/swig/tree/v2.8.3) / 2026-10-21
2
18
  -----------------------------------------------------------------
3
19
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Swig
2
2
  ====
3
3
 
4
- [![CI](https://github.com/gina-io/swig/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/gina-io/swig/actions/workflows/ci.yml) [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![NPM Downloads](https://img.shields.io/npm/dm/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig)](https://socket.dev/npm/package/@rhinostone/swig)
4
+ [![CI](https://github.com/gina-io/swig/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/gina-io/swig/actions/workflows/ci.yml) [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![NPM Downloads](https://img.shields.io/npm/dm/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig)](https://socket.dev/npm/package/@rhinostone/swig) [![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/gina-io/swig?label=openssf%20scorecard&style=flat)](https://scorecard.dev/viewer/?uri=github.com/gina-io/swig)
5
5
 
6
6
  > **Multi-flavor template engine** for Node.js and browsers — native Swig syntax (Jinja2/Django-inspired), Twig syntax, Python Jinja2 syntax, and Django Template Language syntax via dedicated frontends sharing one IR backend. [gina-io/swig](https://github.com/gina-io/swig) started as a maintained continuation of the abandoned `paularmstrong/swig` (last released 2014) and is now a standalone project. Security and bug fixes ship here.
7
7
 
@@ -32,7 +32,7 @@ Features
32
32
  --------
33
33
 
34
34
  * Available for node.js **and** major web browsers.
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.
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`, ~24 KB) — no parser, no `new Function`, no `unsafe-eval` required.
36
36
  * [Express](https://expressjs.com/) compatible.
37
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.
@@ -43,12 +43,14 @@ 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.9.0
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
+ > **Behaviour change in `@rhinostone/swig-twig`, `@rhinostone/swig-jinja2` and `@rhinostone/swig-django`.** `x in list` now tests the list's items, not its indices, and `x in "text"` a substring, as Twig, Jinja2 and Django do. In swig-twig, `x in mapping` now tests the mapping's values, as Twig does: write `x in mapping|keys` to test a key. Native `@rhinostone/swig` is unchanged.
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 — `in` tests membership in the Twig, Jinja2 and Django frontends.** In swig-jinja2 and swig-django a list holds an equal item, a string a substring and a mapping a key; in swig-twig a list or a mapping holds a value, compared loosely, and a string holds a string or number as a substring. Until now `in` was the JavaScript operator, which tests keys: `3 in [1, 2, 3]` was false, `0 in [5]` true, and `"b" in "abc"` threw. A test the reference engine cannot run (a number or null container, for instance) is false instead of an error. Native `@rhinostone/swig` keeps the JavaScript `in`, which tests an object's keys or an array's indices.
52
+ * **Added — `not in` in the Twig, Jinja2 and Django frontends.** `x not in y` negates `x in y` and binds at the same level. Where Django cannot run the test it is false, as in Django; where Jinja2 raises, swig gives true. Until now `x not in y` failed to parse. Native `@rhinostone/swig` still rejects `not in`.
53
+ * **Added — an OpenSSF Scorecard badge** at the top of the README of each of the five packages. It shows the score that a Scorecard workflow publishes for the repository on every push to develop and every week, and links to the Scorecard report. Documentation only; no runtime or API change.
52
54
 
53
55
  See the full [HISTORY.md](./HISTORY.md) and [ROADMAP.md](./ROADMAP.md).
54
56
 
package/ROADMAP.md CHANGED
@@ -10,10 +10,11 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
10
10
 
11
11
  | Status | Item |
12
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
13
  | 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
14
  | Planned | swig-django: two-variable for-loops over a list of pairs unpack each pair, as Django does. |
16
15
  | 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. |
16
+ | Planned | swig-django: `not` binds below the comparisons and `in`, as in Django; today `not x == y` reads as `(not x) == y`. |
17
+ | Planned | swig-jinja2 and swig-django: `==` and `!=` compare as Python does; today they are JavaScript's loose operators, so `1 == "1"` is true and `[1] == [1]` false. |
17
18
 
18
19
  ## Future (post-2.0)
19
20
 
@@ -27,6 +28,19 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
27
28
 
28
29
  ## Completed
29
30
 
31
+ ### v2.9.0 (October 2026)
32
+
33
+ - `in` tests membership in swig-twig, swig-jinja2 and swig-django, as Twig, Jinja2 and Django do: a list holds an item, a string a substring, and a mapping a key (Jinja2, Django) or a value (Twig). Native swig keeps the JavaScript `in`, which tests an object's keys or an array's indices.
34
+ - `not in` in swig-twig, swig-jinja2 and swig-django.
35
+ - An OpenSSF Scorecard badge on the README of each package.
36
+ - All five packages released in lockstep at `2.9.0`.
37
+
38
+ ### v2.8.4 (October 2026)
39
+
40
+ - 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.
41
+ - 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.
42
+ - All five packages released in lockstep at `2.8.4`.
43
+
30
44
  ### v2.8.3 (October 2026)
31
45
 
32
46
  - 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.9.0 | 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;
@@ -250,6 +250,58 @@
250
250
  }
251
251
  return cur;
252
252
  };
253
+ function sameItem(a, b, twig) {
254
+ if (a === b) {
255
+ return true;
256
+ }
257
+ if (!twig) {
258
+ return (a === null || a === void 0) && (b === null || b === void 0);
259
+ }
260
+ if (a !== null && typeof a === "object" || b !== null && typeof b === "object" || typeof a === "function" || typeof b === "function") {
261
+ return false;
262
+ }
263
+ return a == b;
264
+ }
265
+ function membership(needle, haystack, twig) {
266
+ var keys, i;
267
+ if (typeof haystack === "string") {
268
+ if (typeof needle === "string" || twig && typeof needle === "number") {
269
+ return haystack.indexOf(String(needle)) !== -1;
270
+ }
271
+ return twig ? false : null;
272
+ }
273
+ if (haystack === null || typeof haystack !== "object") {
274
+ return twig ? false : null;
275
+ }
276
+ if (exports.isArray(haystack)) {
277
+ for (i = 0; i < haystack.length; i += 1) {
278
+ if (sameItem(needle, haystack[i], twig)) {
279
+ return true;
280
+ }
281
+ }
282
+ return false;
283
+ }
284
+ if (twig) {
285
+ keys = Object.keys(haystack);
286
+ for (i = 0; i < keys.length; i += 1) {
287
+ if (sameItem(needle, haystack[keys[i]], true)) {
288
+ return true;
289
+ }
290
+ }
291
+ return false;
292
+ }
293
+ if (typeof needle === "string" || typeof needle === "number") {
294
+ return Object.prototype.hasOwnProperty.call(haystack, String(needle));
295
+ }
296
+ return needle !== null && typeof needle === "object" ? null : false;
297
+ }
298
+ exports.isIn = function(needle, haystack, mode, negate) {
299
+ var found = membership(needle, haystack, mode === "twig");
300
+ if (found === null) {
301
+ return negate === true && mode !== "django";
302
+ }
303
+ return negate === true ? !found : found;
304
+ };
253
305
  }
254
306
  });
255
307
 
@@ -1618,7 +1670,7 @@
1618
1670
  } else {
1619
1671
  incSelector = "_utils.extend({}, _ctx, " + incCtxJS + ")";
1620
1672
  }
1621
- out += (node.ignoreMissing ? " try {\n" : "") + "_output += _swig.compileFile(" + incPathJS + ', {resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"})(' + incSelector + ");\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
1673
+ out += (node.ignoreMissing ? " try {\n" : "") + "_output += _swig.compileFile(" + incPathJS + ', {resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"})(' + incSelector + ");\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
1622
1674
  return;
1623
1675
  }
1624
1676
  if (node.type === "IncludeDeferred") {
@@ -1643,7 +1695,7 @@
1643
1695
  } else {
1644
1696
  incdSelector = "_utils.extend({}, _ctx, " + incdCtxJS + ")";
1645
1697
  }
1646
- var incdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1698
+ var incdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
1647
1699
  out += (node.ignoreMissing ? " try {\n" : "") + "_output += (await (await _swig.getTemplate(" + incdPathJS + ", " + incdOpts + "))(" + incdSelector + ")).output;\n" + (node.ignoreMissing ? "} catch (e) {}\n" : "");
1648
1700
  return;
1649
1701
  }
@@ -1657,7 +1709,7 @@
1657
1709
  } else {
1658
1710
  impdPathJS = node.path;
1659
1711
  }
1660
- var impdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1712
+ var impdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
1661
1713
  out += "_ctx." + node.alias + " = (await (await _swig.getTemplate(" + impdPathJS + ", " + impdOpts + "))(_ctx)).exports;\n";
1662
1714
  return;
1663
1715
  }
@@ -1678,7 +1730,7 @@
1678
1730
  } else {
1679
1731
  fromdPathJS = node.path;
1680
1732
  }
1681
- var fromdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1733
+ var fromdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
1682
1734
  var fromdBindings = "";
1683
1735
  utils.each(fromdImports, function(entry) {
1684
1736
  var bindName = entry.alias || entry.name;
@@ -1715,7 +1767,7 @@
1715
1767
  } else {
1716
1768
  extPathJS = node.path;
1717
1769
  }
1718
- var extOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || "") + '"}';
1770
+ var extOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || "")) + '"}';
1719
1771
  out += "var _parentTpl = await _swig.getTemplate(" + extPathJS + ", " + extOpts + ");\n";
1720
1772
  out += "var _parentResult = await _parentTpl(_ctx, _mergedBlocks);\n";
1721
1773
  out += "_output = _parentResult.output;\n";
@@ -1923,12 +1975,18 @@
1923
1975
  }
1924
1976
  return js;
1925
1977
  }
1978
+ function membershipMode(flag) {
1979
+ return flag === "twig" || flag === "django" ? flag : "jinja2";
1980
+ }
1926
1981
  function emitBinaryOp(node, d) {
1927
1982
  var left = groupOperand(node.left, emitExpr(node.left, d)), right = groupOperand(node.right, emitExpr(node.right, d));
1928
1983
  if (isArithmeticOp(node.op)) {
1929
1984
  return left + " " + node.op + " " + right;
1930
1985
  }
1931
1986
  if (node.op === "in") {
1987
+ if (node.membership) {
1988
+ return "_utils.isIn(" + left + ", " + right + ', "' + membershipMode(node.membership) + '"' + (node.negate ? ", true" : "") + ")";
1989
+ }
1932
1990
  return left + " in " + right;
1933
1991
  }
1934
1992
  if (node.op === "~") {
@@ -3058,9 +3116,23 @@
3058
3116
  function escapeRegExp(str) {
3059
3117
  return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, "\\$&");
3060
3118
  }
3119
+ function quoteEscapeOption(value) {
3120
+ var quote;
3121
+ if (typeof value !== "string" || value === "") {
3122
+ return value;
3123
+ }
3124
+ quote = value.charAt(0);
3125
+ if (value.length > 1 && (quote === '"' || quote === "'") && value.charAt(value.length - 1) === quote) {
3126
+ value = value.slice(1, -1);
3127
+ }
3128
+ if (/["\n\r\u2028\u2029]/.test(value)) {
3129
+ throw new Error('Option "autoescape" must be true, false or an escape strategy name without double quotes or line breaks.');
3130
+ }
3131
+ return '"' + value + '"';
3132
+ }
3061
3133
  exports.parse = function(swig2, source, opts, tags, filters) {
3062
3134
  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(
3135
+ 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
3136
  "(" + escapedTagOpen + anyChar + escapedTagClose + "|" + escapedVarOpen + anyChar + escapedVarClose + "|" + escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) + ")"
3065
3137
  ), line = 1, stack = [], parent = null, parentExpr = null, tokens = [], blocks = {}, inRaw = false, stripNext;
3066
3138
  function parseVariable(str, line2) {
@@ -3080,7 +3152,7 @@
3080
3152
  if (last && last.name === str.split(/\s+/)[0].replace(/^end/, "") && last.ends) {
3081
3153
  switch (last.name) {
3082
3154
  case "autoescape":
3083
- escape = opts.autoescape;
3155
+ escape = quoteEscapeOption(opts.autoescape);
3084
3156
  break;
3085
3157
  case "raw":
3086
3158
  inRaw = false;
@@ -4822,7 +4894,7 @@
4822
4894
  var loaders = require_loaders2();
4823
4895
  var preWalker = require_pre_walker();
4824
4896
  var engine = require_engine();
4825
- exports.version = "2.8.3";
4897
+ exports.version = "2.9.0";
4826
4898
  var defaultOptions = {
4827
4899
  autoescape: true,
4828
4900
  varControls: ["{{", "}}"],
@@ -5062,6 +5134,19 @@
5062
5134
  * `_dangerousProps` segments are rejected at runtime as defense-in-depth.
5063
5135
  * @private
5064
5136
  */
5137
+ /*!
5138
+ * Item equality for `isIn`. Jinja2 and Django compare with `===` and treat
5139
+ * null and a missing value as equal (Django reads both as None). Twig
5140
+ * compares two scalars with `==`, as swig-twig's `==` does, and anything
5141
+ * else by identity. @private
5142
+ */
5143
+ /*!
5144
+ * The membership test of `isIn`: true or false, or null when Jinja2 and
5145
+ * Django cannot run it, where Python raises a TypeError: a container that is
5146
+ * not a list, a string or a mapping, a needle that is not a string tested
5147
+ * against a string, or a list or an object tested against a mapping's keys.
5148
+ * Twig runs every test. @private
5149
+ */
5065
5150
  /*!
5066
5151
  * Attach `loc` to the node if provided, skipping the assignment otherwise
5067
5152
  * so consumers can tell "no source location available" from
@@ -5163,6 +5248,10 @@
5163
5248
  * wraps a unary operand, for the positions where its prefix would bind
5164
5249
  * differently (`-(-x)`, `(-x)[0]`). @private
5165
5250
  */
5251
+ /*!
5252
+ * The flavor name `emitBinaryOp` writes into an `_utils.isIn` call: one of
5253
+ * the three names `isIn` knows, never the flag's own text. @private
5254
+ */
5166
5255
  /*!
5167
5256
  * Emit a function / method invocation. Three callee shapes:
5168
5257
  * 1. Single-segment VarRef (`foo(...)`) — FUNCTION-token pattern with
@@ -5185,6 +5274,19 @@
5185
5274
  * @return {string}
5186
5275
  * @private
5187
5276
  */
5277
+ /*!
5278
+ * Turn a string autoescape option into the string literal that the
5279
+ * autoescape tail writes into the compiled template as the e filter's
5280
+ * strategy argument; the bare option used to be written there as code.
5281
+ * The tail doubles every backslash, so the value is wrapped in double
5282
+ * quotes without escaping it, and a value holding a double quote or a line
5283
+ * break is rejected. One pair of matching outer quotes is removed first,
5284
+ * so a value that was already quoted keeps working. A value that is not a
5285
+ * non-empty string is returned unchanged.
5286
+ * @param {*} value The autoescape option.
5287
+ * @return {*}
5288
+ * @private
5289
+ */
5188
5290
  /*!
5189
5291
  * Loop over the source, split via the tag/var/comment regular expression splitter.
5190
5292
  * Send each chunk to the appropriate parser.