@rhinostone/swig-jinja2 2.5.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 +56 -0
- package/lib/async/pre-walker.js +267 -0
- package/lib/filters.js +1369 -0
- package/lib/index.js +344 -0
- package/lib/lexer.js +305 -0
- package/lib/parser.js +763 -0
- package/lib/tags/autoescape.js +75 -0
- package/lib/tags/block.js +82 -0
- package/lib/tags/elif.js +33 -0
- package/lib/tags/else.js +27 -0
- package/lib/tags/extends.js +77 -0
- package/lib/tags/filter.js +205 -0
- package/lib/tags/for.js +154 -0
- package/lib/tags/from.js +305 -0
- package/lib/tags/if.js +82 -0
- package/lib/tags/import.js +250 -0
- package/lib/tags/include.js +154 -0
- package/lib/tags/index.js +32 -0
- package/lib/tags/macro.js +174 -0
- package/lib/tags/raw.js +51 -0
- package/lib/tags/set.js +164 -0
- package/lib/tags/with.js +121 -0
- package/lib/tests/index.js +213 -0
- package/lib/tokentypes.js +89 -0
- package/package.json +31 -0
package/lib/filters.js
ADDED
|
@@ -0,0 +1,1369 @@
|
|
|
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
|
+
* Jinja2 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
|
|
10
|
+
* mutation target for `setFilter`. The `.safe = true` convention is
|
|
11
|
+
* inherited from swig-core — filters marked `.safe` suppress the
|
|
12
|
+
* autoescape `e` tail injected in the parser's `parseVariable`.
|
|
13
|
+
*
|
|
14
|
+
* Filter names route through `_filters["<name>"]` at runtime (bracket
|
|
15
|
+
* access on the engine's own filter map), never through the `_ctx`
|
|
16
|
+
* prototype chain, so CVE-2023-25345 guards don't apply at this layer.
|
|
17
|
+
* Filter arg expressions inherit the expression parser's `_dangerousProps`
|
|
18
|
+
* guards.
|
|
19
|
+
*
|
|
20
|
+
* This is the bootstrap set (escape / safe + a couple of basics) that the
|
|
21
|
+
* render pipeline needs to function. The full Jinja2 catalog lands in
|
|
22
|
+
* subsequent commits.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Uppercase the input. Recurses into arrays / objects.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* {{ "swig"|upper }}
|
|
30
|
+
* // => SWIG
|
|
31
|
+
*
|
|
32
|
+
* @param {*} input
|
|
33
|
+
* @return {*}
|
|
34
|
+
*/
|
|
35
|
+
exports.upper = function (input) {
|
|
36
|
+
var out = iterateFilter.apply(exports.upper, arguments);
|
|
37
|
+
if (out !== undefined) {
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
return input.toString().toUpperCase();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Lowercase the input. Recurses into arrays / objects.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* {{ "SWIG"|lower }}
|
|
48
|
+
* // => swig
|
|
49
|
+
*
|
|
50
|
+
* @param {*} input
|
|
51
|
+
* @return {*}
|
|
52
|
+
*/
|
|
53
|
+
exports.lower = function (input) {
|
|
54
|
+
var out = iterateFilter.apply(exports.lower, arguments);
|
|
55
|
+
if (out !== undefined) {
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
return input.toString().toLowerCase();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mark the input as safe, bypassing autoescape. Jinja2 calls this filter
|
|
63
|
+
* `safe`; the value passes through untouched.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* {{ "<b>bold</b>"|safe }}
|
|
67
|
+
* // => <b>bold</b>
|
|
68
|
+
*
|
|
69
|
+
* @param {*} input
|
|
70
|
+
* @return {*}
|
|
71
|
+
*/
|
|
72
|
+
exports.safe = function (input) {
|
|
73
|
+
return input;
|
|
74
|
+
};
|
|
75
|
+
exports.safe.safe = true;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* HTML-escape (default) or JS-escape the input. `e` is the shortcut alias
|
|
79
|
+
* applied by autoescape. The HTML branch preserves already-escaped
|
|
80
|
+
* entities (`&`, `<`, …) so the autoescape tail is idempotent.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* {{ "<b>"|escape }}
|
|
84
|
+
* // => <b>
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* {{ "<b>"|e("js") }}
|
|
88
|
+
* // => <b>
|
|
89
|
+
*
|
|
90
|
+
* @param {*} input
|
|
91
|
+
* @param {string} [type='html'] Pass `'js'` for JavaScript-safe escaping.
|
|
92
|
+
* @return {string}
|
|
93
|
+
*/
|
|
94
|
+
function escapeHtmlRest(ch) {
|
|
95
|
+
return ch === '<' ? '<' : ch === '>' ? '>' : ch === '"' ? '"' : ''';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
exports.escape = function (input, type) {
|
|
99
|
+
var t, inp, out, i, code;
|
|
100
|
+
|
|
101
|
+
if (input === null || input === undefined) {
|
|
102
|
+
return input;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
t = typeof input;
|
|
106
|
+
|
|
107
|
+
if (t !== 'string') {
|
|
108
|
+
if (t === 'object') {
|
|
109
|
+
out = iterateFilter.apply(exports.escape, arguments);
|
|
110
|
+
if (out !== undefined) {
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return input;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (type === 'js') {
|
|
118
|
+
inp = input.replace(/\\/g, '\\u005C');
|
|
119
|
+
out = '';
|
|
120
|
+
for (i = 0; i < inp.length; i += 1) {
|
|
121
|
+
code = inp.charCodeAt(i);
|
|
122
|
+
if (code < 32) {
|
|
123
|
+
code = code.toString(16).toUpperCase();
|
|
124
|
+
code = (code.length < 2) ? '0' + code : code;
|
|
125
|
+
out += '\\u00' + code;
|
|
126
|
+
} else {
|
|
127
|
+
out += inp[i];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out.replace(/&/g, '\\u0026')
|
|
131
|
+
.replace(/</g, '\\u003C')
|
|
132
|
+
.replace(/>/g, '\\u003E')
|
|
133
|
+
.replace(/\'/g, '\\u0027')
|
|
134
|
+
.replace(/"/g, '\\u0022')
|
|
135
|
+
.replace(/\=/g, '\\u003D')
|
|
136
|
+
.replace(/-/g, '\\u002D')
|
|
137
|
+
.replace(/;/g, '\\u003B');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
|
|
141
|
+
.replace(/[<>"']/g, escapeHtmlRest);
|
|
142
|
+
};
|
|
143
|
+
exports.e = exports.escape;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Return the number of items in a sequence (array, string) or the number
|
|
147
|
+
* of keys in a mapping (object). `count` is an alias.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* {{ "Tacos"|length }}
|
|
151
|
+
* // => 5
|
|
152
|
+
*
|
|
153
|
+
* @param {*} input
|
|
154
|
+
* @return {number|string} The length, or "" when the input has none.
|
|
155
|
+
*/
|
|
156
|
+
exports.length = function (input) {
|
|
157
|
+
if (typeof input === 'object' && input !== null && !utils.isArray(input)) {
|
|
158
|
+
return utils.keys(input).length;
|
|
159
|
+
}
|
|
160
|
+
if (input && input.hasOwnProperty('length')) {
|
|
161
|
+
return input.length;
|
|
162
|
+
}
|
|
163
|
+
return '';
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Alias of `length`.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* {{ items|count }}
|
|
171
|
+
* // => 3
|
|
172
|
+
*
|
|
173
|
+
* @param {*} input
|
|
174
|
+
* @return {number|string}
|
|
175
|
+
*/
|
|
176
|
+
exports.count = exports.length;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Return the first item of an array, the first character of a string, or
|
|
180
|
+
* the first value of an object.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* {{ ["a", "b", "c"]|first }}
|
|
184
|
+
* // => a
|
|
185
|
+
*
|
|
186
|
+
* @param {*} input
|
|
187
|
+
* @return {*}
|
|
188
|
+
*/
|
|
189
|
+
exports.first = function (input) {
|
|
190
|
+
if (typeof input === 'object' && input !== null && !utils.isArray(input)) {
|
|
191
|
+
var keys = utils.keys(input);
|
|
192
|
+
return input[keys[0]];
|
|
193
|
+
}
|
|
194
|
+
if (typeof input === 'string') {
|
|
195
|
+
return input.substr(0, 1);
|
|
196
|
+
}
|
|
197
|
+
if (utils.isArray(input)) {
|
|
198
|
+
return input[0];
|
|
199
|
+
}
|
|
200
|
+
return input;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Return the last item of an array, the last character of a string, or
|
|
205
|
+
* the last value of an object.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* {{ ["a", "b", "c"]|last }}
|
|
209
|
+
* // => c
|
|
210
|
+
*
|
|
211
|
+
* @param {*} input
|
|
212
|
+
* @return {*}
|
|
213
|
+
*/
|
|
214
|
+
exports.last = function (input) {
|
|
215
|
+
if (typeof input === 'object' && input !== null && !utils.isArray(input)) {
|
|
216
|
+
var keys = utils.keys(input);
|
|
217
|
+
return input[keys[keys.length - 1]];
|
|
218
|
+
}
|
|
219
|
+
if (typeof input === 'string') {
|
|
220
|
+
return input.charAt(input.length - 1);
|
|
221
|
+
}
|
|
222
|
+
if (utils.isArray(input)) {
|
|
223
|
+
return input[input.length - 1];
|
|
224
|
+
}
|
|
225
|
+
return input;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Join a sequence with a string glue. The default glue is the empty
|
|
230
|
+
* string (Jinja2 default), not a comma. A mapping joins its keys.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* {{ ["foo", "bar", "baz"]|join(", ") }}
|
|
234
|
+
* // => foo, bar, baz
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* {{ [1, 2, 3]|join }}
|
|
238
|
+
* // => 123
|
|
239
|
+
*
|
|
240
|
+
* @param {*} input
|
|
241
|
+
* @param {string} [glue='']
|
|
242
|
+
* @return {string}
|
|
243
|
+
*/
|
|
244
|
+
exports.join = function (input, glue) {
|
|
245
|
+
if (glue === undefined) { glue = ''; }
|
|
246
|
+
if (utils.isArray(input)) {
|
|
247
|
+
return input.join(glue);
|
|
248
|
+
}
|
|
249
|
+
if (input && typeof input === 'object') {
|
|
250
|
+
return utils.keys(input).join(glue);
|
|
251
|
+
}
|
|
252
|
+
return input;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reverse an array or string. Does not sort — items come out in reverse
|
|
257
|
+
* input order.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* {{ [1, 2, 3]|reverse|join(",") }}
|
|
261
|
+
* // => 3,2,1
|
|
262
|
+
*
|
|
263
|
+
* @param {array|string} input
|
|
264
|
+
* @return {array|string}
|
|
265
|
+
*/
|
|
266
|
+
exports.reverse = function (input) {
|
|
267
|
+
if (utils.isArray(input)) {
|
|
268
|
+
return utils.extend([], input).reverse();
|
|
269
|
+
}
|
|
270
|
+
if (typeof input === 'string') {
|
|
271
|
+
return input.split('').reverse().join('');
|
|
272
|
+
}
|
|
273
|
+
return input;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Sort an array ascending, returning a copy (does not mutate the input).
|
|
278
|
+
* Numbers sort numerically; everything else compares case-insensitively.
|
|
279
|
+
* A string sorts its characters; an object sorts its keys. Pass a truthy
|
|
280
|
+
* first argument to sort descending.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* {{ [3, 1, 2]|sort|join(",") }}
|
|
284
|
+
* // => 1,2,3
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* {{ [3, 1, 2]|sort(true)|join(",") }}
|
|
288
|
+
* // => 3,2,1
|
|
289
|
+
*
|
|
290
|
+
* @param {*} input
|
|
291
|
+
* @param {boolean} [reverse=false] Sort descending when truthy.
|
|
292
|
+
* @return {*}
|
|
293
|
+
*/
|
|
294
|
+
exports.sort = function (input, reverse) {
|
|
295
|
+
var arr, isString = false;
|
|
296
|
+
if (utils.isArray(input)) {
|
|
297
|
+
arr = utils.extend([], input);
|
|
298
|
+
} else if (typeof input === 'string') {
|
|
299
|
+
arr = input.split('');
|
|
300
|
+
isString = true;
|
|
301
|
+
} else if (input && typeof input === 'object') {
|
|
302
|
+
arr = utils.keys(input);
|
|
303
|
+
} else {
|
|
304
|
+
return input;
|
|
305
|
+
}
|
|
306
|
+
arr.sort(function (a, b) {
|
|
307
|
+
if (typeof a === 'number' && typeof b === 'number') { return a - b; }
|
|
308
|
+
var sa = String(a).toLowerCase(), sb = String(b).toLowerCase();
|
|
309
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
310
|
+
});
|
|
311
|
+
if (reverse) { arr.reverse(); }
|
|
312
|
+
return isString ? arr.join('') : arr;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/*!
|
|
316
|
+
* Resolve a possibly-dotted attribute path against an object. Returns
|
|
317
|
+
* undefined if any segment is missing. @private
|
|
318
|
+
*/
|
|
319
|
+
function resolveAttr(obj, path) {
|
|
320
|
+
var segs = String(path).split('.'), cur = obj, i;
|
|
321
|
+
for (i = 0; i < segs.length; i += 1) {
|
|
322
|
+
if (cur === null || cur === undefined) { return undefined; }
|
|
323
|
+
cur = cur[segs[i]];
|
|
324
|
+
}
|
|
325
|
+
return cur;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Return the input, or a default value when the input is undefined, null,
|
|
330
|
+
* or the empty string. (A missing variable arrives here as "" because the
|
|
331
|
+
* engine coerces undefined variable lookups, so the empty string counts as
|
|
332
|
+
* "needs a default".) Real falsy values 0 and false are preserved. Pass a
|
|
333
|
+
* truthy second-after argument to fall back on any falsy value instead.
|
|
334
|
+
* `d` is an alias.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* {{ missing|default("anonymous") }}
|
|
338
|
+
* // => anonymous
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* {{ 0|default("n/a", true) }}
|
|
342
|
+
* // => n/a
|
|
343
|
+
*
|
|
344
|
+
* @param {*} input
|
|
345
|
+
* @param {*} [def='']
|
|
346
|
+
* @param {boolean} [bool=false] Fall back on any falsy value when truthy.
|
|
347
|
+
* @return {*}
|
|
348
|
+
*/
|
|
349
|
+
exports['default'] = function (input, def, bool) {
|
|
350
|
+
if (def === undefined) { def = ''; }
|
|
351
|
+
if (bool) {
|
|
352
|
+
return input ? input : def;
|
|
353
|
+
}
|
|
354
|
+
return (input === undefined || input === null || input === '') ? def : input;
|
|
355
|
+
};
|
|
356
|
+
exports.d = exports['default'];
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Absolute value of a number.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* {{ -42|abs }}
|
|
363
|
+
* // => 42
|
|
364
|
+
*
|
|
365
|
+
* @param {number} input
|
|
366
|
+
* @return {number}
|
|
367
|
+
*/
|
|
368
|
+
exports.abs = function (input) {
|
|
369
|
+
var n = Number(input);
|
|
370
|
+
return isNaN(n) ? input : Math.abs(n);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Round a number to a given precision. Method is `"common"` (round half
|
|
375
|
+
* away from zero, the default), `"ceil"`, or `"floor"`.
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* {{ 42.55|round(1) }}
|
|
379
|
+
* // => 42.6
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* {{ 42.55|round(1, "floor") }}
|
|
383
|
+
* // => 42.5
|
|
384
|
+
*
|
|
385
|
+
* @param {number} input
|
|
386
|
+
* @param {number} [precision=0]
|
|
387
|
+
* @param {string} [method='common']
|
|
388
|
+
* @return {number}
|
|
389
|
+
*/
|
|
390
|
+
exports.round = function (input, precision, method) {
|
|
391
|
+
var n = Number(input);
|
|
392
|
+
if (isNaN(n)) { return input; }
|
|
393
|
+
var factor = Math.pow(10, precision || 0);
|
|
394
|
+
n = n * factor;
|
|
395
|
+
if (method === 'ceil') {
|
|
396
|
+
n = Math.ceil(n);
|
|
397
|
+
} else if (method === 'floor') {
|
|
398
|
+
n = Math.floor(n);
|
|
399
|
+
} else {
|
|
400
|
+
n = (n < 0) ? -Math.round(-n) : Math.round(n);
|
|
401
|
+
}
|
|
402
|
+
return n / factor;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Convert the input to an integer, returning a default (0) when the
|
|
407
|
+
* conversion fails. A non-decimal base may be given as the third argument.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* {{ "42.7"|int }}
|
|
411
|
+
* // => 42
|
|
412
|
+
*
|
|
413
|
+
* @param {*} input
|
|
414
|
+
* @param {number} [def=0]
|
|
415
|
+
* @param {number} [base=10]
|
|
416
|
+
* @return {number}
|
|
417
|
+
*/
|
|
418
|
+
exports.int = function (input, def, base) {
|
|
419
|
+
if (def === undefined) { def = 0; }
|
|
420
|
+
var n = parseInt(input, base || 10);
|
|
421
|
+
return isNaN(n) ? def : n;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Convert the input to a floating-point number, returning a default (0)
|
|
426
|
+
* when the conversion fails.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* {{ "42.5"|float }}
|
|
430
|
+
* // => 42.5
|
|
431
|
+
*
|
|
432
|
+
* @param {*} input
|
|
433
|
+
* @param {number} [def=0]
|
|
434
|
+
* @return {number}
|
|
435
|
+
*/
|
|
436
|
+
exports.float = function (input, def) {
|
|
437
|
+
if (def === undefined) { def = 0; }
|
|
438
|
+
var n = parseFloat(input);
|
|
439
|
+
return isNaN(n) ? def : n;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Truncate a string to a length, appending an ellipsis when cut. By
|
|
444
|
+
* default the cut falls back to the last word boundary; pass a truthy
|
|
445
|
+
* third argument to cut mid-word. Strings within `leeway` characters of
|
|
446
|
+
* the limit are left whole.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* {{ "foo bar baz qux"|truncate(9) }}
|
|
450
|
+
* // => foo...
|
|
451
|
+
*
|
|
452
|
+
* @param {*} input
|
|
453
|
+
* @param {number} [length=255]
|
|
454
|
+
* @param {boolean} [killwords=false]
|
|
455
|
+
* @param {string} [end='...']
|
|
456
|
+
* @param {number} [leeway=5]
|
|
457
|
+
* @return {string}
|
|
458
|
+
*/
|
|
459
|
+
exports.truncate = function (input, length, killwords, end, leeway) {
|
|
460
|
+
input = String(input);
|
|
461
|
+
length = length || 255;
|
|
462
|
+
if (end === undefined) { end = '...'; }
|
|
463
|
+
if (leeway === undefined) { leeway = 5; }
|
|
464
|
+
if (input.length <= length + leeway) {
|
|
465
|
+
return input;
|
|
466
|
+
}
|
|
467
|
+
var truncated = input.substr(0, length - end.length);
|
|
468
|
+
if (!killwords) {
|
|
469
|
+
var lastSpace = truncated.lastIndexOf(' ');
|
|
470
|
+
if (lastSpace !== -1) {
|
|
471
|
+
truncated = truncated.substr(0, lastSpace);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return truncated + end;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Serialize the input to a JSON string, escaping the HTML-significant
|
|
479
|
+
* characters (`<`, `>`, `&`, `'`) so the result is safe to embed in a
|
|
480
|
+
* page. Marked `.safe`, so the autoescape tail does not double-escape it.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* {{ {"a": 1}|tojson }}
|
|
484
|
+
* // => {"a":1}
|
|
485
|
+
*
|
|
486
|
+
* @param {*} input
|
|
487
|
+
* @param {number} [indent] Spaces of indentation for pretty output.
|
|
488
|
+
* @return {string}
|
|
489
|
+
*/
|
|
490
|
+
exports.tojson = function (input, indent) {
|
|
491
|
+
var json = JSON.stringify(input, null, indent || undefined);
|
|
492
|
+
if (json === undefined) {
|
|
493
|
+
return '';
|
|
494
|
+
}
|
|
495
|
+
return json
|
|
496
|
+
.replace(/</g, '\\u003c')
|
|
497
|
+
.replace(/>/g, '\\u003e')
|
|
498
|
+
.replace(/&/g, '\\u0026')
|
|
499
|
+
.replace(/'/g, '\\u0027');
|
|
500
|
+
};
|
|
501
|
+
exports.tojson.safe = true;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Group a sequence of objects by a (possibly dotted) attribute, returning
|
|
505
|
+
* a list of `{ grouper, list }` records sorted by grouper.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* {% for g in users|groupby("dept") %}{{ g.grouper }}:{{ g.list|length }} {% endfor %}
|
|
509
|
+
*
|
|
510
|
+
* @param {Array} input
|
|
511
|
+
* @param {string} attribute
|
|
512
|
+
* @return {Array} List of `{ grouper, list }`.
|
|
513
|
+
*/
|
|
514
|
+
exports.groupby = function (input, attribute) {
|
|
515
|
+
if (!utils.isArray(input)) {
|
|
516
|
+
return input;
|
|
517
|
+
}
|
|
518
|
+
var groups = {}, order = [];
|
|
519
|
+
utils.each(input, function (item) {
|
|
520
|
+
var key = resolveAttr(item, attribute);
|
|
521
|
+
if (!groups.hasOwnProperty(key)) {
|
|
522
|
+
groups[key] = [];
|
|
523
|
+
order.push(key);
|
|
524
|
+
}
|
|
525
|
+
groups[key].push(item);
|
|
526
|
+
});
|
|
527
|
+
order.sort();
|
|
528
|
+
return utils.map(order, function (key) {
|
|
529
|
+
return { grouper: key, list: groups[key] };
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Title-case the input: every word starts with an uppercase letter and the
|
|
535
|
+
* rest are lowercased. Word boundaries are whitespace and any of
|
|
536
|
+
* `- ( [ { <` (matching Jinja2's `title`), so `foo-bar` becomes `Foo-Bar`
|
|
537
|
+
* but an apostrophe does not split a word (`don't` => `Don't`). Recurses
|
|
538
|
+
* into arrays / objects.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* {{ "hello world"|title }}
|
|
542
|
+
* // => Hello World
|
|
543
|
+
*
|
|
544
|
+
* @param {*} input
|
|
545
|
+
* @return {*}
|
|
546
|
+
*/
|
|
547
|
+
exports.title = function (input) {
|
|
548
|
+
var out = iterateFilter.apply(exports.title, arguments),
|
|
549
|
+
parts,
|
|
550
|
+
res,
|
|
551
|
+
item,
|
|
552
|
+
i;
|
|
553
|
+
if (out !== undefined) {
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
parts = String(input).split(/([-\s(\[{<]+)/);
|
|
557
|
+
res = '';
|
|
558
|
+
for (i = 0; i < parts.length; i += 1) {
|
|
559
|
+
item = parts[i];
|
|
560
|
+
if (!item) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
res += item.charAt(0).toUpperCase() + item.substr(1).toLowerCase();
|
|
564
|
+
}
|
|
565
|
+
return res;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Capitalize the input: uppercase the first character, lowercase the rest.
|
|
570
|
+
* Recurses into arrays / objects.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* {{ "hello WORLD"|capitalize }}
|
|
574
|
+
* // => Hello world
|
|
575
|
+
*
|
|
576
|
+
* @param {*} input
|
|
577
|
+
* @return {*}
|
|
578
|
+
*/
|
|
579
|
+
exports.capitalize = function (input) {
|
|
580
|
+
var out = iterateFilter.apply(exports.capitalize, arguments);
|
|
581
|
+
if (out !== undefined) {
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
input = input.toString();
|
|
585
|
+
return input.charAt(0).toUpperCase() + input.substr(1).toLowerCase();
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Strip SGML/XML tags (and HTML comments) from the input, then collapse
|
|
590
|
+
* runs of whitespace to a single space and trim the ends — matching
|
|
591
|
+
* Jinja2's `striptags`. Recurses into arrays / objects.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* {{ "<p>hello world</p>"|striptags }}
|
|
595
|
+
* // => hello world
|
|
596
|
+
*
|
|
597
|
+
* @param {*} input
|
|
598
|
+
* @return {*}
|
|
599
|
+
*/
|
|
600
|
+
exports.striptags = function (input) {
|
|
601
|
+
var out = iterateFilter.apply(exports.striptags, arguments);
|
|
602
|
+
if (out !== undefined) {
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
return String(input)
|
|
606
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
607
|
+
.replace(/<[^>]*>/g, '')
|
|
608
|
+
.replace(/\s+/g, ' ')
|
|
609
|
+
.replace(/^\s+|\s+$/g, '');
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Strip leading and trailing characters from a string. With no argument
|
|
614
|
+
* the stripped set is whitespace; pass a string of characters to strip
|
|
615
|
+
* those instead (Jinja2's `trim`). Both ends are always stripped.
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* {{ " hello "|trim }}
|
|
619
|
+
* // => hello
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* {{ "xxhixx"|trim("x") }}
|
|
623
|
+
* // => hi
|
|
624
|
+
*
|
|
625
|
+
* @param {*} input
|
|
626
|
+
* @param {string} [chars] Characters to strip; defaults to whitespace.
|
|
627
|
+
* @return {*}
|
|
628
|
+
*/
|
|
629
|
+
exports.trim = function (input, chars) {
|
|
630
|
+
var pattern;
|
|
631
|
+
if (typeof input !== 'string') {
|
|
632
|
+
return input;
|
|
633
|
+
}
|
|
634
|
+
if (chars === undefined || chars === null || chars === '') {
|
|
635
|
+
pattern = '\\s';
|
|
636
|
+
} else {
|
|
637
|
+
pattern = '[' + String(chars).replace(/[\\\[\]\^\-]/g, '\\$&') + ']';
|
|
638
|
+
}
|
|
639
|
+
return input
|
|
640
|
+
.replace(new RegExp('^' + pattern + '+'), '')
|
|
641
|
+
.replace(new RegExp(pattern + '+$'), '');
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Replace occurrences of a literal substring with another. Unlike the
|
|
646
|
+
* native swig `replace` (regex) and the Twig `replace` (mapping), Jinja2's
|
|
647
|
+
* `replace` is a plain left-to-right literal substitution. An optional
|
|
648
|
+
* count limits how many occurrences are replaced.
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* {{ "Hello World"|replace("Hello", "Goodbye") }}
|
|
652
|
+
* // => Goodbye World
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* {{ "aaa"|replace("a", "b", 2) }}
|
|
656
|
+
* // => bba
|
|
657
|
+
*
|
|
658
|
+
* @param {*} input
|
|
659
|
+
* @param {string} old The substring to find.
|
|
660
|
+
* @param {string} [neu=''] The replacement.
|
|
661
|
+
* @param {number} [count] Cap on the number of replacements.
|
|
662
|
+
* @return {*}
|
|
663
|
+
*/
|
|
664
|
+
exports.replace = function (input, old, neu, count) {
|
|
665
|
+
var limited, max, out, i, n;
|
|
666
|
+
if (typeof input !== 'string') {
|
|
667
|
+
return input;
|
|
668
|
+
}
|
|
669
|
+
old = String(old);
|
|
670
|
+
neu = (neu === undefined || neu === null) ? '' : String(neu);
|
|
671
|
+
if (old === '') {
|
|
672
|
+
return input;
|
|
673
|
+
}
|
|
674
|
+
limited = (count !== undefined && count !== null);
|
|
675
|
+
max = limited ? Number(count) : Infinity;
|
|
676
|
+
out = '';
|
|
677
|
+
i = 0;
|
|
678
|
+
n = 0;
|
|
679
|
+
while (i < input.length) {
|
|
680
|
+
if (n < max && input.substr(i, old.length) === old) {
|
|
681
|
+
out += neu;
|
|
682
|
+
i += old.length;
|
|
683
|
+
n += 1;
|
|
684
|
+
} else {
|
|
685
|
+
out += input.charAt(i);
|
|
686
|
+
i += 1;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/*!
|
|
693
|
+
* printf-style format expansion backing the `format` filter. Supports the
|
|
694
|
+
* common conversions (s d i u f F e E g G x X o c %) with optional flags
|
|
695
|
+
* (`-` `+` space `0`), width, and `.precision`. @private
|
|
696
|
+
*/
|
|
697
|
+
function sprintf(fmt, args) {
|
|
698
|
+
var idx = 0;
|
|
699
|
+
return fmt.replace(/%([-+ 0]*)(\d+)?(?:\.(\d+))?([sdiufFeEgGxXoc%])/g, function (m, flags, width, prec, conv) {
|
|
700
|
+
var arg, hasPrec, sign, s, num, pad;
|
|
701
|
+
|
|
702
|
+
function withSign(value) {
|
|
703
|
+
if (value < 0) { sign = '-'; return -value; }
|
|
704
|
+
if (flags.indexOf('+') !== -1) { sign = '+'; } else if (flags.indexOf(' ') !== -1) { sign = ' '; }
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (conv === '%') {
|
|
709
|
+
return '%';
|
|
710
|
+
}
|
|
711
|
+
arg = args[idx];
|
|
712
|
+
idx += 1;
|
|
713
|
+
flags = flags || '';
|
|
714
|
+
hasPrec = (prec !== undefined && prec !== '');
|
|
715
|
+
width = width ? parseInt(width, 10) : 0;
|
|
716
|
+
prec = hasPrec ? parseInt(prec, 10) : undefined;
|
|
717
|
+
sign = '';
|
|
718
|
+
|
|
719
|
+
switch (conv) {
|
|
720
|
+
case 's':
|
|
721
|
+
s = String(arg);
|
|
722
|
+
if (hasPrec) { s = s.substring(0, prec); }
|
|
723
|
+
break;
|
|
724
|
+
case 'd':
|
|
725
|
+
case 'i':
|
|
726
|
+
case 'u':
|
|
727
|
+
num = parseInt(arg, 10);
|
|
728
|
+
if (isNaN(num)) { num = 0; }
|
|
729
|
+
s = String(withSign(num));
|
|
730
|
+
break;
|
|
731
|
+
case 'f':
|
|
732
|
+
case 'F':
|
|
733
|
+
num = parseFloat(arg);
|
|
734
|
+
if (isNaN(num)) { num = 0; }
|
|
735
|
+
s = withSign(num).toFixed(hasPrec ? prec : 6);
|
|
736
|
+
break;
|
|
737
|
+
case 'e':
|
|
738
|
+
case 'E':
|
|
739
|
+
num = parseFloat(arg);
|
|
740
|
+
if (isNaN(num)) { num = 0; }
|
|
741
|
+
s = withSign(num).toExponential(hasPrec ? prec : 6);
|
|
742
|
+
if (conv === 'E') { s = s.toUpperCase(); }
|
|
743
|
+
break;
|
|
744
|
+
case 'g':
|
|
745
|
+
case 'G':
|
|
746
|
+
num = parseFloat(arg);
|
|
747
|
+
if (isNaN(num)) { num = 0; }
|
|
748
|
+
s = String(withSign(num));
|
|
749
|
+
if (conv === 'G') { s = s.toUpperCase(); }
|
|
750
|
+
break;
|
|
751
|
+
case 'x':
|
|
752
|
+
s = (parseInt(arg, 10) >>> 0).toString(16);
|
|
753
|
+
break;
|
|
754
|
+
case 'X':
|
|
755
|
+
s = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase();
|
|
756
|
+
break;
|
|
757
|
+
case 'o':
|
|
758
|
+
s = (parseInt(arg, 10) >>> 0).toString(8);
|
|
759
|
+
break;
|
|
760
|
+
case 'c':
|
|
761
|
+
s = String.fromCharCode(parseInt(arg, 10));
|
|
762
|
+
break;
|
|
763
|
+
default:
|
|
764
|
+
return m;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
pad = width - (sign.length + s.length);
|
|
768
|
+
if (pad > 0) {
|
|
769
|
+
if (flags.indexOf('-') !== -1) {
|
|
770
|
+
s = sign + s + new Array(pad + 1).join(' ');
|
|
771
|
+
} else if (flags.indexOf('0') !== -1 && conv !== 's') {
|
|
772
|
+
s = sign + new Array(pad + 1).join('0') + s;
|
|
773
|
+
} else {
|
|
774
|
+
s = new Array(pad + 1).join(' ') + sign + s;
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
s = sign + s;
|
|
778
|
+
}
|
|
779
|
+
return s;
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* printf-style string formatting (Jinja2's `format`). The `%` placeholders
|
|
785
|
+
* in the format string are filled from the filter arguments in order.
|
|
786
|
+
* Supports conversions `s d i u f F e E g G x X o c` and `%%`, with
|
|
787
|
+
* optional flags (`-` `+` space `0`), a width, and a `.precision`. Use
|
|
788
|
+
* `%%` for a literal percent sign.
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* {{ "%s is %d"|format("age", 42) }}
|
|
792
|
+
* // => age is 42
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* {{ "%.2f"|format(3.14159) }}
|
|
796
|
+
* // => 3.14
|
|
797
|
+
*
|
|
798
|
+
* @param {string} input The format string.
|
|
799
|
+
* @return {*}
|
|
800
|
+
*/
|
|
801
|
+
exports.format = function (input) {
|
|
802
|
+
if (typeof input !== 'string') {
|
|
803
|
+
return input;
|
|
804
|
+
}
|
|
805
|
+
return sprintf(input, Array.prototype.slice.call(arguments, 1));
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Count the words in a string. Words are runs of word characters
|
|
810
|
+
* (`[A-Za-z0-9_]`), matching Jinja2's `wordcount`.
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* {{ "one, two; three!"|wordcount }}
|
|
814
|
+
* // => 3
|
|
815
|
+
*
|
|
816
|
+
* @param {*} input
|
|
817
|
+
* @return {number}
|
|
818
|
+
*/
|
|
819
|
+
exports.wordcount = function (input) {
|
|
820
|
+
var m = String(input).match(/\w+/g);
|
|
821
|
+
return m ? m.length : 0;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Word-wrap a string to a given line width (Jinja2's `wordwrap`). Words are
|
|
826
|
+
* packed greedily; a word longer than the width is split when
|
|
827
|
+
* `breakLongWords` is truthy (the default). Lines are joined with
|
|
828
|
+
* `wrapstring` (default newline).
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* {{ "the quick brown fox"|wordwrap(10) }}
|
|
832
|
+
* // => the quick\nbrown fox
|
|
833
|
+
*
|
|
834
|
+
* @param {*} input
|
|
835
|
+
* @param {number} [width=79]
|
|
836
|
+
* @param {boolean} [breakLongWords=true]
|
|
837
|
+
* @param {string} [wrapstring='\n']
|
|
838
|
+
* @return {*}
|
|
839
|
+
*/
|
|
840
|
+
exports.wordwrap = function (input, width, breakLongWords, wrapstring) {
|
|
841
|
+
var words, lines, cur, i, word, space, head;
|
|
842
|
+
if (typeof input !== 'string') {
|
|
843
|
+
return input;
|
|
844
|
+
}
|
|
845
|
+
if (width === undefined || width === null) { width = 79; }
|
|
846
|
+
if (breakLongWords === undefined) { breakLongWords = true; }
|
|
847
|
+
if (wrapstring === undefined || wrapstring === null) { wrapstring = '\n'; }
|
|
848
|
+
words = input.split(/\s+/);
|
|
849
|
+
lines = [];
|
|
850
|
+
cur = '';
|
|
851
|
+
for (i = 0; i < words.length; i += 1) {
|
|
852
|
+
word = words[i];
|
|
853
|
+
if (word === '') { continue; }
|
|
854
|
+
while (breakLongWords && word.length > width) {
|
|
855
|
+
space = (cur === '') ? width : width - cur.length - 1;
|
|
856
|
+
if (space <= 0) {
|
|
857
|
+
lines.push(cur);
|
|
858
|
+
cur = '';
|
|
859
|
+
space = width;
|
|
860
|
+
}
|
|
861
|
+
head = word.substr(0, space);
|
|
862
|
+
cur = (cur === '') ? head : cur + ' ' + head;
|
|
863
|
+
lines.push(cur);
|
|
864
|
+
cur = '';
|
|
865
|
+
word = word.substr(space);
|
|
866
|
+
}
|
|
867
|
+
if (word === '') { continue; }
|
|
868
|
+
if (cur === '') {
|
|
869
|
+
cur = word;
|
|
870
|
+
} else if (cur.length + 1 + word.length <= width) {
|
|
871
|
+
cur += ' ' + word;
|
|
872
|
+
} else {
|
|
873
|
+
lines.push(cur);
|
|
874
|
+
cur = word;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (cur !== '') { lines.push(cur); }
|
|
878
|
+
return lines.join(wrapstring);
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Indent every line of a string by `width` spaces (or a literal string).
|
|
883
|
+
* The first line and blank lines are not indented by default — pass a
|
|
884
|
+
* truthy `first` to indent the first line and a truthy `blank` to indent
|
|
885
|
+
* blank lines (Jinja2's `indent`).
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* {{ "line1\nline2"|indent(4) }}
|
|
889
|
+
* // => line1\n line2
|
|
890
|
+
*
|
|
891
|
+
* @param {*} input
|
|
892
|
+
* @param {number|string} [width=4]
|
|
893
|
+
* @param {boolean} [first=false]
|
|
894
|
+
* @param {boolean} [blank=false]
|
|
895
|
+
* @return {*}
|
|
896
|
+
*/
|
|
897
|
+
exports.indent = function (input, width, first, blank) {
|
|
898
|
+
var indent, lines, rv, head;
|
|
899
|
+
if (width === undefined || width === null) { width = 4; }
|
|
900
|
+
indent = (typeof width === 'number') ? new Array(width + 1).join(' ') : String(width);
|
|
901
|
+
lines = (String(input) + '\n').split('\n');
|
|
902
|
+
if (lines.length && lines[lines.length - 1] === '') {
|
|
903
|
+
lines.pop();
|
|
904
|
+
}
|
|
905
|
+
if (blank) {
|
|
906
|
+
rv = lines.join('\n' + indent);
|
|
907
|
+
} else {
|
|
908
|
+
head = lines.shift();
|
|
909
|
+
rv = (head === undefined) ? '' : head;
|
|
910
|
+
if (lines.length) {
|
|
911
|
+
rv += '\n' + utils.map(lines, function (line) {
|
|
912
|
+
return line ? indent + line : line;
|
|
913
|
+
}).join('\n');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (first) {
|
|
917
|
+
rv = indent + rv;
|
|
918
|
+
}
|
|
919
|
+
return rv;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Center a string in a field of the given width using spaces (Jinja2's
|
|
924
|
+
* `center`, which defers to Python `str.center`). When an odd number of
|
|
925
|
+
* pad characters is needed the extra space goes on the right.
|
|
926
|
+
*
|
|
927
|
+
* @example
|
|
928
|
+
* {{ "foo"|center(9) }}
|
|
929
|
+
* // => " foo "
|
|
930
|
+
*
|
|
931
|
+
* @param {*} input
|
|
932
|
+
* @param {number} [width=80]
|
|
933
|
+
* @return {*}
|
|
934
|
+
*/
|
|
935
|
+
exports.center = function (input, width) {
|
|
936
|
+
var marg, left, right;
|
|
937
|
+
input = String(input);
|
|
938
|
+
if (width === undefined || width === null) { width = 80; }
|
|
939
|
+
marg = width - input.length;
|
|
940
|
+
if (marg <= 0) {
|
|
941
|
+
return input;
|
|
942
|
+
}
|
|
943
|
+
left = Math.floor(marg / 2) + (marg & width & 1);
|
|
944
|
+
right = marg - left;
|
|
945
|
+
return new Array(left + 1).join(' ') + input + new Array(right + 1).join(' ');
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Convert the input into a list (Jinja2's `list`). A string becomes a list
|
|
950
|
+
* of its characters, an array is copied, and a mapping yields its keys. A
|
|
951
|
+
* scalar is wrapped in a single-element list (Jinja2 would raise; this
|
|
952
|
+
* degrades gracefully); null / undefined yields an empty list.
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* {{ "abc"|list|join("-") }}
|
|
956
|
+
* // => a-b-c
|
|
957
|
+
*
|
|
958
|
+
* @param {*} input
|
|
959
|
+
* @return {Array}
|
|
960
|
+
*/
|
|
961
|
+
exports.list = function (input) {
|
|
962
|
+
if (typeof input === 'string') {
|
|
963
|
+
return input.split('');
|
|
964
|
+
}
|
|
965
|
+
if (utils.isArray(input)) {
|
|
966
|
+
return utils.extend([], input);
|
|
967
|
+
}
|
|
968
|
+
if (input && typeof input === 'object') {
|
|
969
|
+
return utils.keys(input);
|
|
970
|
+
}
|
|
971
|
+
return (input === null || input === undefined) ? [] : [input];
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Return the unique items of a sequence, preserving first-seen order
|
|
976
|
+
* (Jinja2's `unique`). String comparison is case-insensitive by default;
|
|
977
|
+
* pass a truthy `caseSensitive` to compare exactly.
|
|
978
|
+
*
|
|
979
|
+
* @example
|
|
980
|
+
* {{ [1, 2, 2, 3, 1]|unique|join(",") }}
|
|
981
|
+
* // => 1,2,3
|
|
982
|
+
*
|
|
983
|
+
* @param {*} input
|
|
984
|
+
* @param {boolean} [caseSensitive=false]
|
|
985
|
+
* @return {*}
|
|
986
|
+
*/
|
|
987
|
+
exports.unique = function (input, caseSensitive) {
|
|
988
|
+
var seen = [], out = [];
|
|
989
|
+
if (typeof input === 'string') {
|
|
990
|
+
input = input.split('');
|
|
991
|
+
} else if (!utils.isArray(input)) {
|
|
992
|
+
return input;
|
|
993
|
+
}
|
|
994
|
+
utils.each(input, function (item) {
|
|
995
|
+
var key = (!caseSensitive && typeof item === 'string') ? item.toLowerCase() : item;
|
|
996
|
+
if (seen.indexOf(key) === -1) {
|
|
997
|
+
seen.push(key);
|
|
998
|
+
out.push(item);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
return out;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Batch items into rows of `size` (Jinja2's `batch`). The last row holds
|
|
1006
|
+
* the remainder; an optional `fill` pads the last row up to `size`. A
|
|
1007
|
+
* mapping batches its values.
|
|
1008
|
+
*
|
|
1009
|
+
* @example
|
|
1010
|
+
* {% for row in [1,2,3,4,5]|batch(2) %}[{{ row|join(",") }}]{% endfor %}
|
|
1011
|
+
* // => [1,2][3,4][5]
|
|
1012
|
+
*
|
|
1013
|
+
* @param {*} input
|
|
1014
|
+
* @param {number} size
|
|
1015
|
+
* @param {*} [fill] Pad the last row up to `size` with this value.
|
|
1016
|
+
* @return {Array}
|
|
1017
|
+
*/
|
|
1018
|
+
exports.batch = function (input, size, fill) {
|
|
1019
|
+
var items, n, out, i, last;
|
|
1020
|
+
if (utils.isArray(input)) {
|
|
1021
|
+
items = input;
|
|
1022
|
+
} else if (input && typeof input === 'object') {
|
|
1023
|
+
items = [];
|
|
1024
|
+
utils.each(input, function (v) { items.push(v); });
|
|
1025
|
+
} else {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
n = Number(size);
|
|
1029
|
+
if (!isFinite(n) || n <= 0) {
|
|
1030
|
+
return [];
|
|
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);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return out;
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Distribute items across `slices` columns (Jinja2's `slice` filter — the
|
|
1050
|
+
* inverse of `batch`, and distinct from the `seq[start:stop:step]`
|
|
1051
|
+
* subscript). The first columns receive the extra items when the count
|
|
1052
|
+
* does not divide evenly; an optional `fill` pads the shorter columns.
|
|
1053
|
+
*
|
|
1054
|
+
* @example
|
|
1055
|
+
* {% for col in [1,2,3,4,5,6,7,8,9,10]|slice(3) %}[{{ col|join(",") }}]{% endfor %}
|
|
1056
|
+
* // => [1,2,3,4][5,6,7][8,9,10]
|
|
1057
|
+
*
|
|
1058
|
+
* @param {*} input
|
|
1059
|
+
* @param {number} slices Number of columns.
|
|
1060
|
+
* @param {*} [fill] Pad the shorter columns with this value.
|
|
1061
|
+
* @return {Array}
|
|
1062
|
+
*/
|
|
1063
|
+
exports.slice = function (input, slices, fill) {
|
|
1064
|
+
var seq, length, perSlice, withExtra, out, offset, n, start, end, tmp, i;
|
|
1065
|
+
if (utils.isArray(input)) {
|
|
1066
|
+
seq = input;
|
|
1067
|
+
} else if (typeof input === 'string') {
|
|
1068
|
+
seq = input.split('');
|
|
1069
|
+
} else if (input && typeof input === 'object') {
|
|
1070
|
+
seq = [];
|
|
1071
|
+
utils.each(input, function (v) { seq.push(v); });
|
|
1072
|
+
} else {
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
n = Number(slices);
|
|
1076
|
+
if (!isFinite(n) || n <= 0) {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
n = Math.ceil(n);
|
|
1080
|
+
length = seq.length;
|
|
1081
|
+
perSlice = Math.floor(length / n);
|
|
1082
|
+
withExtra = length % n;
|
|
1083
|
+
out = [];
|
|
1084
|
+
offset = 0;
|
|
1085
|
+
for (i = 0; i < n; i += 1) {
|
|
1086
|
+
start = offset + i * perSlice;
|
|
1087
|
+
if (i < withExtra) {
|
|
1088
|
+
offset += 1;
|
|
1089
|
+
}
|
|
1090
|
+
end = offset + (i + 1) * perSlice;
|
|
1091
|
+
tmp = seq.slice(start, end);
|
|
1092
|
+
if (fill !== undefined && i >= withExtra) {
|
|
1093
|
+
tmp.push(fill);
|
|
1094
|
+
}
|
|
1095
|
+
out.push(tmp);
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Sort a mapping and return a list of `[key, value]` pairs (Jinja2's
|
|
1102
|
+
* `dictsort`). Sorts by key by default; pass `by='value'` to sort by value.
|
|
1103
|
+
* String comparison is case-insensitive unless `caseSensitive` is truthy,
|
|
1104
|
+
* and `reverse` flips the order.
|
|
1105
|
+
*
|
|
1106
|
+
* @example
|
|
1107
|
+
* {% for k, v in {"b":2,"a":1}|dictsort %}{{ k }}={{ v }};{% endfor %}
|
|
1108
|
+
* // => a=1;b=2;
|
|
1109
|
+
*
|
|
1110
|
+
* @param {object} input
|
|
1111
|
+
* @param {boolean} [caseSensitive=false]
|
|
1112
|
+
* @param {string} [by='key'] `'key'` or `'value'`.
|
|
1113
|
+
* @param {boolean} [reverse=false]
|
|
1114
|
+
* @return {Array} List of `[key, value]` pairs.
|
|
1115
|
+
*/
|
|
1116
|
+
exports.dictsort = function (input, caseSensitive, by, reverse) {
|
|
1117
|
+
var pairs, idx;
|
|
1118
|
+
if (!input || typeof input !== 'object' || utils.isArray(input)) {
|
|
1119
|
+
return input;
|
|
1120
|
+
}
|
|
1121
|
+
idx = (by === 'value') ? 1 : 0;
|
|
1122
|
+
pairs = utils.map(utils.keys(input), function (k) {
|
|
1123
|
+
return [k, input[k]];
|
|
1124
|
+
});
|
|
1125
|
+
pairs.sort(function (a, b) {
|
|
1126
|
+
var x = a[idx], y = b[idx];
|
|
1127
|
+
if (!caseSensitive && typeof x === 'string') { x = x.toLowerCase(); }
|
|
1128
|
+
if (!caseSensitive && typeof y === 'string') { y = y.toLowerCase(); }
|
|
1129
|
+
if (typeof x === 'number' && typeof y === 'number') {
|
|
1130
|
+
return x - y;
|
|
1131
|
+
}
|
|
1132
|
+
x = String(x);
|
|
1133
|
+
y = String(y);
|
|
1134
|
+
return (x < y) ? -1 : (x > y) ? 1 : 0;
|
|
1135
|
+
});
|
|
1136
|
+
if (reverse) {
|
|
1137
|
+
pairs.reverse();
|
|
1138
|
+
}
|
|
1139
|
+
return pairs;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Sum the items of a sequence (Jinja2's `sum`). An optional `attribute`
|
|
1144
|
+
* (dotted path) sums that attribute of each item; an optional `start` is
|
|
1145
|
+
* added to the total. To pass a start without an attribute, use an empty
|
|
1146
|
+
* attribute string: `sum("", 10)`. (Keyword-calling — `sum(start=10)` — is
|
|
1147
|
+
* not supported, matching the family-wide keyword-argument non-goal.)
|
|
1148
|
+
*
|
|
1149
|
+
* @example
|
|
1150
|
+
* {{ [1, 2, 3]|sum }}
|
|
1151
|
+
* // => 6
|
|
1152
|
+
*
|
|
1153
|
+
* @example
|
|
1154
|
+
* {{ items|sum("price") }}
|
|
1155
|
+
* // sums each item's price
|
|
1156
|
+
*
|
|
1157
|
+
* @param {*} input
|
|
1158
|
+
* @param {string} [attribute] Dotted path to sum on each item.
|
|
1159
|
+
* @param {number} [start=0]
|
|
1160
|
+
* @return {number}
|
|
1161
|
+
*/
|
|
1162
|
+
exports.sum = function (input, attribute, start) {
|
|
1163
|
+
var total, vals;
|
|
1164
|
+
total = (start === undefined || start === null) ? 0 : Number(start);
|
|
1165
|
+
if (isNaN(total)) { total = 0; }
|
|
1166
|
+
if (!utils.isArray(input)) {
|
|
1167
|
+
if (input && typeof input === 'object') {
|
|
1168
|
+
vals = [];
|
|
1169
|
+
utils.each(input, function (v) { vals.push(v); });
|
|
1170
|
+
input = vals;
|
|
1171
|
+
} else {
|
|
1172
|
+
return total;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
utils.each(input, function (item) {
|
|
1176
|
+
var v = (attribute === undefined || attribute === null || attribute === '') ? item : resolveAttr(item, attribute),
|
|
1177
|
+
n = Number(v);
|
|
1178
|
+
total += isNaN(n) ? 0 : n;
|
|
1179
|
+
});
|
|
1180
|
+
return total;
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
/*!
|
|
1184
|
+
* Shared comparison core for the `min` / `max` filters. String comparison
|
|
1185
|
+
* is case-insensitive unless `caseSensitive` is truthy; numbers compare
|
|
1186
|
+
* numerically. @private
|
|
1187
|
+
*/
|
|
1188
|
+
function minmax(input, caseSensitive, want) {
|
|
1189
|
+
var best, found = false, vals;
|
|
1190
|
+
if (typeof input === 'string') {
|
|
1191
|
+
input = input.split('');
|
|
1192
|
+
} else if (input && typeof input === 'object' && !utils.isArray(input)) {
|
|
1193
|
+
vals = [];
|
|
1194
|
+
utils.each(input, function (v) { vals.push(v); });
|
|
1195
|
+
input = vals;
|
|
1196
|
+
} else if (!utils.isArray(input)) {
|
|
1197
|
+
return input;
|
|
1198
|
+
}
|
|
1199
|
+
utils.each(input, function (item) {
|
|
1200
|
+
var a, b, cmp;
|
|
1201
|
+
if (!found) { best = item; found = true; return; }
|
|
1202
|
+
a = item;
|
|
1203
|
+
b = best;
|
|
1204
|
+
if (!caseSensitive) {
|
|
1205
|
+
if (typeof a === 'string') { a = a.toLowerCase(); }
|
|
1206
|
+
if (typeof b === 'string') { b = b.toLowerCase(); }
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
1209
|
+
cmp = a - b;
|
|
1210
|
+
} else {
|
|
1211
|
+
a = String(a);
|
|
1212
|
+
b = String(b);
|
|
1213
|
+
cmp = (a < b) ? -1 : (a > b) ? 1 : 0;
|
|
1214
|
+
}
|
|
1215
|
+
if (want === 'min' ? cmp < 0 : cmp > 0) { best = item; }
|
|
1216
|
+
});
|
|
1217
|
+
return found ? best : undefined;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Return the smallest item of a sequence (Jinja2's `min`). String
|
|
1222
|
+
* comparison is case-insensitive unless `caseSensitive` is truthy.
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* {{ [3, 1, 2]|min }}
|
|
1226
|
+
* // => 1
|
|
1227
|
+
*
|
|
1228
|
+
* @param {*} input
|
|
1229
|
+
* @param {boolean} [caseSensitive=false]
|
|
1230
|
+
* @return {*}
|
|
1231
|
+
*/
|
|
1232
|
+
exports.min = function (input, caseSensitive) {
|
|
1233
|
+
return minmax(input, caseSensitive, 'min');
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Return the largest item of a sequence (Jinja2's `max`). String
|
|
1238
|
+
* comparison is case-insensitive unless `caseSensitive` is truthy.
|
|
1239
|
+
*
|
|
1240
|
+
* @example
|
|
1241
|
+
* {{ [3, 1, 2]|max }}
|
|
1242
|
+
* // => 3
|
|
1243
|
+
*
|
|
1244
|
+
* @param {*} input
|
|
1245
|
+
* @param {boolean} [caseSensitive=false]
|
|
1246
|
+
* @return {*}
|
|
1247
|
+
*/
|
|
1248
|
+
exports.max = function (input, caseSensitive) {
|
|
1249
|
+
return minmax(input, caseSensitive, 'max');
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
/*!
|
|
1253
|
+
* Percent-encode a string for use in a URL. Outside query-string context
|
|
1254
|
+
* `/` is preserved; in query-string context (`forQs`) every reserved
|
|
1255
|
+
* character is encoded and spaces become `+`. @private
|
|
1256
|
+
*/
|
|
1257
|
+
function urlQuote(s, forQs) {
|
|
1258
|
+
var out = encodeURIComponent(String(s)).replace(/[!*'()]/g, function (c) {
|
|
1259
|
+
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
|
|
1260
|
+
});
|
|
1261
|
+
if (!forQs) {
|
|
1262
|
+
out = out.replace(/%2F/g, '/');
|
|
1263
|
+
} else {
|
|
1264
|
+
out = out.replace(/%20/g, '+');
|
|
1265
|
+
}
|
|
1266
|
+
return out;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* URL-encode the input (Jinja2's `urlencode`). A string is percent-encoded
|
|
1271
|
+
* for a URL path, preserving the `/` separator. A mapping or a list of
|
|
1272
|
+
* `[key, value]` pairs is encoded as a query string, with spaces becoming
|
|
1273
|
+
* `+`. Not marked safe — under autoescape the `&` separators are escaped
|
|
1274
|
+
* on output, matching Jinja2.
|
|
1275
|
+
*
|
|
1276
|
+
* @example
|
|
1277
|
+
* {{ "a b/c"|urlencode }}
|
|
1278
|
+
* // => a%20b/c
|
|
1279
|
+
*
|
|
1280
|
+
* @example
|
|
1281
|
+
* {{ {"q": "a b"}|urlencode }}
|
|
1282
|
+
* // => q=a+b
|
|
1283
|
+
*
|
|
1284
|
+
* @param {*} input
|
|
1285
|
+
* @return {string}
|
|
1286
|
+
*/
|
|
1287
|
+
exports.urlencode = function (input) {
|
|
1288
|
+
if (typeof input === 'string') {
|
|
1289
|
+
return urlQuote(input, false);
|
|
1290
|
+
}
|
|
1291
|
+
if (utils.isArray(input)) {
|
|
1292
|
+
return utils.map(input, function (pair) {
|
|
1293
|
+
return urlQuote(pair[0], true) + '=' + urlQuote(pair[1], true);
|
|
1294
|
+
}).join('&');
|
|
1295
|
+
}
|
|
1296
|
+
if (input && typeof input === 'object') {
|
|
1297
|
+
return utils.map(utils.keys(input), function (k) {
|
|
1298
|
+
return urlQuote(k, true) + '=' + urlQuote(input[k], true);
|
|
1299
|
+
}).join('&');
|
|
1300
|
+
}
|
|
1301
|
+
return urlQuote(input, false);
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Return a random item from a sequence (Jinja2's `random`). A string is
|
|
1306
|
+
* treated as a sequence of characters. Non-deterministic — uses
|
|
1307
|
+
* `Math.random()`.
|
|
1308
|
+
*
|
|
1309
|
+
* @example
|
|
1310
|
+
* {{ [1, 2, 3]|random }}
|
|
1311
|
+
* // => one of 1, 2, 3
|
|
1312
|
+
*
|
|
1313
|
+
* @param {*} input
|
|
1314
|
+
* @return {*}
|
|
1315
|
+
*/
|
|
1316
|
+
exports.random = function (input) {
|
|
1317
|
+
var seq = input;
|
|
1318
|
+
if (typeof input === 'string') {
|
|
1319
|
+
seq = input.split('');
|
|
1320
|
+
} else if (!utils.isArray(input)) {
|
|
1321
|
+
return input;
|
|
1322
|
+
}
|
|
1323
|
+
if (!seq.length) {
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1326
|
+
return seq[Math.floor(Math.random() * seq.length)];
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Format a date with PHP-style `date()` tokens via the swig-core date
|
|
1331
|
+
* formatter. Jinja2 core has no `date` filter; this is a swig-family
|
|
1332
|
+
* extension shared with the native and Twig flavors. A backslash escapes a
|
|
1333
|
+
* literal character; `offset` shifts the timezone (minutes from GMT) and
|
|
1334
|
+
* `abbr` sets the output-only timezone abbreviation.
|
|
1335
|
+
*
|
|
1336
|
+
* @example
|
|
1337
|
+
* {{ published|date("F jS, Y") }}
|
|
1338
|
+
* // => September 23rd, 2011
|
|
1339
|
+
*
|
|
1340
|
+
* @param {?(Date|string|number)} input
|
|
1341
|
+
* @param {string} format
|
|
1342
|
+
* @param {number} [offset] Timezone offset in minutes from GMT.
|
|
1343
|
+
* @param {string} [abbr] Output timezone abbreviation.
|
|
1344
|
+
* @return {string}
|
|
1345
|
+
*/
|
|
1346
|
+
exports.date = function (input, format, offset, abbr) {
|
|
1347
|
+
var l = format.length,
|
|
1348
|
+
date = new dateFormatter.DateZ(input),
|
|
1349
|
+
cur,
|
|
1350
|
+
i = 0,
|
|
1351
|
+
out = '';
|
|
1352
|
+
|
|
1353
|
+
if (offset) {
|
|
1354
|
+
date.setTimezoneOffset(offset, abbr);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
for (i; i < l; i += 1) {
|
|
1358
|
+
cur = format.charAt(i);
|
|
1359
|
+
if (cur === '\\') {
|
|
1360
|
+
i += 1;
|
|
1361
|
+
out += (i < l) ? format.charAt(i) : cur;
|
|
1362
|
+
} else if (dateFormatter.hasOwnProperty(cur)) {
|
|
1363
|
+
out += dateFormatter[cur](date, offset, abbr);
|
|
1364
|
+
} else {
|
|
1365
|
+
out += cur;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return out;
|
|
1369
|
+
};
|