@rhinostone/swig-django 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 +87 -10
- package/lib/index.js +20 -3
- package/lib/parser.js +2 -2
- package/lib/tags/block.js +1 -1
- package/lib/tags/for.js +1 -1
- package/lib/tags/include.js +2 -2
- package/lib/tags/with.js +1 -1
- package/package.json +2 -2
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** — `
|
|
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
|
|
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.
|
|
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
|
-
|
|
898
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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.3";
|
|
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) ? '
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
}
|
package/lib/tags/include.js
CHANGED
|
@@ -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 (
|
|
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();
|
|
@@ -178,7 +178,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
178
178
|
}
|
|
179
179
|
var pathExpr = parser.parseExpr(pathTokens);
|
|
180
180
|
|
|
181
|
-
var resolveFrom = (opts.filename || '')
|
|
181
|
+
var resolveFrom = (opts.filename || '');
|
|
182
182
|
token.irExpr = ir.include(pathExpr, ctxExpr, isolated, false, resolveFrom);
|
|
183
183
|
return true;
|
|
184
184
|
};
|
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 (
|
|
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.
|
|
3
|
+
"version": "2.8.3",
|
|
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.
|
|
25
|
+
"@rhinostone/swig-core": "2.8.3"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|