@rhinostone/swig-core 2.8.4 → 2.9.1

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
@@ -910,6 +910,14 @@ function groupOperand(node, js, unary) {
910
910
  return js;
911
911
  }
912
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
+
913
921
  function emitBinaryOp(node, d) {
914
922
  var left = groupOperand(node.left, emitExpr(node.left, d)),
915
923
  right = groupOperand(node.right, emitExpr(node.right, d));
@@ -917,6 +925,13 @@ function emitBinaryOp(node, d) {
917
925
  return left + ' ' + node.op + ' ' + right;
918
926
  }
919
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
+ }
920
935
  return left + ' in ' + right;
921
936
  }
922
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.4";
18
+ exports.version = "2.9.1";
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
@@ -2,13 +2,104 @@ var security = require('./security');
2
2
 
3
3
  var isArray;
4
4
 
5
+ /*!
6
+ * What a \s regex atom matches, tested one code unit at a time.
7
+ * @private
8
+ */
9
+ var whitespace = /\s/;
10
+
5
11
  /**
6
- * Strip leading and trailing whitespace from a string.
12
+ * Strip leading and trailing whitespace from a string. Linear in the input
13
+ * length.
7
14
  * @param {string} input
8
15
  * @return {string} Stripped input.
9
16
  */
10
17
  exports.strip = function (input) {
11
- return input.replace(/^\s+|\s+$/g, '');
18
+ // A /\s+$/ alternative takes quadratic time on a long run of whitespace
19
+ // that does not end the string; the trailing run is found by trimEnd.
20
+ // return input.replace(/^\s+|\s+$/g, '');
21
+ return exports.trimEnd(input.replace(/^\s+/, ''));
22
+ };
23
+
24
+ /**
25
+ * Remove trailing characters from a string: whitespace (what `\s` matches)
26
+ * when `chars` is omitted, otherwise every UTF-16 code unit that `chars`
27
+ * contains, as a regex character class built from `chars` would. Linear in
28
+ * the input length, where a `/[...]+$/` regex takes quadratic time on a long
29
+ * run of those characters that does not end the string. Used by the flavor
30
+ * filters.
31
+ * @param {string} input
32
+ * @param {string} [chars] Characters to remove; whitespace when omitted.
33
+ * @return {string}
34
+ */
35
+ exports.trimEnd = function (input, chars) {
36
+ var end = input.length;
37
+ if (chars === undefined) {
38
+ while (end > 0 && whitespace.test(input.charAt(end - 1))) {
39
+ end -= 1;
40
+ }
41
+ } else {
42
+ while (end > 0 && chars.indexOf(input.charAt(end - 1)) !== -1) {
43
+ end -= 1;
44
+ }
45
+ }
46
+ return input.slice(0, end);
47
+ };
48
+
49
+ /**
50
+ * Remove every `<...>` span from a string, with the output of
51
+ * `input.replace(/<[^>]+>/g, '')`, or of `/<[^>]*>/g` (which also removes
52
+ * `<>`) when `allowEmpty` is true. Linear in the input length, where those
53
+ * regexes take quadratic time when many `<` have no `>` after them. Used by
54
+ * the flavor filters.
55
+ * @param {string} input
56
+ * @param {boolean} [allowEmpty] Remove `<>` too.
57
+ * @return {string}
58
+ */
59
+ exports.stripTags = function (input, allowEmpty) {
60
+ var out = '',
61
+ from = 0,
62
+ lt = input.indexOf('<'),
63
+ gt;
64
+ while (lt !== -1) {
65
+ gt = input.indexOf('>', lt + 1);
66
+ if (gt === -1) {
67
+ break;
68
+ }
69
+ if (gt === lt + 1 && !allowEmpty) {
70
+ lt = input.indexOf('<', lt + 1);
71
+ continue;
72
+ }
73
+ out += input.slice(from, lt);
74
+ from = gt + 1;
75
+ lt = input.indexOf('<', from);
76
+ }
77
+ return out + input.slice(from);
78
+ };
79
+
80
+ /**
81
+ * Remove every `<!-- ... -->` comment from a string, with the output of
82
+ * `input.replace(/<!--[\s\S]*?-->/g, '')`. Linear in the input length, where
83
+ * that regex takes quadratic time when many `<!--` have no `-->` after them.
84
+ * Used by the flavor filters.
85
+ * @param {string} input
86
+ * @return {string}
87
+ */
88
+ exports.stripComments = function (input) {
89
+ var out = '',
90
+ from = 0,
91
+ open = input.indexOf('<!--'),
92
+ close;
93
+ while (open !== -1) {
94
+ close = input.indexOf('-->', open + 4);
95
+ if (close === -1) {
96
+ break;
97
+ }
98
+ out += input.slice(from, open);
99
+ from = close + 3;
100
+ open = input.indexOf('<!--', from);
101
+ }
102
+ return out + input.slice(from);
12
103
  };
13
104
 
14
105
  /**
@@ -388,3 +479,95 @@ exports.resolve = function (obj, path) {
388
479
  }
389
480
  return cur;
390
481
  };
482
+
483
+ /*!
484
+ * Item equality for `isIn`. Jinja2 and Django compare with `===` and treat
485
+ * null and a missing value as equal (Django reads both as None). Twig
486
+ * compares two scalars with `==`, as swig-twig's `==` does, and anything
487
+ * else by identity. @private
488
+ */
489
+ function sameItem(a, b, twig) {
490
+ if (a === b) {
491
+ return true;
492
+ }
493
+ if (!twig) {
494
+ return (a === null || a === undefined) && (b === null || b === undefined);
495
+ }
496
+ if ((a !== null && typeof a === 'object') || (b !== null && typeof b === 'object') ||
497
+ typeof a === 'function' || typeof b === 'function') {
498
+ return false;
499
+ }
500
+ return a == b;
501
+ }
502
+
503
+ /*!
504
+ * The membership test of `isIn`: true or false, or null when Jinja2 and
505
+ * Django cannot run it, where Python raises a TypeError: a container that is
506
+ * not a list, a string or a mapping, a needle that is not a string tested
507
+ * against a string, or a list or an object tested against a mapping's keys.
508
+ * Twig runs every test. @private
509
+ */
510
+ function membership(needle, haystack, twig) {
511
+ var keys, i;
512
+ if (typeof haystack === 'string') {
513
+ if (typeof needle === 'string' || (twig && typeof needle === 'number')) {
514
+ return haystack.indexOf(String(needle)) !== -1;
515
+ }
516
+ return twig ? false : null;
517
+ }
518
+ if (haystack === null || typeof haystack !== 'object') {
519
+ return twig ? false : null;
520
+ }
521
+ if (exports.isArray(haystack)) {
522
+ for (i = 0; i < haystack.length; i += 1) {
523
+ if (sameItem(needle, haystack[i], twig)) {
524
+ return true;
525
+ }
526
+ }
527
+ return false;
528
+ }
529
+ if (twig) {
530
+ keys = Object.keys(haystack);
531
+ for (i = 0; i < keys.length; i += 1) {
532
+ if (sameItem(needle, haystack[keys[i]], true)) {
533
+ return true;
534
+ }
535
+ }
536
+ return false;
537
+ }
538
+ if (typeof needle === 'string' || typeof needle === 'number') {
539
+ return Object.prototype.hasOwnProperty.call(haystack, String(needle));
540
+ }
541
+ return (needle !== null && typeof needle === 'object') ? null : false;
542
+ }
543
+
544
+ /**
545
+ * Membership test behind the `in` operator of the Twig, Jinja2 and Django
546
+ * frontends, which set `IRBinaryOp.membership` to their flavor name; native
547
+ * swig keeps the JavaScript `in` operator, which tests keys. Each flavor
548
+ * follows its reference engine:
549
+ *
550
+ * - Jinja2 and Django (Python's `in`): a list holds an equal item, a string
551
+ * a substring (the needle must be a string), a mapping a key (a number
552
+ * needle tests the key with the same digits).
553
+ * - Twig: a list or a mapping holds an equal value, compared loosely; a
554
+ * string holds a string or number needle as a substring.
555
+ *
556
+ * A test the engine cannot run (Jinja2 raises; Django's if tag reads it as
557
+ * false) gives false, as does any other container, rather than an error.
558
+ * With `negate` (the `not in` operator) the result is negated, except that
559
+ * Django reads a test it cannot run as false for `not in` too.
560
+ *
561
+ * @param {*} needle
562
+ * @param {*} haystack
563
+ * @param {string} mode "twig", "jinja2" or "django".
564
+ * @param {boolean} [negate] The `not in` form.
565
+ * @return {boolean}
566
+ */
567
+ exports.isIn = function (needle, haystack, mode, negate) {
568
+ var found = membership(needle, haystack, mode === 'twig');
569
+ if (found === null) {
570
+ return negate === true && mode !== 'django';
571
+ }
572
+ return negate === true ? !found : found;
573
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.8.4",
3
+ "version": "2.9.1",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",