@rhinostone/swig-core 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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  @rhinostone/swig-core
2
2
  =====================
3
3
 
4
- [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig-core.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig-core) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig-core)](https://socket.dev/npm/package/@rhinostone/swig-core)
4
+ [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig-core.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig-core) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig-core)](https://socket.dev/npm/package/@rhinostone/swig-core) [![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
  > **Shared runtime** for the `@rhinostone/swig` family of template engines. Not intended for direct consumption unless you are building a custom frontend. Install [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) for the default Swig (Jinja2/Django-inspired) flavor, [@rhinostone/swig-twig](https://www.npmjs.com/package/@rhinostone/swig-twig) for the Twig flavor, [@rhinostone/swig-jinja2](https://www.npmjs.com/package/@rhinostone/swig-jinja2) for the Python Jinja2 flavor, or [@rhinostone/swig-django](https://www.npmjs.com/package/@rhinostone/swig-django) for the Django flavor — they all pull this package in pinned to the matching version.
7
7
 
package/lib/backend.js CHANGED
@@ -391,7 +391,8 @@ exports.compile = function (template, parents, options, blockName) {
391
391
  // else verbatim string fallback preserves the userland setTag
392
392
  // path (compile functions that still hand in raw JS-source
393
393
  // fragments). `resolveFrom` is a plain filesystem path that must
394
- // be escaped into a string literal — escapeTextValue below (backslash,
394
+ // be escaped into a string literal (a number is converted to a
395
+ // string first) — escapeTextValue below (backslash,
395
396
  // newline, double-quote) is the single chokepoint that does it for all
396
397
  // five node types, every flavor and userland setTag, so no unescaped
397
398
  // filename can break out of the literal (GHSA-q89c). `ignoreMissing` wraps
@@ -420,7 +421,7 @@ exports.compile = function (template, parents, options, blockName) {
420
421
  }
421
422
  out += (node.ignoreMissing ? ' try {\n' : '') +
422
423
  '_output += _swig.compileFile(' + incPathJS + ', {' +
423
- 'resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"' +
424
+ 'resolveFrom: "' + escapeTextValue(String(node.resolveFrom || '')) + '"' +
424
425
  '})(' + incSelector + ');\n' +
425
426
  (node.ignoreMissing ? '} catch (e) {}\n' : '');
426
427
  return;
@@ -454,7 +455,7 @@ exports.compile = function (template, parents, options, blockName) {
454
455
  } else {
455
456
  incdSelector = '_utils.extend({}, _ctx, ' + incdCtxJS + ')';
456
457
  }
457
- var incdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
458
+ var incdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || '')) + '"}';
458
459
  out += (node.ignoreMissing ? ' try {\n' : '') +
459
460
  '_output += (await (await _swig.getTemplate(' + incdPathJS + ', ' + incdOpts + '))(' + incdSelector + ')).output;\n' +
460
461
  (node.ignoreMissing ? '} catch (e) {}\n' : '');
@@ -476,7 +477,7 @@ exports.compile = function (template, parents, options, blockName) {
476
477
  } else {
477
478
  impdPathJS = node.path;
478
479
  }
479
- var impdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
480
+ var impdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || '')) + '"}';
480
481
  out += '_ctx.' + node.alias + ' = (await (await _swig.getTemplate(' + impdPathJS + ', ' + impdOpts + '))(_ctx)).exports;\n';
481
482
  return;
482
483
  }
@@ -501,7 +502,7 @@ exports.compile = function (template, parents, options, blockName) {
501
502
  } else {
502
503
  fromdPathJS = node.path;
503
504
  }
504
- var fromdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
505
+ var fromdOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || '')) + '"}';
505
506
  var fromdBindings = '';
506
507
  utils.each(fromdImports, function (entry) {
507
508
  var bindName = entry.alias || entry.name;
@@ -570,7 +571,7 @@ exports.compile = function (template, parents, options, blockName) {
570
571
  } else {
571
572
  extPathJS = node.path;
572
573
  }
573
- var extOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
574
+ var extOpts = '{resolveFrom: "' + escapeTextValue(String(node.resolveFrom || '')) + '"}';
574
575
  out += 'var _parentTpl = await _swig.getTemplate(' + extPathJS + ', ' + extOpts + ');\n';
575
576
  out += 'var _parentResult = await _parentTpl(_ctx, _mergedBlocks);\n';
576
577
  out += '_output = _parentResult.output;\n';
@@ -909,6 +910,14 @@ function groupOperand(node, js, unary) {
909
910
  return js;
910
911
  }
911
912
 
913
+ /*!
914
+ * The flavor name `emitBinaryOp` writes into an `_utils.isIn` call: one of
915
+ * the three names `isIn` knows, never the flag's own text. @private
916
+ */
917
+ function membershipMode(flag) {
918
+ return (flag === 'twig' || flag === 'django') ? flag : 'jinja2';
919
+ }
920
+
912
921
  function emitBinaryOp(node, d) {
913
922
  var left = groupOperand(node.left, emitExpr(node.left, d)),
914
923
  right = groupOperand(node.right, emitExpr(node.right, d));
@@ -916,6 +925,13 @@ function emitBinaryOp(node, d) {
916
925
  return left + ' ' + node.op + ' ' + right;
917
926
  }
918
927
  if (node.op === 'in') {
928
+ // A Twig, Jinja2 or Django `in` carries its flavor name in
929
+ // `membership` and calls that engine's membership test, which `negate`
930
+ // turns into `not in`; native swig keeps the JavaScript `in` operator,
931
+ // which tests keys.
932
+ if (node.membership) {
933
+ return '_utils.isIn(' + left + ', ' + right + ', "' + membershipMode(node.membership) + '"' + (node.negate ? ', true' : '') + ')';
934
+ }
919
935
  return left + ' in ' + right;
920
936
  }
921
937
  // Twig/Jinja2 `~` is explicit string-concat: both sides coerce to
package/lib/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * @type {String}
17
17
  */
18
- exports.version = "2.8.3";
18
+ exports.version = "2.9.0";
19
19
 
20
20
  exports.utils = require('./utils');
21
21
  exports.security = require('./security');
package/lib/ir.js CHANGED
@@ -446,6 +446,8 @@
446
446
  * @property {string} op
447
447
  * @property {IRExpr} left
448
448
  * @property {IRExpr} right
449
+ * @property {string} [membership] Opt-in for `in`: "twig", "jinja2" or "django" emits `_utils.isIn(left, right, mode)`, that engine's membership test; without it, the JavaScript `in` operator (native swig).
450
+ * @property {boolean} [negate] With `membership`: the `not in` form, negated inside `_utils.isIn` (Django reads a test it cannot run as false either way).
449
451
  * @property {IRLoc} [loc]
450
452
  */
451
453
 
package/lib/utils.js CHANGED
@@ -388,3 +388,95 @@ exports.resolve = function (obj, path) {
388
388
  }
389
389
  return cur;
390
390
  };
391
+
392
+ /*!
393
+ * Item equality for `isIn`. Jinja2 and Django compare with `===` and treat
394
+ * null and a missing value as equal (Django reads both as None). Twig
395
+ * compares two scalars with `==`, as swig-twig's `==` does, and anything
396
+ * else by identity. @private
397
+ */
398
+ function sameItem(a, b, twig) {
399
+ if (a === b) {
400
+ return true;
401
+ }
402
+ if (!twig) {
403
+ return (a === null || a === undefined) && (b === null || b === undefined);
404
+ }
405
+ if ((a !== null && typeof a === 'object') || (b !== null && typeof b === 'object') ||
406
+ typeof a === 'function' || typeof b === 'function') {
407
+ return false;
408
+ }
409
+ return a == b;
410
+ }
411
+
412
+ /*!
413
+ * The membership test of `isIn`: true or false, or null when Jinja2 and
414
+ * Django cannot run it, where Python raises a TypeError: a container that is
415
+ * not a list, a string or a mapping, a needle that is not a string tested
416
+ * against a string, or a list or an object tested against a mapping's keys.
417
+ * Twig runs every test. @private
418
+ */
419
+ function membership(needle, haystack, twig) {
420
+ var keys, i;
421
+ if (typeof haystack === 'string') {
422
+ if (typeof needle === 'string' || (twig && typeof needle === 'number')) {
423
+ return haystack.indexOf(String(needle)) !== -1;
424
+ }
425
+ return twig ? false : null;
426
+ }
427
+ if (haystack === null || typeof haystack !== 'object') {
428
+ return twig ? false : null;
429
+ }
430
+ if (exports.isArray(haystack)) {
431
+ for (i = 0; i < haystack.length; i += 1) {
432
+ if (sameItem(needle, haystack[i], twig)) {
433
+ return true;
434
+ }
435
+ }
436
+ return false;
437
+ }
438
+ if (twig) {
439
+ keys = Object.keys(haystack);
440
+ for (i = 0; i < keys.length; i += 1) {
441
+ if (sameItem(needle, haystack[keys[i]], true)) {
442
+ return true;
443
+ }
444
+ }
445
+ return false;
446
+ }
447
+ if (typeof needle === 'string' || typeof needle === 'number') {
448
+ return Object.prototype.hasOwnProperty.call(haystack, String(needle));
449
+ }
450
+ return (needle !== null && typeof needle === 'object') ? null : false;
451
+ }
452
+
453
+ /**
454
+ * Membership test behind the `in` operator of the Twig, Jinja2 and Django
455
+ * frontends, which set `IRBinaryOp.membership` to their flavor name; native
456
+ * swig keeps the JavaScript `in` operator, which tests keys. Each flavor
457
+ * follows its reference engine:
458
+ *
459
+ * - Jinja2 and Django (Python's `in`): a list holds an equal item, a string
460
+ * a substring (the needle must be a string), a mapping a key (a number
461
+ * needle tests the key with the same digits).
462
+ * - Twig: a list or a mapping holds an equal value, compared loosely; a
463
+ * string holds a string or number needle as a substring.
464
+ *
465
+ * A test the engine cannot run (Jinja2 raises; Django's if tag reads it as
466
+ * false) gives false, as does any other container, rather than an error.
467
+ * With `negate` (the `not in` operator) the result is negated, except that
468
+ * Django reads a test it cannot run as false for `not in` too.
469
+ *
470
+ * @param {*} needle
471
+ * @param {*} haystack
472
+ * @param {string} mode "twig", "jinja2" or "django".
473
+ * @param {boolean} [negate] The `not in` form.
474
+ * @return {boolean}
475
+ */
476
+ exports.isIn = function (needle, haystack, mode, negate) {
477
+ var found = membership(needle, haystack, mode === 'twig');
478
+ if (found === null) {
479
+ return negate === true && mode !== 'django';
480
+ }
481
+ return negate === true ? !found : found;
482
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.8.3",
3
+ "version": "2.9.0",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",