miki-template 1.2.0 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/release-notes/v1.3.1.md +55 -0
- package/CHANGELOG.md +72 -0
- package/README.md +43 -26
- package/assets/banner.png +0 -0
- package/benchmarks/stress.mjs +647 -0
- package/dir/base.html +23 -0
- package/dir/cmpnt.html +11 -0
- package/dir/footer.html +3 -0
- package/dir/home.html +80 -0
- package/dir/navbar.html +9 -0
- package/docs/api.md +20 -3
- package/docs/filters.md +301 -133
- package/docs/partialdef.md +30 -1
- package/docs/tags.md +63 -0
- package/docs/usage.md +50 -3
- package/eslint.config.mjs +9 -1
- package/ex.mjs +33 -0
- package/miki-template-extension/.github/workflows/ci.yml +116 -0
- package/miki-template-extension/.vscodeignore +7 -0
- package/miki-template-extension/CHANGELOG.md +99 -0
- package/miki-template-extension/README.md +244 -53
- package/miki-template-extension/extension.js +1013 -0
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
- package/miki-template-extension/package.json +244 -10
- package/miki-template-extension/snippets/miki-template.json +612 -72
- package/miki-template-extension/syntaxes/language-configuration.json +101 -13
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +270 -61
- package/miki-template-extension/tests/grammar-tests.json +162 -0
- package/miki-template-extension/tests/run-grammar-tests.js +82 -0
- package/package.json +7 -4
- package/scripts/build-vsix.js +129 -0
- package/scripts/build-vsix.ps1 +15 -0
- package/src/cache.js +41 -2
- package/src/context.js +9 -5
- package/src/context_processors.js +9 -2
- package/src/esm.mjs +12 -0
- package/src/filters.js +472 -24
- package/src/index.js +571 -85
- package/src/lexer.js +76 -54
- package/src/libraries.js +134 -3
- package/src/parser.js +22 -2
- package/src/security.js +4 -2
- package/src/tags/control.js +150 -21
- package/src/tags/extra.js +154 -0
- package/src/tags/i18n.js +49 -23
- package/src/tags/inheritance.js +142 -23
- package/src/tags/util.js +102 -24
- package/tests/esm.test.mjs +37 -2
- package/tests/filters.test.js +155 -0
- package/tests/integration/README.md +32 -0
- package/tests/integration/features.test.cjs +1681 -0
- package/tests/integration/features.test.mjs +1697 -0
- package/tests/integration/templates/base.miki +6 -0
- package/tests/integration/templates/child.miki +6 -0
- package/tests/integration/templates/index.html +17 -0
- package/tests/parser.test.js +5 -3
- package/tests/partialdef.test.js +40 -1
- package/tests/tags.test.js +30 -0
- package/miki-template-1.2.0.vsix +0 -0
package/src/filters.js
CHANGED
|
@@ -12,7 +12,9 @@ function getFilter(name) {
|
|
|
12
12
|
|
|
13
13
|
function escapeValue(val) {
|
|
14
14
|
const { escapeHtml, markSafe } = require('./security');
|
|
15
|
-
|
|
15
|
+
// force=true ensures even SafeString values are re-escaped, matching
|
|
16
|
+
// Django's `{{ value|escape }}` semantics.
|
|
17
|
+
return markSafe(escapeHtml(val, true));
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function markValueSafe(val) {
|
|
@@ -208,23 +210,55 @@ registerFilter('date', (val, arg) => {
|
|
|
208
210
|
}
|
|
209
211
|
if (isNaN(date.getTime())) return '';
|
|
210
212
|
const formatStr = arg || 'Y-m-d';
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
213
|
+
// Multi-character tokens are matched longest-first so that "yyyy" is
|
|
214
|
+
// one token and not four "y" tokens. Conventions:
|
|
215
|
+
// yyyy 4-digit year yy 2-digit year
|
|
216
|
+
// MM padded month number M month number (no pad)
|
|
217
|
+
// mm padded minutes m month number (Django-style alias)
|
|
218
|
+
// dd padded day d day number (no pad)
|
|
219
|
+
// HH padded 24h hour H hour (no pad)
|
|
220
|
+
// ii padded minutes i minutes (no pad)
|
|
221
|
+
// ss padded seconds s seconds (no pad)
|
|
222
|
+
// F long month name D short day name
|
|
223
|
+
const tokenMap = [
|
|
224
|
+
['yyyy', () => String(date.getFullYear()).padStart(4, '0')],
|
|
225
|
+
['yy', () => String(date.getFullYear()).slice(-2).padStart(2, '0')],
|
|
226
|
+
['MM', () => String(date.getMonth() + 1).padStart(2, '0')],
|
|
227
|
+
['dd', () => String(date.getDate()).padStart(2, '0')],
|
|
228
|
+
['HH', () => String(date.getHours()).padStart(2, '0')],
|
|
229
|
+
['mm', () => String(date.getMinutes()).padStart(2, '0')],
|
|
230
|
+
['ii', () => String(date.getMinutes()).padStart(2, '0')],
|
|
231
|
+
['ss', () => String(date.getSeconds()).padStart(2, '0')],
|
|
232
|
+
['F', () => date.toLocaleString('default', { month: 'long' })],
|
|
233
|
+
['D', () => date.toLocaleString('default', { weekday: 'short' })],
|
|
234
|
+
['M', () => String(date.getMonth() + 1)],
|
|
235
|
+
['Y', () => String(date.getFullYear())],
|
|
236
|
+
['y', () => String(date.getFullYear()).slice(-2)],
|
|
237
|
+
['m', () => String(date.getMonth() + 1)],
|
|
238
|
+
['d', () => String(date.getDate())],
|
|
239
|
+
['j', () => String(date.getDate())],
|
|
240
|
+
['n', () => String(date.getMonth() + 1)],
|
|
241
|
+
['H', () => String(date.getHours())],
|
|
242
|
+
['i', () => String(date.getMinutes())],
|
|
243
|
+
['s', () => String(date.getSeconds())],
|
|
244
|
+
['G', () => String(date.getHours())],
|
|
245
|
+
];
|
|
224
246
|
let output = '';
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
let i = 0;
|
|
248
|
+
while (i < formatStr.length) {
|
|
249
|
+
let matched = false;
|
|
250
|
+
for (const [tok, fn] of tokenMap) {
|
|
251
|
+
if (formatStr.startsWith(tok, i)) {
|
|
252
|
+
output += fn();
|
|
253
|
+
i += tok.length;
|
|
254
|
+
matched = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!matched) {
|
|
259
|
+
output += formatStr[i];
|
|
260
|
+
i++;
|
|
261
|
+
}
|
|
228
262
|
}
|
|
229
263
|
return output;
|
|
230
264
|
});
|
|
@@ -280,6 +314,24 @@ registerFilter('add', (val, arg) => {
|
|
|
280
314
|
return String(val) + String(arg);
|
|
281
315
|
});
|
|
282
316
|
|
|
317
|
+
registerFilter('sub', (val, arg) => {
|
|
318
|
+
const numVal = Number(val);
|
|
319
|
+
const numArg = Number(arg);
|
|
320
|
+
if (!isNaN(numVal) && !isNaN(numArg)) {
|
|
321
|
+
return numVal - numArg;
|
|
322
|
+
}
|
|
323
|
+
return String(val);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
registerFilter('mult', (val, arg) => {
|
|
327
|
+
const numVal = Number(val);
|
|
328
|
+
const numArg = Number(arg);
|
|
329
|
+
if (!isNaN(numVal) && !isNaN(numArg)) {
|
|
330
|
+
return numVal * numArg;
|
|
331
|
+
}
|
|
332
|
+
return String(val);
|
|
333
|
+
});
|
|
334
|
+
|
|
283
335
|
registerFilter('divisibleby', (val, arg) => {
|
|
284
336
|
const numVal = Number(val);
|
|
285
337
|
const numArg = Number(arg);
|
|
@@ -287,6 +339,13 @@ registerFilter('divisibleby', (val, arg) => {
|
|
|
287
339
|
return numVal % numArg === 0;
|
|
288
340
|
});
|
|
289
341
|
|
|
342
|
+
registerFilter('mod', (val, arg) => {
|
|
343
|
+
const numVal = Number(val);
|
|
344
|
+
const numArg = Number(arg);
|
|
345
|
+
if (isNaN(numVal) || isNaN(numArg) || numArg === 0) return 0;
|
|
346
|
+
return numVal % numArg;
|
|
347
|
+
});
|
|
348
|
+
|
|
290
349
|
registerFilter('floatformat', (val, arg) => {
|
|
291
350
|
const num = Number(val);
|
|
292
351
|
if (isNaN(num)) return '';
|
|
@@ -301,15 +360,61 @@ registerFilter('floatformat', (val, arg) => {
|
|
|
301
360
|
return num.toFixed(Math.max(0, decimals));
|
|
302
361
|
});
|
|
303
362
|
|
|
363
|
+
registerFilter('square', (val) => {
|
|
364
|
+
const num = Number(val);
|
|
365
|
+
if (isNaN(num)) return 0;
|
|
366
|
+
return num * num;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
registerFilter('sqrt', (val) => {
|
|
370
|
+
const num = Number(val);
|
|
371
|
+
if (isNaN(num) || num < 0) return 0;
|
|
372
|
+
return Math.sqrt(num);
|
|
373
|
+
});
|
|
374
|
+
|
|
304
375
|
// --- Default Filters ---
|
|
305
|
-
|
|
306
|
-
|
|
376
|
+
// When the fallback contains template syntax ({% ... %} or {{ ... }}),
|
|
377
|
+
// it is rendered against the current context. This matches Django's
|
|
378
|
+
// behavior where `{{ x|default:"{% lorem 1 %}" }}` produces lorem text
|
|
379
|
+
// when x is empty.
|
|
380
|
+
registerFilter('default', (val, arg, ctx) => {
|
|
381
|
+
if (val !== null && val !== undefined && val !== '') return val;
|
|
382
|
+
return renderFallbackTemplate(arg, ctx);
|
|
307
383
|
});
|
|
308
384
|
|
|
309
|
-
registerFilter('default_if_none', (val, arg) => {
|
|
310
|
-
|
|
385
|
+
registerFilter('default_if_none', (val, arg, ctx) => {
|
|
386
|
+
if (val !== null && val !== undefined) return val;
|
|
387
|
+
return renderFallbackTemplate(arg, ctx);
|
|
311
388
|
});
|
|
312
389
|
|
|
390
|
+
/**
|
|
391
|
+
* If `arg` looks like a mini-template (contains `{% ... %}` or
|
|
392
|
+
* `{{ ... }}`), compile and render it. Otherwise return it as-is.
|
|
393
|
+
*/
|
|
394
|
+
function renderFallbackTemplate(arg, ctx) {
|
|
395
|
+
if (typeof arg !== 'string') return arg;
|
|
396
|
+
if (!/\{[{%]/.test(arg)) return arg;
|
|
397
|
+
try {
|
|
398
|
+
// Lazy require to avoid circular dependency with index.js
|
|
399
|
+
const { render } = require('./index');
|
|
400
|
+
// Merge all active scopes (top-most wins) into a plain object
|
|
401
|
+
let merged = {};
|
|
402
|
+
if (ctx && Array.isArray(ctx.scopes)) {
|
|
403
|
+
// scopes[0] is the topmost (most local). Iterate from bottom
|
|
404
|
+
// (root) to top so topmost values win.
|
|
405
|
+
for (let i = ctx.scopes.length - 1; i >= 0; i--) {
|
|
406
|
+
const s = ctx.scopes[i];
|
|
407
|
+
if (s && typeof s === 'object' && !s.__forloop && !s.__block) {
|
|
408
|
+
Object.assign(merged, s);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return render(arg, merged);
|
|
413
|
+
} catch {
|
|
414
|
+
return arg;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
313
418
|
registerFilter('firstof', (...args) => {
|
|
314
419
|
for (const arg of args) {
|
|
315
420
|
if (arg !== null && arg !== undefined && arg !== '') {
|
|
@@ -445,15 +550,19 @@ registerFilter('addslashes', (val) => {
|
|
|
445
550
|
registerFilter('removetags', (val, arg) => {
|
|
446
551
|
const str = String(val === null || val === undefined ? '' : val);
|
|
447
552
|
if (!arg) return str;
|
|
448
|
-
|
|
553
|
+
// Django's removetags accepts space-separated tag names (and tolerates commas).
|
|
554
|
+
const tags = String(arg).split(/[\s,]+/).map(t => t.trim()).filter(Boolean);
|
|
449
555
|
let result = str;
|
|
450
556
|
for (const tag of tags) {
|
|
451
557
|
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
452
558
|
result = result.replace(new RegExp(`<\\/${escaped}>`, 'gi'), '');
|
|
453
|
-
result = result.replace(new RegExp(`<${escaped}[^>]*>`, 'gi'), '');
|
|
559
|
+
result = result.replace(new RegExp(`<${escaped}\\b[^>]*>`, 'gi'), '');
|
|
454
560
|
result = result.replace(new RegExp(`<${escaped}>`, 'gi'), '');
|
|
455
561
|
}
|
|
456
|
-
|
|
562
|
+
// The result is intentionally safe (we just produced raw HTML by
|
|
563
|
+
// removing tags) and must not be re-escaped.
|
|
564
|
+
const { markSafe } = require('./security');
|
|
565
|
+
return markSafe(result);
|
|
457
566
|
});
|
|
458
567
|
|
|
459
568
|
/**
|
|
@@ -523,5 +632,344 @@ registerFilter('strftime', (val, arg) => {
|
|
|
523
632
|
}
|
|
524
633
|
});
|
|
525
634
|
|
|
635
|
+
// --- Math Filters ---
|
|
636
|
+
registerFilter('sub', (val, arg) => {
|
|
637
|
+
const numVal = Number(val);
|
|
638
|
+
const numArg = Number(arg);
|
|
639
|
+
if (!isNaN(numVal) && !isNaN(numArg)) {
|
|
640
|
+
return numVal - numArg;
|
|
641
|
+
}
|
|
642
|
+
return String(val);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
registerFilter('mult', (val, arg) => {
|
|
646
|
+
const numVal = Number(val);
|
|
647
|
+
const numArg = Number(arg);
|
|
648
|
+
if (!isNaN(numVal) && !isNaN(numArg)) {
|
|
649
|
+
return numVal * numArg;
|
|
650
|
+
}
|
|
651
|
+
return String(val);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
registerFilter('square', (val) => {
|
|
655
|
+
const num = Number(val);
|
|
656
|
+
if (isNaN(num)) return 0;
|
|
657
|
+
return num * num;
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
registerFilter('sqrt', (val) => {
|
|
661
|
+
const num = Number(val);
|
|
662
|
+
if (isNaN(num) || num < 0) return 0;
|
|
663
|
+
return Math.sqrt(num);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
registerFilter('mod', (val, arg) => {
|
|
667
|
+
const numVal = Number(val);
|
|
668
|
+
const numArg = Number(arg);
|
|
669
|
+
if (isNaN(numVal) || isNaN(numArg) || numArg === 0) return 0;
|
|
670
|
+
return numVal % numArg;
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// --- Data Formatting Filters ---
|
|
674
|
+
registerFilter('currency', (val, arg) => {
|
|
675
|
+
const num = Number(val);
|
|
676
|
+
if (isNaN(num)) return '';
|
|
677
|
+
const symbol = arg || '$';
|
|
678
|
+
const formatted = num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
679
|
+
return symbol + formatted;
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
registerFilter('phone_number', (val) => {
|
|
683
|
+
const str = String(val === null || val === undefined ? '' : val).replace(/\D/g, '');
|
|
684
|
+
if (str.length === 10) {
|
|
685
|
+
return `(${str.slice(0, 3)}) ${str.slice(3, 6)}-${str.slice(6)}`;
|
|
686
|
+
}
|
|
687
|
+
if (str.length === 11 && str.startsWith('1')) {
|
|
688
|
+
return `+1 (${str.slice(1, 4)}) ${str.slice(4, 7)}-${str.slice(7)}`;
|
|
689
|
+
}
|
|
690
|
+
return String(val);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
registerFilter('email', (val) => {
|
|
694
|
+
const str = String(val === null || val === undefined ? '' : val).trim();
|
|
695
|
+
if (!str) return '';
|
|
696
|
+
return `mailto:${str}`;
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
registerFilter('url', (val) => {
|
|
700
|
+
let str = String(val === null || val === undefined ? '' : val).trim();
|
|
701
|
+
if (!str) return '';
|
|
702
|
+
if (!/^https?:\/\//i.test(str)) {
|
|
703
|
+
str = 'https://' + str;
|
|
704
|
+
}
|
|
705
|
+
return str;
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
registerFilter('mask', (val, arg) => {
|
|
709
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
710
|
+
const char = arg || '*';
|
|
711
|
+
const visible = 4;
|
|
712
|
+
if (str.length <= visible) return str;
|
|
713
|
+
return char.repeat(str.length - visible) + str.slice(-visible);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
registerFilter('whatsapp_link', (val, arg) => {
|
|
717
|
+
let str = String(val === null || val === undefined ? '' : val).trim();
|
|
718
|
+
if (!str) return '';
|
|
719
|
+
const digits = str.replace(/\D/g, '');
|
|
720
|
+
const message = arg || '';
|
|
721
|
+
const url = new URL('https://wa.me/' + digits);
|
|
722
|
+
if (message) {
|
|
723
|
+
url.searchParams.set('text', message);
|
|
724
|
+
}
|
|
725
|
+
return url.toString();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// --- Math Filters ---
|
|
729
|
+
registerFilter('abs', (val) => {
|
|
730
|
+
const num = Number(val);
|
|
731
|
+
return isNaN(num) ? 0 : Math.abs(num);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
registerFilter('round', (val, arg) => {
|
|
735
|
+
const num = Number(val);
|
|
736
|
+
if (isNaN(num)) return 0;
|
|
737
|
+
const decimals = arg !== undefined ? parseInt(arg, 10) : 0;
|
|
738
|
+
if (isNaN(decimals)) return Math.round(num);
|
|
739
|
+
const factor = Math.pow(10, decimals);
|
|
740
|
+
return Math.round(num * factor) / factor;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
registerFilter('floor', (val) => {
|
|
744
|
+
const num = Number(val);
|
|
745
|
+
return isNaN(num) ? 0 : Math.floor(num);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
registerFilter('ceil', (val) => {
|
|
749
|
+
const num = Number(val);
|
|
750
|
+
return isNaN(num) ? 0 : Math.ceil(num);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
registerFilter('min', (val, arg) => {
|
|
754
|
+
if (Array.isArray(val)) return Math.min(...val);
|
|
755
|
+
const num = Number(val);
|
|
756
|
+
const numArg = Number(arg);
|
|
757
|
+
if (!isNaN(num) && !isNaN(numArg)) return Math.min(num, numArg);
|
|
758
|
+
return num;
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
registerFilter('max', (val, arg) => {
|
|
762
|
+
if (Array.isArray(val)) return Math.max(...val);
|
|
763
|
+
const num = Number(val);
|
|
764
|
+
const numArg = Number(arg);
|
|
765
|
+
if (!isNaN(num) && !isNaN(numArg)) return Math.max(num, numArg);
|
|
766
|
+
return num;
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
registerFilter('sum', (val) => {
|
|
770
|
+
if (!Array.isArray(val)) return Number(val) || 0;
|
|
771
|
+
return val.reduce((acc, item) => acc + (Number(item) || 0), 0);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
registerFilter('average', (val) => {
|
|
775
|
+
if (!Array.isArray(val)) return Number(val) || 0;
|
|
776
|
+
if (val.length === 0) return 0;
|
|
777
|
+
return val.reduce((acc, item) => acc + (Number(item) || 0), 0) / val.length;
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// --- String / Array Manipulation Filters ---
|
|
781
|
+
registerFilter('reverse', (val) => {
|
|
782
|
+
if (Array.isArray(val)) return val.slice().reverse();
|
|
783
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
784
|
+
return str.split('').reverse().join('');
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
registerFilter('sort', (val) => {
|
|
788
|
+
if (!Array.isArray(val)) return val;
|
|
789
|
+
return [...val].sort((a, b) => {
|
|
790
|
+
if (a === b) return 0;
|
|
791
|
+
if (a === null || a === undefined) return 1;
|
|
792
|
+
if (b === null || b === undefined) return -1;
|
|
793
|
+
if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b);
|
|
794
|
+
return a < b ? -1 : 1;
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
registerFilter('unique', (val) => {
|
|
799
|
+
if (!Array.isArray(val)) return val;
|
|
800
|
+
return [...new Set(val)];
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
registerFilter('random', (val) => {
|
|
804
|
+
if (!Array.isArray(val) || val.length === 0) return '';
|
|
805
|
+
return val[Math.floor(Math.random() * val.length)];
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
registerFilter('split', (val, arg) => {
|
|
809
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
810
|
+
const sep = arg || ' ';
|
|
811
|
+
return str.split(sep);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
registerFilter('replace', (val, arg) => {
|
|
815
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
816
|
+
if (!arg) return str;
|
|
817
|
+
const parts = String(arg).split(',');
|
|
818
|
+
if (parts.length >= 2) {
|
|
819
|
+
const oldStr = parts[0].trim();
|
|
820
|
+
const newStr = parts.slice(1).join(',').trim();
|
|
821
|
+
return str.split(oldStr).join(newStr);
|
|
822
|
+
}
|
|
823
|
+
return str;
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// --- Encoding Filters ---
|
|
827
|
+
registerFilter('base64_encode', (val) => {
|
|
828
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
829
|
+
return Buffer.from(str).toString('base64');
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
registerFilter('base64_decode', (val) => {
|
|
833
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
834
|
+
try {
|
|
835
|
+
const decoded = Buffer.from(str, 'base64').toString('utf8');
|
|
836
|
+
const reEncoded = Buffer.from(decoded).toString('base64');
|
|
837
|
+
if (reEncoded.replace(/=+$/, '') === str.replace(/=+$/, '')) {
|
|
838
|
+
return decoded;
|
|
839
|
+
}
|
|
840
|
+
return String(val);
|
|
841
|
+
} catch {
|
|
842
|
+
return String(val);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// --- Text Formatting Filters ---
|
|
847
|
+
registerFilter('urlize', (val) => {
|
|
848
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
849
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
850
|
+
return str.replace(urlRegex, '<a href="$1">$1</a>');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
registerFilter('json', (val) => {
|
|
854
|
+
const { markSafe } = require('./security');
|
|
855
|
+
try {
|
|
856
|
+
return markSafe(JSON.stringify(val));
|
|
857
|
+
} catch {
|
|
858
|
+
return String(val);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
registerFilter('truncatechars_html', (val, arg) => {
|
|
863
|
+
const str = String(val === null || val === undefined ? '' : val);
|
|
864
|
+
const count = parseInt(arg, 10);
|
|
865
|
+
if (isNaN(count) || count <= 0) return str;
|
|
866
|
+
const { markSafe } = require('./security');
|
|
867
|
+
if (str.length <= count) return markSafe(str);
|
|
868
|
+
let currentLen = 0;
|
|
869
|
+
let result = '';
|
|
870
|
+
let inTag = false;
|
|
871
|
+
for (let i = 0; i < str.length && currentLen < count; i++) {
|
|
872
|
+
const ch = str[i];
|
|
873
|
+
if (ch === '<') inTag = true;
|
|
874
|
+
if (inTag) {
|
|
875
|
+
result += ch;
|
|
876
|
+
if (ch === '>') inTag = false;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
currentLen++;
|
|
880
|
+
result += ch;
|
|
881
|
+
}
|
|
882
|
+
if (!inTag && currentLen >= count) {
|
|
883
|
+
result += '...';
|
|
884
|
+
}
|
|
885
|
+
return markSafe(result);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// --- Time Filters ---
|
|
889
|
+
registerFilter('time_diff', (val, arg) => {
|
|
890
|
+
const d1 = new Date(val);
|
|
891
|
+
const d2 = arg ? new Date(arg) : new Date();
|
|
892
|
+
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return '';
|
|
893
|
+
const diffMs = Math.max(0, Math.abs(d2 - d1));
|
|
894
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
895
|
+
const diffMins = Math.floor(seconds / 60);
|
|
896
|
+
if (diffMins < 1) return '0 minutes';
|
|
897
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''}`;
|
|
898
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
899
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''}`;
|
|
900
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
901
|
+
return `${diffDays} day${diffDays !== 1 ? 's' : ''}`;
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
registerFilter('ago', (val) => {
|
|
905
|
+
const d = new Date(val);
|
|
906
|
+
if (isNaN(d.getTime())) return '';
|
|
907
|
+
const now = new Date();
|
|
908
|
+
const diffMs = Math.max(0, now - d);
|
|
909
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
910
|
+
const diffMins = Math.floor(seconds / 60);
|
|
911
|
+
if (diffMins < 1) return 'just now';
|
|
912
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
|
|
913
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
914
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
|
915
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
916
|
+
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
|
917
|
+
const diffWeeks = Math.floor(diffDays / 7);
|
|
918
|
+
if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
|
|
919
|
+
const diffMonths = Math.floor(diffDays / 30);
|
|
920
|
+
if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
|
|
921
|
+
const diffYears = Math.floor(diffDays / 365);
|
|
922
|
+
return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
registerFilter('until', (val) => {
|
|
926
|
+
const d = new Date(val);
|
|
927
|
+
if (isNaN(d.getTime())) return '';
|
|
928
|
+
const now = new Date();
|
|
929
|
+
const diffMs = Math.max(0, d - now);
|
|
930
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
931
|
+
const diffMins = Math.floor(seconds / 60);
|
|
932
|
+
if (diffMins < 1) return 'now';
|
|
933
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''}`;
|
|
934
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
935
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''}`;
|
|
936
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
937
|
+
return `${diffDays} day${diffDays !== 1 ? 's' : ''}`;
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// --- Data Formatting Filters ---
|
|
941
|
+
registerFilter('credit_card', (val) => {
|
|
942
|
+
const str = String(val === null || val === undefined ? '' : val).replace(/\D/g, '');
|
|
943
|
+
if (str.length < 13 || str.length > 19) return String(val);
|
|
944
|
+
const parts = [];
|
|
945
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
946
|
+
parts.push(str.slice(i, i + 4));
|
|
947
|
+
}
|
|
948
|
+
return parts.join('-');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
registerFilter('ssn', (val) => {
|
|
952
|
+
const str = String(val === null || val === undefined ? '' : val).replace(/\D/g, '');
|
|
953
|
+
if (str.length !== 9) return String(val);
|
|
954
|
+
return `${str.slice(0, 3)}-${str.slice(3, 5)}-${str.slice(5)}`;
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
registerFilter('ip_address', (val) => {
|
|
958
|
+
const str = String(val === null || val === undefined ? '' : val).replace(/\D/g, '');
|
|
959
|
+
if (str.length !== 10 && str.length !== 12) return String(val);
|
|
960
|
+
if (str.length === 12) {
|
|
961
|
+
return `${str.slice(0, 4)}.${str.slice(4, 8)}.${str.slice(8, 10)}.${str.slice(10)}`;
|
|
962
|
+
}
|
|
963
|
+
return `${str.slice(0, 3)}.${str.slice(3, 6)}.${str.slice(6, 9)}.${str.slice(9)}`;
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
registerFilter('uuid', () => {
|
|
967
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
968
|
+
const r = (Math.random() * 16) | 0;
|
|
969
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
970
|
+
return v.toString(16);
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
|
|
526
974
|
module.exports = { registerFilter, getFilter };
|
|
527
975
|
|