@rhinostone/swig-django 2.7.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 +57 -0
- package/lib/async/pre-walker.js +261 -0
- package/lib/filters.js +1241 -0
- package/lib/index.js +345 -0
- package/lib/lexer.js +302 -0
- package/lib/parser.js +739 -0
- package/lib/tags/autoescape.js +76 -0
- package/lib/tags/block.js +92 -0
- package/lib/tags/comment.js +42 -0
- package/lib/tags/cycle.js +104 -0
- package/lib/tags/elif.js +33 -0
- package/lib/tags/else.js +30 -0
- package/lib/tags/empty.js +28 -0
- package/lib/tags/extends.js +95 -0
- package/lib/tags/firstof.js +101 -0
- package/lib/tags/for.js +262 -0
- package/lib/tags/if.js +82 -0
- package/lib/tags/include.js +200 -0
- package/lib/tags/index.js +37 -0
- package/lib/tags/spaceless.js +69 -0
- package/lib/tags/verbatim.js +51 -0
- package/lib/tags/with.js +157 -0
- package/lib/tokentypes.js +87 -0
- package/package.json +30 -0
package/lib/filters.js
ADDED
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
var utils = require('@rhinostone/swig-core/lib/utils'),
|
|
2
|
+
dateFormatter = require('@rhinostone/swig-core/lib/dateformatter'),
|
|
3
|
+
iterateFilter = require('@rhinostone/swig-core/lib/filters').iterateFilter;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Django filter catalog.
|
|
7
|
+
*
|
|
8
|
+
* Per-flavor map consumed by `engine.install(self, frontend)` as both the
|
|
9
|
+
* `_filters` runtime map in the compiled template function and the mutation
|
|
10
|
+
* target for `setFilter`. The `.safe = true` convention is inherited from
|
|
11
|
+
* swig-core — filters marked `.safe` suppress the autoescape `e` tail
|
|
12
|
+
* injected in the parser's `parseVariable`.
|
|
13
|
+
*
|
|
14
|
+
* Filter names route through `_filters["<name>"]` at runtime (bracket access
|
|
15
|
+
* on the engine's own filter map), never through the `_ctx` prototype chain,
|
|
16
|
+
* so CVE-2023-25345 guards don't apply at this layer. Filter argument
|
|
17
|
+
* expressions inherit the expression parser's `_dangerousProps` guards.
|
|
18
|
+
*
|
|
19
|
+
* Fidelity notes (cross-checked against Django 5.2 `defaultfilters.py`):
|
|
20
|
+
*
|
|
21
|
+
* - **`.safe` vs Django `is_safe`.** Django's `is_safe=True` does NOT bypass
|
|
22
|
+
* autoescape — it only means the filter preserves an already-safe input.
|
|
23
|
+
* Only filters that Django wraps in `mark_safe()` (always returning trusted
|
|
24
|
+
* markup) get `.safe = true` here: `safe`, `escapejs`, `force_escape`,
|
|
25
|
+
* `linebreaks`, `linebreaksbr`, `linenumbers`. `floatformat` is NOT marked
|
|
26
|
+
* safe even though Django `mark_safe`s its success path, because its
|
|
27
|
+
* error / passthrough path returns the raw input — a `.safe` flag there
|
|
28
|
+
* would let unescaped input through (its numeric output has no HTML, so the
|
|
29
|
+
* redundant escape pass is a harmless no-op).
|
|
30
|
+
* - **String filters coerce to string.** Django's `@stringfilter` coerces the
|
|
31
|
+
* value before the filter runs; the string filters here mirror that.
|
|
32
|
+
* - **No settings system.** `date` / `time` have no locale `DATE_FORMAT` /
|
|
33
|
+
* `TIME_FORMAT` to fall back on, so they default to fixed formats; pass an
|
|
34
|
+
* explicit format for full control. The shared PHP-style date formatter
|
|
35
|
+
* covers the common Django date codes but not the Django-only ones
|
|
36
|
+
* (`N` AP-month, `P`, `f`, `T`, `e`, `I`, `u`).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/*!
|
|
40
|
+
* Map a single HTML-significant character to its entity. @private
|
|
41
|
+
*/
|
|
42
|
+
function escapeHtmlRest(ch) {
|
|
43
|
+
return ch === '<' ? '<' : ch === '>' ? '>' : ch === '"' ? '"' : ''';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/*!
|
|
47
|
+
* Normalize CRLF / CR newlines to LF, matching Django's
|
|
48
|
+
* `utils.text.normalize_newlines`. @private
|
|
49
|
+
*/
|
|
50
|
+
function normalizeNewlines(value) {
|
|
51
|
+
return String(value).replace(/\r\n|\r|\n/g, '\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*!
|
|
55
|
+
* Round half away from zero to `places` decimals (Django floatformat uses
|
|
56
|
+
* Decimal ROUND_HALF_UP). The `e`-notation shift avoids the common
|
|
57
|
+
* `x * 10^n` float-representation error for the documented cases; rare
|
|
58
|
+
* half-way values at the float-representation boundary may still differ from
|
|
59
|
+
* Python's exact Decimal rounding. @private
|
|
60
|
+
*/
|
|
61
|
+
function roundHalfUp(num, places) {
|
|
62
|
+
var sign = num < 0 ? -1 : 1,
|
|
63
|
+
n = Math.abs(num);
|
|
64
|
+
return sign * Number(Math.round(Number(n + 'e' + places)) + 'e-' + places);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*!
|
|
68
|
+
* Django `escapejs` character map (`django.utils.html._js_escapes`). @private
|
|
69
|
+
*/
|
|
70
|
+
var _jsEscapes = {
|
|
71
|
+
'\\': '\\u005C',
|
|
72
|
+
'\'': '\\u0027',
|
|
73
|
+
'"': '\\u0022',
|
|
74
|
+
'>': '\\u003E',
|
|
75
|
+
'<': '\\u003C',
|
|
76
|
+
'&': '\\u0026',
|
|
77
|
+
'=': '\\u003D',
|
|
78
|
+
'-': '\\u002D',
|
|
79
|
+
';': '\\u003B',
|
|
80
|
+
'`': '\\u0060',
|
|
81
|
+
'\u2028': '\\u2028',
|
|
82
|
+
'\u2029': '\\u2029'
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/*!
|
|
86
|
+
* Percent-encode one character's UTF-8 bytes. `encodeURIComponent` leaves
|
|
87
|
+
* `! * ' ( )` un-encoded, which Python's `quote` escapes — so finish them by
|
|
88
|
+
* hand. @private
|
|
89
|
+
*/
|
|
90
|
+
function pctEncode(ch) {
|
|
91
|
+
return encodeURIComponent(ch).replace(/[!*'()]/g, function (m) {
|
|
92
|
+
return '%' + m.charCodeAt(0).toString(16).toUpperCase();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/*!
|
|
97
|
+
* Encode a string for a URL, leaving the unreserved set
|
|
98
|
+
* (`A-Z a-z 0-9 _ . - ~`) and any character in `safe` untouched. Mirrors
|
|
99
|
+
* Python's `urllib.parse.quote`. @private
|
|
100
|
+
*/
|
|
101
|
+
function urlQuote(value, safe) {
|
|
102
|
+
var s = String(value),
|
|
103
|
+
out = '',
|
|
104
|
+
i,
|
|
105
|
+
c;
|
|
106
|
+
for (i = 0; i < s.length; i += 1) {
|
|
107
|
+
c = s.charAt(i);
|
|
108
|
+
if (/[A-Za-z0-9_.\-~]/.test(c) || (safe && safe.indexOf(c) !== -1)) {
|
|
109
|
+
out += c;
|
|
110
|
+
} else {
|
|
111
|
+
out += pctEncode(c);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ------------------------------------------------------------------ *
|
|
118
|
+
* Filters (alphabetical) *
|
|
119
|
+
* ------------------------------------------------------------------ */
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add the argument to the value. Numeric addition is tried first (Django's
|
|
123
|
+
* `int(value) + int(arg)`); on failure it falls back to concatenation
|
|
124
|
+
* (strings and arrays), then to the empty string.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* {{ 4|add:2 }}
|
|
128
|
+
* // => 6
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* {{ first|add:second }} // first = [1, 2], second = [3, 4]
|
|
132
|
+
* // => [1, 2, 3, 4]
|
|
133
|
+
*
|
|
134
|
+
* @param {*} input
|
|
135
|
+
* @param {*} arg
|
|
136
|
+
* @return {*}
|
|
137
|
+
*/
|
|
138
|
+
exports.add = function (input, arg) {
|
|
139
|
+
var a = parseInt(input, 10),
|
|
140
|
+
b = parseInt(arg, 10);
|
|
141
|
+
if (!isNaN(a) && !isNaN(b) && String(input).trim() === String(a) && String(arg).trim() === String(b)) {
|
|
142
|
+
return a + b;
|
|
143
|
+
}
|
|
144
|
+
if (utils.isArray(input) && utils.isArray(arg)) {
|
|
145
|
+
return input.concat(arg);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return input + arg;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Backslash-escape quotes and backslashes (Django's `addslashes`). Escapes
|
|
156
|
+
* `\`, `"`, and `'`.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* {{ "I'm using Django"|addslashes }}
|
|
160
|
+
* // => I\'m using Django
|
|
161
|
+
*
|
|
162
|
+
* @param {*} input
|
|
163
|
+
* @return {string}
|
|
164
|
+
*/
|
|
165
|
+
exports.addslashes = function (input) {
|
|
166
|
+
return String(input).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Capitalize the first character of the value, leaving the rest unchanged
|
|
171
|
+
* (Django's `capfirst` — distinct from a title-case or fully-lowercased
|
|
172
|
+
* capitalize).
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* {{ "django"|capfirst }}
|
|
176
|
+
* // => Django
|
|
177
|
+
*
|
|
178
|
+
* @param {*} input
|
|
179
|
+
* @return {string}
|
|
180
|
+
*/
|
|
181
|
+
exports.capfirst = function (input) {
|
|
182
|
+
var s = String(input);
|
|
183
|
+
if (!s.length) {
|
|
184
|
+
return s;
|
|
185
|
+
}
|
|
186
|
+
return s.charAt(0).toUpperCase() + s.substr(1);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Center the value in a field of the given width using spaces (Django's
|
|
191
|
+
* `center`, which defers to Python `str.center`). When an odd number of pad
|
|
192
|
+
* characters is needed, the extra space goes on the right.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* "{{ "Django"|center:"15" }}"
|
|
196
|
+
* // => " Django "
|
|
197
|
+
*
|
|
198
|
+
* @param {*} input
|
|
199
|
+
* @param {(number|string)} width
|
|
200
|
+
* @return {string}
|
|
201
|
+
*/
|
|
202
|
+
exports.center = function (input, width) {
|
|
203
|
+
var s = String(input),
|
|
204
|
+
w = parseInt(width, 10),
|
|
205
|
+
marg,
|
|
206
|
+
left,
|
|
207
|
+
right;
|
|
208
|
+
if (isNaN(w) || s.length >= w) {
|
|
209
|
+
return s;
|
|
210
|
+
}
|
|
211
|
+
marg = w - s.length;
|
|
212
|
+
left = Math.floor(marg / 2) + (marg & w & 1);
|
|
213
|
+
right = marg - left;
|
|
214
|
+
return new Array(left + 1).join(' ') + s + new Array(right + 1).join(' ');
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove all occurrences of the argument from the value (Django's `cut`).
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* {{ "String with spaces"|cut:" " }}
|
|
222
|
+
* // => Stringwithspaces
|
|
223
|
+
*
|
|
224
|
+
* @param {*} input
|
|
225
|
+
* @param {string} arg
|
|
226
|
+
* @return {string}
|
|
227
|
+
*/
|
|
228
|
+
exports.cut = function (input, arg) {
|
|
229
|
+
return String(input).split(String(arg)).join('');
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Format a date / time. The format string uses PHP-style date tokens (the
|
|
234
|
+
* same set shared with the native and Twig flavors), which covers the common
|
|
235
|
+
* Django date codes; a backslash escapes a literal character. `offset` shifts
|
|
236
|
+
* the timezone (minutes west of GMT) and `abbr` sets the output-only timezone
|
|
237
|
+
* abbreviation. A null / empty input renders the empty string (Django
|
|
238
|
+
* behavior). With no format, defaults to `'F j, Y'` (Django uses the locale
|
|
239
|
+
* `DATE_FORMAT`, which this engine has no settings system for).
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* {{ value|date:"D d M Y" }} // value = 2008-01-09
|
|
243
|
+
* // => Wed 09 Jan 2008
|
|
244
|
+
*
|
|
245
|
+
* @param {?(Date|string|number)} input
|
|
246
|
+
* @param {string} [format='F j, Y']
|
|
247
|
+
* @param {number} [offset] Timezone offset in minutes west of GMT.
|
|
248
|
+
* @param {string} [abbr] Output timezone abbreviation.
|
|
249
|
+
* @return {string}
|
|
250
|
+
*/
|
|
251
|
+
exports.date = function (input, format, offset, abbr) {
|
|
252
|
+
var l,
|
|
253
|
+
date,
|
|
254
|
+
cur,
|
|
255
|
+
i = 0,
|
|
256
|
+
out = '';
|
|
257
|
+
|
|
258
|
+
if (input === null || input === undefined || input === '') {
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
format = (format === undefined || format === null || format === '') ? 'F j, Y' : String(format);
|
|
263
|
+
l = format.length;
|
|
264
|
+
date = new dateFormatter.DateZ(input);
|
|
265
|
+
|
|
266
|
+
if (offset) {
|
|
267
|
+
date.setTimezoneOffset(offset, abbr);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (i = 0; i < l; i += 1) {
|
|
271
|
+
cur = format.charAt(i);
|
|
272
|
+
if (cur === '\\') {
|
|
273
|
+
i += 1;
|
|
274
|
+
out += (i < l) ? format.charAt(i) : cur;
|
|
275
|
+
} else if (dateFormatter.hasOwnProperty(cur)) {
|
|
276
|
+
out += dateFormatter[cur](date, offset, abbr);
|
|
277
|
+
} else {
|
|
278
|
+
out += cur;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* If the value is falsy, use the given default (Django's `default`). Django
|
|
286
|
+
* truthiness applies: `False`, `0`, `''`, `None`, an empty list, and an empty
|
|
287
|
+
* mapping all fall back to the argument.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* {{ ""|default:"nothing" }}
|
|
291
|
+
* // => nothing
|
|
292
|
+
*
|
|
293
|
+
* @param {*} input
|
|
294
|
+
* @param {*} def Fallback value.
|
|
295
|
+
* @return {*}
|
|
296
|
+
*/
|
|
297
|
+
exports['default'] = function (input, def) {
|
|
298
|
+
if (input === undefined || input === null || input === false || input === 0 || input === '') {
|
|
299
|
+
return def;
|
|
300
|
+
}
|
|
301
|
+
if (utils.isArray(input) && input.length === 0) {
|
|
302
|
+
return def;
|
|
303
|
+
}
|
|
304
|
+
if (typeof input === 'object' && utils.keys(input).length === 0) {
|
|
305
|
+
return def;
|
|
306
|
+
}
|
|
307
|
+
return input;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Use the default only when the value is `None` (Django's `default_if_none`).
|
|
312
|
+
* Unlike `default`, other falsy values (`0`, `''`, `False`) pass through.
|
|
313
|
+
*
|
|
314
|
+
* Note: until the Django variable resolver lands, a context value that is
|
|
315
|
+
* `null` is coerced to `""` before reaching this filter, so a context-`None`
|
|
316
|
+
* does not yet trigger the fallback (an empty string does not, by design); a
|
|
317
|
+
* value that arrives genuinely `null` / `undefined` does.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* {{ value|default_if_none:"nothing" }} // value = None
|
|
321
|
+
* // => nothing
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* {{ value|default_if_none:"nothing" }} // value = 0
|
|
325
|
+
* // => 0
|
|
326
|
+
*
|
|
327
|
+
* @param {*} input
|
|
328
|
+
* @param {*} def
|
|
329
|
+
* @return {*}
|
|
330
|
+
*/
|
|
331
|
+
exports.default_if_none = function (input, def) {
|
|
332
|
+
return (input === null || input === undefined) ? def : input;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Return whether the value is divisible by the argument (Django's
|
|
337
|
+
* `divisibleby`). Returns a boolean — typically used in `{% if %}`.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* {{ 21|divisibleby:3 }}
|
|
341
|
+
* // => true
|
|
342
|
+
*
|
|
343
|
+
* @param {*} input
|
|
344
|
+
* @param {*} arg
|
|
345
|
+
* @return {boolean}
|
|
346
|
+
*/
|
|
347
|
+
exports.divisibleby = function (input, arg) {
|
|
348
|
+
var v = parseInt(input, 10),
|
|
349
|
+
a = parseInt(arg, 10);
|
|
350
|
+
if (isNaN(v) || isNaN(a) || a === 0) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return v % a === 0;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* HTML-escape (default) or JS-escape the input. `e` is the shortcut alias
|
|
358
|
+
* applied by autoescape. The HTML branch preserves already-escaped entities
|
|
359
|
+
* (`&`, `<`, …) so the autoescape tail is idempotent. Mirrors Django's
|
|
360
|
+
* HTML escaping of `< > & " '`.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* {{ "<b>"|escape }}
|
|
364
|
+
* // => <b>
|
|
365
|
+
*
|
|
366
|
+
* @param {*} input
|
|
367
|
+
* @param {string} [type='html'] Pass `'js'` for JavaScript-safe escaping.
|
|
368
|
+
* @return {string}
|
|
369
|
+
*/
|
|
370
|
+
exports.escape = function (input, type) {
|
|
371
|
+
var t, inp, out, i, code;
|
|
372
|
+
|
|
373
|
+
if (input === null || input === undefined) {
|
|
374
|
+
return input;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
t = typeof input;
|
|
378
|
+
|
|
379
|
+
if (t !== 'string') {
|
|
380
|
+
if (t === 'object') {
|
|
381
|
+
out = iterateFilter.apply(exports.escape, arguments);
|
|
382
|
+
if (out !== undefined) {
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return input;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (type === 'js') {
|
|
390
|
+
inp = input.replace(/\\/g, '\\u005C');
|
|
391
|
+
out = '';
|
|
392
|
+
for (i = 0; i < inp.length; i += 1) {
|
|
393
|
+
code = inp.charCodeAt(i);
|
|
394
|
+
if (code < 32) {
|
|
395
|
+
code = code.toString(16).toUpperCase();
|
|
396
|
+
code = (code.length < 2) ? '0' + code : code;
|
|
397
|
+
out += '\\u00' + code;
|
|
398
|
+
} else {
|
|
399
|
+
out += inp[i];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return out.replace(/&/g, '\\u0026')
|
|
403
|
+
.replace(/</g, '\\u003C')
|
|
404
|
+
.replace(/>/g, '\\u003E')
|
|
405
|
+
.replace(/\'/g, '\\u0027')
|
|
406
|
+
.replace(/"/g, '\\u0022')
|
|
407
|
+
.replace(/\=/g, '\\u003D')
|
|
408
|
+
.replace(/-/g, '\\u002D')
|
|
409
|
+
.replace(/;/g, '\\u003B');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
|
|
413
|
+
.replace(/[<>"']/g, escapeHtmlRest);
|
|
414
|
+
};
|
|
415
|
+
exports.e = exports.escape;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Hex-encode characters for use inside a JavaScript string literal (Django's
|
|
419
|
+
* `escapejs`). Escapes backslash, quotes, `< > & = - ; \``, the line / para
|
|
420
|
+
* separators U+2028 / U+2029, and all control characters. The output is safe
|
|
421
|
+
* to embed in a `<script>` and cannot break out of it, so it is marked safe.
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* {{ "</script>"|escapejs }}
|
|
425
|
+
* // => </script>
|
|
426
|
+
*
|
|
427
|
+
* @param {*} input
|
|
428
|
+
* @return {string}
|
|
429
|
+
*/
|
|
430
|
+
exports.escapejs = function (input) {
|
|
431
|
+
var s = (input === null || input === undefined) ? '' : String(input),
|
|
432
|
+
out = '',
|
|
433
|
+
i,
|
|
434
|
+
ch,
|
|
435
|
+
code,
|
|
436
|
+
hex;
|
|
437
|
+
for (i = 0; i < s.length; i += 1) {
|
|
438
|
+
ch = s.charAt(i);
|
|
439
|
+
if (_jsEscapes.hasOwnProperty(ch)) {
|
|
440
|
+
out += _jsEscapes[ch];
|
|
441
|
+
} else {
|
|
442
|
+
code = s.charCodeAt(i);
|
|
443
|
+
if (code < 32) {
|
|
444
|
+
hex = code.toString(16).toUpperCase();
|
|
445
|
+
out += '\\u00' + (hex.length < 2 ? '0' + hex : hex);
|
|
446
|
+
} else {
|
|
447
|
+
out += ch;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
};
|
|
453
|
+
exports.escapejs.safe = true;
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Format a byte count as a human-readable file size (Django's
|
|
457
|
+
* `filesizeformat`). Uses 1024-based units (bytes, KB, MB, GB, TB, PB), one
|
|
458
|
+
* decimal place for KB and up, and an integer for bytes. The input is
|
|
459
|
+
* truncated to an integer first. Like Django's `avoid_wrapping`, the number
|
|
460
|
+
* and unit are separated by a non-breaking space (U+00A0), so the output is
|
|
461
|
+
* byte-identical to Django; it renders visually as an ordinary space.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* {{ 123456789|filesizeformat }}
|
|
465
|
+
* // => 117.7 MB (the separator is U+00A0)
|
|
466
|
+
*
|
|
467
|
+
* @param {*} input
|
|
468
|
+
* @return {string}
|
|
469
|
+
*/
|
|
470
|
+
exports.filesizeformat = function (input) {
|
|
471
|
+
var bytes = parseInt(input, 10),
|
|
472
|
+
negative,
|
|
473
|
+
KB = 1024,
|
|
474
|
+
MB = KB * 1024,
|
|
475
|
+
GB = MB * 1024,
|
|
476
|
+
TB = GB * 1024,
|
|
477
|
+
PB = TB * 1024,
|
|
478
|
+
value;
|
|
479
|
+
|
|
480
|
+
function fnum(v) {
|
|
481
|
+
return (Math.round(v * 10) / 10).toFixed(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (isNaN(bytes)) {
|
|
485
|
+
return '0\u00a0bytes';
|
|
486
|
+
}
|
|
487
|
+
negative = bytes < 0;
|
|
488
|
+
if (negative) {
|
|
489
|
+
bytes = -bytes;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (bytes < KB) {
|
|
493
|
+
value = bytes + (bytes === 1 ? '\u00a0byte' : '\u00a0bytes');
|
|
494
|
+
} else if (bytes < MB) {
|
|
495
|
+
value = fnum(bytes / KB) + '\u00a0KB';
|
|
496
|
+
} else if (bytes < GB) {
|
|
497
|
+
value = fnum(bytes / MB) + '\u00a0MB';
|
|
498
|
+
} else if (bytes < TB) {
|
|
499
|
+
value = fnum(bytes / GB) + '\u00a0GB';
|
|
500
|
+
} else if (bytes < PB) {
|
|
501
|
+
value = fnum(bytes / TB) + '\u00a0TB';
|
|
502
|
+
} else {
|
|
503
|
+
value = fnum(bytes / PB) + '\u00a0PB';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return negative ? '-' + value : value;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Return the first item of an array, the first character of a string, or the
|
|
511
|
+
* first value of a mapping (Django's `first`). An empty sequence yields the
|
|
512
|
+
* empty string.
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* {{ "Joel"|first }}
|
|
516
|
+
* // => J
|
|
517
|
+
*
|
|
518
|
+
* @param {*} input
|
|
519
|
+
* @return {*}
|
|
520
|
+
*/
|
|
521
|
+
exports.first = function (input) {
|
|
522
|
+
var keys;
|
|
523
|
+
if (typeof input === 'string') {
|
|
524
|
+
return input.length ? input.charAt(0) : '';
|
|
525
|
+
}
|
|
526
|
+
if (utils.isArray(input)) {
|
|
527
|
+
return input.length ? input[0] : '';
|
|
528
|
+
}
|
|
529
|
+
if (input && typeof input === 'object') {
|
|
530
|
+
keys = utils.keys(input);
|
|
531
|
+
return keys.length ? input[keys[0]] : '';
|
|
532
|
+
}
|
|
533
|
+
return '';
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Format a floating-point number (Django's `floatformat`). With no argument,
|
|
538
|
+
* rounds to one decimal place but drops the decimal if the value is whole. A
|
|
539
|
+
* positive argument always shows that many decimals; a negative argument
|
|
540
|
+
* shows that many decimals only when the value has a fractional part; `0`
|
|
541
|
+
* rounds to an integer. Rounds half away from zero. The locale grouping
|
|
542
|
+
* suffixes (`g` / `u`) are accepted but ignored (no locale system).
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* {{ 34.26000|floatformat }}
|
|
546
|
+
* // => 34.3
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* {{ 34.00000|floatformat:"-3" }}
|
|
550
|
+
* // => 34
|
|
551
|
+
*
|
|
552
|
+
* @param {*} input
|
|
553
|
+
* @param {(number|string)} [arg=-1]
|
|
554
|
+
* @return {string}
|
|
555
|
+
*/
|
|
556
|
+
exports.floatformat = function (input, arg) {
|
|
557
|
+
var p, num, places, a;
|
|
558
|
+
|
|
559
|
+
if (arg === undefined || arg === null || arg === '') {
|
|
560
|
+
p = -1;
|
|
561
|
+
} else {
|
|
562
|
+
a = String(arg).replace(/[gu]+$/, '');
|
|
563
|
+
p = parseInt(a, 10);
|
|
564
|
+
if (isNaN(p)) {
|
|
565
|
+
return String(input);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
num = parseFloat(input);
|
|
570
|
+
if (isNaN(num)) {
|
|
571
|
+
return '';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (num === Math.floor(num) && p < 0) {
|
|
575
|
+
return String(num);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
places = Math.abs(p);
|
|
579
|
+
return roundHalfUp(num, places).toFixed(places);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Apply HTML escaping to the value immediately and mark it safe (Django's
|
|
584
|
+
* `force_escape`). Distinct from `escape` only in that the escaped result is
|
|
585
|
+
* not re-escaped by autoescape or a following filter.
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* {{ "<b>"|force_escape }}
|
|
589
|
+
* // => <b>
|
|
590
|
+
*
|
|
591
|
+
* @param {*} input
|
|
592
|
+
* @return {string}
|
|
593
|
+
*/
|
|
594
|
+
exports.force_escape = function (input) {
|
|
595
|
+
return exports.escape(String(input));
|
|
596
|
+
};
|
|
597
|
+
exports.force_escape.safe = true;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Return the digit of the value at the given 1-based position counted from
|
|
601
|
+
* the right (Django's `get_digit`). Returns 0 when the position is past the
|
|
602
|
+
* number; returns the value unchanged for a non-numeric value or a position
|
|
603
|
+
* below 1.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* {{ 123456789|get_digit:"2" }}
|
|
607
|
+
* // => 8
|
|
608
|
+
*
|
|
609
|
+
* @param {*} input
|
|
610
|
+
* @param {*} arg 1-based position from the right.
|
|
611
|
+
* @return {(number|*)}
|
|
612
|
+
*/
|
|
613
|
+
exports.get_digit = function (input, arg) {
|
|
614
|
+
var a = parseInt(arg, 10),
|
|
615
|
+
v = parseInt(input, 10),
|
|
616
|
+
s,
|
|
617
|
+
d;
|
|
618
|
+
if (isNaN(a) || isNaN(v)) {
|
|
619
|
+
return input;
|
|
620
|
+
}
|
|
621
|
+
if (a < 1) {
|
|
622
|
+
return v;
|
|
623
|
+
}
|
|
624
|
+
s = String(v);
|
|
625
|
+
if (a > s.length) {
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
d = parseInt(s.charAt(s.length - a), 10);
|
|
629
|
+
return isNaN(d) ? 0 : d;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Convert an IRI to a URI, percent-encoding non-ASCII and spaces while
|
|
634
|
+
* preserving URI-reserved characters (Django's `iriencode`).
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* {{ "?test=I ♥ Django"|iriencode }}
|
|
638
|
+
* // => ?test=I%20%E2%99%A5%20Django
|
|
639
|
+
*
|
|
640
|
+
* @param {*} input
|
|
641
|
+
* @return {string}
|
|
642
|
+
*/
|
|
643
|
+
exports.iriencode = function (input) {
|
|
644
|
+
var s = String(input),
|
|
645
|
+
out = '',
|
|
646
|
+
i,
|
|
647
|
+
c,
|
|
648
|
+
safe = "/#%[]=:;$&()+,!?*@'~";
|
|
649
|
+
for (i = 0; i < s.length; i += 1) {
|
|
650
|
+
c = s.charAt(i);
|
|
651
|
+
if (/[A-Za-z0-9\-_.]/.test(c) || safe.indexOf(c) !== -1) {
|
|
652
|
+
out += c;
|
|
653
|
+
} else {
|
|
654
|
+
out += pctEncode(c);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return out;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Join a list with a string (Django's `join`).
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* {{ list|join:", " }} // list = ['a', 'b', 'c']
|
|
665
|
+
* // => a, b, c
|
|
666
|
+
*
|
|
667
|
+
* @param {*} input
|
|
668
|
+
* @param {string} [glue=""]
|
|
669
|
+
* @return {*}
|
|
670
|
+
*/
|
|
671
|
+
exports.join = function (input, glue) {
|
|
672
|
+
if (utils.isArray(input)) {
|
|
673
|
+
return input.join(glue !== undefined ? glue : '');
|
|
674
|
+
}
|
|
675
|
+
return input;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Return the last item of an array, the last character of a string, or the
|
|
680
|
+
* last value of a mapping (Django's `last`). An empty sequence yields the
|
|
681
|
+
* empty string.
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* {{ "Joel"|last }}
|
|
685
|
+
* // => l
|
|
686
|
+
*
|
|
687
|
+
* @param {*} input
|
|
688
|
+
* @return {*}
|
|
689
|
+
*/
|
|
690
|
+
exports.last = function (input) {
|
|
691
|
+
var keys;
|
|
692
|
+
if (typeof input === 'string') {
|
|
693
|
+
return input.length ? input.charAt(input.length - 1) : '';
|
|
694
|
+
}
|
|
695
|
+
if (utils.isArray(input)) {
|
|
696
|
+
return input.length ? input[input.length - 1] : '';
|
|
697
|
+
}
|
|
698
|
+
if (input && typeof input === 'object') {
|
|
699
|
+
keys = utils.keys(input);
|
|
700
|
+
return keys.length ? input[keys[keys.length - 1]] : '';
|
|
701
|
+
}
|
|
702
|
+
return '';
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Return the number of items in a sequence (array, string) or the number of
|
|
707
|
+
* keys in a mapping (Django's `length`). A value with no length yields 0.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* {{ "Tacos"|length }}
|
|
711
|
+
* // => 5
|
|
712
|
+
*
|
|
713
|
+
* @param {*} input
|
|
714
|
+
* @return {number}
|
|
715
|
+
*/
|
|
716
|
+
exports.length = function (input) {
|
|
717
|
+
if (typeof input === 'object' && input !== null && !utils.isArray(input)) {
|
|
718
|
+
return utils.keys(input).length;
|
|
719
|
+
}
|
|
720
|
+
if (input && input.hasOwnProperty('length')) {
|
|
721
|
+
return input.length;
|
|
722
|
+
}
|
|
723
|
+
return 0;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Convert newlines in plain text to HTML: a blank line becomes a paragraph
|
|
728
|
+
* break and a single newline becomes a `<br>` (Django's `linebreaks`). The
|
|
729
|
+
* text content is HTML-escaped, and the result is marked safe.
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* {{ "Joel\nis a slug"|linebreaks }}
|
|
733
|
+
* // => <p>Joel<br>is a slug</p>
|
|
734
|
+
*
|
|
735
|
+
* @param {*} input
|
|
736
|
+
* @return {string}
|
|
737
|
+
*/
|
|
738
|
+
exports.linebreaks = function (input) {
|
|
739
|
+
var paras = normalizeNewlines(input).split(/\n{2,}/);
|
|
740
|
+
return utils.map(paras, function (p) {
|
|
741
|
+
return '<p>' + exports.escape(p).replace(/\n/g, '<br>') + '</p>';
|
|
742
|
+
}).join('\n\n');
|
|
743
|
+
};
|
|
744
|
+
exports.linebreaks.safe = true;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Convert every newline to a `<br>` (Django's `linebreaksbr`). The text
|
|
748
|
+
* content is HTML-escaped, and the result is marked safe.
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* {{ "Joel\nis a slug"|linebreaksbr }}
|
|
752
|
+
* // => Joel<br>is a slug
|
|
753
|
+
*
|
|
754
|
+
* @param {*} input
|
|
755
|
+
* @return {string}
|
|
756
|
+
*/
|
|
757
|
+
exports.linebreaksbr = function (input) {
|
|
758
|
+
return exports.escape(normalizeNewlines(input)).replace(/\n/g, '<br>');
|
|
759
|
+
};
|
|
760
|
+
exports.linebreaksbr.safe = true;
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Prefix each line with its line number, zero-padded to a consistent width
|
|
764
|
+
* (Django's `linenumbers`). The line content is HTML-escaped, and the result
|
|
765
|
+
* is marked safe.
|
|
766
|
+
*
|
|
767
|
+
* @example
|
|
768
|
+
* {{ "one\ntwo"|linenumbers }}
|
|
769
|
+
* // => 1. one\n2. two
|
|
770
|
+
*
|
|
771
|
+
* @param {*} input
|
|
772
|
+
* @return {string}
|
|
773
|
+
*/
|
|
774
|
+
exports.linenumbers = function (input) {
|
|
775
|
+
var lines = String(input).split('\n'),
|
|
776
|
+
width = String(lines.length).length;
|
|
777
|
+
return utils.map(lines, function (line, i) {
|
|
778
|
+
var n = String(i + 1);
|
|
779
|
+
while (n.length < width) {
|
|
780
|
+
n = '0' + n;
|
|
781
|
+
}
|
|
782
|
+
return n + '. ' + exports.escape(line);
|
|
783
|
+
}).join('\n');
|
|
784
|
+
};
|
|
785
|
+
exports.linenumbers.safe = true;
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Left-align the value in a field of the given width, padding with spaces
|
|
789
|
+
* (Django's `ljust`).
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* "{{ "Django"|ljust:"10" }}"
|
|
793
|
+
* // => "Django "
|
|
794
|
+
*
|
|
795
|
+
* @param {*} input
|
|
796
|
+
* @param {(number|string)} width
|
|
797
|
+
* @return {string}
|
|
798
|
+
*/
|
|
799
|
+
exports.ljust = function (input, width) {
|
|
800
|
+
var s = String(input),
|
|
801
|
+
w = parseInt(width, 10);
|
|
802
|
+
if (isNaN(w) || s.length >= w) {
|
|
803
|
+
return s;
|
|
804
|
+
}
|
|
805
|
+
return s + new Array(w - s.length + 1).join(' ');
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Return the input in all lowercase letters (Django's `lower`). Recurses into
|
|
810
|
+
* arrays / objects.
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* {{ "SWIG"|lower }}
|
|
814
|
+
* // => swig
|
|
815
|
+
*
|
|
816
|
+
* @param {*} input
|
|
817
|
+
* @return {*}
|
|
818
|
+
*/
|
|
819
|
+
exports.lower = function (input) {
|
|
820
|
+
var out = iterateFilter.apply(exports.lower, arguments);
|
|
821
|
+
if (out !== undefined) {
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
return input.toString().toLowerCase();
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Turn the value into a list of its characters / digits (Django's
|
|
829
|
+
* `make_list`).
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* {{ "Joel"|make_list }}
|
|
833
|
+
* // => ['J', 'o', 'e', 'l']
|
|
834
|
+
*
|
|
835
|
+
* @param {*} input
|
|
836
|
+
* @return {Array}
|
|
837
|
+
*/
|
|
838
|
+
exports.make_list = function (input) {
|
|
839
|
+
return String(input).split('');
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Convert the letters of a phone number to their dial-pad digits (Django's
|
|
844
|
+
* `phone2numeric`). Non-letters pass through.
|
|
845
|
+
*
|
|
846
|
+
* @example
|
|
847
|
+
* {{ "800-COLLECT"|phone2numeric }}
|
|
848
|
+
* // => 800-2655328
|
|
849
|
+
*
|
|
850
|
+
* @param {*} input
|
|
851
|
+
* @return {string}
|
|
852
|
+
*/
|
|
853
|
+
exports.phone2numeric = function (input) {
|
|
854
|
+
var map = {
|
|
855
|
+
a: '2', b: '2', c: '2', d: '3', e: '3', f: '3', g: '4', h: '4', i: '4',
|
|
856
|
+
j: '5', k: '5', l: '5', m: '6', n: '6', o: '6', p: '7', q: '7', r: '7',
|
|
857
|
+
s: '7', t: '8', u: '8', v: '8', w: '9', x: '9', y: '9', z: '9'
|
|
858
|
+
};
|
|
859
|
+
return String(input).toLowerCase().replace(/[a-z]/g, function (c) {
|
|
860
|
+
return map[c];
|
|
861
|
+
});
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Return a plural suffix based on a count (Django's `pluralize`). With no
|
|
866
|
+
* argument the suffix is `"s"`; a `"singular,plural"` argument supplies both
|
|
867
|
+
* forms. The count may be a number or anything with a `length`.
|
|
868
|
+
*
|
|
869
|
+
* @example
|
|
870
|
+
* message{{ 2|pluralize }}
|
|
871
|
+
* // => messages
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* cherr{{ 1|pluralize:"y,ies" }}
|
|
875
|
+
* // => cherry
|
|
876
|
+
*
|
|
877
|
+
* @param {*} input
|
|
878
|
+
* @param {string} [arg="s"]
|
|
879
|
+
* @return {string}
|
|
880
|
+
*/
|
|
881
|
+
exports.pluralize = function (input, arg) {
|
|
882
|
+
var a = (arg === undefined || arg === null) ? 's' : String(arg),
|
|
883
|
+
bits,
|
|
884
|
+
singular,
|
|
885
|
+
plural,
|
|
886
|
+
n;
|
|
887
|
+
if (a.indexOf(',') === -1) {
|
|
888
|
+
a = ',' + a;
|
|
889
|
+
}
|
|
890
|
+
bits = a.split(',');
|
|
891
|
+
if (bits.length > 2) {
|
|
892
|
+
return '';
|
|
893
|
+
}
|
|
894
|
+
singular = bits[0];
|
|
895
|
+
plural = bits[1];
|
|
896
|
+
|
|
897
|
+
n = parseFloat(input);
|
|
898
|
+
if (typeof input === 'number' || (typeof input === 'string' && input.replace(/^\s+|\s+$/g, '') !== '' && !isNaN(n))) {
|
|
899
|
+
return n === 1 ? singular : plural;
|
|
900
|
+
}
|
|
901
|
+
if (input !== null && input !== undefined && typeof input.length === 'number') {
|
|
902
|
+
return input.length === 1 ? singular : plural;
|
|
903
|
+
}
|
|
904
|
+
return '';
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Right-align the value in a field of the given width, padding with spaces
|
|
909
|
+
* (Django's `rjust`).
|
|
910
|
+
*
|
|
911
|
+
* @example
|
|
912
|
+
* "{{ "Django"|rjust:"10" }}"
|
|
913
|
+
* // => " Django"
|
|
914
|
+
*
|
|
915
|
+
* @param {*} input
|
|
916
|
+
* @param {(number|string)} width
|
|
917
|
+
* @return {string}
|
|
918
|
+
*/
|
|
919
|
+
exports.rjust = function (input, width) {
|
|
920
|
+
var s = String(input),
|
|
921
|
+
w = parseInt(width, 10);
|
|
922
|
+
if (isNaN(w) || s.length >= w) {
|
|
923
|
+
return s;
|
|
924
|
+
}
|
|
925
|
+
return new Array(w - s.length + 1).join(' ') + s;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Mark the value as safe, bypassing autoescape (Django's `safe`).
|
|
930
|
+
*
|
|
931
|
+
* @example
|
|
932
|
+
* {{ "<b>bold</b>"|safe }}
|
|
933
|
+
* // => <b>bold</b>
|
|
934
|
+
*
|
|
935
|
+
* @param {*} input
|
|
936
|
+
* @return {*}
|
|
937
|
+
*/
|
|
938
|
+
exports.safe = function (input) {
|
|
939
|
+
return input;
|
|
940
|
+
};
|
|
941
|
+
exports.safe.safe = true;
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Return a slice of a list or string using Python `start:stop:step` subscript
|
|
945
|
+
* syntax (Django's `slice`). Omitted bounds take their step-direction
|
|
946
|
+
* default; a negative step walks backwards (`"::-1"` reverses).
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* {{ list|slice:":2" }} // list = ['a', 'b', 'c']
|
|
950
|
+
* // => ['a', 'b']
|
|
951
|
+
*
|
|
952
|
+
* @param {*} input
|
|
953
|
+
* @param {string} arg A `start:stop:step` slice expression.
|
|
954
|
+
* @return {*}
|
|
955
|
+
*/
|
|
956
|
+
exports.slice = function (input, arg) {
|
|
957
|
+
var bits,
|
|
958
|
+
start,
|
|
959
|
+
stop,
|
|
960
|
+
step;
|
|
961
|
+
|
|
962
|
+
if (!utils.isArray(input) && typeof input !== 'string') {
|
|
963
|
+
return input;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function p(x) {
|
|
967
|
+
return (x === undefined || x === '') ? null : parseInt(x, 10);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
bits = String(arg === undefined || arg === null ? '' : arg).split(':');
|
|
971
|
+
if (bits.length === 1) {
|
|
972
|
+
start = null;
|
|
973
|
+
stop = p(bits[0]);
|
|
974
|
+
step = null;
|
|
975
|
+
} else if (bits.length === 2) {
|
|
976
|
+
start = p(bits[0]);
|
|
977
|
+
stop = p(bits[1]);
|
|
978
|
+
step = null;
|
|
979
|
+
} else {
|
|
980
|
+
start = p(bits[0]);
|
|
981
|
+
stop = p(bits[1]);
|
|
982
|
+
step = p(bits[2]);
|
|
983
|
+
}
|
|
984
|
+
return utils.slice(input, start, stop, step);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Slugify the value: lowercase, ASCII-fold accents, strip non-word
|
|
989
|
+
* characters, and collapse whitespace / hyphen runs to single hyphens
|
|
990
|
+
* (Django's `slugify`).
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* {{ "Joel is a slug"|slugify }}
|
|
994
|
+
* // => joel-is-a-slug
|
|
995
|
+
*
|
|
996
|
+
* @param {*} input
|
|
997
|
+
* @return {string}
|
|
998
|
+
*/
|
|
999
|
+
exports.slugify = function (input) {
|
|
1000
|
+
var value = String(input);
|
|
1001
|
+
if (value.normalize) {
|
|
1002
|
+
value = value.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
|
|
1003
|
+
}
|
|
1004
|
+
value = value.toLowerCase().replace(/[^\w\s-]/g, '');
|
|
1005
|
+
return value.replace(/[-\s]+/g, '-').replace(/^[-_]+|[-_]+$/g, '');
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Strip all HTML / XML tags from the value (Django's `striptags`).
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* {{ "<p>foobar</p>"|striptags }}
|
|
1013
|
+
* // => foobar
|
|
1014
|
+
*
|
|
1015
|
+
* @param {*} input
|
|
1016
|
+
* @return {string}
|
|
1017
|
+
*/
|
|
1018
|
+
exports.striptags = function (input) {
|
|
1019
|
+
return String(input).replace(/(<([^>]+)>)/ig, '');
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Format the time portion of a value (Django's `time`). Uses the same
|
|
1024
|
+
* PHP-style date tokens as `date`; defaults to `'g:i A'` (Django uses the
|
|
1025
|
+
* locale `TIME_FORMAT`, which this engine has no settings system for).
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* {{ value|time:"H:i" }}
|
|
1029
|
+
* // => 01:23
|
|
1030
|
+
*
|
|
1031
|
+
* @param {?(Date|string|number)} input
|
|
1032
|
+
* @param {string} [format='g:i A']
|
|
1033
|
+
* @param {number} [offset]
|
|
1034
|
+
* @param {string} [abbr]
|
|
1035
|
+
* @return {string}
|
|
1036
|
+
*/
|
|
1037
|
+
exports.time = function (input, format, offset, abbr) {
|
|
1038
|
+
return exports.date(input, (format === undefined || format === null || format === '') ? 'g:i A' : format, offset, abbr);
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Title-case the value: uppercase the first letter of each word and lowercase
|
|
1043
|
+
* the rest (Django's `title`).
|
|
1044
|
+
*
|
|
1045
|
+
* @example
|
|
1046
|
+
* {{ "my FIRST post"|title }}
|
|
1047
|
+
* // => My First Post
|
|
1048
|
+
*
|
|
1049
|
+
* @param {*} input
|
|
1050
|
+
* @return {string}
|
|
1051
|
+
*/
|
|
1052
|
+
exports.title = function (input) {
|
|
1053
|
+
return String(input).replace(/\w\S*/g, function (str) {
|
|
1054
|
+
return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Truncate the value to a number of characters, ending with an ellipsis
|
|
1060
|
+
* (Django's `truncatechars`). The ellipsis (`…`) counts toward the length.
|
|
1061
|
+
*
|
|
1062
|
+
* @example
|
|
1063
|
+
* {{ "Joel is a slug"|truncatechars:7 }}
|
|
1064
|
+
* // => Joel i…
|
|
1065
|
+
*
|
|
1066
|
+
* @param {*} input
|
|
1067
|
+
* @param {(number|string)} arg Maximum length including the ellipsis.
|
|
1068
|
+
* @return {string}
|
|
1069
|
+
*/
|
|
1070
|
+
exports.truncatechars = function (input, arg) {
|
|
1071
|
+
var s = String(input),
|
|
1072
|
+
n = parseInt(arg, 10);
|
|
1073
|
+
if (isNaN(n)) {
|
|
1074
|
+
return s;
|
|
1075
|
+
}
|
|
1076
|
+
if (s.length <= n) {
|
|
1077
|
+
return s;
|
|
1078
|
+
}
|
|
1079
|
+
return s.substr(0, n - 1 < 0 ? 0 : n - 1) + '…';
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Truncate the value to a number of words, ending with a space + ellipsis
|
|
1084
|
+
* (Django's `truncatewords`). Whitespace runs within the kept portion are
|
|
1085
|
+
* collapsed to single spaces.
|
|
1086
|
+
*
|
|
1087
|
+
* @example
|
|
1088
|
+
* {{ "Joel is a slug"|truncatewords:2 }}
|
|
1089
|
+
* // => Joel is …
|
|
1090
|
+
*
|
|
1091
|
+
* @param {*} input
|
|
1092
|
+
* @param {(number|string)} arg Maximum number of words.
|
|
1093
|
+
* @return {string}
|
|
1094
|
+
*/
|
|
1095
|
+
exports.truncatewords = function (input, arg) {
|
|
1096
|
+
var n = parseInt(arg, 10),
|
|
1097
|
+
words;
|
|
1098
|
+
if (isNaN(n)) {
|
|
1099
|
+
return String(input);
|
|
1100
|
+
}
|
|
1101
|
+
words = String(input).replace(/^\s+|\s+$/g, '').split(/\s+/);
|
|
1102
|
+
if (words.length === 1 && words[0] === '') {
|
|
1103
|
+
return '';
|
|
1104
|
+
}
|
|
1105
|
+
if (words.length <= n) {
|
|
1106
|
+
return words.join(' ');
|
|
1107
|
+
}
|
|
1108
|
+
return words.slice(0, n).join(' ') + ' …';
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Convert the input to all uppercase letters (Django's `upper`). Recurses
|
|
1113
|
+
* into arrays / objects.
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* {{ "swig"|upper }}
|
|
1117
|
+
* // => SWIG
|
|
1118
|
+
*
|
|
1119
|
+
* @param {*} input
|
|
1120
|
+
* @return {*}
|
|
1121
|
+
*/
|
|
1122
|
+
exports.upper = function (input) {
|
|
1123
|
+
var out = iterateFilter.apply(exports.upper, arguments);
|
|
1124
|
+
if (out !== undefined) {
|
|
1125
|
+
return out;
|
|
1126
|
+
}
|
|
1127
|
+
return input.toString().toUpperCase();
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* URL-encode the value for use in a URL path (Django's `urlencode`). The `/`
|
|
1132
|
+
* separator is preserved by default; pass a string of characters to leave
|
|
1133
|
+
* unencoded as the argument.
|
|
1134
|
+
*
|
|
1135
|
+
* @example
|
|
1136
|
+
* {{ "a b/c"|urlencode }}
|
|
1137
|
+
* // => a%20b/c
|
|
1138
|
+
*
|
|
1139
|
+
* @param {*} input
|
|
1140
|
+
* @param {string} [safe="/"] Characters to leave unencoded.
|
|
1141
|
+
* @return {string}
|
|
1142
|
+
*/
|
|
1143
|
+
exports.urlencode = function (input, safe) {
|
|
1144
|
+
var safeChars = (safe === undefined || safe === null) ? '/' : String(safe);
|
|
1145
|
+
return urlQuote(input, safeChars);
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Count the words in the value (Django's `wordcount`). Words are runs of
|
|
1150
|
+
* non-whitespace characters.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* {{ "Joel is a slug"|wordcount }}
|
|
1154
|
+
* // => 4
|
|
1155
|
+
*
|
|
1156
|
+
* @param {*} input
|
|
1157
|
+
* @return {number}
|
|
1158
|
+
*/
|
|
1159
|
+
exports.wordcount = function (input) {
|
|
1160
|
+
var m = String(input).match(/\S+/g);
|
|
1161
|
+
return m ? m.length : 0;
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Wrap the value's words to a maximum line width (Django's `wordwrap`). Words
|
|
1166
|
+
* are packed greedily; lines are joined with a newline.
|
|
1167
|
+
*
|
|
1168
|
+
* @example
|
|
1169
|
+
* {{ "the quick brown fox"|wordwrap:10 }}
|
|
1170
|
+
* // => the quick\nbrown fox
|
|
1171
|
+
*
|
|
1172
|
+
* @param {*} input
|
|
1173
|
+
* @param {(number|string)} width
|
|
1174
|
+
* @return {string}
|
|
1175
|
+
*/
|
|
1176
|
+
exports.wordwrap = function (input, width) {
|
|
1177
|
+
var w = parseInt(width, 10),
|
|
1178
|
+
words,
|
|
1179
|
+
lines,
|
|
1180
|
+
cur,
|
|
1181
|
+
i,
|
|
1182
|
+
word;
|
|
1183
|
+
if (isNaN(w) || w <= 0) {
|
|
1184
|
+
return String(input);
|
|
1185
|
+
}
|
|
1186
|
+
words = String(input).split(/(\s+)/);
|
|
1187
|
+
lines = [];
|
|
1188
|
+
cur = '';
|
|
1189
|
+
for (i = 0; i < words.length; i += 1) {
|
|
1190
|
+
word = words[i];
|
|
1191
|
+
if (/^\s+$/.test(word) || word === '') {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (cur === '') {
|
|
1195
|
+
cur = word;
|
|
1196
|
+
} else if (cur.length + 1 + word.length <= w) {
|
|
1197
|
+
cur += ' ' + word;
|
|
1198
|
+
} else {
|
|
1199
|
+
lines.push(cur);
|
|
1200
|
+
cur = word;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (cur !== '') {
|
|
1204
|
+
lines.push(cur);
|
|
1205
|
+
}
|
|
1206
|
+
return lines.join('\n');
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Map the value to one of up to three words based on whether it is truthy,
|
|
1211
|
+
* falsy, or `None` (Django's `yesno`). The default mapping is
|
|
1212
|
+
* `"yes,no,maybe"`; with only two words, `None` maps to the second.
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* {{ value|yesno:"yeah,no,maybe" }} // value = None
|
|
1216
|
+
* // => maybe
|
|
1217
|
+
*
|
|
1218
|
+
* @param {*} input
|
|
1219
|
+
* @param {string} [arg="yes,no,maybe"]
|
|
1220
|
+
* @return {*}
|
|
1221
|
+
*/
|
|
1222
|
+
exports.yesno = function (input, arg) {
|
|
1223
|
+
var a = (arg === undefined || arg === null) ? 'yes,no,maybe' : String(arg),
|
|
1224
|
+
bits = a.split(','),
|
|
1225
|
+
yes,
|
|
1226
|
+
no,
|
|
1227
|
+
maybe;
|
|
1228
|
+
if (bits.length < 2) {
|
|
1229
|
+
return input;
|
|
1230
|
+
}
|
|
1231
|
+
yes = bits[0];
|
|
1232
|
+
no = bits[1];
|
|
1233
|
+
maybe = (bits.length === 3) ? bits[2] : bits[1];
|
|
1234
|
+
if (input === null || input === undefined) {
|
|
1235
|
+
return maybe;
|
|
1236
|
+
}
|
|
1237
|
+
if (input) {
|
|
1238
|
+
return yes;
|
|
1239
|
+
}
|
|
1240
|
+
return no;
|
|
1241
|
+
};
|