@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/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 === '<' ? '&lt;' : ch === '>' ? '&gt;' : ch === '"' ? '&quot;' : '&#39;';
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
+ * (`&amp;`, `&lt;`, …) so the autoescape tail is idempotent. Mirrors Django's
360
+ * HTML escaping of `< > & " '`.
361
+ *
362
+ * @example
363
+ * {{ "<b>"|escape }}
364
+ * // => &lt;b&gt;
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, '&amp;')
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
+ * // => &lt;b&gt;
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
+ };