@rhinostone/swig-jinja2 2.8.1 → 2.8.3
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 +1 -1
- package/lib/filters.js +128 -53
- package/lib/index.js +20 -3
- package/lib/parser.js +118 -75
- package/lib/tags/block.js +1 -1
- package/lib/tags/filter.js +1 -1
- package/lib/tags/for.js +1 -1
- package/lib/tags/from.js +1 -1
- package/lib/tags/import.js +1 -1
- package/lib/tags/include.js +1 -1
- package/lib/tags/macro.js +1 -1
- package/lib/tags/set.js +6 -5
- package/lib/tags/with.js +1 -1
- package/lib/tests/index.js +43 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ A near-subset of Python Jinja2 — everything below was cross-checked against Ji
|
|
|
33
33
|
* **Operators** — `**` power, `//` floor-division, `~` string concat, inline-if (`a if c else b`), Python slicing (`seq[start:stop:step]`), `is <test>` / `is not <test>`, plus `{{- … -}}` / `{%- … -%}` whitespace control.
|
|
34
34
|
* **Built-in `is` tests** — `defined`, `undefined`, `none`, `even`, `odd`, `divisibleby`, `iterable`, `mapping`, `sequence`, `string`, `number`, `boolean`, `callable`, `lower`, `upper`, `sameas`.
|
|
35
35
|
* **Filters** — 39 built-ins (`upper`, `lower`, `capitalize`, `title`, `trim`, `truncate`, `replace`, `striptags`, `format`, `wordcount`, `wordwrap`, `indent`, `center`, `urlencode`, `escape` / `e`, `safe`, `first`, `last`, `join`, `reverse`, `sort`, `length` / `count`, `list`, `unique`, `batch`, `slice`, `dictsort`, `groupby`, `min`, `max`, `sum`, `random`, `abs`, `round`, `int`, `float`, `default` / `d`, `tojson`, `date`). See `lib/filters.js` for the full list.
|
|
36
|
-
* **Async** — `renderFileAsync` / `compileFileAsync`
|
|
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
|
@@ -177,7 +177,7 @@ exports.count = exports.length;
|
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
179
|
* Return the first item of an array, the first character of a string, or
|
|
180
|
-
* the first
|
|
180
|
+
* the first key of a mapping (Jinja2 iterates a mapping's keys).
|
|
181
181
|
*
|
|
182
182
|
* @example
|
|
183
183
|
* {{ ["a", "b", "c"]|first }}
|
|
@@ -188,8 +188,7 @@ exports.count = exports.length;
|
|
|
188
188
|
*/
|
|
189
189
|
exports.first = function (input) {
|
|
190
190
|
if (typeof input === 'object' && input !== null && !utils.isArray(input)) {
|
|
191
|
-
|
|
192
|
-
return input[keys[0]];
|
|
191
|
+
return utils.keys(input)[0];
|
|
193
192
|
}
|
|
194
193
|
if (typeof input === 'string') {
|
|
195
194
|
return input.substr(0, 1);
|
|
@@ -202,7 +201,7 @@ exports.first = function (input) {
|
|
|
202
201
|
|
|
203
202
|
/**
|
|
204
203
|
* Return the last item of an array, the last character of a string, or
|
|
205
|
-
* the last
|
|
204
|
+
* the last key of a mapping (Jinja2 iterates a mapping's keys).
|
|
206
205
|
*
|
|
207
206
|
* @example
|
|
208
207
|
* {{ ["a", "b", "c"]|last }}
|
|
@@ -214,7 +213,7 @@ exports.first = function (input) {
|
|
|
214
213
|
exports.last = function (input) {
|
|
215
214
|
if (typeof input === 'object' && input !== null && !utils.isArray(input)) {
|
|
216
215
|
var keys = utils.keys(input);
|
|
217
|
-
return
|
|
216
|
+
return keys[keys.length - 1];
|
|
218
217
|
}
|
|
219
218
|
if (typeof input === 'string') {
|
|
220
219
|
return input.charAt(input.length - 1);
|
|
@@ -254,13 +253,13 @@ exports.join = function (input, glue) {
|
|
|
254
253
|
|
|
255
254
|
/**
|
|
256
255
|
* Reverse an array or string. Does not sort — items come out in reverse
|
|
257
|
-
* input order.
|
|
256
|
+
* input order. A mapping gives its keys in reverse order.
|
|
258
257
|
*
|
|
259
258
|
* @example
|
|
260
259
|
* {{ [1, 2, 3]|reverse|join(",") }}
|
|
261
260
|
* // => 3,2,1
|
|
262
261
|
*
|
|
263
|
-
* @param {array|string} input
|
|
262
|
+
* @param {array|string|object} input
|
|
264
263
|
* @return {array|string}
|
|
265
264
|
*/
|
|
266
265
|
exports.reverse = function (input) {
|
|
@@ -270,14 +269,17 @@ exports.reverse = function (input) {
|
|
|
270
269
|
if (typeof input === 'string') {
|
|
271
270
|
return input.split('').reverse().join('');
|
|
272
271
|
}
|
|
272
|
+
if (input && typeof input === 'object') {
|
|
273
|
+
return utils.keys(input).reverse();
|
|
274
|
+
}
|
|
273
275
|
return input;
|
|
274
276
|
};
|
|
275
277
|
|
|
276
278
|
/**
|
|
277
279
|
* Sort an array ascending, returning a copy (does not mutate the input).
|
|
278
280
|
* Numbers sort numerically; everything else compares case-insensitively.
|
|
279
|
-
* A string
|
|
280
|
-
* first argument to sort descending.
|
|
281
|
+
* A string gives its characters as a sorted list, as in Jinja2; a mapping
|
|
282
|
+
* gives its sorted keys. Pass a truthy first argument to sort descending.
|
|
281
283
|
*
|
|
282
284
|
* @example
|
|
283
285
|
* {{ [3, 1, 2]|sort|join(",") }}
|
|
@@ -292,12 +294,11 @@ exports.reverse = function (input) {
|
|
|
292
294
|
* @return {*}
|
|
293
295
|
*/
|
|
294
296
|
exports.sort = function (input, reverse) {
|
|
295
|
-
var arr
|
|
297
|
+
var arr;
|
|
296
298
|
if (utils.isArray(input)) {
|
|
297
299
|
arr = utils.extend([], input);
|
|
298
300
|
} else if (typeof input === 'string') {
|
|
299
301
|
arr = input.split('');
|
|
300
|
-
isString = true;
|
|
301
302
|
} else if (input && typeof input === 'object') {
|
|
302
303
|
arr = utils.keys(input);
|
|
303
304
|
} else {
|
|
@@ -309,9 +310,17 @@ exports.sort = function (input, reverse) {
|
|
|
309
310
|
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
310
311
|
});
|
|
311
312
|
if (reverse) { arr.reverse(); }
|
|
312
|
-
return
|
|
313
|
+
return arr;
|
|
313
314
|
};
|
|
314
315
|
|
|
316
|
+
/*!
|
|
317
|
+
* Split a string into code points, keeping a surrogate pair together as
|
|
318
|
+
* Python's `str` iteration does. @private
|
|
319
|
+
*/
|
|
320
|
+
function codePoints(s) {
|
|
321
|
+
return s.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || [];
|
|
322
|
+
}
|
|
323
|
+
|
|
315
324
|
/*!
|
|
316
325
|
* Resolve a possibly-dotted attribute path against an object. Returns
|
|
317
326
|
* undefined if any segment is missing. @private
|
|
@@ -645,7 +654,10 @@ exports.trim = function (input, chars) {
|
|
|
645
654
|
* Replace occurrences of a literal substring with another. Unlike the
|
|
646
655
|
* native swig `replace` (regex) and the Twig `replace` (mapping), Jinja2's
|
|
647
656
|
* `replace` is a plain left-to-right literal substitution. An optional
|
|
648
|
-
* count limits how many occurrences are replaced.
|
|
657
|
+
* count limits how many occurrences are replaced. As in Python, an empty
|
|
658
|
+
* search string inserts the replacement before each character and at the
|
|
659
|
+
* end, a negative count replaces every occurrence, and a number input is
|
|
660
|
+
* converted to a string first.
|
|
649
661
|
*
|
|
650
662
|
* @example
|
|
651
663
|
* {{ "Hello World"|replace("Hello", "Goodbye") }}
|
|
@@ -662,20 +674,36 @@ exports.trim = function (input, chars) {
|
|
|
662
674
|
* @return {*}
|
|
663
675
|
*/
|
|
664
676
|
exports.replace = function (input, old, neu, count) {
|
|
665
|
-
var
|
|
677
|
+
var max, out, chars, i, n;
|
|
678
|
+
if (typeof input === 'number') {
|
|
679
|
+
input = String(input);
|
|
680
|
+
}
|
|
666
681
|
if (typeof input !== 'string') {
|
|
667
682
|
return input;
|
|
668
683
|
}
|
|
669
684
|
old = String(old);
|
|
670
685
|
neu = (neu === undefined || neu === null) ? '' : String(neu);
|
|
671
|
-
|
|
672
|
-
|
|
686
|
+
max = (count !== undefined && count !== null) ? Number(count) : Infinity;
|
|
687
|
+
if (max < 0) {
|
|
688
|
+
max = Infinity;
|
|
673
689
|
}
|
|
674
|
-
limited = (count !== undefined && count !== null);
|
|
675
|
-
max = limited ? Number(count) : Infinity;
|
|
676
690
|
out = '';
|
|
677
|
-
i = 0;
|
|
678
691
|
n = 0;
|
|
692
|
+
if (old === '') {
|
|
693
|
+
chars = codePoints(input);
|
|
694
|
+
for (i = 0; i < chars.length; i += 1) {
|
|
695
|
+
if (n < max) {
|
|
696
|
+
out += neu;
|
|
697
|
+
n += 1;
|
|
698
|
+
}
|
|
699
|
+
out += chars[i];
|
|
700
|
+
}
|
|
701
|
+
if (n < max) {
|
|
702
|
+
out += neu;
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
i = 0;
|
|
679
707
|
while (i < input.length) {
|
|
680
708
|
if (n < max && input.substr(i, old.length) === old) {
|
|
681
709
|
out += neu;
|
|
@@ -689,6 +717,35 @@ exports.replace = function (input, old, neu, count) {
|
|
|
689
717
|
return out;
|
|
690
718
|
};
|
|
691
719
|
|
|
720
|
+
/*!
|
|
721
|
+
* Pad the exponent of a JavaScript exponential string to at least two
|
|
722
|
+
* digits, as Python's `%e` prints it (`1e+4` becomes `1e+04`). @private
|
|
723
|
+
*/
|
|
724
|
+
function padExponent(s) {
|
|
725
|
+
return s.replace(/e([+-])(\d)$/, function (m, sign, digit) {
|
|
726
|
+
return 'e' + sign + '0' + digit;
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/*!
|
|
731
|
+
* Python's `%g` for a non-negative number: precision `prec` (0 counts as
|
|
732
|
+
* 1) significant digits, fixed notation when the decimal exponent X of
|
|
733
|
+
* the rounded value satisfies -4 <= X < precision and exponential
|
|
734
|
+
* notation otherwise, then trailing zeros and a bare point removed from
|
|
735
|
+
* the fractional part. @private
|
|
736
|
+
*/
|
|
737
|
+
function formatGeneral(num, prec) {
|
|
738
|
+
var p = (prec === 0) ? 1 : prec, digits = Math.min(p - 1, 100), exp, parts;
|
|
739
|
+
// toExponential and toFixed take at most 100 digits; for most numbers the
|
|
740
|
+
// digits past those are zeros, which are stripped below anyway.
|
|
741
|
+
exp = parseInt(num.toExponential(digits).split('e')[1], 10);
|
|
742
|
+
parts = ((exp >= -4 && exp < p) ? num.toFixed(Math.min(p - 1 - exp, 100)) : num.toExponential(digits)).split('e');
|
|
743
|
+
if (parts[0].indexOf('.') !== -1) {
|
|
744
|
+
parts[0] = parts[0].replace(/0+$/, '').replace(/\.$/, '');
|
|
745
|
+
}
|
|
746
|
+
return (parts.length > 1) ? padExponent(parts[0] + 'e' + parts[1]) : parts[0];
|
|
747
|
+
}
|
|
748
|
+
|
|
692
749
|
/*!
|
|
693
750
|
* printf-style format expansion backing the `format` filter. Supports the
|
|
694
751
|
* common conversions (s d i u f F e E g G x X o c %) with optional flags
|
|
@@ -738,14 +795,14 @@ function sprintf(fmt, args) {
|
|
|
738
795
|
case 'E':
|
|
739
796
|
num = parseFloat(arg);
|
|
740
797
|
if (isNaN(num)) { num = 0; }
|
|
741
|
-
s = withSign(num).toExponential(hasPrec ? prec : 6);
|
|
798
|
+
s = padExponent(withSign(num).toExponential(hasPrec ? prec : 6));
|
|
742
799
|
if (conv === 'E') { s = s.toUpperCase(); }
|
|
743
800
|
break;
|
|
744
801
|
case 'g':
|
|
745
802
|
case 'G':
|
|
746
803
|
num = parseFloat(arg);
|
|
747
804
|
if (isNaN(num)) { num = 0; }
|
|
748
|
-
s =
|
|
805
|
+
s = formatGeneral(withSign(num), hasPrec ? prec : 6);
|
|
749
806
|
if (conv === 'G') { s = s.toUpperCase(); }
|
|
750
807
|
break;
|
|
751
808
|
case 'x':
|
|
@@ -825,7 +882,9 @@ exports.wordcount = function (input) {
|
|
|
825
882
|
* Word-wrap a string to a given line width (Jinja2's `wordwrap`). Words are
|
|
826
883
|
* packed greedily; a word longer than the width is split when
|
|
827
884
|
* `breakLongWords` is truthy (the default). Lines are joined with
|
|
828
|
-
* `wrapstring` (default newline).
|
|
885
|
+
* `wrapstring` (default newline). A width of 0 or less returns the input
|
|
886
|
+
* unchanged (Jinja2 raises), and a width between 0 and 1 breaks one
|
|
887
|
+
* character at a time, as Python's textwrap does.
|
|
829
888
|
*
|
|
830
889
|
* @example
|
|
831
890
|
* {{ "the quick brown fox"|wordwrap(10) }}
|
|
@@ -843,6 +902,12 @@ exports.wordwrap = function (input, width, breakLongWords, wrapstring) {
|
|
|
843
902
|
return input;
|
|
844
903
|
}
|
|
845
904
|
if (width === undefined || width === null) { width = 79; }
|
|
905
|
+
if (width <= 0) {
|
|
906
|
+
return input;
|
|
907
|
+
}
|
|
908
|
+
if (width < 1) {
|
|
909
|
+
width = 1;
|
|
910
|
+
}
|
|
846
911
|
if (breakLongWords === undefined) { breakLongWords = true; }
|
|
847
912
|
if (wrapstring === undefined || wrapstring === null) { wrapstring = '\n'; }
|
|
848
913
|
words = input.split(/\s+/);
|
|
@@ -854,7 +919,9 @@ exports.wordwrap = function (input, width, breakLongWords, wrapstring) {
|
|
|
854
919
|
while (breakLongWords && word.length > width) {
|
|
855
920
|
space = (cur === '') ? width : width - cur.length - 1;
|
|
856
921
|
if (space <= 0) {
|
|
857
|
-
|
|
922
|
+
// Python's textwrap keeps the separating space when the line is
|
|
923
|
+
// exactly full.
|
|
924
|
+
lines.push(space === 0 ? cur + ' ' : cur);
|
|
858
925
|
cur = '';
|
|
859
926
|
space = width;
|
|
860
927
|
}
|
|
@@ -974,7 +1041,8 @@ exports.list = function (input) {
|
|
|
974
1041
|
/**
|
|
975
1042
|
* Return the unique items of a sequence, preserving first-seen order
|
|
976
1043
|
* (Jinja2's `unique`). String comparison is case-insensitive by default;
|
|
977
|
-
* pass a truthy `caseSensitive` to compare exactly.
|
|
1044
|
+
* pass a truthy `caseSensitive` to compare exactly. A mapping gives its
|
|
1045
|
+
* unique keys.
|
|
978
1046
|
*
|
|
979
1047
|
* @example
|
|
980
1048
|
* {{ [1, 2, 2, 3, 1]|unique|join(",") }}
|
|
@@ -988,6 +1056,8 @@ exports.unique = function (input, caseSensitive) {
|
|
|
988
1056
|
var seen = [], out = [];
|
|
989
1057
|
if (typeof input === 'string') {
|
|
990
1058
|
input = input.split('');
|
|
1059
|
+
} else if (input && typeof input === 'object' && !utils.isArray(input)) {
|
|
1060
|
+
input = utils.keys(input);
|
|
991
1061
|
} else if (!utils.isArray(input)) {
|
|
992
1062
|
return input;
|
|
993
1063
|
}
|
|
@@ -1002,9 +1072,13 @@ exports.unique = function (input, caseSensitive) {
|
|
|
1002
1072
|
};
|
|
1003
1073
|
|
|
1004
1074
|
/**
|
|
1005
|
-
* Batch items into rows of `size` (Jinja2's `batch`).
|
|
1075
|
+
* Batch items into rows of `size` (Jinja2's `batch`). A new row starts
|
|
1076
|
+
* when the current one holds exactly `size` items, so the last row holds
|
|
1006
1077
|
* the remainder; an optional `fill` pads the last row up to `size`. A
|
|
1007
|
-
*
|
|
1078
|
+
* string batches its characters and a mapping its keys; any other input
|
|
1079
|
+
* that is not a list gives an empty list. As in Jinja2, a size of 0 gives
|
|
1080
|
+
* an empty first row, and a size that no row length can equal (negative,
|
|
1081
|
+
* fractional or not a number) gives a single row.
|
|
1008
1082
|
*
|
|
1009
1083
|
* @example
|
|
1010
1084
|
* {% for row in [1,2,3,4,5]|batch(2) %}[{{ row|join(",") }}]{% endfor %}
|
|
@@ -1016,31 +1090,32 @@ exports.unique = function (input, caseSensitive) {
|
|
|
1016
1090
|
* @return {Array}
|
|
1017
1091
|
*/
|
|
1018
1092
|
exports.batch = function (input, size, fill) {
|
|
1019
|
-
var items,
|
|
1093
|
+
var items, out = [], tmp = [], i;
|
|
1020
1094
|
if (utils.isArray(input)) {
|
|
1021
1095
|
items = input;
|
|
1096
|
+
} else if (typeof input === 'string') {
|
|
1097
|
+
items = codePoints(input);
|
|
1022
1098
|
} else if (input && typeof input === 'object') {
|
|
1023
|
-
items =
|
|
1024
|
-
utils.each(input, function (v) { items.push(v); });
|
|
1099
|
+
items = utils.keys(input);
|
|
1025
1100
|
} else {
|
|
1026
1101
|
return [];
|
|
1027
1102
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
n = Math.ceil(n);
|
|
1033
|
-
out = [];
|
|
1034
|
-
i = 0;
|
|
1035
|
-
while (i < items.length) {
|
|
1036
|
-
out.push(items.slice(i, i + n));
|
|
1037
|
-
i += n;
|
|
1038
|
-
}
|
|
1039
|
-
if (fill !== undefined && out.length > 0) {
|
|
1040
|
-
last = out[out.length - 1];
|
|
1041
|
-
while (last.length < n) {
|
|
1042
|
-
last.push(fill);
|
|
1103
|
+
for (i = 0; i < items.length; i += 1) {
|
|
1104
|
+
if (tmp.length === size) {
|
|
1105
|
+
out.push(tmp);
|
|
1106
|
+
tmp = [];
|
|
1043
1107
|
}
|
|
1108
|
+
tmp.push(items[i]);
|
|
1109
|
+
}
|
|
1110
|
+
if (tmp.length) {
|
|
1111
|
+
// Pad only up to a finite numeric size, since a non-finite one would
|
|
1112
|
+
// never end the loop (Jinja2 raises unless the size is an integer).
|
|
1113
|
+
if (fill !== undefined && typeof size === 'number' && isFinite(size)) {
|
|
1114
|
+
while (tmp.length < size) {
|
|
1115
|
+
tmp.push(fill);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
out.push(tmp);
|
|
1044
1119
|
}
|
|
1045
1120
|
return out;
|
|
1046
1121
|
};
|
|
@@ -1049,7 +1124,8 @@ exports.batch = function (input, size, fill) {
|
|
|
1049
1124
|
* Distribute items across `slices` columns (Jinja2's `slice` filter — the
|
|
1050
1125
|
* inverse of `batch`, and distinct from the `seq[start:stop:step]`
|
|
1051
1126
|
* subscript). The first columns receive the extra items when the count
|
|
1052
|
-
* does not divide evenly; an optional `fill` pads the shorter columns.
|
|
1127
|
+
* does not divide evenly; an optional `fill` pads the shorter columns. A
|
|
1128
|
+
* string distributes its characters and a mapping its keys.
|
|
1053
1129
|
*
|
|
1054
1130
|
* @example
|
|
1055
1131
|
* {% for col in [1,2,3,4,5,6,7,8,9,10]|slice(3) %}[{{ col|join(",") }}]{% endfor %}
|
|
@@ -1067,8 +1143,7 @@ exports.slice = function (input, slices, fill) {
|
|
|
1067
1143
|
} else if (typeof input === 'string') {
|
|
1068
1144
|
seq = input.split('');
|
|
1069
1145
|
} else if (input && typeof input === 'object') {
|
|
1070
|
-
seq =
|
|
1071
|
-
utils.each(input, function (v) { seq.push(v); });
|
|
1146
|
+
seq = utils.keys(input);
|
|
1072
1147
|
} else {
|
|
1073
1148
|
return [];
|
|
1074
1149
|
}
|
|
@@ -1186,13 +1261,11 @@ exports.sum = function (input, attribute, start) {
|
|
|
1186
1261
|
* numerically. @private
|
|
1187
1262
|
*/
|
|
1188
1263
|
function minmax(input, caseSensitive, want) {
|
|
1189
|
-
var best, found = false
|
|
1264
|
+
var best, found = false;
|
|
1190
1265
|
if (typeof input === 'string') {
|
|
1191
1266
|
input = input.split('');
|
|
1192
1267
|
} else if (input && typeof input === 'object' && !utils.isArray(input)) {
|
|
1193
|
-
|
|
1194
|
-
utils.each(input, function (v) { vals.push(v); });
|
|
1195
|
-
input = vals;
|
|
1268
|
+
input = utils.keys(input);
|
|
1196
1269
|
} else if (!utils.isArray(input)) {
|
|
1197
1270
|
return input;
|
|
1198
1271
|
}
|
|
@@ -1219,7 +1292,8 @@ function minmax(input, caseSensitive, want) {
|
|
|
1219
1292
|
|
|
1220
1293
|
/**
|
|
1221
1294
|
* Return the smallest item of a sequence (Jinja2's `min`). String
|
|
1222
|
-
* comparison is case-insensitive unless `caseSensitive` is truthy.
|
|
1295
|
+
* comparison is case-insensitive unless `caseSensitive` is truthy. A
|
|
1296
|
+
* mapping compares its keys.
|
|
1223
1297
|
*
|
|
1224
1298
|
* @example
|
|
1225
1299
|
* {{ [3, 1, 2]|min }}
|
|
@@ -1235,7 +1309,8 @@ exports.min = function (input, caseSensitive) {
|
|
|
1235
1309
|
|
|
1236
1310
|
/**
|
|
1237
1311
|
* Return the largest item of a sequence (Jinja2's `max`). String
|
|
1238
|
-
* comparison is case-insensitive unless `caseSensitive` is truthy.
|
|
1312
|
+
* comparison is case-insensitive unless `caseSensitive` is truthy. A
|
|
1313
|
+
* mapping compares its keys.
|
|
1239
1314
|
*
|
|
1240
1315
|
* @example
|
|
1241
1316
|
* {{ [3, 1, 2]|max }}
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,16 @@ var utils = require('@rhinostone/swig-core/lib/utils'),
|
|
|
19
19
|
|
|
20
20
|
exports.name = 'jinja2';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Package version number as a string. The packages of the family are
|
|
24
|
+
* released together, so they share one version.
|
|
25
|
+
* @example
|
|
26
|
+
* if (jinja2.version === "2.8.2") { ... }
|
|
27
|
+
*
|
|
28
|
+
* @type {String}
|
|
29
|
+
*/
|
|
30
|
+
exports.version = "2.8.3";
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Expression-level parser — Pratt-style recursive descent that consumes
|
|
24
34
|
* Jinja2 lexer tokens and returns swig-core IRExpr nodes, plus the
|
|
@@ -76,7 +86,7 @@ function validateOptions(options) {
|
|
|
76
86
|
if (!options.hasOwnProperty(key)) {
|
|
77
87
|
return;
|
|
78
88
|
}
|
|
79
|
-
if (!utils.isArray(options[key]) || options[key].length !== 2) {
|
|
89
|
+
if (!utils.isArray(options[key]) || options[key].length !== 2 || typeof options[key][0] !== 'string' || typeof options[key][1] !== 'string') {
|
|
80
90
|
throw new Error('Option "' + key + '" must be an array containing 2 different control strings.');
|
|
81
91
|
}
|
|
82
92
|
if (options[key][0] === options[key][1]) {
|
|
@@ -84,7 +94,7 @@ function validateOptions(options) {
|
|
|
84
94
|
}
|
|
85
95
|
utils.each(options[key], function (a, i) {
|
|
86
96
|
if (a.length < 2) {
|
|
87
|
-
throw new Error('Option "' + key + '" ' + ((i) ? '
|
|
97
|
+
throw new Error('Option "' + key + '" ' + ((i) ? 'close ' : 'open ') + 'control must be at least 2 characters. Saw "' + a + '" instead.');
|
|
88
98
|
}
|
|
89
99
|
});
|
|
90
100
|
});
|
|
@@ -152,7 +162,14 @@ exports.Jinja2 = function (opts) {
|
|
|
152
162
|
filters: _filters,
|
|
153
163
|
validateOptions: validateOptions,
|
|
154
164
|
onCompileError: function (err, options) {
|
|
155
|
-
|
|
165
|
+
var filename = options && options.filename;
|
|
166
|
+
// An error with no file name to add, or one whose message already
|
|
167
|
+
// names the file (a tag's own throwError does), is rethrown
|
|
168
|
+
// unchanged instead of being wrapped a second time.
|
|
169
|
+
if (err instanceof Error && (!filename || err.message.indexOf(' in file ' + filename) !== -1)) {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
utils.throwError(err, null, filename);
|
|
156
173
|
}
|
|
157
174
|
});
|
|
158
175
|
|
package/lib/parser.js
CHANGED
|
@@ -34,24 +34,31 @@ var _reserved = ['break', 'case', 'catch', 'continue', 'debugger', 'default', 'd
|
|
|
34
34
|
* FUNCTION/FUNCTIONEMPTY callee names — same checkpoints as the native
|
|
35
35
|
* frontend.
|
|
36
36
|
*
|
|
37
|
-
* Binding
|
|
37
|
+
* Binding levels, loosest first, following Jinja2's parser (parse_condexpr,
|
|
38
|
+
* parse_or, parse_and, parse_not, parse_compare ... parse_pow,
|
|
39
|
+
* parse_unary, parse_postfix, parse_filter_expr):
|
|
38
40
|
*
|
|
39
|
-
* Level
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
41
|
+
* Level | Tokens | Assoc
|
|
42
|
+
* -------+------------------------------------------+--------
|
|
43
|
+
* 0 | inline `a if c else b` | right
|
|
44
|
+
* 1 | || / or (LOGIC) | left
|
|
45
|
+
* 2 | && / and (LOGIC) | left
|
|
46
|
+
* not | NOT starting an operand of the levels | prefix
|
|
47
|
+
* | above; its operand runs at level 3 |
|
|
48
|
+
* 3 | == != === !== (COMPARATOR) | left
|
|
49
|
+
* 4 | < > <= >= in (COMPARATOR) | left
|
|
50
|
+
* 6 | + - (OPERATOR) | left
|
|
51
|
+
* 7 | ~ (TILDE) | left
|
|
52
|
+
* 8 | * / % (OPERATOR), // (FLOORDIV) | left
|
|
53
|
+
* pow | ** (POWER), over unary operands | left
|
|
54
|
+
* filter | FILTER FILTEREMPTY, IS ISNOT tests | postfix
|
|
55
|
+
* unary | unary + / - | prefix
|
|
56
|
+
* post | DOTKEY BRACKETOPEN PARENOPEN | postfix
|
|
50
57
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
58
|
+
* So `not a == b` is not (a == b), `1 + 2 is number` tests 2, `-x|abs` is
|
|
59
|
+
* abs(-x) and `2 ** 3 ** 2` is (2 ** 3) ** 2. A NOT in any other operand
|
|
60
|
+
* position (`a == not b`) binds its unary operand; Jinja2 rejects that
|
|
61
|
+
* form.
|
|
55
62
|
*
|
|
56
63
|
* @param {object[]} tokens LexerToken[] from swig-jinja2's lexer.
|
|
57
64
|
* @param {object} [filters] Filter catalog for name validation.
|
|
@@ -83,12 +90,12 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
83
90
|
|
|
84
91
|
function guardSegment(segment) {
|
|
85
92
|
if (_dangerousProps.indexOf(segment) !== -1) {
|
|
86
|
-
bail('Unsafe access to "' + segment + '" is not allowed in templates (
|
|
93
|
+
bail('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)');
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
function guardBracketString(value) {
|
|
90
97
|
if (_dangerousProps.indexOf(value) !== -1) {
|
|
91
|
-
bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (
|
|
98
|
+
bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (prototype-chain guard)');
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -105,12 +112,6 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
105
112
|
}
|
|
106
113
|
return { op: m, prec: 4 };
|
|
107
114
|
}
|
|
108
|
-
if (tok.type === _t.IS) {
|
|
109
|
-
return { op: 'is', prec: 3 };
|
|
110
|
-
}
|
|
111
|
-
if (tok.type === _t.ISNOT) {
|
|
112
|
-
return { op: 'is not', prec: 3 };
|
|
113
|
-
}
|
|
114
115
|
if (tok.type === _t.OPERATOR) {
|
|
115
116
|
m = tok.match;
|
|
116
117
|
if (m === '+' || m === '-') { return { op: m, prec: 6 }; }
|
|
@@ -182,7 +183,7 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
function parseTest() {
|
|
185
|
-
var nameTok = consume();
|
|
186
|
+
var nameTok = consume(), pk;
|
|
186
187
|
if (!nameTok) { bail('Expected test name after "is" / "is not"'); }
|
|
187
188
|
var testName;
|
|
188
189
|
var testArgs = [];
|
|
@@ -191,6 +192,13 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
191
192
|
bail('Dotted names are not valid test names');
|
|
192
193
|
}
|
|
193
194
|
testName = nameTok.match;
|
|
195
|
+
pk = peek();
|
|
196
|
+
if (pk && pk.type === _t.PARENOPEN) {
|
|
197
|
+
consume();
|
|
198
|
+
testArgs = parseArgList(_t.PARENCLOSE);
|
|
199
|
+
} else if (pk && isBareTestArgStart(pk)) {
|
|
200
|
+
testArgs = [parsePostfix(parsePrimary())];
|
|
201
|
+
}
|
|
194
202
|
} else if (nameTok.type === _t.FUNCTIONEMPTY) {
|
|
195
203
|
testName = nameTok.match;
|
|
196
204
|
} else if (nameTok.type === _t.FUNCTION) {
|
|
@@ -206,6 +214,40 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
206
214
|
return { name: testName, args: testArgs };
|
|
207
215
|
}
|
|
208
216
|
|
|
217
|
+
// Jinja2's parse_test takes one argument without parentheses when the
|
|
218
|
+
// next token starts a primary (`x is divisibleby 3`, `x is sameas y`);
|
|
219
|
+
// the argument is a primary with its subscripts and calls, no filter or
|
|
220
|
+
// test. `else` ends an inline-if, and `if` is excluded too, so
|
|
221
|
+
// `x is defined if c else d` keeps parsing as an inline-if (Jinja2
|
|
222
|
+
// rejects it).
|
|
223
|
+
function isBareTestArgStart(tok) {
|
|
224
|
+
if (tok.type === _t.VAR) {
|
|
225
|
+
return tok.match !== 'if' && tok.match !== 'else';
|
|
226
|
+
}
|
|
227
|
+
return tok.type === _t.STRING || tok.type === _t.NUMBER || tok.type === _t.BOOL ||
|
|
228
|
+
tok.type === _t.FUNCTION || tok.type === _t.FUNCTIONEMPTY ||
|
|
229
|
+
tok.type === _t.BRACKETOPEN || tok.type === _t.CURLYOPEN;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Lower `<subject> is [not] <test>` to `_ext._test_<name>(subject,
|
|
233
|
+
// ...args)`; `is not` wraps the call in a unary `!`. `defined` / `none` /
|
|
234
|
+
// `undefined` on a VarRef subject route through IRVarRefExists instead,
|
|
235
|
+
// because emitVarRef coerces a missing or null lookup to "" and so loses
|
|
236
|
+
// the defined/undefined signal those tests depend on. Other subjects
|
|
237
|
+
// evaluate to a concrete value (no coercion) and use the generic helper
|
|
238
|
+
// the engine registers.
|
|
239
|
+
function lowerTest(subject, negated) {
|
|
240
|
+
var test = parseTest(), testCall;
|
|
241
|
+
if (test.args.length === 0 && subject.type === 'VarRef' && test.name === 'defined') {
|
|
242
|
+
testCall = ir.varRefExists(subject.path, subject.loc);
|
|
243
|
+
} else if (test.args.length === 0 && subject.type === 'VarRef' && (test.name === 'none' || test.name === 'undefined')) {
|
|
244
|
+
testCall = ir.unaryOp('!', ir.varRefExists(subject.path, subject.loc));
|
|
245
|
+
} else {
|
|
246
|
+
testCall = ir.fnCall(ir.varRef(['_ext', '_test_' + test.name]), [subject].concat(test.args));
|
|
247
|
+
}
|
|
248
|
+
return negated ? ir.unaryOp('!', testCall) : testCall;
|
|
249
|
+
}
|
|
250
|
+
|
|
209
251
|
function expectBracketClose() {
|
|
210
252
|
var close = consume();
|
|
211
253
|
if (!close || close.type !== _t.BRACKETCLOSE) {
|
|
@@ -268,7 +310,11 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
268
310
|
]);
|
|
269
311
|
}
|
|
270
312
|
|
|
271
|
-
|
|
313
|
+
// Subscripts and calls, then (when `withFilter`) filters and tests, as in
|
|
314
|
+
// Jinja2's parse_postfix and parse_filter_expr. A unary minus/plus
|
|
315
|
+
// operand is read without them (Jinja2's parse_unary(False)); they then
|
|
316
|
+
// apply to the negated value.
|
|
317
|
+
function parsePostfix(expr, withFilter) {
|
|
272
318
|
while (true) {
|
|
273
319
|
var tok = peek();
|
|
274
320
|
if (!tok) { break; }
|
|
@@ -286,7 +332,10 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
286
332
|
} else if (tok.type === _t.PARENOPEN) {
|
|
287
333
|
consume();
|
|
288
334
|
expr = ir.fnCall(expr, parseArgList(_t.PARENCLOSE));
|
|
289
|
-
} else if (tok.type === _t.
|
|
335
|
+
} else if (withFilter && (tok.type === _t.IS || tok.type === _t.ISNOT)) {
|
|
336
|
+
consume();
|
|
337
|
+
expr = lowerTest(expr, tok.type === _t.ISNOT);
|
|
338
|
+
} else if (withFilter && (tok.type === _t.FILTER || tok.type === _t.FILTEREMPTY)) {
|
|
290
339
|
consume();
|
|
291
340
|
var fname = tok.match;
|
|
292
341
|
if (filters.hasOwnProperty(fname) && typeof filters[fname] !== 'function') {
|
|
@@ -317,22 +366,22 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
317
366
|
var m;
|
|
318
367
|
switch (tok.type) {
|
|
319
368
|
case _t.STRING:
|
|
320
|
-
return
|
|
369
|
+
return ir.literal('string', unquoteString(tok.match));
|
|
321
370
|
case _t.NUMBER:
|
|
322
|
-
return
|
|
371
|
+
return ir.literal('number', parseFloat(tok.match));
|
|
323
372
|
case _t.BOOL:
|
|
324
|
-
return
|
|
373
|
+
return ir.literal('bool', tok.match === 'true');
|
|
325
374
|
case _t.PARENOPEN:
|
|
326
375
|
var grouped = parseExpression(0);
|
|
327
376
|
var close = consume();
|
|
328
377
|
if (!close || close.type !== _t.PARENCLOSE) {
|
|
329
378
|
bail('Mismatched nesting state');
|
|
330
379
|
}
|
|
331
|
-
return
|
|
380
|
+
return grouped;
|
|
332
381
|
case _t.BRACKETOPEN:
|
|
333
|
-
return
|
|
382
|
+
return ir.arrayLiteral(parseArgList(_t.BRACKETCLOSE));
|
|
334
383
|
case _t.CURLYOPEN:
|
|
335
|
-
return
|
|
384
|
+
return parseObjectLiteral();
|
|
336
385
|
case _t.VAR:
|
|
337
386
|
var path = tok.match.split('.');
|
|
338
387
|
if (_reserved.indexOf(path[0]) !== -1) {
|
|
@@ -341,7 +390,7 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
341
390
|
utils.each(path, function (segment) {
|
|
342
391
|
guardSegment(segment);
|
|
343
392
|
});
|
|
344
|
-
return
|
|
393
|
+
return ir.varRef(path);
|
|
345
394
|
case _t.FUNCTION:
|
|
346
395
|
case _t.FUNCTIONEMPTY:
|
|
347
396
|
m = tok.match;
|
|
@@ -350,75 +399,69 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
350
399
|
}
|
|
351
400
|
guardSegment(m);
|
|
352
401
|
if (tok.type === _t.FUNCTIONEMPTY) {
|
|
353
|
-
return
|
|
402
|
+
return ir.fnCall(ir.varRef([m]), []);
|
|
354
403
|
}
|
|
355
|
-
return
|
|
404
|
+
return ir.fnCall(ir.varRef([m]), parseArgList(_t.PARENCLOSE));
|
|
356
405
|
}
|
|
357
406
|
bail('Unexpected token "' + tok.match + '"');
|
|
358
407
|
return null;
|
|
359
408
|
}
|
|
360
409
|
|
|
361
|
-
|
|
362
|
-
|
|
410
|
+
// Jinja2 parse_unary: a unary minus/plus takes its operand without the
|
|
411
|
+
// filters that follow, which then apply to the negated value (`-x|abs`
|
|
412
|
+
// is abs(-x)). A `not` reached here sits where Jinja2 has no not-level
|
|
413
|
+
// (`a == not b`, `-not x`, `2 ** not x`); it binds its unary operand.
|
|
414
|
+
function parseUnary(withFilter) {
|
|
415
|
+
var tok = peek(), node;
|
|
363
416
|
if (tok && tok.type === _t.NOT) {
|
|
364
417
|
consume();
|
|
365
|
-
return ir.unaryOp('!', parseUnary());
|
|
418
|
+
return ir.unaryOp('!', parseUnary(withFilter));
|
|
366
419
|
}
|
|
367
420
|
if (tok && tok.type === _t.OPERATOR && (tok.match === '+' || tok.match === '-')) {
|
|
368
421
|
consume();
|
|
369
|
-
|
|
422
|
+
node = ir.unaryOp(tok.match, parseUnary(false));
|
|
423
|
+
} else {
|
|
424
|
+
node = parsePrimary();
|
|
370
425
|
}
|
|
371
|
-
return
|
|
426
|
+
return parsePostfix(node, withFilter);
|
|
372
427
|
}
|
|
373
428
|
|
|
374
429
|
function parsePower() {
|
|
375
|
-
var left =
|
|
430
|
+
var left = parseUnary(true);
|
|
376
431
|
var next = peek();
|
|
377
|
-
|
|
432
|
+
while (next && next.type === _t.POWER) {
|
|
378
433
|
consume();
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
// is
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
434
|
+
// Left-associative, as Jinja2's parse_pow reads its unary operands in
|
|
435
|
+
// a loop: `2 ** 3 ** 2` is (2 ** 3) ** 2 = 64 (Python gives 512). The
|
|
436
|
+
// base is a unary operand, so a leading minus belongs to it: `-2 ** 2`
|
|
437
|
+
// is (-2) ** 2 = 4, as Jinja2 renders (Python gives -4). Lowered to
|
|
438
|
+
// Math.pow because the emit is parenthesis-safe — a bare `a ** b`
|
|
439
|
+
// emission would mis-group when an operand is itself a binary op and
|
|
440
|
+
// would SyntaxError when the base is a unary (`-2 ** 3`).
|
|
441
|
+
left = ir.fnCall(ir.varRef(['Math', 'pow']), [left, parseUnary(true)]);
|
|
442
|
+
next = peek();
|
|
388
443
|
}
|
|
389
444
|
return left;
|
|
390
445
|
}
|
|
391
446
|
|
|
447
|
+
// Jinja2's parse_not: a `not` that starts an operand of `and`, `or` or
|
|
448
|
+
// the whole expression (minPrec up to the comparison level) negates a
|
|
449
|
+
// level-3 expression, so `not a == b` is not (a == b) and `not x + y` is
|
|
450
|
+
// not (x + y).
|
|
392
451
|
function parseExpression(minPrec) {
|
|
393
|
-
var left =
|
|
452
|
+
var left, ntok = peek();
|
|
453
|
+
if (ntok && ntok.type === _t.NOT && minPrec <= 3) {
|
|
454
|
+
consume();
|
|
455
|
+
left = ir.unaryOp('!', parseExpression(3));
|
|
456
|
+
} else {
|
|
457
|
+
left = parsePower();
|
|
458
|
+
}
|
|
394
459
|
while (true) {
|
|
395
460
|
var tok = peek();
|
|
396
461
|
if (!tok) { break; }
|
|
397
462
|
var info = getBinaryOpInfo(tok);
|
|
398
463
|
if (!info || info.prec < minPrec) { break; }
|
|
399
464
|
consume();
|
|
400
|
-
// `is` / `is not` — the RHS is a constrained test name + optional arg
|
|
401
|
-
// list, not a full expression. Lower to `_ext._test_<name>(subject,
|
|
402
|
-
// ...args)`; `is not` wraps the call in a unary `!`. `defined` /
|
|
403
|
-
// `none` / `undefined` on a VarRef subject route through
|
|
404
|
-
// IRVarRefExists instead, because emitVarRef coerces a missing or
|
|
405
|
-
// null lookup to "" and so loses the defined/undefined signal those
|
|
406
|
-
// tests depend on. Non-VarRef subjects evaluate to a concrete value
|
|
407
|
-
// (no coercion) and fall through to the generic `_ext._test_<name>`
|
|
408
|
-
// helper registered by the engine.
|
|
409
|
-
if (info.op === 'is' || info.op === 'is not') {
|
|
410
|
-
var test = parseTest();
|
|
411
|
-
var testCall;
|
|
412
|
-
if (test.args.length === 0 && left.type === 'VarRef' && test.name === 'defined') {
|
|
413
|
-
testCall = ir.varRefExists(left.path, left.loc);
|
|
414
|
-
} else if (test.args.length === 0 && left.type === 'VarRef' && (test.name === 'none' || test.name === 'undefined')) {
|
|
415
|
-
testCall = ir.unaryOp('!', ir.varRefExists(left.path, left.loc));
|
|
416
|
-
} else {
|
|
417
|
-
testCall = ir.fnCall(ir.varRef(['_ext', '_test_' + test.name]), [left].concat(test.args));
|
|
418
|
-
}
|
|
419
|
-
left = info.op === 'is not' ? ir.unaryOp('!', testCall) : testCall;
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
465
|
var right = parseExpression(info.prec + 1);
|
|
423
466
|
if (info.op === '//') {
|
|
424
467
|
// Floor division — JS `a // b` is a line comment, so lower to
|
package/lib/tags/block.js
CHANGED
|
@@ -56,7 +56,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
56
56
|
utils.throwError('Block name "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
|
|
57
57
|
}
|
|
58
58
|
if (_dangerousProps.indexOf(nameTok.match) !== -1) {
|
|
59
|
-
utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (
|
|
59
|
+
utils.throwError('Unsafe block name "' + nameTok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
pos += 1;
|
package/lib/tags/filter.js
CHANGED
|
@@ -108,7 +108,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
108
108
|
|
|
109
109
|
function checkName(name) {
|
|
110
110
|
if (_dangerousProps.indexOf(name) !== -1) {
|
|
111
|
-
utils.throwError('Unsafe filter name "' + name + '" is not allowed (
|
|
111
|
+
utils.throwError('Unsafe filter name "' + name + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
package/lib/tags/for.js
CHANGED
|
@@ -74,7 +74,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
74
74
|
utils.throwError('Loop variable "' + tok.match + '" must be a bare identifier in "for" tag', line, opts.filename);
|
|
75
75
|
}
|
|
76
76
|
if (_dangerousProps.indexOf(tok.match) !== -1) {
|
|
77
|
-
utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (
|
|
77
|
+
utils.throwError('Unsafe loop variable "' + tok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
78
78
|
}
|
|
79
79
|
return tok.match;
|
|
80
80
|
}
|
package/lib/tags/from.js
CHANGED
|
@@ -91,7 +91,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
91
91
|
utils.throwError(role + ' "' + name + '" must be a bare identifier in "from" tag', line, opts.filename);
|
|
92
92
|
}
|
|
93
93
|
if (_dangerousProps.indexOf(name) !== -1) {
|
|
94
|
-
utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (
|
|
94
|
+
utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
package/lib/tags/import.js
CHANGED
|
@@ -106,7 +106,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
106
106
|
utils.throwError('Import alias "' + aliasTok.match + '" must be a bare identifier in "import" tag', line, opts.filename);
|
|
107
107
|
}
|
|
108
108
|
if (_dangerousProps.indexOf(aliasTok.match) !== -1) {
|
|
109
|
-
utils.throwError('Unsafe import alias "' + aliasTok.match + '" is not allowed (
|
|
109
|
+
utils.throwError('Unsafe import alias "' + aliasTok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
if (peek()) {
|
package/lib/tags/include.js
CHANGED
|
@@ -122,7 +122,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
122
122
|
}
|
|
123
123
|
var pathExpr = parser.parseExpr(pathTokens);
|
|
124
124
|
|
|
125
|
-
var resolveFrom = (opts.filename || '')
|
|
125
|
+
var resolveFrom = (opts.filename || '');
|
|
126
126
|
|
|
127
127
|
// `without context` -> empty-object context + isolated, so the backend's
|
|
128
128
|
// Include selector emits `{}` rather than `_ctx`. `with context` (and the
|
package/lib/tags/macro.js
CHANGED
|
@@ -80,7 +80,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
80
80
|
utils.throwError(role + ' "' + name + '" must be a bare identifier in "macro" tag', line, opts.filename);
|
|
81
81
|
}
|
|
82
82
|
if (_dangerousProps.indexOf(name) !== -1) {
|
|
83
|
-
utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (
|
|
83
|
+
utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
package/lib/tags/set.js
CHANGED
|
@@ -81,17 +81,18 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
81
81
|
var path = lhsTok.match.split('.');
|
|
82
82
|
utils.each(path, function (segment) {
|
|
83
83
|
if (_dangerousProps.indexOf(segment) !== -1) {
|
|
84
|
-
utils.throwError('Unsafe assignment to "' + segment + '" is not allowed (
|
|
84
|
+
utils.throwError('Unsafe assignment to "' + segment + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
// DOTKEY tail — the Jinja2 lexer
|
|
89
|
-
//
|
|
90
|
-
//
|
|
88
|
+
// DOTKEY tail — the Jinja2 lexer folds `foo.bar` into one VAR match, but a
|
|
89
|
+
// dot after whitespace (`foo .bar`) lexes as its own DOTKEY token, so this
|
|
90
|
+
// loop runs today; it also keeps the tag safe if a future lexer splits
|
|
91
|
+
// `foo.bar` into VAR + DOTKEY.
|
|
91
92
|
while (peek() && peek().type === types.DOTKEY) {
|
|
92
93
|
var dk = consume();
|
|
93
94
|
if (_dangerousProps.indexOf(dk.match) !== -1) {
|
|
94
|
-
utils.throwError('Unsafe assignment to "' + dk.match + '" is not allowed (
|
|
95
|
+
utils.throwError('Unsafe assignment to "' + dk.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
95
96
|
}
|
|
96
97
|
path.push(dk.match);
|
|
97
98
|
}
|
package/lib/tags/with.js
CHANGED
|
@@ -77,7 +77,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
77
77
|
utils.throwError('"with" assignment target "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
|
|
78
78
|
}
|
|
79
79
|
if (_dangerousProps.indexOf(nameTok.match) !== -1) {
|
|
80
|
-
utils.throwError('Unsafe "with" assignment to "' + nameTok.match + '" is not allowed (
|
|
80
|
+
utils.throwError('Unsafe "with" assignment to "' + nameTok.match + '" is not allowed (prototype-chain guard)', line, opts.filename);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
var eqTok = consume();
|
package/lib/tests/index.js
CHANGED
|
@@ -33,7 +33,9 @@ function isArr(v) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/*!
|
|
36
|
-
*
|
|
36
|
+
* Number guard shared by `even`, `odd` and `divisibleby`: any number but
|
|
37
|
+
* NaN. An infinity passes and then fails each test's remainder check.
|
|
38
|
+
* @private
|
|
37
39
|
*/
|
|
38
40
|
function isNumber(v) {
|
|
39
41
|
return typeof v === 'number' && !isNaN(v);
|
|
@@ -83,13 +85,14 @@ exports['boolean'] = function (v) {
|
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
|
-
* `foo is number` — true for
|
|
88
|
+
* `foo is number` — true for any JavaScript number, NaN and the
|
|
89
|
+
* infinities included, as Python's `numbers.Number` test.
|
|
87
90
|
*
|
|
88
91
|
* @param {*} v
|
|
89
92
|
* @return {boolean}
|
|
90
93
|
*/
|
|
91
94
|
exports['number'] = function (v) {
|
|
92
|
-
return
|
|
95
|
+
return typeof v === 'number';
|
|
93
96
|
};
|
|
94
97
|
|
|
95
98
|
/**
|
|
@@ -158,26 +161,53 @@ exports['sameas'] = function (v, other) {
|
|
|
158
161
|
return v === other;
|
|
159
162
|
};
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
+
/*!
|
|
165
|
+
* Unicode case properties for `lower` / `upper`, as Python's
|
|
166
|
+
* `str.islower` / `str.isupper` read them: a string needs at least one
|
|
167
|
+
* cased letter, and none of the other case (a titlecase letter counts
|
|
168
|
+
* against both). Built in a try/catch because an engine without Unicode
|
|
169
|
+
* property escapes (ES2018) throws on them; it keeps the case-mapping
|
|
170
|
+
* fallback below. @private
|
|
171
|
+
*/
|
|
172
|
+
var caseProps = (function () {
|
|
173
|
+
try {
|
|
174
|
+
return {
|
|
175
|
+
lower: new RegExp('\\p{Lowercase}', 'u'),
|
|
176
|
+
upper: new RegExp('\\p{Uppercase}', 'u'),
|
|
177
|
+
notLower: new RegExp('[\\p{Uppercase}\\p{Lt}]', 'u'),
|
|
178
|
+
notUpper: new RegExp('[\\p{Lowercase}\\p{Lt}]', 'u')
|
|
179
|
+
};
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}());
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `foo is lower` — true when the subject is a string with at least one
|
|
187
|
+
* cased letter and no uppercase or titlecase letter, as Python's
|
|
188
|
+
* `str.islower`: `''`, `'123'` and `'-'` are neither lower nor upper.
|
|
164
189
|
*
|
|
165
190
|
* @param {*} v
|
|
166
191
|
* @return {boolean}
|
|
167
192
|
*/
|
|
168
193
|
exports['lower'] = function (v) {
|
|
169
|
-
|
|
194
|
+
if (typeof v !== 'string') { return false; }
|
|
195
|
+
if (caseProps) { return !caseProps.notLower.test(v) && caseProps.lower.test(v); }
|
|
196
|
+
return v === v.toLowerCase() && v !== v.toUpperCase();
|
|
170
197
|
};
|
|
171
198
|
|
|
172
199
|
/**
|
|
173
|
-
* `foo is upper` — true when the subject is a string
|
|
174
|
-
*
|
|
200
|
+
* `foo is upper` — true when the subject is a string with at least one
|
|
201
|
+
* cased letter and no lowercase or titlecase letter, as Python's
|
|
202
|
+
* `str.isupper`.
|
|
175
203
|
*
|
|
176
204
|
* @param {*} v
|
|
177
205
|
* @return {boolean}
|
|
178
206
|
*/
|
|
179
207
|
exports['upper'] = function (v) {
|
|
180
|
-
|
|
208
|
+
if (typeof v !== 'string') { return false; }
|
|
209
|
+
if (caseProps) { return !caseProps.notUpper.test(v) && caseProps.upper.test(v); }
|
|
210
|
+
return v === v.toUpperCase() && v !== v.toLowerCase();
|
|
181
211
|
};
|
|
182
212
|
|
|
183
213
|
/**
|
|
@@ -191,13 +221,14 @@ exports['even'] = function (v) {
|
|
|
191
221
|
};
|
|
192
222
|
|
|
193
223
|
/**
|
|
194
|
-
* `n is odd` — true for
|
|
224
|
+
* `n is odd` — true for an odd integer, as Python's `n % 2 == 1`: a
|
|
225
|
+
* negative odd integer is odd; a non-integer or an infinity is not.
|
|
195
226
|
*
|
|
196
227
|
* @param {number} v
|
|
197
228
|
* @return {boolean}
|
|
198
229
|
*/
|
|
199
230
|
exports['odd'] = function (v) {
|
|
200
|
-
return isNumber(v) && v % 2
|
|
231
|
+
return isNumber(v) && ((v % 2) + 2) % 2 === 1;
|
|
201
232
|
};
|
|
202
233
|
|
|
203
234
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig-jinja2",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
4
4
|
"description": "Jinja2-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"node": ">=12"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@rhinostone/swig-core": "2.8.
|
|
26
|
+
"@rhinostone/swig-core": "2.8.3"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|