ngx-transforms 0.0.4 → 0.1.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.
@@ -60,63 +60,623 @@ import * as QRCode from 'qrcode';
60
60
  * @example
61
61
  * // With inverted colors
62
62
  * {{ 'DARK' | asciiArt:{ inverted: true, charset: CharsetPreset.MINIMAL } }}
63
+ */
64
+ class AsciiArtPipe {
65
+ generator;
66
+ constructor() {
67
+ this.generator = new AsciiGenerator({
68
+ charset: CharsetPreset.STANDARD,
69
+ width: 80,
70
+ optimized: true,
71
+ });
72
+ }
73
+ transform(value, options = {}) {
74
+ if (!value || typeof value !== 'string') {
75
+ return '';
76
+ }
77
+ const maxLength = 100;
78
+ if (value.length > maxLength) {
79
+ console.warn(`AsciiArtPipe: Input truncated to ${maxLength} characters for security`);
80
+ value = value.substring(0, maxLength);
81
+ }
82
+ const { format = 'html', textOptions, ...config } = options;
83
+ try {
84
+ if (Object.keys(config).length > 0) {
85
+ this.generator.updateConfig(config);
86
+ }
87
+ const result = this.generator.convertText(value, textOptions);
88
+ if (format === 'text') {
89
+ return `<pre class="ascii-art">${this.escapeHtml(result.text)}</pre>`;
90
+ }
91
+ // Return HTML format (already safe from ts-ascii-engine)
92
+ return result.html;
93
+ }
94
+ catch (error) {
95
+ console.error('AsciiArtPipe: Error generating ASCII art', error);
96
+ return `<pre class="ascii-art-error">Error: Unable to generate ASCII art</pre>`;
97
+ }
98
+ }
99
+ /**
100
+ * Escapes HTML special characters to prevent XSS
101
+ * @private
102
+ */
103
+ escapeHtml(text) {
104
+ const div = document.createElement('div');
105
+ div.textContent = text;
106
+ return div.innerHTML;
107
+ }
108
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AsciiArtPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
109
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: AsciiArtPipe, isStandalone: true, name: "asciiArt" });
110
+ }
111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AsciiArtPipe, decorators: [{
112
+ type: Pipe,
113
+ args: [{
114
+ name: 'asciiArt',
115
+ standalone: true,
116
+ }]
117
+ }], ctorParameters: () => [] });
118
+
119
+ /**
120
+ * CamelCasePipe: Converts text to camelCase (e.g., "hello world" → "helloWorld").
121
+ *
122
+ * @param {string} value - The input string to transform.
123
+ * @returns {string} The string in camelCase, or an empty string if input is invalid.
124
+ *
125
+ * @example
126
+ * ```html
127
+ * {{ 'hello world' | camelCase }} <!-- Outputs: helloWorld -->
128
+ * ```
129
+ */
130
+ class CamelCasePipe {
131
+ transform(value) {
132
+ if (!value || typeof value !== 'string')
133
+ return '';
134
+ return value
135
+ .toLowerCase()
136
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
137
+ .trim()
138
+ .split(' ')
139
+ .filter(word => word.length > 0)
140
+ .map((word, index) => index === 0
141
+ ? word.toLowerCase()
142
+ : word.charAt(0).toUpperCase() + word.slice(1))
143
+ .join('');
144
+ }
145
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
146
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, isStandalone: true, name: "camelCase" });
147
+ }
148
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, decorators: [{
149
+ type: Pipe,
150
+ args: [{
151
+ name: 'camelCase',
152
+ standalone: true
153
+ }]
154
+ }] });
155
+
156
+ /**
157
+ * HighlightPipe: Highlights occurrences of a search term within a string.
158
+ *
159
+ * This Angular pipe transforms a string input by wrapping all occurrences of a specified
160
+ * search term with a `<span>` element that has the class "highlight".
161
+ * It uses the Angular `DomSanitizer` to bypass security and render the highlighted HTML.
162
+ *
163
+ * @param {string} value - The input string in which to highlight the search term.
164
+ * @param {string} searchTerm - The string to search for and highlight.
165
+ * @returns {SafeHtml} - The input string with the search term highlighted, or an empty string if input or searchTerm are falsy.
166
+ *
167
+ * @example
168
+ * {{ 'This is a test string' | highlight: 'test' }} // Returns 'This is a <span class="highlight">test</span> string'
169
+ * {{ 'This is a test TEST string' | highlight: 'test' }} // Returns 'This is a <span class="highlight">test</span> <span class="highlight">TEST</span> string'
170
+ * {{ 'This is a test string' | highlight: '' }} // Returns 'This is a test string'
171
+ * {{ null | highlight: 'test' }} // Returns ''
172
+ * {{ undefined | highlight: 'test' }} // Returns ''
173
+ */
174
+ class HighlightPipe {
175
+ sanitizer = inject(DomSanitizer);
176
+ transform(value, searchTerm) {
177
+ if (!value || !searchTerm) {
178
+ return this.sanitizer.bypassSecurityTrustHtml(value || '');
179
+ }
180
+ const escapedSearch = searchTerm.replace(/[.*+?${}()|[\\]/g, '\\$&');
181
+ const regex = new RegExp(`(${escapedSearch})`, 'gi');
182
+ const highlighed = value.replace(regex, '<span class="highlight">$1</span>');
183
+ return this.sanitizer.bypassSecurityTrustHtml(highlighed);
184
+ }
185
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
186
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, isStandalone: true, name: "highlight" });
187
+ }
188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, decorators: [{
189
+ type: Pipe,
190
+ args: [{
191
+ name: 'highlight',
192
+ standalone: true
193
+ }]
194
+ }] });
195
+
196
+ /**
197
+ * InitialsPipe: Extracts initials from a name.
198
+ *
199
+ * @param {string} value - The full name.
200
+ *
201
+ * @returns {string} - The initials (e.g., 'John Doe' → 'JD').
202
+ *
203
+ * @example
204
+ * {{ 'John Doe' | initials }} // Outputs: JD
205
+ * {{ 'Mary Jane Watson' | initials }} // Outputs: MJW
206
+ */
207
+ class InitialsPipe {
208
+ transform(value) {
209
+ if (!value)
210
+ return '';
211
+ return value
212
+ .trim()
213
+ .split(/\s+/)
214
+ .map(word => word.charAt(0).toUpperCase())
215
+ .join('');
216
+ }
217
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
218
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, isStandalone: true, name: "initials" });
219
+ }
220
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, decorators: [{
221
+ type: Pipe,
222
+ args: [{
223
+ name: 'initials',
224
+ standalone: true
225
+ }]
226
+ }] });
227
+
228
+ /**
229
+ * KebabCasePipe: Converts text to kebab-case (e.g., "hello world" → "hello-world").
230
+ *
231
+ * @param {string} value - The input string to transform.
232
+ * @returns {string} The string in kebab-case, or an empty string if input is invalid.
233
+ *
234
+ * @example
235
+ * ```html
236
+ * {{ 'hello world' | kebabCase }} <!-- Outputs: hello-world -->
237
+ * ```
238
+ */
239
+ class KebabCasePipe {
240
+ transform(value) {
241
+ if (!value || typeof value !== 'string')
242
+ return '';
243
+ return value
244
+ .trim()
245
+ .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') // Add hyphen between camelCase words
246
+ .toLowerCase()
247
+ .replace(/[^a-z0-9-]+/g, '-') // Replace non-alphanumeric (except hyphen) with hyphen
248
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
249
+ }
250
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
251
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, isStandalone: true, name: "kebabCase" });
252
+ }
253
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, decorators: [{
254
+ type: Pipe,
255
+ args: [{
256
+ name: 'kebabCase',
257
+ standalone: true
258
+ }]
259
+ }] });
260
+
261
+ /**
262
+ * MorseCodePipe: Converts text to Morse code.
263
+ *
264
+ * @param {string} value - The text to convert to Morse code.
265
+ *
266
+ * @returns {string} - The Morse code representation (e.g., 'SOS' → '... --- ...').
267
+ *
268
+ * @example
269
+ * {{ 'SOS' | morseCode }} // Outputs: '... --- ...'
270
+ * {{ 'HELP' | morseCode }} // Outputs: '.... . .-.. .--.'
271
+ * <p>{{ userInput | morseCode }}</p>
272
+ */
273
+ class MorseCodePipe {
274
+ morseCodeMap = {
275
+ 'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 'F': '..-.',
276
+ 'G': '--.', 'H': '....', 'I': '..', 'J': '.---', 'K': '-.-', 'L': '.-..',
277
+ 'M': '--', 'N': '-.', 'O': '---', 'P': '.--.', 'Q': '--.-', 'R': '.-.',
278
+ 'S': '...', 'T': '-', 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-',
279
+ 'Y': '-.--', 'Z': '--..', '0': '-----', '1': '.----', '2': '..---',
280
+ '3': '...--', '4': '....-', '5': '.....', '6': '-....', '7': '--...',
281
+ '8': '---..', '9': '----.'
282
+ };
283
+ transform(value) {
284
+ if (!value || typeof value !== 'string') {
285
+ return '';
286
+ }
287
+ return value
288
+ .toUpperCase()
289
+ .split('')
290
+ .map(char => this.morseCodeMap[char] || '')
291
+ .filter(code => code)
292
+ .join(' ');
293
+ }
294
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
295
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, isStandalone: true, name: "morseCode" });
296
+ }
297
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, decorators: [{
298
+ type: Pipe,
299
+ args: [{
300
+ name: 'morseCode',
301
+ standalone: true
302
+ }]
303
+ }] });
304
+
305
+ /**
306
+ * ReplacePipe: A custom Angular pipe that either highlights or replaces text based on a pattern.
307
+ *
308
+ * - If `isReplace` is `false`, it highlights occurrences of the pattern (if `highlightClass` is provided).
309
+ * - If `isReplace` is `true`, it replaces occurrences of the pattern with the replacement string, optionally highlighting the replacement.
310
+ *
311
+ * @param {string} value - The input string to transform.
312
+ * @param {string | RegExp} pattern - The pattern to match (string or RegExp). If an empty string, the value is returned as-is.
313
+ * @param {string} replacement - The string to replace matches with.
314
+ * @param {string} [highlightClass] - Optional CSS class for highlighting matched or replaced text (e.g., 'highlight').
315
+ * @param {boolean} [isReplace=true] - Whether to perform replacement (true) or only highlight matches (false).
316
+ *
317
+ * @returns {string | SafeHtml} - Returns the transformed string or SafeHtml with highlights.
318
+ *
319
+ * @example
320
+ * {{ 'Hello World' | replace:'World':'Universe' }}
321
+ * // Output: Hello Universe
322
+ *
323
+ * {{ 'test123' | replace:/\d+/g:'X':'highlight' }}
324
+ * // Output: test<span class="highlight">X</span>
325
+ *
326
+ * {{ 'Angular is great' | replace:'great':'awesome':'highlight':true }}
327
+ * // Output: Angular is <span class="highlight">awesome</span>
328
+ *
329
+ * {{ 'Angular is great' | replace:'great':'awesome':'highlight':false }}
330
+ * // Output: Angular is <span class="highlight">great</span>
331
+ *
332
+ * <div [innerHTML]="'Angular is great' | replace:'great':'awesome':'highlight':false"></div>
333
+ * // Renders: Angular is <span class="highlight">great</span>
334
+ */
335
+ class ReplacePipe {
336
+ sanitizer = inject(DomSanitizer);
337
+ transform(value, pattern, replacement, highlightClass, isReplace = true) {
338
+ if (!value)
339
+ return '';
340
+ // handles empty string pattern
341
+ if (!pattern || (typeof pattern === 'string' && pattern.trim() === '')) {
342
+ return value;
343
+ }
344
+ const finalPattern = typeof pattern === 'string' ? new RegExp(pattern, 'gi') : pattern;
345
+ if (!highlightClass) {
346
+ return isReplace ? value.replace(finalPattern, replacement) : value;
347
+ }
348
+ // Sanitize the replacement to prevent XSS
349
+ const sanitizedReplacement = replacement.replace(/</g, '&lt;').replace(/>/g, '&gt;');
350
+ if (isReplace) {
351
+ const highlightedReplacement = `<span class="${highlightClass}">${sanitizedReplacement}</span>`;
352
+ const replaced = value.replace(finalPattern, highlightedReplacement);
353
+ return this.sanitizer.bypassSecurityTrustHtml(replaced);
354
+ }
355
+ const highlightedMatch = `<span class="${highlightClass}">$&</span>`;
356
+ const result = value.replace(finalPattern, highlightedMatch);
357
+ return this.sanitizer.bypassSecurityTrustHtml(result);
358
+ }
359
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
360
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" });
361
+ }
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, decorators: [{
363
+ type: Pipe,
364
+ args: [{
365
+ name: 'replace',
366
+ standalone: true
367
+ }]
368
+ }] });
369
+
370
+ /**
371
+ * SnakeCasePipe: Converts text to snake_case (e.g., "hello world" → "hello_world").
372
+ *
373
+ * @param {string} value - The input string to transform.
374
+ * @returns {string} The string in snake_case, or an empty string if input is invalid.
375
+ *
376
+ * @example
377
+ * ```html
378
+ * {{ 'hello world' | snakeCase }} <!-- Outputs: hello_world -->
379
+ * ```
380
+ */
381
+ class SnakeCasePipe {
382
+ transform(value) {
383
+ if (!value || typeof value !== 'string')
384
+ return '';
385
+ return value
386
+ .trim()
387
+ .replace(/([A-Z])/g, '_$1') // Convert camelCase to snake_case (e.g., helloWorld -> hello_World)
388
+ .toLowerCase() // Convert everything to lowercase
389
+ .replace(/[\s-]+/g, '_') // Replace spaces and hyphens with underscores
390
+ .replace(/[^a-z0-9_]+/g, '') // Remove all non-alphanumeric and non-underscore characters
391
+ .replace(/_+/g, '_') // Collapse multiple underscores
392
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
393
+ }
394
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
395
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, isStandalone: true, name: "snakeCase" });
396
+ }
397
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, decorators: [{
398
+ type: Pipe,
399
+ args: [{
400
+ name: 'snakeCase',
401
+ standalone: true
402
+ }]
403
+ }] });
404
+
405
+ /**
406
+ * TitleCasePipe: Capitalizes the first letter of each word in a string.
407
+ *
408
+ * @param {string} value - The input string to transform.
409
+ * @returns {string} The string with each word capitalized, or an empty string if input is invalid.
410
+ *
411
+ * @example
412
+ * ```html
413
+ * {{ 'hello world' | titleCase }} <!-- Outputs: Hello World -->
414
+ * ```
415
+ */
416
+ class TitleCasePipe {
417
+ transform(value) {
418
+ if (!value || typeof value !== 'string')
419
+ return '';
420
+ return value
421
+ .split(' ')
422
+ .filter(word => word.length > 0)
423
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
424
+ .join(' ');
425
+ }
426
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
427
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, isStandalone: true, name: "titleCase" });
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, decorators: [{
430
+ type: Pipe,
431
+ args: [{
432
+ name: 'titleCase',
433
+ standalone: true
434
+ }]
435
+ }] });
436
+
437
+ /**
438
+ * TruncatePipe: Truncates a string to a specified maximum length, optionally preserving words.
439
+ *
440
+ * This Angular pipe transforms a string input by truncating it to the given `maxLength`.
441
+ * It provides options to customize the ellipsis and preserve word boundaries.
442
+ *
443
+ * @param {string} value - The input string to be truncated.
444
+ * @param {number} [maxLength=10] - The maximum length of the truncated string. Defaults to 10.
445
+ * @param {string} [ellipsis='...'] - The string to append to the truncated portion. Defaults to '...'.
446
+ * @param {boolean} [preserveWords=false] - If true, truncates at the last space before `maxLength` to avoid cutting words. Defaults to false.
447
+ * @returns {string} - The truncated string. Returns an empty string if the input is null, undefined, or not a string.
448
+ *
449
+ * @example
450
+ * {{ 'This is a long sentence' | truncate }} // Returns 'This is a...'
451
+ * {{ 'This is a long sentence' | truncate: 20 }} // Returns 'This is a long sente...'
452
+ * {{ 'This is a long sentence' | truncate: 15: ' [more]' }} // Returns 'This is a long [more]'
453
+ * {{ 'This is a long sentence' | truncate: 15: '...' : true }} // Returns 'This is a...'
454
+ * {{ 'This is a long sentence' | truncate: 20: '...' : true }} // Returns 'This is a long...'
455
+ * {{ null | truncate }} // Returns ''
456
+ * {{ undefined | truncate }} // Returns ''
457
+ */
458
+ class TruncatePipe {
459
+ transform(value, maxLength = 10, ellipsis = '...', preserveWords = false) {
460
+ if (!value || typeof value !== 'string') {
461
+ return '';
462
+ }
463
+ if (value.length <= maxLength) {
464
+ return value;
465
+ }
466
+ const charsToKeep = maxLength - ellipsis.length;
467
+ // If maxLength is too small to even include the ellipsis, just return the ellipsis.
468
+ if (charsToKeep < 0) {
469
+ return ellipsis;
470
+ }
471
+ let truncated = value.substring(0, charsToKeep);
472
+ if (preserveWords) {
473
+ const lastSpaceIndex = truncated.lastIndexOf(' ');
474
+ // If a space is found and it's not the very beginning of the string
475
+ if (lastSpaceIndex !== -1 && lastSpaceIndex !== 0) {
476
+ truncated = truncated.substring(0, lastSpaceIndex);
477
+ }
478
+ }
479
+ return truncated.trim() + ellipsis;
480
+ }
481
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
482
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, isStandalone: true, name: "truncate" });
483
+ }
484
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, decorators: [{
485
+ type: Pipe,
486
+ args: [{
487
+ name: 'truncate',
488
+ standalone: true
489
+ }]
490
+ }] });
491
+
492
+ /**
493
+ * CreditCardMaskPipe: Masks all but the last four digits of a string, optionally controlled by a boolean flag.
494
+ * By default, masking is applied.
495
+ *
496
+ * @param {string} value - The input string to mask (e.g., credit card number).
497
+ * @param {boolean} shouldMask - (Optional) Determines if masking should be applied. Defaults to true.
498
+ * @returns {string} - The masked string or the original value if `shouldMask` is false or the value is too short.
499
+ *
500
+ * @example
501
+ * {{ '1234567890123456' | creditCardMask }} // Outputs: **** **** **** 3456
502
+ * {{ '1234-5678-9012-3456' | creditCardMask }} // Outputs: **** **** **** 3456
503
+ * {{ '1234567890123456' | creditCardMask: true }} // Outputs: **** **** **** 3456
504
+ * {{ '1234567890123456' | creditCardMask: false }} // Outputs: 1234567890123456
505
+ */
506
+ class CreditCardMaskPipe {
507
+ transform(value, shouldMask = true) {
508
+ if (!value) {
509
+ return value;
510
+ }
511
+ if (shouldMask) {
512
+ const cleanedValue = value.replace(/[\s-]/g, '');
513
+ const cleanedLength = cleanedValue.length;
514
+ if (cleanedLength < 4) {
515
+ return value;
516
+ }
517
+ const visibleDigits = cleanedValue.slice(-4);
518
+ const maskedSection = '*'.repeat(cleanedLength - 4);
519
+ const groupedMask = maskedSection.match(/.{1,4}/g)?.join(' ') ?? '';
520
+ return `${groupedMask} ${visibleDigits}`.trim();
521
+ }
522
+ return value;
523
+ }
524
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
525
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, isStandalone: true, name: "creditCardMask" });
526
+ }
527
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, decorators: [{
528
+ type: Pipe,
529
+ args: [{
530
+ name: 'creditCardMask',
531
+ standalone: true,
532
+ }]
533
+ }] });
534
+
535
+ /**
536
+ * EmailMaskPipe: Masks the local part of an email address, revealing only the first and last characters.
537
+ *
538
+ * This Angular pipe transforms an email address string by replacing the characters between the first and last characters of the local part (before the '@') with "***".
539
+ * If the local part is 2 characters or less, it masks all characters except the first.
540
+ *
541
+ * @param {string} value - The email address string to be masked.
542
+ * @returns {string} - The masked email address, or the original value if it's not a valid email or falsy.
543
+ *
544
+ * @example
545
+ * {{ 'test@example.com' | emailMask }} // Returns 't***t@example.com'
546
+ * {{ 'te@example.com' | emailMask }} // Returns 't***@example.com'
547
+ * {{ 't@example.com' | emailMask }} // Returns 't***@example.com'
548
+ * {{ 'example.com' | emailMask }} // Returns 'example.com'
549
+ * {{ null | emailMask }} // Returns ''
550
+ * {{ undefined | emailMask }} // Returns ''
551
+ */
552
+ class EmailMaskPipe {
553
+ transform(value) {
554
+ if (!value || !value.includes('@')) {
555
+ return value || '';
556
+ }
557
+ const [local, domain] = value.split('@');
558
+ if (local.length <= 2) {
559
+ return `${local[0]}***@${domain}`;
560
+ }
561
+ const firstChar = local[0];
562
+ const lastChar = local[local.length - 1];
563
+ return `${firstChar}***${lastChar}@${domain}`;
564
+ }
565
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
566
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, isStandalone: true, name: "emailMask" });
567
+ }
568
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, decorators: [{
569
+ type: Pipe,
570
+ args: [{
571
+ name: 'emailMask',
572
+ standalone: true
573
+ }]
574
+ }] });
575
+
576
+ /**
577
+ * Converts special HTML characters in a string to their corresponding HTML entities.
578
+ * This prevents the browser from interpreting the input as HTML, rendering it as plain text.
579
+ *
580
+ * @param {string} value - The input string containing HTML to escape.
581
+ * @returns {string} The string with special HTML characters escaped, or an empty string if input is invalid.
582
+ *
583
+ * @example
584
+ * ```html
585
+ * {{ '<p>Hello</p>' | htmlEscape }} <!-- Outputs: &lt;p&gt;Hello&lt;/p&gt; -->
586
+ * ```
587
+ */
588
+ class HtmlEscapePipe {
589
+ transform(value) {
590
+ if (!value || typeof value !== 'string')
591
+ return '';
592
+ const escapeMap = {
593
+ '&': '&amp;',
594
+ '<': '&lt;',
595
+ '>': '&gt;',
596
+ '"': '&quot;',
597
+ "'": '&apos;'
598
+ };
599
+ return value.replace(/[&<>"']/g, char => escapeMap[char]);
600
+ }
601
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
602
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, isStandalone: true, name: "htmlEscape" });
603
+ }
604
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, decorators: [{
605
+ type: Pipe,
606
+ args: [{
607
+ name: 'htmlEscape',
608
+ standalone: true
609
+ }]
610
+ }] });
611
+
612
+ /**
613
+ * Sanitizes HTML input to remove unsafe elements while allowing safe HTML to be rendered.
614
+ * Uses Angular's DomSanitizer to mark the output as trusted for use in [innerHTML].
615
+ *
616
+ * @param {string} value - The input string containing HTML to sanitize.
617
+ * @returns {SafeHtml} The sanitized HTML marked as safe, or an empty string if input is invalid.
618
+ *
619
+ * @remarks
620
+ * WARNING: Use with caution. Only apply to trusted input to avoid XSS risks.
621
+ * Ensure input is pre-validated or sourced from a secure origin (e.g., a controlled rich-text editor).
622
+ *
623
+ * @example
624
+ * ```html
625
+ * <div [innerHTML]="'<p>Hello</p><script>alert(1)</script>' | htmlSanitize"></div>
626
+ * <!-- Renders: <p>Hello</p> (script tag removed) -->
627
+ * ```
628
+ */
629
+ class HtmlSanitizePipe {
630
+ sanitizer = inject(DomSanitizer);
631
+ transform(value) {
632
+ if (!value || typeof value !== 'string')
633
+ return this.sanitizer.bypassSecurityTrustHtml('');
634
+ return this.sanitizer.sanitize(0, value) || this.sanitizer.bypassSecurityTrustHtml('');
635
+ }
636
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
637
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, isStandalone: true, name: "htmlSanitize" });
638
+ }
639
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, decorators: [{
640
+ type: Pipe,
641
+ args: [{
642
+ name: 'htmlSanitize',
643
+ standalone: true
644
+ }]
645
+ }] });
646
+
647
+ /**
648
+ * IpAddressMaskPipe: Masks the last two octets of an IPv4 address.
649
+ *
650
+ * @param {string} value - The IPv4 address (e.g., 192.168.1.1).
651
+ * @param {boolean} shouldMask - (Optional) Determines if masking should be applied. Defaults to true..
652
+ *
653
+ * @returns {string} - The masked IP address (e.g., 192.168.*.*).
63
654
  *
64
- * @author Mofiro Jean
655
+ * @example
656
+ * {{ '192.168.1.1' | ipAddressMask }} // Outputs: 192.168.*.*
657
+ * {{ '10.0.0.255' | ipAddressMask }} // Outputs: 10.0.*.*
65
658
  */
66
- class AsciiArtPipe {
67
- generator;
68
- constructor() {
69
- this.generator = new AsciiGenerator({
70
- charset: CharsetPreset.STANDARD,
71
- width: 80,
72
- optimized: true,
73
- });
74
- }
75
- transform(value, options = {}) {
76
- if (!value || typeof value !== 'string') {
77
- return '';
78
- }
79
- const maxLength = 100;
80
- if (value.length > maxLength) {
81
- console.warn(`AsciiArtPipe: Input truncated to ${maxLength} characters for security`);
82
- value = value.substring(0, maxLength);
83
- }
84
- const { format = 'html', textOptions, ...config } = options;
85
- try {
86
- if (Object.keys(config).length > 0) {
87
- this.generator.updateConfig(config);
88
- }
89
- const result = this.generator.convertText(value, textOptions);
90
- if (format === 'text') {
91
- return `<pre class="ascii-art">${this.escapeHtml(result.text)}</pre>`;
92
- }
93
- // Return HTML format (already safe from ts-ascii-engine)
94
- return result.html;
659
+ class IpAddressMaskPipe {
660
+ transform(value, shouldMask = true) {
661
+ if (!value || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(value)) {
662
+ return value;
95
663
  }
96
- catch (error) {
97
- console.error('AsciiArtPipe: Error generating ASCII art', error);
98
- return `<pre class="ascii-art-error">Error: Unable to generate ASCII art</pre>`;
664
+ if (shouldMask) {
665
+ const parts = value.split('.');
666
+ return `${parts[0]}.${parts[1]}.*.*`;
99
667
  }
668
+ return value;
100
669
  }
101
- /**
102
- * Escapes HTML special characters to prevent XSS
103
- * @private
104
- */
105
- escapeHtml(text) {
106
- const div = document.createElement('div');
107
- div.textContent = text;
108
- return div.innerHTML;
109
- }
110
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AsciiArtPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
111
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: AsciiArtPipe, isStandalone: true, name: "asciiArt" });
670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
671
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, isStandalone: true, name: "ipAddressMask" });
112
672
  }
113
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AsciiArtPipe, decorators: [{
673
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, decorators: [{
114
674
  type: Pipe,
115
675
  args: [{
116
- name: 'asciiArt',
117
- standalone: true,
676
+ name: 'ipAddressMask',
677
+ standalone: true
118
678
  }]
119
- }], ctorParameters: () => [] });
679
+ }] });
120
680
 
121
681
  /**
122
682
  * BarcodePipe: Generates a barcode from a string value.
@@ -129,8 +689,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
129
689
  * @example
130
690
  * <div [innerHTML]="'123456789' | barcode:{elementType:'svg',format:'CODE128'} | async"></div>
131
691
  * <img [src]="'123456789' | barcode:{elementType:'img'} | async" />
132
- *
133
- * @author Mofiro Jean
134
692
  */
135
693
  class BarcodePipe {
136
694
  sanitizer = inject(DomSanitizer);
@@ -171,45 +729,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
171
729
  }]
172
730
  }] });
173
731
 
174
- /**
175
- * CamelCasePipe: Converts text to camelCase (e.g., "hello world" → "helloWorld").
176
- *
177
- * @param {string} value - The input string to transform.
178
- * @returns {string} The string in camelCase, or an empty string if input is invalid.
179
- *
180
- * @example
181
- * ```html
182
- * {{ 'hello world' | camelCase }} <!-- Outputs: helloWorld -->
183
- * ```
184
- *
185
- * @author Mofiro Jean
186
- */
187
- class CamelCasePipe {
188
- transform(value) {
189
- if (!value || typeof value !== 'string')
190
- return '';
191
- return value
192
- .toLowerCase()
193
- .replace(/[^a-zA-Z0-9]+/g, ' ')
194
- .trim()
195
- .split(' ')
196
- .filter(word => word.length > 0)
197
- .map((word, index) => index === 0
198
- ? word.toLowerCase()
199
- : word.charAt(0).toUpperCase() + word.slice(1))
200
- .join('');
201
- }
202
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
203
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, isStandalone: true, name: "camelCase" });
204
- }
205
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, decorators: [{
206
- type: Pipe,
207
- args: [{
208
- name: 'camelCase',
209
- standalone: true
210
- }]
211
- }] });
212
-
213
732
  // Pre-compiled regex patterns for performance
214
733
  const HEX_6_PATTERN = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
215
734
  const HEX_8_PATTERN = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
@@ -361,8 +880,6 @@ function parseColor(value) {
361
880
  * // Alpha channel support
362
881
  * {{ '#FF000080' | colorConvert:'rgba' }} // rgba(255, 0, 0, 0.5)
363
882
  * {{ 'rgba(255, 0, 0, 0.5)' | colorConvert:'hex' }} // #FF000080
364
- *
365
- * @author Mofiro Jean
366
883
  */
367
884
  class ColorConvertPipe {
368
885
  transform(value, target) {
@@ -378,172 +895,25 @@ class ColorConvertPipe {
378
895
  switch (target) {
379
896
  case 'hex':
380
897
  if (a < 1) {
381
- return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaToHex(a)}`;
382
- }
383
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
384
- case 'rgb':
385
- return `rgb(${clampChannel(r)}, ${clampChannel(g)}, ${clampChannel(b)})`;
386
- case 'rgba':
387
- return `rgba(${clampChannel(r)}, ${clampChannel(g)}, ${clampChannel(b)}, ${clampAlpha(a)})`;
388
- default:
389
- return value;
390
- }
391
- }
392
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
393
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, isStandalone: true, name: "colorConvert" });
394
- }
395
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, decorators: [{
396
- type: Pipe,
397
- args: [{
398
- name: 'colorConvert',
399
- standalone: true,
400
- }]
401
- }] });
402
-
403
- class CountPipe {
404
- transform(value) {
405
- if (value === null || value === undefined) {
406
- return 0;
407
- }
408
- if (Array.isArray(value) || typeof value === 'string') {
409
- return value.length;
410
- }
411
- if (typeof value === 'object') {
412
- return Object.keys(value).length;
413
- }
414
- return 0;
415
- }
416
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
417
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, isStandalone: true, name: "count" });
418
- }
419
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, decorators: [{
420
- type: Pipe,
421
- args: [{
422
- name: 'count',
423
- standalone: true
424
- }]
425
- }] });
426
-
427
- /**
428
- * CreditCardMaskPipe: Masks all but the last four digits of a string, optionally controlled by a boolean flag.
429
- * By default, masking is applied.
430
- *
431
- * @param {string} value - The input string to mask (e.g., credit card number).
432
- * @param {boolean} shouldMask - (Optional) Determines if masking should be applied. Defaults to true.
433
- * @returns {string} - The masked string or the original value if `shouldMask` is false or the value is too short.
434
- *
435
- * @example
436
- * {{ '1234567890123456' | creditCardMask }} // Outputs: **** **** **** 3456
437
- * {{ '1234-5678-9012-3456' | creditCardMask }} // Outputs: **** **** **** 3456
438
- * {{ '1234567890123456' | creditCardMask: true }} // Outputs: **** **** **** 3456
439
- * {{ '1234567890123456' | creditCardMask: false }} // Outputs: 1234567890123456
440
- *
441
- * @author Mofiro Jean
442
- */
443
- class CreditCardMaskPipe {
444
- transform(value, shouldMask = true) {
445
- if (!value) {
446
- return value;
447
- }
448
- if (shouldMask) {
449
- const cleanedValue = value.replace(/[\s-]/g, '');
450
- const cleanedLength = cleanedValue.length;
451
- if (cleanedLength < 4) {
452
- return value;
453
- }
454
- const visibleDigits = cleanedValue.slice(-4);
455
- const maskedSection = '*'.repeat(cleanedLength - 4);
456
- const groupedMask = maskedSection.match(/.{1,4}/g)?.join(' ') ?? '';
457
- return `${groupedMask} ${visibleDigits}`.trim();
458
- }
459
- return value;
460
- }
461
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
462
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, isStandalone: true, name: "creditCardMask" });
463
- }
464
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, decorators: [{
465
- type: Pipe,
466
- args: [{
467
- name: 'creditCardMask',
468
- standalone: true,
469
- }]
470
- }] });
471
-
472
- /**
473
- * DeviceTypePipe: Detects the device type based on the user agent string.
474
- *
475
- * @param {string} value - The user agent string (defaults to navigator.userAgent).
476
- *
477
- * @returns {'mobile' | 'tablet' | 'desktop' | 'unknown'} - The detected device type.
478
- *
479
- * @example
480
- * {{ '' | device }} // Outputs: 'mobile' (on a mobile device)
481
- * <div *ngIf="'' | device === 'desktop'">Desktop-only content</div>
482
- */
483
- class DeviceTypePipe {
484
- transform(value = typeof navigator !== 'undefined' ? navigator.userAgent : '') {
485
- if (!value)
486
- return 'unknown';
487
- const userAgent = value.toLowerCase();
488
- const isMobile = /mobile|android|iphone|ipod|blackberry|opera mini|iemobile|windows phone/i.test(userAgent);
489
- const isTablet = /ipad|tablet|kindle|playbook|silk|nexus 7|nexus 10|android(?!.*mobile)/i.test(userAgent);
490
- if (isMobile)
491
- return 'mobile';
492
- if (isTablet)
493
- return 'tablet';
494
- return 'desktop';
495
- }
496
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DeviceTypePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
497
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: DeviceTypePipe, isStandalone: true, name: "device" });
498
- }
499
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DeviceTypePipe, decorators: [{
500
- type: Pipe,
501
- args: [{
502
- name: 'device',
503
- standalone: true
504
- }]
505
- }] });
506
-
507
- /**
508
- * EmailMaskPipe: Masks the local part of an email address, revealing only the first and last characters.
509
- *
510
- * This Angular pipe transforms an email address string by replacing the characters between the first and last characters of the local part (before the '@') with "***".
511
- * If the local part is 2 characters or less, it masks all characters except the first.
512
- *
513
- * @param {string} value - The email address string to be masked.
514
- * @returns {string} - The masked email address, or the original value if it's not a valid email or falsy.
515
- *
516
- * @example
517
- * {{ 'test@example.com' | emailMask }} // Returns 't***t@example.com'
518
- * {{ 'te@example.com' | emailMask }} // Returns 't***@example.com'
519
- * {{ 't@example.com' | emailMask }} // Returns 't***@example.com'
520
- * {{ 'example.com' | emailMask }} // Returns 'example.com'
521
- * {{ null | emailMask }} // Returns ''
522
- * {{ undefined | emailMask }} // Returns ''
523
- *
524
- * @author Mofiro Jean
525
- */
526
- class EmailMaskPipe {
527
- transform(value) {
528
- if (!value || !value.includes('@')) {
529
- return value || '';
530
- }
531
- const [local, domain] = value.split('@');
532
- if (local.length <= 2) {
533
- return `${local[0]}***@${domain}`;
898
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaToHex(a)}`;
899
+ }
900
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
901
+ case 'rgb':
902
+ return `rgb(${clampChannel(r)}, ${clampChannel(g)}, ${clampChannel(b)})`;
903
+ case 'rgba':
904
+ return `rgba(${clampChannel(r)}, ${clampChannel(g)}, ${clampChannel(b)}, ${clampAlpha(a)})`;
905
+ default:
906
+ return value;
534
907
  }
535
- const firstChar = local[0];
536
- const lastChar = local[local.length - 1];
537
- return `${firstChar}***${lastChar}@${domain}`;
538
908
  }
539
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
540
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, isStandalone: true, name: "emailMask" });
909
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
910
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, isStandalone: true, name: "colorConvert" });
541
911
  }
542
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, decorators: [{
912
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, decorators: [{
543
913
  type: Pipe,
544
914
  args: [{
545
- name: 'emailMask',
546
- standalone: true
915
+ name: 'colorConvert',
916
+ standalone: true,
547
917
  }]
548
918
  }] });
549
919
 
@@ -557,8 +927,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
557
927
  *
558
928
  * @example
559
929
  * <img [src]="'user@example.com' | gravatar:100" />
560
- *
561
- * @author Mofiro Jean
562
930
  */
563
931
  class GravatarPipe {
564
932
  transform(value, size = 80) {
@@ -579,184 +947,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
579
947
  }] });
580
948
 
581
949
  /**
582
- * HighlightPipe: Highlights occurrences of a search term within a string.
950
+ * QrCodePipe: Generates a QR code from a string.
583
951
  *
584
- * This Angular pipe transforms a string input by wrapping all occurrences of a specified
585
- * search term with a `<span>` element that has the class "highlight".
586
- * It uses the Angular `DomSanitizer` to bypass security and render the highlighted HTML.
952
+ * @param {string} value - The string to encode.
953
+ * @param {QrCodeOptions} [options] - The QR code options.
587
954
  *
588
- * @param {string} value - The input string in which to highlight the search term.
589
- * @param {string} searchTerm - The string to search for and highlight.
590
- * @returns {SafeHtml} - The input string with the search term highlighted, or an empty string if input or searchTerm are falsy.
955
+ * @returns {Promise<string>} - A promise that resolves with the QR code data URL.
591
956
  *
592
957
  * @example
593
- * {{ 'This is a test string' | highlight: 'test' }} // Returns 'This is a <span class="highlight">test</span> string'
594
- * {{ 'This is a test TEST string' | highlight: 'test' }} // Returns 'This is a <span class="highlight">test</span> <span class="highlight">TEST</span> string'
595
- * {{ 'This is a test string' | highlight: '' }} // Returns 'This is a test string'
596
- * {{ null | highlight: 'test' }} // Returns ''
597
- * {{ undefined | highlight: 'test' }} // Returns ''
598
- *
599
- * @author Mofiro Jean
958
+ * <img [src]="'Hello, World!' | qrCode | async" />
600
959
  */
601
- class HighlightPipe {
602
- sanitizer = inject(DomSanitizer);
603
- transform(value, searchTerm) {
604
- if (!value || !searchTerm) {
605
- return this.sanitizer.bypassSecurityTrustHtml(value || '');
960
+ class QrCodePipe {
961
+ transform(value, options) {
962
+ if (!value) {
963
+ return Promise.resolve('');
606
964
  }
607
- const escapedSearch = searchTerm.replace(/[.*+?${}()|[\\]/g, '\\$&');
608
- const regex = new RegExp(`(${escapedSearch})`, 'gi');
609
- const highlighed = value.replace(regex, '<span class="highlight">$1</span>');
610
- return this.sanitizer.bypassSecurityTrustHtml(highlighed);
611
- }
612
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
613
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, isStandalone: true, name: "highlight" });
614
- }
615
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, decorators: [{
616
- type: Pipe,
617
- args: [{
618
- name: 'highlight',
619
- standalone: true
620
- }]
621
- }] });
622
-
623
- /**
624
- * Converts special HTML characters in a string to their corresponding HTML entities.
625
- * This prevents the browser from interpreting the input as HTML, rendering it as plain text.
626
- *
627
- * @param {string} value - The input string containing HTML to escape.
628
- * @returns {string} The string with special HTML characters escaped, or an empty string if input is invalid.
629
- *
630
- * @example
631
- * ```html
632
- * {{ '<p>Hello</p>' | htmlEscape }} <!-- Outputs: &lt;p&gt;Hello&lt;/p&gt; -->
633
- * ```
634
- */
635
- class HtmlEscapePipe {
636
- transform(value) {
637
- if (!value || typeof value !== 'string')
638
- return '';
639
- const escapeMap = {
640
- '&': '&amp;',
641
- '<': '&lt;',
642
- '>': '&gt;',
643
- '"': '&quot;',
644
- "'": '&apos;'
645
- };
646
- return value.replace(/[&<>"']/g, char => escapeMap[char]);
965
+ return QRCode.toDataURL(value, options);
647
966
  }
648
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
649
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, isStandalone: true, name: "htmlEscape" });
967
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
968
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, isStandalone: true, name: "qrCode" });
650
969
  }
651
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, decorators: [{
970
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, decorators: [{
652
971
  type: Pipe,
653
972
  args: [{
654
- name: 'htmlEscape',
973
+ name: 'qrCode',
655
974
  standalone: true
656
975
  }]
657
976
  }] });
658
977
 
659
- /**
660
- * Sanitizes HTML input to remove unsafe elements while allowing safe HTML to be rendered.
661
- * Uses Angular's DomSanitizer to mark the output as trusted for use in [innerHTML].
662
- *
663
- * @param {string} value - The input string containing HTML to sanitize.
664
- * @returns {SafeHtml} The sanitized HTML marked as safe, or an empty string if input is invalid.
665
- *
666
- * @remarks
667
- * WARNING: Use with caution. Only apply to trusted input to avoid XSS risks.
668
- * Ensure input is pre-validated or sourced from a secure origin (e.g., a controlled rich-text editor).
669
- *
670
- * @example
671
- * ```html
672
- * <div [innerHTML]="'<p>Hello</p><script>alert(1)</script>' | htmlSanitize"></div>
673
- * <!-- Renders: <p>Hello</p> (script tag removed) -->
674
- * ```
675
- */
676
- class HtmlSanitizePipe {
677
- sanitizer = inject(DomSanitizer);
978
+ class CountPipe {
678
979
  transform(value) {
679
- if (!value || typeof value !== 'string')
680
- return this.sanitizer.bypassSecurityTrustHtml('');
681
- return this.sanitizer.sanitize(0, value) || this.sanitizer.bypassSecurityTrustHtml('');
980
+ if (value === null || value === undefined) {
981
+ return 0;
982
+ }
983
+ if (Array.isArray(value) || typeof value === 'string') {
984
+ return value.length;
985
+ }
986
+ if (typeof value === 'object') {
987
+ return Object.keys(value).length;
988
+ }
989
+ return 0;
682
990
  }
683
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
684
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, isStandalone: true, name: "htmlSanitize" });
991
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
992
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, isStandalone: true, name: "count" });
685
993
  }
686
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, decorators: [{
994
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, decorators: [{
687
995
  type: Pipe,
688
996
  args: [{
689
- name: 'htmlSanitize',
997
+ name: 'count',
690
998
  standalone: true
691
999
  }]
692
1000
  }] });
693
1001
 
694
1002
  /**
695
- * InitialsPipe: Extracts initials from a name.
1003
+ * DeviceTypePipe: Detects the device type based on the user agent string.
696
1004
  *
697
- * @param {string} value - The full name.
1005
+ * @param {string} value - The user agent string (defaults to navigator.userAgent).
698
1006
  *
699
- * @returns {string} - The initials (e.g., 'John Doe' 'JD').
1007
+ * @returns {'mobile' | 'tablet' | 'desktop' | 'unknown'} - The detected device type.
700
1008
  *
701
1009
  * @example
702
- * {{ 'John Doe' | initials }} // Outputs: JD
703
- * {{ 'Mary Jane Watson' | initials }} // Outputs: MJW
704
- *
705
- * @author Mofiro Jean
1010
+ * {{ '' | device }} // Outputs: 'mobile' (on a mobile device)
1011
+ * <div *ngIf="'' | device === 'desktop'">Desktop-only content</div>
706
1012
  */
707
- class InitialsPipe {
708
- transform(value) {
1013
+ class DeviceTypePipe {
1014
+ transform(value = typeof navigator !== 'undefined' ? navigator.userAgent : '') {
709
1015
  if (!value)
710
- return '';
711
- return value
712
- .trim()
713
- .split(/\s+/)
714
- .map(word => word.charAt(0).toUpperCase())
715
- .join('');
716
- }
717
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
718
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, isStandalone: true, name: "initials" });
719
- }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, decorators: [{
721
- type: Pipe,
722
- args: [{
723
- name: 'initials',
724
- standalone: true
725
- }]
726
- }] });
727
-
728
- /**
729
- * IpAddressMaskPipe: Masks the last two octets of an IPv4 address.
730
- *
731
- * @param {string} value - The IPv4 address (e.g., 192.168.1.1).
732
- * @param {boolean} shouldMask - (Optional) Determines if masking should be applied. Defaults to true..
733
- *
734
- * @returns {string} - The masked IP address (e.g., 192.168.*.*).
735
- *
736
- * @example
737
- * {{ '192.168.1.1' | ipAddressMask }} // Outputs: 192.168.*.*
738
- * {{ '10.0.0.255' | ipAddressMask }} // Outputs: 10.0.*.*
739
- *
740
- * @author Mofiro Jean
741
- */
742
- class IpAddressMaskPipe {
743
- transform(value, shouldMask = true) {
744
- if (!value || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(value)) {
745
- return value;
746
- }
747
- if (shouldMask) {
748
- const parts = value.split('.');
749
- return `${parts[0]}.${parts[1]}.*.*`;
750
- }
751
- return value;
1016
+ return 'unknown';
1017
+ const userAgent = value.toLowerCase();
1018
+ const isMobile = /mobile|android|iphone|ipod|blackberry|opera mini|iemobile|windows phone/i.test(userAgent);
1019
+ const isTablet = /ipad|tablet|kindle|playbook|silk|nexus 7|nexus 10|android(?!.*mobile)/i.test(userAgent);
1020
+ if (isMobile)
1021
+ return 'mobile';
1022
+ if (isTablet)
1023
+ return 'tablet';
1024
+ return 'desktop';
752
1025
  }
753
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
754
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, isStandalone: true, name: "ipAddressMask" });
1026
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DeviceTypePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1027
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: DeviceTypePipe, isStandalone: true, name: "device" });
755
1028
  }
756
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, decorators: [{
1029
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DeviceTypePipe, decorators: [{
757
1030
  type: Pipe,
758
1031
  args: [{
759
- name: 'ipAddressMask',
1032
+ name: 'device',
760
1033
  standalone: true
761
1034
  }]
762
1035
  }] });
@@ -772,8 +1045,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
772
1045
  * @example
773
1046
  * {{ '{"name": "John", "age": 30}' | jsonPretty }} // Outputs: Colorful, indented JSON
774
1047
  * <pre [innerHTML]="data | jsonPretty:4"></pre> // 4-space indentation
775
- *
776
- * @author Mofiro Jean
777
1048
  */
778
1049
  class JsonPrettyPipe {
779
1050
  sanitizer = inject(DomSanitizer);
@@ -841,180 +1112,162 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
841
1112
  }] });
842
1113
 
843
1114
  /**
844
- * KebabCasePipe: Converts text to kebab-case (e.g., "hello world" "hello-world").
1115
+ * TextToSpeechPipe: Converts text to speech using the Web Speech API.
845
1116
  *
846
- * @param {string} value - The input string to transform.
847
- * @returns {string} The string in kebab-case, or an empty string if input is invalid.
1117
+ * @param {string} value - The text to convert to speech.
1118
+ * @param {string} [lang='en-US'] - The language (local) for speech synthesis.
848
1119
  *
849
- * @example
850
- * ```html
851
- * {{ 'hello world' | kebabCase }} <!-- Outputs: hello-world -->
852
- * ```
1120
+ * @returns {void} - Triggers speech synthesis (no return value).
853
1121
  *
854
- * @author Mofiro Jean
1122
+ * @example
1123
+ * <div>{{ Hello World' | textToSpeech }}</div>
1124
+ * <div>{{ 'Bonjour' | textToSpeech:'fr-FR' }}h</div>
855
1125
  */
856
- class KebabCasePipe {
857
- transform(value) {
858
- if (!value || typeof value !== 'string')
859
- return '';
860
- return value
861
- .trim()
862
- .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') // Add hyphen between camelCase words
863
- .toLowerCase()
864
- .replace(/[^a-z0-9-]+/g, '-') // Replace non-alphanumeric (except hyphen) with hyphen
865
- .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
1126
+ class TextToSpeechPipe {
1127
+ transform(value, lang = 'en-US') {
1128
+ if (!value || typeof window === 'undefined' || !window.speechSynthesis)
1129
+ return;
1130
+ const uttrance = new SpeechSynthesisUtterance(value);
1131
+ uttrance.lang = lang;
1132
+ window.speechSynthesis.speak(uttrance);
866
1133
  }
867
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
868
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, isStandalone: true, name: "kebabCase" });
1134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1135
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, isStandalone: true, name: "textToSpeech" });
869
1136
  }
870
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, decorators: [{
1137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, decorators: [{
871
1138
  type: Pipe,
872
1139
  args: [{
873
- name: 'kebabCase',
1140
+ name: 'textToSpeech',
874
1141
  standalone: true
875
1142
  }]
876
1143
  }] });
877
1144
 
878
1145
  /**
879
- * MorseCodePipe: Converts text to Morse code.
1146
+ * TimeAgo: Converts a date into a localized time string.
880
1147
  *
881
- * @param {string} value - The text to convert to Morse code.
1148
+ * Use the in-built Intl.RelativeTimeFormat to convert a date into a localized time string.
1149
+ * It was chosen over moment.js because it's more lightweight and supports more locales.
1150
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
882
1151
  *
883
- * @returns {string} - The Morse code representation (e.g., 'SOS' '... --- ...').
1152
+ * @param {Date | number | string} value - The date to convert.
1153
+ * @param {string} [local='en'] - BCP 47 local code (e.g., 'en', 'fr', 'es').
1154
+ *
1155
+ * @returns {string} - The localized time string.
884
1156
  *
885
1157
  * @example
886
- * {{ 'SOS' | morseCode }} // Outputs: '... --- ...'
887
- * {{ 'HELP' | morseCode }} // Outputs: '.... . .-.. .--.'
888
- * <p>{{ userInput | morseCode }}</p>
889
- *
890
- * @author Mofiro Jean
891
- */
892
- class MorseCodePipe {
893
- morseCodeMap = {
894
- 'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 'F': '..-.',
895
- 'G': '--.', 'H': '....', 'I': '..', 'J': '.---', 'K': '-.-', 'L': '.-..',
896
- 'M': '--', 'N': '-.', 'O': '---', 'P': '.--.', 'Q': '--.-', 'R': '.-.',
897
- 'S': '...', 'T': '-', 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-',
898
- 'Y': '-.--', 'Z': '--..', '0': '-----', '1': '.----', '2': '..---',
899
- '3': '...--', '4': '....-', '5': '.....', '6': '-....', '7': '--...',
900
- '8': '---..', '9': '----.'
901
- };
902
- transform(value) {
903
- if (!value || typeof value !== 'string') {
904
- return '';
1158
+ * {{ date | timeAgo }} // '5 minutes ago'
1159
+ * {{ date | timeAgo:'fr' }} // 'il y a 5 minutes'
1160
+ *
1161
+ * @note Pure pipe - output won't automatically update as time passes.
1162
+ * Use signals or periodic change detection to re-trigger.
1163
+ * */
1164
+ class TimeAgoPipePipe {
1165
+ static THRESHOLDS = [
1166
+ [60, 1, 'second'],
1167
+ [3600, 60, 'minute'],
1168
+ [86400, 3600, 'hour'],
1169
+ [604800, 86400, 'day'],
1170
+ [2592000, 604800, 'week'],
1171
+ [31536000, 2592000, 'month'],
1172
+ [Infinity, 31536000, 'year'],
1173
+ ];
1174
+ cacheLocal = '';
1175
+ rtf;
1176
+ transform(value, local = 'en') {
1177
+ if (value === null || value === undefined || value === "") {
1178
+ return "";
905
1179
  }
906
- return value
907
- .toUpperCase()
908
- .split('')
909
- .map(char => this.morseCodeMap[char] || '')
910
- .filter(code => code)
911
- .join(' ');
1180
+ const date = new Date(value);
1181
+ if (isNaN(date.getTime())) {
1182
+ return "";
1183
+ }
1184
+ if (local !== this.cacheLocal) {
1185
+ this.rtf = new Intl.RelativeTimeFormat(local, { numeric: "auto" });
1186
+ this.cacheLocal = local;
1187
+ }
1188
+ const seconds = Math.floor((date.getTime() - Date.now()) / 1000);
1189
+ for (const [max, divisor, unit] of TimeAgoPipePipe.THRESHOLDS) {
1190
+ if (Math.abs(seconds) < max) {
1191
+ return this.rtf.format(Math.round(seconds / divisor), unit);
1192
+ }
1193
+ }
1194
+ return "";
912
1195
  }
913
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
914
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, isStandalone: true, name: "morseCode" });
1196
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1197
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, isStandalone: true, name: "timeAgo" });
915
1198
  }
916
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, decorators: [{
1199
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, decorators: [{
917
1200
  type: Pipe,
918
1201
  args: [{
919
- name: 'morseCode',
1202
+ name: 'timeAgo',
920
1203
  standalone: true
921
1204
  }]
922
1205
  }] });
923
1206
 
924
1207
  /**
925
- * QrCodePipe: Generates a QR code from a string.
1208
+ * FlattenPipe: Flattens nested arrays to a specified depth.
926
1209
  *
927
- * @param {string} value - The string to encode.
928
- * @param {QRCode.QRCodeToDataURLOptions} [options] - The QR code options.
1210
+ * @param {unknown[]} value - The nested array to flatten.
1211
+ * @param {number} [depth=Infinity] - How many levels of nesting to flatten.
929
1212
  *
930
- * @returns {Promise<string>} - A promise that resolves with the QR code data URL.
1213
+ * @returns {unknown[]} - A new flattened array.
931
1214
  *
932
1215
  * @example
933
- * <img [src]="'Hello, World!' | qrCode | async" />
934
- *
935
- * @author Mofiro Jean
1216
+ * {{ [[1, 2], [3, 4]] | flatten }} // [1, 2, 3, 4]
1217
+ * {{ [[1, [2, [3]]]] | flatten:1 }} // [1, 2, [3]]
1218
+ * {{ [['a', 'b'], ['c']] | flatten }} // ['a', 'b', 'c']
936
1219
  */
937
- class QrCodePipe {
938
- transform(value, options) {
939
- if (!value) {
940
- return Promise.resolve('');
1220
+ class Flatten {
1221
+ transform(value, depth = Infinity) {
1222
+ if (!Array.isArray(value)) {
1223
+ return [];
941
1224
  }
942
- return QRCode.toDataURL(value, options);
1225
+ return value.flat(depth);
943
1226
  }
944
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
945
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, isStandalone: true, name: "qrCode" });
1227
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: Flatten, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1228
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: Flatten, isStandalone: true, name: "flatten" });
946
1229
  }
947
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, decorators: [{
1230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: Flatten, decorators: [{
948
1231
  type: Pipe,
949
1232
  args: [{
950
- name: 'qrCode',
1233
+ name: 'flatten',
951
1234
  standalone: true
952
1235
  }]
953
1236
  }] });
954
1237
 
955
1238
  /**
956
- * ReplacePipe: A custom Angular pipe that either highlights or replaces text based on a pattern.
957
- *
958
- * - If `isReplace` is `false`, it highlights occurrences of the pattern (if `highlightClass` is provided).
959
- * - If `isReplace` is `true`, it replaces occurrences of the pattern with the replacement string, optionally highlighting the replacement.
1239
+ * InitialPipe: Returns all elements except the last n.
960
1240
  *
961
- * @param {string} value - The input string to transform.
962
- * @param {string | RegExp} pattern - The pattern to match (string or RegExp). If an empty string, the value is returned as-is.
963
- * @param {string} replacement - The string to replace matches with.
964
- * @param {string} [highlightClass] - Optional CSS class for highlighting matched or replaced text (e.g., 'highlight').
965
- * @param {boolean} [isReplace=true] - Whether to perform replacement (true) or only highlight matches (false).
1241
+ * @param {unknown[]} value - The array to slice.
1242
+ * @param {number} [n=1] - Number of elements to exclude from the end.
966
1243
  *
967
- * @returns {string | SafeHtml} - Returns the transformed string or SafeHtml with highlights.
1244
+ * @returns {unknown[]} - A new array without the last n elements.
968
1245
  *
969
1246
  * @example
970
- * {{ 'Hello World' | replace:'World':'Universe' }}
971
- * // Output: Hello Universe
972
- *
973
- * {{ 'test123' | replace:/\d+/g:'X':'highlight' }}
974
- * // Output: test<span class="highlight">X</span>
975
- *
976
- * {{ 'Angular is great' | replace:'great':'awesome':'highlight':true }}
977
- * // Output: Angular is <span class="highlight">awesome</span>
978
- *
979
- * {{ 'Angular is great' | replace:'great':'awesome':'highlight':false }}
980
- * // Output: Angular is <span class="highlight">great</span>
981
- *
982
- * <div [innerHTML]="'Angular is great' | replace:'great':'awesome':'highlight':false"></div>
983
- * // Renders: Angular is <span class="highlight">great</span>
984
- *
985
- * @author Mofiro Jean
1247
+ * {{ [1, 2, 3, 4, 5] | initial }} // [1, 2, 3, 4]
1248
+ * {{ [1, 2, 3, 4, 5] | initial:2 }} // [1, 2, 3]
1249
+ * {{ ['a', 'b', 'c'] | initial }} // ['a', 'b']
986
1250
  */
987
- class ReplacePipe {
988
- sanitizer = inject(DomSanitizer);
989
- transform(value, pattern, replacement, highlightClass, isReplace = true) {
990
- if (!value)
991
- return '';
992
- // handles empty string pattern
993
- if (!pattern || (typeof pattern === 'string' && pattern.trim() === '')) {
994
- return value;
1251
+ class InitialPipe {
1252
+ transform(value, n = 1) {
1253
+ if (!Array.isArray(value)) {
1254
+ return [];
995
1255
  }
996
- const finalPattern = typeof pattern === 'string' ? new RegExp(pattern, 'gi') : pattern;
997
- if (!highlightClass) {
998
- return isReplace ? value.replace(finalPattern, replacement) : value;
1256
+ if (n <= 0) {
1257
+ return [...value];
999
1258
  }
1000
- // Sanitize the replacement to prevent XSS
1001
- const sanitizedReplacement = replacement.replace(/</g, '&lt;').replace(/>/g, '&gt;');
1002
- if (isReplace) {
1003
- const highlightedReplacement = `<span class="${highlightClass}">${sanitizedReplacement}</span>`;
1004
- const replaced = value.replace(finalPattern, highlightedReplacement);
1005
- return this.sanitizer.bypassSecurityTrustHtml(replaced);
1259
+ if (n >= value.length) {
1260
+ return [];
1006
1261
  }
1007
- const highlightedMatch = `<span class="${highlightClass}">$&</span>`;
1008
- const result = value.replace(finalPattern, highlightedMatch);
1009
- return this.sanitizer.bypassSecurityTrustHtml(result);
1262
+ return value.slice(0, -n);
1010
1263
  }
1011
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1012
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" });
1264
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1265
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: InitialPipe, isStandalone: true, name: "initial" });
1013
1266
  }
1014
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, decorators: [{
1267
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialPipe, decorators: [{
1015
1268
  type: Pipe,
1016
1269
  args: [{
1017
- name: 'replace',
1270
+ name: 'initial',
1018
1271
  standalone: true
1019
1272
  }]
1020
1273
  }] });
@@ -1030,15 +1283,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1030
1283
  * {{ 'hello' | reverse }} // Outputs: 'olleh'
1031
1284
  * {{ '12345' | reverse }} // Outputs: '54321'
1032
1285
  * <p>{{ userInput | reverse }}</p>
1033
- *
1034
- * @author Mofiro Jean
1035
1286
  */
1036
1287
  class ReversePipe {
1037
1288
  transform(value) {
1038
- if (!value || typeof value !== 'string') {
1039
- return '';
1289
+ if (Array.isArray(value)) {
1290
+ return [...value].reverse();
1040
1291
  }
1041
- return value.split('').reverse().join('');
1292
+ if (typeof value === 'string') {
1293
+ return value.split('').reverse().join('');
1294
+ }
1295
+ return '';
1042
1296
  }
1043
1297
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1044
1298
  static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, isStandalone: true, name: "reverse" });
@@ -1052,194 +1306,258 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1052
1306
  }] });
1053
1307
 
1054
1308
  /**
1055
- * SnakeCasePipe: Converts text to snake_case (e.g., "hello world" → "hello_world").
1309
+ * SamplePipe: Randomly selects n items from an array.
1056
1310
  *
1057
- * @param {string} value - The input string to transform.
1058
- * @returns {string} The string in snake_case, or an empty string if input is invalid.
1311
+ * Uses Fisher-Yates partial shuffle for unbiased selection.
1312
+ * Returns a single item when n=1 (default), or an array when n>1.
1313
+ *
1314
+ * @param {unknown[]} value - The array to sample from.
1315
+ * @param {number} [n=1] - Number of items to select.
1316
+ *
1317
+ * @returns {unknown | unknown[]} - A single random item (n=1) or array of random items (n>1).
1059
1318
  *
1060
1319
  * @example
1061
- * ```html
1062
- * {{ 'hello world' | snakeCase }} <!-- Outputs: hello_world -->
1063
- * ```
1320
+ * {{ [1, 2, 3, 4, 5] | sample }} // 3 (random single)
1321
+ * {{ [1, 2, 3, 4, 5] | sample:3 }} // [5, 1, 3] (random 3)
1322
+ * {{ users | sample:5 }} // 5 random users
1064
1323
  *
1065
- * @author Mofiro Jean
1324
+ * @note Impure pipe — returns different results on each change detection cycle.
1325
+ * Bind the result to a signal to control when it re-samples.
1066
1326
  */
1067
- class SnakeCasePipe {
1068
- transform(value) {
1069
- if (!value || typeof value !== 'string')
1070
- return '';
1071
- return value
1072
- .trim()
1073
- .replace(/([A-Z])/g, '_$1') // Convert camelCase to snake_case (e.g., helloWorld -> hello_World)
1074
- .toLowerCase() // Convert everything to lowercase
1075
- .replace(/[\s-]+/g, '_') // Replace spaces and hyphens with underscores
1076
- .replace(/[^a-z0-9_]+/g, '') // Remove all non-alphanumeric and non-underscore characters
1077
- .replace(/_+/g, '_') // Collapse multiple underscores
1078
- .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
1327
+ class SamplePipe {
1328
+ transform(value, n = 1) {
1329
+ if (!Array.isArray(value) || value.length === 0) {
1330
+ return n === 1 ? undefined : [];
1331
+ }
1332
+ if (n <= 0) {
1333
+ return [];
1334
+ }
1335
+ // Clamp n to array length
1336
+ const count = Math.min(n, value.length);
1337
+ // Fisher-Yates partial shuffle only shuffle `count` positions
1338
+ const arr = [...value];
1339
+ for (let i = arr.length - 1; i > arr.length - 1 - count; i--) {
1340
+ const j = Math.floor(Math.random() * (i + 1));
1341
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1342
+ }
1343
+ const result = arr.slice(arr.length - count);
1344
+ // Return single item for n=1, array otherwise
1345
+ return n === 1 ? result[0] : result;
1079
1346
  }
1080
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1081
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, isStandalone: true, name: "snakeCase" });
1347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SamplePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1348
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: SamplePipe, isStandalone: true, name: "sample", pure: false });
1082
1349
  }
1083
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, decorators: [{
1350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SamplePipe, decorators: [{
1084
1351
  type: Pipe,
1085
1352
  args: [{
1086
- name: 'snakeCase',
1087
- standalone: true
1353
+ name: 'sample',
1354
+ standalone: true,
1355
+ pure: false,
1088
1356
  }]
1089
1357
  }] });
1090
1358
 
1091
1359
  /**
1092
- * TextToSpeechPipe: Converts text to speech using the Web Speech API.
1360
+ * ShufflePipe: Randomly reorders elements in an array using the Fisher-Yates
1361
+ algorithm.
1093
1362
  *
1094
- * @param {string} value - The text to convert to speech.
1095
- * @param {string} [lang='en-US'] - The language (local) for speech synthesis.
1363
+ * Uses the Fisher-Yates (Knuth) shuffle for unbiased randomization,
1364
+ * guaranteeing every permutation has equal probability.
1096
1365
  *
1097
- * @returns {void} - Triggers speech synthesis (no return value).
1366
+ * @param {unknown[]} value - The array to shuffle.
1367
+ *
1368
+ * @returns {unknown[]} - A new array with elements in random order.
1098
1369
  *
1099
1370
  * @example
1100
- * <div>{{ Hello World' | textToSpeech }}</div>
1101
- * <div>{{ 'Bonjour' | textToSpeech:'fr-FR' }}h</div>
1371
+ * {{ [1, 2, 3, 4, 5] | shuffle }} // [3, 1, 5, 2, 4]
1372
+ * {{ ['a', 'b', 'c'] | shuffle }} // ['c', 'a', 'b']
1102
1373
  *
1103
- * @author Mofiro Jean
1374
+ * @note Impure pipe — runs on every change detection cycle.
1375
+ * Avoid using in performance-critical templates or bind the result
1376
+ * to a signal/variable to control when it re-shuffles.
1104
1377
  */
1105
- class TextToSpeechPipe {
1106
- transform(value, lang = 'en-US') {
1107
- if (!value || typeof window === 'undefined' || !window.speechSynthesis)
1108
- return;
1109
- const uttrance = new SpeechSynthesisUtterance(value);
1110
- uttrance.lang = lang;
1111
- window.speechSynthesis.speak(uttrance);
1378
+ class ShufflePipe {
1379
+ transform(value) {
1380
+ if (!Array.isArray(value)) {
1381
+ return [];
1382
+ }
1383
+ const arr = [...value];
1384
+ for (let i = arr.length - 1; i >= 0; i--) {
1385
+ const j = Math.floor(Math.random() * (i + 1));
1386
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1387
+ }
1388
+ return arr;
1112
1389
  }
1113
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1114
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, isStandalone: true, name: "textToSpeech" });
1390
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ShufflePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1391
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ShufflePipe, isStandalone: true, name: "shuffle", pure: false });
1115
1392
  }
1116
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, decorators: [{
1393
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ShufflePipe, decorators: [{
1117
1394
  type: Pipe,
1118
1395
  args: [{
1119
- name: 'textToSpeech',
1396
+ name: 'shuffle',
1397
+ pure: false,
1120
1398
  standalone: true
1121
1399
  }]
1122
1400
  }] });
1123
1401
 
1124
1402
  /**
1125
- * TitleCasePipe: Capitalizes the first letter of each word in a string.
1403
+ * TailPipe: Returns all elements except the first n.
1126
1404
  *
1127
- * @param {string} value - The input string to transform.
1128
- * @returns {string} The string with each word capitalized, or an empty string if input is invalid.
1405
+ * @param {unknown[]} value - The array to slice.
1406
+ * @param {number} [n=1] - Number of elements to exclude from the start.
1407
+ *
1408
+ * @returns {unknown[]} - A new array without the first n elements.
1129
1409
  *
1130
1410
  * @example
1131
- * ```html
1132
- * {{ 'hello world' | titleCase }} <!-- Outputs: Hello World -->
1133
- * ```
1411
+ * {{ [1, 2, 3, 4, 5] | tail }} // [2, 3, 4, 5]
1412
+ * {{ [1, 2, 3, 4, 5] | tail:2 }} // [3, 4, 5]
1413
+ * {{ ['a', 'b', 'c'] | tail }} // ['b', 'c']
1134
1414
  */
1135
- class TitleCasePipe {
1136
- transform(value) {
1137
- if (!value || typeof value !== 'string')
1138
- return '';
1139
- return value
1140
- .split(' ')
1141
- .filter(word => word.length > 0)
1142
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1143
- .join(' ');
1415
+ class TailPipe {
1416
+ transform(value, n = 1) {
1417
+ if (!Array.isArray(value)) {
1418
+ return [];
1419
+ }
1420
+ // n = 0 means keep everything
1421
+ if (n <= 0)
1422
+ return [...value];
1423
+ // n >= length means remove everything
1424
+ if (n >= value.length)
1425
+ return [];
1426
+ return value.slice(n);
1144
1427
  }
1145
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1146
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, isStandalone: true, name: "titleCase" });
1428
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TailPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1429
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TailPipe, isStandalone: true, name: "tail" });
1147
1430
  }
1148
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, decorators: [{
1431
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TailPipe, decorators: [{
1149
1432
  type: Pipe,
1150
1433
  args: [{
1151
- name: 'titleCase',
1152
- standalone: true
1434
+ name: 'tail',
1153
1435
  }]
1154
1436
  }] });
1155
1437
 
1156
1438
  /**
1157
- * TruncatePipe: Truncates a string to a specified maximum length, optionally preserving words.
1439
+ * TruthifyPipe: Removes all falsy values from an array.
1158
1440
  *
1159
- * This Angular pipe transforms a string input by truncating it to the given `maxLength`.
1160
- * It provides options to customize the ellipsis and preserve word boundaries.
1441
+ * Falsy values: false, 0, -0, '', null, undefined, NaN
1161
1442
  *
1162
- * @param {string} value - The input string to be truncated.
1163
- * @param {number} [maxLength=10] - The maximum length of the truncated string. Defaults to 10.
1164
- * @param {string} [ellipsis='...'] - The string to append to the truncated portion. Defaults to '...'.
1165
- * @param {boolean} [preserveWords=false] - If true, truncates at the last space before `maxLength` to avoid cutting words. Defaults to false.
1166
- * @returns {string} - The truncated string. Returns an empty string if the input is null, undefined, or not a string.
1443
+ * @param {unknown[]} value - The array to filter.
1167
1444
  *
1168
- * @example
1169
- * {{ 'This is a long sentence' | truncate }} // Returns 'This is a...'
1170
- * {{ 'This is a long sentence' | truncate: 20 }} // Returns 'This is a long sente...'
1171
- * {{ 'This is a long sentence' | truncate: 15: ' [more]' }} // Returns 'This is a long [more]'
1172
- * {{ 'This is a long sentence' | truncate: 15: '...' : true }} // Returns 'This is a...'
1173
- * {{ 'This is a long sentence' | truncate: 20: '...' : true }} // Returns 'This is a long...'
1174
- * {{ null | truncate }} // Returns ''
1175
- * {{ undefined | truncate }} // Returns ''
1445
+ * @returns {unknown[]} - A new array with only truthy values.
1176
1446
  *
1177
- * @author Mofiro Jean
1447
+ * @example
1448
+ * {{ [0, 1, '', 'hello', null, true] | truthify }} // [1, 'hello', true]
1449
+ * {{ ['', 'a', '', 'b'] | truthify }} // ['a', 'b']
1178
1450
  */
1179
- class TruncatePipe {
1180
- transform(value, maxLength = 10, ellipsis = '...', preserveWords = false) {
1181
- if (!value || typeof value !== 'string') {
1182
- return '';
1183
- }
1184
- if (value.length <= maxLength) {
1185
- return value;
1451
+ class TruthifyPipe {
1452
+ transform(value) {
1453
+ if (!Array.isArray(value)) {
1454
+ return [];
1186
1455
  }
1187
- const charsToKeep = maxLength - ellipsis.length;
1188
- // If maxLength is too small to even include the ellipsis, just return the ellipsis.
1189
- if (charsToKeep < 0) {
1190
- return ellipsis;
1456
+ return value.filter(Boolean);
1457
+ }
1458
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruthifyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1459
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TruthifyPipe, isStandalone: true, name: "truthify" });
1460
+ }
1461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruthifyPipe, decorators: [{
1462
+ type: Pipe,
1463
+ args: [{
1464
+ name: 'truthify',
1465
+ standalone: true,
1466
+ }]
1467
+ }] });
1468
+
1469
+ /**
1470
+ * UniquePipe: Removes duplicate values from an array.
1471
+ *
1472
+ * Supports primitives, objects by property key, and deep nested keys via dot notation.
1473
+ *
1474
+ * @param {unknown[]} value - The array to deduplicate.
1475
+ * @param {string} [key] - Optional property path to compare objects by (e.g., 'id', 'user.email').
1476
+ *
1477
+ * @returns {unknown[]} - A new array with duplicates removed, preserving first occurrence.
1478
+ *
1479
+ * @example
1480
+ * {{ [1, 2, 2, 3] | unique }} // [1, 2, 3]
1481
+ * {{ users | unique:'email' }} // unique by email
1482
+ * {{ orders | unique:'customer.email' }} // unique by nested property
1483
+ */
1484
+ class UniquePipe {
1485
+ transform(value, key) {
1486
+ if (!Array.isArray(value)) {
1487
+ return [];
1191
1488
  }
1192
- let truncated = value.substring(0, charsToKeep);
1193
- if (preserveWords) {
1194
- const lastSpaceIndex = truncated.lastIndexOf(' ');
1195
- // If a space is found and it's not the very beginning of the string
1196
- if (lastSpaceIndex !== -1 && lastSpaceIndex !== 0) {
1197
- truncated = truncated.substring(0, lastSpaceIndex);
1198
- }
1489
+ if (!key) {
1490
+ return [...new Set(value)];
1199
1491
  }
1200
- return truncated.trim() + ellipsis;
1492
+ const seen = new Set();
1493
+ return value.filter(item => {
1494
+ const val = this.getNestedValue(item, key);
1495
+ if (seen.has(val))
1496
+ return false;
1497
+ seen.add(val);
1498
+ return true;
1499
+ });
1201
1500
  }
1202
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1203
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, isStandalone: true, name: "truncate" });
1501
+ getNestedValue(obj, path) {
1502
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1503
+ }
1504
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UniquePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1505
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: UniquePipe, isStandalone: true, name: "unique" });
1204
1506
  }
1205
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, decorators: [{
1507
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UniquePipe, decorators: [{
1206
1508
  type: Pipe,
1207
1509
  args: [{
1208
- name: 'truncate',
1510
+ name: 'unique',
1209
1511
  standalone: true
1210
1512
  }]
1211
1513
  }] });
1212
1514
 
1515
+ // Text
1213
1516
  const ALL_PIPES = [
1517
+ // Text
1214
1518
  AsciiArtPipe,
1215
- BarcodePipe,
1216
1519
  CamelCasePipe,
1217
- ColorConvertPipe,
1218
- CreditCardMaskPipe,
1219
- DeviceTypePipe,
1220
- EmailMaskPipe,
1221
- GravatarPipe,
1222
1520
  HighlightPipe,
1223
- HtmlEscapePipe,
1224
- HtmlSanitizePipe,
1225
1521
  InitialsPipe,
1226
- IpAddressMaskPipe,
1227
- JsonPrettyPipe,
1228
1522
  KebabCasePipe,
1229
1523
  MorseCodePipe,
1230
- QrCodePipe,
1231
1524
  ReplacePipe,
1232
- ReversePipe,
1233
1525
  SnakeCasePipe,
1234
- TextToSpeechPipe,
1235
1526
  TitleCasePipe,
1236
1527
  TruncatePipe,
1237
- CountPipe
1528
+ // Security & Privacy
1529
+ CreditCardMaskPipe,
1530
+ EmailMaskPipe,
1531
+ HtmlEscapePipe,
1532
+ HtmlSanitizePipe,
1533
+ IpAddressMaskPipe,
1534
+ // Media & Visual
1535
+ BarcodePipe,
1536
+ ColorConvertPipe,
1537
+ GravatarPipe,
1538
+ QrCodePipe,
1539
+ // Data & Utility
1540
+ CountPipe,
1541
+ DeviceTypePipe,
1542
+ JsonPrettyPipe,
1543
+ TextToSpeechPipe,
1544
+ TimeAgoPipePipe,
1545
+ // Array
1546
+ Flatten,
1547
+ InitialPipe,
1548
+ ReversePipe,
1549
+ SamplePipe,
1550
+ ShufflePipe,
1551
+ TailPipe,
1552
+ TruthifyPipe,
1553
+ UniquePipe,
1238
1554
  ];
1239
1555
 
1556
+ // Text
1557
+
1240
1558
  /**
1241
1559
  * Generated bundle index. Do not edit.
1242
1560
  */
1243
1561
 
1244
- export { ALL_PIPES, AsciiArtPipe, BarcodePipe, CamelCasePipe, ColorConvertPipe, CountPipe, CreditCardMaskPipe, DeviceTypePipe, EmailMaskPipe, GravatarPipe, HighlightPipe, HtmlEscapePipe, HtmlSanitizePipe, InitialsPipe, IpAddressMaskPipe, JsonPrettyPipe, KebabCasePipe, MorseCodePipe, QrCodePipe, ReplacePipe, ReversePipe, SnakeCasePipe, TextToSpeechPipe, TitleCasePipe, TruncatePipe };
1562
+ export { ALL_PIPES, AsciiArtPipe, BarcodePipe, CamelCasePipe, ColorConvertPipe, CountPipe, CreditCardMaskPipe, DeviceTypePipe, EmailMaskPipe, Flatten, GravatarPipe, HighlightPipe, HtmlEscapePipe, HtmlSanitizePipe, InitialPipe, InitialsPipe, IpAddressMaskPipe, JsonPrettyPipe, KebabCasePipe, MorseCodePipe, QrCodePipe, ReplacePipe, ReversePipe, SamplePipe, ShufflePipe, SnakeCasePipe, TailPipe, TextToSpeechPipe, TimeAgoPipePipe, TitleCasePipe, TruncatePipe, TruthifyPipe, UniquePipe };
1245
1563
  //# sourceMappingURL=ngx-transforms.mjs.map