ngx-transforms 0.0.5 → 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,8 +60,6 @@ import * as QRCode from 'qrcode';
60
60
  * @example
61
61
  * // With inverted colors
62
62
  * {{ 'DARK' | asciiArt:{ inverted: true, charset: CharsetPreset.MINIMAL } }}
63
- *
64
- * @author Mofiro Jean
65
63
  */
66
64
  class AsciiArtPipe {
67
65
  generator;
@@ -119,94 +117,615 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
119
117
  }], ctorParameters: () => [] });
120
118
 
121
119
  /**
122
- * BarcodePipe: Generates a barcode from a string value.
123
- *
124
- * @param {string} value - The value to encode (e.g., '123456789').
125
- * @param {BarcodeOptions} [options={}] - Configuration options.
120
+ * CamelCasePipe: Converts text to camelCase (e.g., "hello world" → "helloWorld").
126
121
  *
127
- * @returns {Promise<SafeHtml | SafeResourceUrl>} - SVG markup or image data URL.
122
+ * @param {string} value - The input string to transform.
123
+ * @returns {string} The string in camelCase, or an empty string if input is invalid.
128
124
  *
129
125
  * @example
130
- * <div [innerHTML]="'123456789' | barcode:{elementType:'svg',format:'CODE128'} | async"></div>
131
- * <img [src]="'123456789' | barcode:{elementType:'img'} | async" />
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.
132
166
  *
133
- * @author Mofiro Jean
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 ''
134
173
  */
135
- class BarcodePipe {
174
+ class HighlightPipe {
136
175
  sanitizer = inject(DomSanitizer);
137
- async transform(value, options = {}) {
138
- const { elementType = 'svg', format = 'CODE128', lineColor = '#000000', width = 2, height = 100, displayValue = true, } = options;
139
- if (!value) {
140
- return '';
141
- }
142
- // Sanitize the value to prevent XSS
143
- const sanitizedValue = value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
144
- try {
145
- const config = { format, lineColor, width, height, displayValue };
146
- if (elementType === 'svg') {
147
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
148
- JsBarcode(svg, sanitizedValue, config);
149
- return this.sanitizer.bypassSecurityTrustHtml(svg.outerHTML);
150
- }
151
- else {
152
- const canvas = document.createElement('canvas');
153
- JsBarcode(canvas, sanitizedValue, config);
154
- const dataUrl = canvas.toDataURL('image/png');
155
- return this.sanitizer.bypassSecurityTrustResourceUrl(dataUrl);
156
- }
176
+ transform(value, searchTerm) {
177
+ if (!value || !searchTerm) {
178
+ return this.sanitizer.bypassSecurityTrustHtml(value || '');
157
179
  }
158
- catch (error) {
159
- console.error('Barcode generation failed:', error);
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)
160
210
  return '';
161
- }
211
+ return value
212
+ .trim()
213
+ .split(/\s+/)
214
+ .map(word => word.charAt(0).toUpperCase())
215
+ .join('');
162
216
  }
163
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
164
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, isStandalone: true, name: "barcode" });
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" });
165
219
  }
166
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, decorators: [{
220
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, decorators: [{
167
221
  type: Pipe,
168
222
  args: [{
169
- name: 'barcode',
170
- standalone: true,
223
+ name: 'initials',
224
+ standalone: true
171
225
  }]
172
226
  }] });
173
227
 
174
228
  /**
175
- * CamelCasePipe: Converts text to camelCase (e.g., "hello world" → "helloWorld").
229
+ * KebabCasePipe: Converts text to kebab-case (e.g., "hello world" → "hello-world").
176
230
  *
177
231
  * @param {string} value - The input string to transform.
178
- * @returns {string} The string in camelCase, or an empty string if input is invalid.
232
+ * @returns {string} The string in kebab-case, or an empty string if input is invalid.
179
233
  *
180
234
  * @example
181
235
  * ```html
182
- * {{ 'hello world' | camelCase }} <!-- Outputs: helloWorld -->
236
+ * {{ 'hello world' | kebabCase }} <!-- Outputs: hello-world -->
183
237
  * ```
184
- *
185
- * @author Mofiro Jean
186
238
  */
187
- class CamelCasePipe {
239
+ class KebabCasePipe {
188
240
  transform(value) {
189
241
  if (!value || typeof value !== 'string')
190
242
  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('');
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.*.*).
654
+ *
655
+ * @example
656
+ * {{ '192.168.1.1' | ipAddressMask }} // Outputs: 192.168.*.*
657
+ * {{ '10.0.0.255' | ipAddressMask }} // Outputs: 10.0.*.*
658
+ */
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;
663
+ }
664
+ if (shouldMask) {
665
+ const parts = value.split('.');
666
+ return `${parts[0]}.${parts[1]}.*.*`;
667
+ }
668
+ return value;
669
+ }
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" });
672
+ }
673
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, decorators: [{
674
+ type: Pipe,
675
+ args: [{
676
+ name: 'ipAddressMask',
677
+ standalone: true
678
+ }]
679
+ }] });
680
+
681
+ /**
682
+ * BarcodePipe: Generates a barcode from a string value.
683
+ *
684
+ * @param {string} value - The value to encode (e.g., '123456789').
685
+ * @param {BarcodeOptions} [options={}] - Configuration options.
686
+ *
687
+ * @returns {Promise<SafeHtml | SafeResourceUrl>} - SVG markup or image data URL.
688
+ *
689
+ * @example
690
+ * <div [innerHTML]="'123456789' | barcode:{elementType:'svg',format:'CODE128'} | async"></div>
691
+ * <img [src]="'123456789' | barcode:{elementType:'img'} | async" />
692
+ */
693
+ class BarcodePipe {
694
+ sanitizer = inject(DomSanitizer);
695
+ async transform(value, options = {}) {
696
+ const { elementType = 'svg', format = 'CODE128', lineColor = '#000000', width = 2, height = 100, displayValue = true, } = options;
697
+ if (!value) {
698
+ return '';
699
+ }
700
+ // Sanitize the value to prevent XSS
701
+ const sanitizedValue = value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
702
+ try {
703
+ const config = { format, lineColor, width, height, displayValue };
704
+ if (elementType === 'svg') {
705
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
706
+ JsBarcode(svg, sanitizedValue, config);
707
+ return this.sanitizer.bypassSecurityTrustHtml(svg.outerHTML);
708
+ }
709
+ else {
710
+ const canvas = document.createElement('canvas');
711
+ JsBarcode(canvas, sanitizedValue, config);
712
+ const dataUrl = canvas.toDataURL('image/png');
713
+ return this.sanitizer.bypassSecurityTrustResourceUrl(dataUrl);
714
+ }
715
+ }
716
+ catch (error) {
717
+ console.error('Barcode generation failed:', error);
718
+ return '';
719
+ }
201
720
  }
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" });
721
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
722
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, isStandalone: true, name: "barcode" });
204
723
  }
205
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CamelCasePipe, decorators: [{
724
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, decorators: [{
206
725
  type: Pipe,
207
726
  args: [{
208
- name: 'camelCase',
209
- standalone: true
727
+ name: 'barcode',
728
+ standalone: true,
210
729
  }]
211
730
  }] });
212
731
 
@@ -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) {
@@ -370,393 +887,149 @@ class ColorConvertPipe {
370
887
  return '';
371
888
  }
372
889
  const trimmedValue = value.trim();
373
- const parsed = parseColor(trimmedValue);
374
- if (!parsed) {
375
- return value;
376
- }
377
- const { r, g, b, a } = parsed;
378
- switch (target) {
379
- case 'hex':
380
- 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}`;
534
- }
535
- const firstChar = local[0];
536
- const lastChar = local[local.length - 1];
537
- return `${firstChar}***${lastChar}@${domain}`;
538
- }
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" });
541
- }
542
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, decorators: [{
543
- type: Pipe,
544
- args: [{
545
- name: 'emailMask',
546
- standalone: true
547
- }]
548
- }] });
549
-
550
- /**
551
- * GravatarPipe: Generates Gravatar URLs from email addresses.
552
- *
553
- * @param {string} value - The email address.
554
- * @param {number} [size=80] - The avatar size in pixels.
555
- *
556
- * @returns {string} - The Gravatar URL.
557
- *
558
- * @example
559
- * <img [src]="'user@example.com' | gravatar:100" />
560
- *
561
- * @author Mofiro Jean
562
- */
563
- class GravatarPipe {
564
- transform(value, size = 80) {
565
- if (!value)
566
- return `https://www.gravatar.com/avatar/?s=${size}`;
567
- const hash = md5(value.trim().toLowerCase());
568
- return `https://www.gravatar.com/avatar/${hash}?s=${size}`;
569
- }
570
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
571
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, isStandalone: true, name: "gravatar" });
572
- }
573
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, decorators: [{
574
- type: Pipe,
575
- args: [{
576
- name: 'gravatar',
577
- standalone: true
578
- }]
579
- }] });
580
-
581
- /**
582
- * HighlightPipe: Highlights occurrences of a search term within a string.
583
- *
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.
587
- *
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.
591
- *
592
- * @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
600
- */
601
- class HighlightPipe {
602
- sanitizer = inject(DomSanitizer);
603
- transform(value, searchTerm) {
604
- if (!value || !searchTerm) {
605
- return this.sanitizer.bypassSecurityTrustHtml(value || '');
890
+ const parsed = parseColor(trimmedValue);
891
+ if (!parsed) {
892
+ return value;
893
+ }
894
+ const { r, g, b, a } = parsed;
895
+ switch (target) {
896
+ case 'hex':
897
+ if (a < 1) {
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;
606
907
  }
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
908
  }
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" });
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" });
614
911
  }
615
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, decorators: [{
912
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, decorators: [{
616
913
  type: Pipe,
617
914
  args: [{
618
- name: 'highlight',
619
- standalone: true
915
+ name: 'colorConvert',
916
+ standalone: true,
620
917
  }]
621
918
  }] });
622
919
 
623
920
  /**
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.
921
+ * GravatarPipe: Generates Gravatar URLs from email addresses.
626
922
  *
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.
923
+ * @param {string} value - The email address.
924
+ * @param {number} [size=80] - The avatar size in pixels.
925
+ *
926
+ * @returns {string} - The Gravatar URL.
629
927
  *
630
928
  * @example
631
- * ```html
632
- * {{ '<p>Hello</p>' | htmlEscape }} <!-- Outputs: &lt;p&gt;Hello&lt;/p&gt; -->
633
- * ```
929
+ * <img [src]="'user@example.com' | gravatar:100" />
634
930
  */
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]);
931
+ class GravatarPipe {
932
+ transform(value, size = 80) {
933
+ if (!value)
934
+ return `https://www.gravatar.com/avatar/?s=${size}`;
935
+ const hash = md5(value.trim().toLowerCase());
936
+ return `https://www.gravatar.com/avatar/${hash}?s=${size}`;
647
937
  }
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" });
938
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
939
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, isStandalone: true, name: "gravatar" });
650
940
  }
651
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, decorators: [{
941
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, decorators: [{
652
942
  type: Pipe,
653
943
  args: [{
654
- name: 'htmlEscape',
944
+ name: 'gravatar',
655
945
  standalone: true
656
946
  }]
657
947
  }] });
658
948
 
659
949
  /**
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].
950
+ * QrCodePipe: Generates a QR code from a string.
662
951
  *
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.
952
+ * @param {string} value - The string to encode.
953
+ * @param {QrCodeOptions} [options] - The QR code options.
665
954
  *
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).
955
+ * @returns {Promise<string>} - A promise that resolves with the QR code data URL.
669
956
  *
670
957
  * @example
671
- * ```html
672
- * <div [innerHTML]="'<p>Hello</p><script>alert(1)</script>' | htmlSanitize"></div>
673
- * <!-- Renders: <p>Hello</p> (script tag removed) -->
674
- * ```
958
+ * <img [src]="'Hello, World!' | qrCode | async" />
675
959
  */
676
- class HtmlSanitizePipe {
677
- sanitizer = inject(DomSanitizer);
678
- transform(value) {
679
- if (!value || typeof value !== 'string')
680
- return this.sanitizer.bypassSecurityTrustHtml('');
681
- return this.sanitizer.sanitize(0, value) || this.sanitizer.bypassSecurityTrustHtml('');
960
+ class QrCodePipe {
961
+ transform(value, options) {
962
+ if (!value) {
963
+ return Promise.resolve('');
964
+ }
965
+ return QRCode.toDataURL(value, options);
682
966
  }
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" });
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" });
685
969
  }
686
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, decorators: [{
970
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, decorators: [{
687
971
  type: Pipe,
688
972
  args: [{
689
- name: 'htmlSanitize',
973
+ name: 'qrCode',
690
974
  standalone: true
691
975
  }]
692
976
  }] });
693
977
 
694
- /**
695
- * InitialsPipe: Extracts initials from a name.
696
- *
697
- * @param {string} value - The full name.
698
- *
699
- * @returns {string} - The initials (e.g., 'John Doe' → 'JD').
700
- *
701
- * @example
702
- * {{ 'John Doe' | initials }} // Outputs: JD
703
- * {{ 'Mary Jane Watson' | initials }} // Outputs: MJW
704
- *
705
- * @author Mofiro Jean
706
- */
707
- class InitialsPipe {
978
+ class CountPipe {
708
979
  transform(value) {
709
- if (!value)
710
- return '';
711
- return value
712
- .trim()
713
- .split(/\s+/)
714
- .map(word => word.charAt(0).toUpperCase())
715
- .join('');
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;
716
990
  }
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" });
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" });
719
993
  }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, decorators: [{
994
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CountPipe, decorators: [{
721
995
  type: Pipe,
722
996
  args: [{
723
- name: 'initials',
997
+ name: 'count',
724
998
  standalone: true
725
999
  }]
726
1000
  }] });
727
1001
 
728
1002
  /**
729
- * IpAddressMaskPipe: Masks the last two octets of an IPv4 address.
1003
+ * DeviceTypePipe: Detects the device type based on the user agent string.
730
1004
  *
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..
1005
+ * @param {string} value - The user agent string (defaults to navigator.userAgent).
733
1006
  *
734
- * @returns {string} - The masked IP address (e.g., 192.168.*.*).
1007
+ * @returns {'mobile' | 'tablet' | 'desktop' | 'unknown'} - The detected device type.
735
1008
  *
736
1009
  * @example
737
- * {{ '192.168.1.1' | ipAddressMask }} // Outputs: 192.168.*.*
738
- * {{ '10.0.0.255' | ipAddressMask }} // Outputs: 10.0.*.*
739
- *
740
- * @author Mofiro Jean
1010
+ * {{ '' | device }} // Outputs: 'mobile' (on a mobile device)
1011
+ * <div *ngIf="'' | device === 'desktop'">Desktop-only content</div>
741
1012
  */
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;
1013
+ class DeviceTypePipe {
1014
+ transform(value = typeof navigator !== 'undefined' ? navigator.userAgent : '') {
1015
+ if (!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").
845
- *
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.
848
- *
849
- * @example
850
- * ```html
851
- * {{ 'hello world' | kebabCase }} <!-- Outputs: hello-world -->
852
- * ```
853
- *
854
- * @author Mofiro Jean
855
- */
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
866
- }
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" });
869
- }
870
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, decorators: [{
871
- type: Pipe,
872
- args: [{
873
- name: 'kebabCase',
874
- standalone: true
875
- }]
876
- }] });
877
-
878
- /**
879
- * MorseCodePipe: Converts text to Morse code.
1115
+ * TextToSpeechPipe: Converts text to speech using the Web Speech API.
880
1116
  *
881
- * @param {string} value - The text to convert to Morse code.
1117
+ * @param {string} value - The text to convert to speech.
1118
+ * @param {string} [lang='en-US'] - The language (local) for speech synthesis.
882
1119
  *
883
- * @returns {string} - The Morse code representation (e.g., 'SOS' → '... --- ...').
1120
+ * @returns {void} - Triggers speech synthesis (no return value).
884
1121
  *
885
1122
  * @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 '';
905
- }
906
- return value
907
- .toUpperCase()
908
- .split('')
909
- .map(char => this.morseCodeMap[char] || '')
910
- .filter(code => code)
911
- .join(' ');
1123
+ * <div>{{ Hello World' | textToSpeech }}</div>
1124
+ * <div>{{ 'Bonjour' | textToSpeech:'fr-FR' }}h</div>
1125
+ */
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);
912
1133
  }
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" });
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" });
915
1136
  }
916
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, decorators: [{
1137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, decorators: [{
917
1138
  type: Pipe,
918
1139
  args: [{
919
- name: 'morseCode',
1140
+ name: 'textToSpeech',
920
1141
  standalone: true
921
1142
  }]
922
1143
  }] });
923
1144
 
924
1145
  /**
925
- * QrCodePipe: Generates a QR code from a string.
1146
+ * TimeAgo: Converts a date into a localized time string.
926
1147
  *
927
- * @param {string} value - The string to encode.
928
- * @param {QrCodeOptions} [options] - The QR code options.
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
929
1151
  *
930
- * @returns {Promise<string>} - A promise that resolves with the QR code data URL.
1152
+ * @param {Date | number | string} value - The date to convert.
1153
+ * @param {string} [local='en'] - BCP 47 local code (e.g., 'en', 'fr', 'es').
931
1154
  *
932
- * @example
933
- * <img [src]="'Hello, World!' | qrCode | async" />
1155
+ * @returns {string} - The localized time string.
934
1156
  *
935
- * @author Mofiro Jean
936
- */
937
- class QrCodePipe {
938
- transform(value, options) {
939
- if (!value) {
940
- return Promise.resolve('');
1157
+ * @example
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 "";
941
1179
  }
942
- return QRCode.toDataURL(value, options);
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 "";
943
1195
  }
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" });
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" });
946
1198
  }
947
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, decorators: [{
1199
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, decorators: [{
948
1200
  type: Pipe,
949
1201
  args: [{
950
- name: 'qrCode',
1202
+ name: 'timeAgo',
951
1203
  standalone: true
952
1204
  }]
953
1205
  }] });
954
1206
 
955
1207
  /**
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.
1208
+ * FlattenPipe: Flattens nested arrays to a specified depth.
960
1209
  *
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).
1210
+ * @param {unknown[]} value - The nested array to flatten.
1211
+ * @param {number} [depth=Infinity] - How many levels of nesting to flatten.
966
1212
  *
967
- * @returns {string | SafeHtml} - Returns the transformed string or SafeHtml with highlights.
1213
+ * @returns {unknown[]} - A new flattened array.
968
1214
  *
969
1215
  * @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>
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']
1219
+ */
1220
+ class Flatten {
1221
+ transform(value, depth = Infinity) {
1222
+ if (!Array.isArray(value)) {
1223
+ return [];
1224
+ }
1225
+ return value.flat(depth);
1226
+ }
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" });
1229
+ }
1230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: Flatten, decorators: [{
1231
+ type: Pipe,
1232
+ args: [{
1233
+ name: 'flatten',
1234
+ standalone: true
1235
+ }]
1236
+ }] });
1237
+
1238
+ /**
1239
+ * InitialPipe: Returns all elements except the last n.
978
1240
  *
979
- * {{ 'Angular is great' | replace:'great':'awesome':'highlight':false }}
980
- * // Output: Angular is <span class="highlight">great</span>
1241
+ * @param {unknown[]} value - The array to slice.
1242
+ * @param {number} [n=1] - Number of elements to exclude from the end.
981
1243
  *
982
- * <div [innerHTML]="'Angular is great' | replace:'great':'awesome':'highlight':false"></div>
983
- * // Renders: Angular is <span class="highlight">great</span>
1244
+ * @returns {unknown[]} - A new array without the last n elements.
984
1245
  *
985
- * @author Mofiro Jean
1246
+ * @example
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