@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/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 (`&amp;`, `&lt;`, …) so the autoescape tail is idempotent.
81
+ *
82
+ * @example
83
+ * {{ "<b>"|escape }}
84
+ * // => &lt;b&gt;
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 === '<' ? '&lt;' : ch === '>' ? '&gt;' : ch === '"' ? '&quot;' : '&#39;';
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, '&amp;')
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
+ };