@rhinostone/swig-django 2.8.0 → 2.8.2

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
@@ -33,7 +33,7 @@ Renders real Django templates — everything below was cross-checked against Dja
33
33
  * **Variable resolution** — Django-faithful attribute / dictionary / index lookup. Callable attributes are auto-called (honoring `alters_data` / `do_not_call_in_templates`), numeric indexing works (`{{ list.0 }}`), and dicts iterate via `.keys` / `.values` / `.items`.
34
34
  * **Filter arguments** — Django colon syntax (`{{ value|date:"Y-m-d" }}`).
35
35
  * **Filters** — 42 built-ins (`add`, `addslashes`, `capfirst`, `center`, `cut`, `date`, `default`, `default_if_none`, `divisibleby`, `escape` / `e`, `escapejs`, `filesizeformat`, `first`, `floatformat`, `force_escape`, `get_digit`, `iriencode`, `join`, `last`, `length`, `linebreaks`, `linebreaksbr`, `linenumbers`, `ljust`, `lower`, `make_list`, `phone2numeric`, `pluralize`, `rjust`, `safe`, `slice`, `slugify`, `striptags`, `time`, `title`, `truncatechars`, `truncatewords`, `upper`, `urlencode`, `wordcount`, `wordwrap`, `yesno`). See `lib/filters.js` for the full list.
36
- * **Async** — `renderFileAsync` / `compileFileAsync` for async loaders, plus `renderFile(path, locals, cb)` against a loader with `async === true`.
36
+ * **Async** — `renderFile(path, locals, cb)` against a loader with `async === true`, or `compileFile` with `codegenMode: 'async'`; `renderFileAsync` / `compileFileAsync` still work but are deprecated.
37
37
 
38
38
  Explicitly unsupported (parse-time throw or absent)
39
39
  ---------------------------------------------------
package/lib/filters.js CHANGED
@@ -657,8 +657,18 @@ exports.iriencode = function (input) {
657
657
  return out;
658
658
  };
659
659
 
660
+ /*!
661
+ * Split a string into code points, keeping a surrogate pair together as
662
+ * Python's `str` iteration does. @private
663
+ */
664
+ function codePoints(s) {
665
+ return s.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || [];
666
+ }
667
+
660
668
  /**
661
- * Join a list with a string (Django's `join`).
669
+ * Join a list with a string (Django's `join`). A string joins its
670
+ * characters and a mapping its keys, as Python's `str.join` iterates them;
671
+ * any other input is returned unchanged.
662
672
  *
663
673
  * @example
664
674
  * {{ list|join:", " }} // list = ['a', 'b', 'c']
@@ -669,8 +679,15 @@ exports.iriencode = function (input) {
669
679
  * @return {*}
670
680
  */
671
681
  exports.join = function (input, glue) {
682
+ if (glue === undefined) { glue = ''; }
683
+ if (typeof input === 'string') {
684
+ return codePoints(input).join(glue);
685
+ }
672
686
  if (utils.isArray(input)) {
673
- return input.join(glue !== undefined ? glue : '');
687
+ return input.join(glue);
688
+ }
689
+ if (input && typeof input === 'object') {
690
+ return utils.keys(input).join(glue);
674
691
  }
675
692
  return input;
676
693
  };
@@ -861,10 +878,32 @@ exports.phone2numeric = function (input) {
861
878
  });
862
879
  };
863
880
 
881
+ /*!
882
+ * Parse a string as Python's `float()` does: surrounding whitespace, an
883
+ * optional sign, then digits (single underscores allowed between them)
884
+ * with an optional fraction and exponent, or inf / infinity / nan in any
885
+ * case. Returns undefined where Python raises ValueError. @private
886
+ */
887
+ function pyFloat(s) {
888
+ var t = s.replace(/^\s+|\s+$/g, '');
889
+ if (/^[+-]?(inf|infinity)$/i.test(t)) {
890
+ return (t.charAt(0) === '-') ? -Infinity : Infinity;
891
+ }
892
+ if (/^[+-]?nan$/i.test(t)) {
893
+ return NaN;
894
+ }
895
+ if (!/^[+-]?(\d(_?\d)*(\.(\d(_?\d)*)?)?|\.\d(_?\d)*)([eE][+-]?\d(_?\d)*)?$/.test(t)) {
896
+ return undefined;
897
+ }
898
+ return parseFloat(t.replace(/_/g, ''));
899
+ }
900
+
864
901
  /**
865
902
  * Return a plural suffix based on a count (Django's `pluralize`). With no
866
903
  * argument the suffix is `"s"`; a `"singular,plural"` argument supplies both
867
- * forms. The count may be a number or anything with a `length`.
904
+ * forms. As in Django, a number or boolean is compared with 1, a string must
905
+ * parse as a Python float (otherwise the result is the empty string), and a
906
+ * list or mapping counts its items; any other input gives the empty string.
868
907
  *
869
908
  * @example
870
909
  * message{{ 2|pluralize }}
@@ -894,13 +933,22 @@ exports.pluralize = function (input, arg) {
894
933
  singular = bits[0];
895
934
  plural = bits[1];
896
935
 
897
- n = parseFloat(input);
898
- if (typeof input === 'number' || (typeof input === 'string' && input.replace(/^\s+|\s+$/g, '') !== '' && !isNaN(n))) {
936
+ if (typeof input === 'number' || typeof input === 'boolean') {
937
+ return Number(input) === 1 ? singular : plural;
938
+ }
939
+ if (typeof input === 'string') {
940
+ n = pyFloat(input);
941
+ if (n === undefined) {
942
+ return '';
943
+ }
899
944
  return n === 1 ? singular : plural;
900
945
  }
901
- if (input !== null && input !== undefined && typeof input.length === 'number') {
946
+ if (utils.isArray(input)) {
902
947
  return input.length === 1 ? singular : plural;
903
948
  }
949
+ if (Object.prototype.toString.call(input) === '[object Object]') {
950
+ return utils.keys(input).length === 1 ? singular : plural;
951
+ }
904
952
  return '';
905
953
  };
906
954
 
@@ -1055,9 +1103,31 @@ exports.title = function (input) {
1055
1103
  });
1056
1104
  };
1057
1105
 
1106
+ /*!
1107
+ * Convert a count as Python's `int()` does: a number truncates toward
1108
+ * zero, a boolean gives 0 or 1, and a string must be an optionally signed
1109
+ * run of digits (single underscores allowed between them, surrounding
1110
+ * whitespace allowed). Returns NaN where Python raises. @private
1111
+ */
1112
+ function pyInt(arg) {
1113
+ if (typeof arg === 'number') {
1114
+ if (!isFinite(arg)) { return NaN; }
1115
+ return (arg < 0) ? Math.ceil(arg) : Math.floor(arg);
1116
+ }
1117
+ if (typeof arg === 'boolean') {
1118
+ return arg ? 1 : 0;
1119
+ }
1120
+ if (typeof arg === 'string' && /^\s*[+-]?\d(_?\d)*\s*$/.test(arg)) {
1121
+ return parseInt(arg.replace(/_/g, ''), 10);
1122
+ }
1123
+ return NaN;
1124
+ }
1125
+
1058
1126
  /**
1059
1127
  * Truncate the value to a number of characters, ending with an ellipsis
1060
1128
  * (Django's `truncatechars`). The ellipsis (`…`) counts toward the length.
1129
+ * A length of 0 or less gives the empty string; a length Python's `int()`
1130
+ * cannot read (such as `"3abc"` or `"2.5"`) returns the value unchanged.
1061
1131
  *
1062
1132
  * @example
1063
1133
  * {{ "Joel is a slug"|truncatechars:7 }}
@@ -1069,20 +1139,24 @@ exports.title = function (input) {
1069
1139
  */
1070
1140
  exports.truncatechars = function (input, arg) {
1071
1141
  var s = String(input),
1072
- n = parseInt(arg, 10);
1142
+ n = pyInt(arg);
1073
1143
  if (isNaN(n)) {
1074
1144
  return s;
1075
1145
  }
1146
+ if (n <= 0) {
1147
+ return '';
1148
+ }
1076
1149
  if (s.length <= n) {
1077
1150
  return s;
1078
1151
  }
1079
- return s.substr(0, n - 1 < 0 ? 0 : n - 1) + '…';
1152
+ return s.substr(0, n - 1) + '…';
1080
1153
  };
1081
1154
 
1082
1155
  /**
1083
1156
  * Truncate the value to a number of words, ending with a space + ellipsis
1084
1157
  * (Django's `truncatewords`). Whitespace runs within the kept portion are
1085
- * collapsed to single spaces.
1158
+ * collapsed to single spaces. A count of 0 or less gives the empty string;
1159
+ * a count Python's `int()` cannot read returns the value unchanged.
1086
1160
  *
1087
1161
  * @example
1088
1162
  * {{ "Joel is a slug"|truncatewords:2 }}
@@ -1093,11 +1167,14 @@ exports.truncatechars = function (input, arg) {
1093
1167
  * @return {string}
1094
1168
  */
1095
1169
  exports.truncatewords = function (input, arg) {
1096
- var n = parseInt(arg, 10),
1170
+ var n = pyInt(arg),
1097
1171
  words;
1098
1172
  if (isNaN(n)) {
1099
1173
  return String(input);
1100
1174
  }
1175
+ if (n <= 0) {
1176
+ return '';
1177
+ }
1101
1178
  words = String(input).replace(/^\s+|\s+$/g, '').split(/\s+/);
1102
1179
  if (words.length === 1 && words[0] === '') {
1103
1180
  return '';
package/lib/index.js CHANGED
@@ -24,6 +24,16 @@ var utils = require('@rhinostone/swig-core/lib/utils'),
24
24
 
25
25
  exports.name = 'django';
26
26
 
27
+ /**
28
+ * Package version number as a string. The packages of the family are
29
+ * released together, so they share one version.
30
+ * @example
31
+ * if (django.version === "2.8.2") { ... }
32
+ *
33
+ * @type {String}
34
+ */
35
+ exports.version = "2.8.2";
36
+
27
37
  /**
28
38
  * Expression-level parser — Pratt-style recursive descent that consumes
29
39
  * Django lexer tokens and returns swig-core IRExpr nodes, plus the top-level
@@ -81,7 +91,7 @@ function validateOptions(options) {
81
91
  if (!options.hasOwnProperty(key)) {
82
92
  return;
83
93
  }
84
- if (!utils.isArray(options[key]) || options[key].length !== 2) {
94
+ if (!utils.isArray(options[key]) || options[key].length !== 2 || typeof options[key][0] !== 'string' || typeof options[key][1] !== 'string') {
85
95
  throw new Error('Option "' + key + '" must be an array containing 2 different control strings.');
86
96
  }
87
97
  if (options[key][0] === options[key][1]) {
@@ -89,7 +99,7 @@ function validateOptions(options) {
89
99
  }
90
100
  utils.each(options[key], function (a, i) {
91
101
  if (a.length < 2) {
92
- throw new Error('Option "' + key + '" ' + ((i) ? 'open ' : 'close ') + 'control must be at least 2 characters. Saw "' + a + '" instead.');
102
+ throw new Error('Option "' + key + '" ' + ((i) ? 'close ' : 'open ') + 'control must be at least 2 characters. Saw "' + a + '" instead.');
93
103
  }
94
104
  });
95
105
  });
@@ -158,7 +168,14 @@ exports.Django = function (opts) {
158
168
  filters: _filters,
159
169
  validateOptions: validateOptions,
160
170
  onCompileError: function (err, options) {
161
- utils.throwError(err, null, options.filename);
171
+ var filename = options && options.filename;
172
+ // An error with no file name to add, or one whose message already
173
+ // names the file (a tag's own throwError does), is rethrown
174
+ // unchanged instead of being wrapped a second time.
175
+ if (err instanceof Error && (!filename || err.message.indexOf(' in file ' + filename) !== -1)) {
176
+ throw err;
177
+ }
178
+ utils.throwError(err, null, filename);
162
179
  }
163
180
  });
164
181
 
package/lib/parser.js CHANGED
@@ -115,12 +115,12 @@ exports.parseExpr = function (tokens, filters, _posOut) {
115
115
 
116
116
  function guardSegment(segment) {
117
117
  if (_dangerousProps.indexOf(segment) !== -1) {
118
- bail('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)');
118
+ bail('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)');
119
119
  }
120
120
  }
121
121
  function guardBracketString(value) {
122
122
  if (_dangerousProps.indexOf(value) !== -1) {
123
- bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (CVE-2023-25345)');
123
+ bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (prototype-chain guard)');
124
124
  }
125
125
  }
126
126
 
package/lib/tags/block.js CHANGED
@@ -66,7 +66,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
66
66
  utils.throwError('Block name "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
67
67
  }
68
68
  if (_dangerousProps.indexOf(nameTok.match) !== -1) {
69
- utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
69
+ utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
70
70
  }
71
71
 
72
72
  pos += 1;
package/lib/tags/for.js CHANGED
@@ -163,7 +163,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
163
163
  utils.throwError('Loop variable "' + tok.match + '" must be a bare identifier in "for" tag', line, opts.filename);
164
164
  }
165
165
  if (_dangerousProps.indexOf(tok.match) !== -1) {
166
- utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
166
+ utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
167
167
  }
168
168
  return tok.match;
169
169
  }
@@ -107,7 +107,7 @@ function parsePairs(clause, parser, types, line, opts) {
107
107
  utils.throwError('"with" assignment target "' + nameTok.match + '" must be a bare identifier in "include" tag', line, opts.filename);
108
108
  }
109
109
  if (_dangerousProps.indexOf(nameTok.match) !== -1) {
110
- utils.throwError('Unsafe "with" assignment to "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
110
+ utils.throwError('Unsafe "with" assignment to "' + nameTok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
111
111
  }
112
112
  pos = j + 1;
113
113
  skipWS();
package/lib/tags/with.js CHANGED
@@ -59,7 +59,7 @@ function guardName(name, line, opts) {
59
59
  utils.throwError('"with" assignment target "' + name + '" must be a bare identifier', line, opts.filename);
60
60
  }
61
61
  if (_dangerousProps.indexOf(name) !== -1) {
62
- utils.throwError('Unsafe "with" assignment to "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
62
+ utils.throwError('Unsafe "with" assignment to "' + name + '" is not allowed (prototype-chain guard)', line, opts.filename);
63
63
  }
64
64
  }
65
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-django",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "Django-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
5
5
  "keywords": [
6
6
  "template",
@@ -22,7 +22,7 @@
22
22
  "node": ">=12"
23
23
  },
24
24
  "peerDependencies": {
25
- "@rhinostone/swig-core": "2.8.0"
25
+ "@rhinostone/swig-core": "2.8.2"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"