ngx-transforms 0.0.5 → 0.2.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;
@@ -118,59 +116,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
118
116
  }]
119
117
  }], ctorParameters: () => [] });
120
118
 
121
- /**
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.
126
- *
127
- * @returns {Promise<SafeHtml | SafeResourceUrl>} - SVG markup or image data URL.
128
- *
129
- * @example
130
- * <div [innerHTML]="'123456789' | barcode:{elementType:'svg',format:'CODE128'} | async"></div>
131
- * <img [src]="'123456789' | barcode:{elementType:'img'} | async" />
132
- *
133
- * @author Mofiro Jean
134
- */
135
- class BarcodePipe {
136
- 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
- }
157
- }
158
- catch (error) {
159
- console.error('Barcode generation failed:', error);
160
- return '';
161
- }
162
- }
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" });
165
- }
166
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, decorators: [{
167
- type: Pipe,
168
- args: [{
169
- name: 'barcode',
170
- standalone: true,
171
- }]
172
- }] });
173
-
174
119
  /**
175
120
  * CamelCasePipe: Converts text to camelCase (e.g., "hello world" → "helloWorld").
176
121
  *
@@ -181,8 +126,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
181
126
  * ```html
182
127
  * {{ 'hello world' | camelCase }} <!-- Outputs: helloWorld -->
183
128
  * ```
184
- *
185
- * @author Mofiro Jean
186
129
  */
187
130
  class CamelCasePipe {
188
131
  transform(value) {
@@ -210,48 +153,608 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
210
153
  }]
211
154
  }] });
212
155
 
213
- // Pre-compiled regex patterns for performance
214
- const HEX_6_PATTERN = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
215
- const HEX_8_PATTERN = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
216
- const HEX_3_PATTERN = /^#([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/;
217
- const HEX_4_PATTERN = /^#([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/;
218
- const RGB_PATTERN = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
219
- const RGBA_PATTERN = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d.]+)\s*\)$/i;
220
156
  /**
221
- * Clamps a value between 0 and 255 for valid RGB channel values.
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 ''
222
173
  */
223
- function clampChannel(value) {
224
- return Math.max(0, Math.min(255, Math.round(value)));
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" });
225
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
+
226
196
  /**
227
- * Clamps alpha value between 0 and 1.
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
228
206
  */
229
- function clampAlpha(value) {
230
- return Math.max(0, Math.min(1, value));
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" });
231
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
+
232
228
  /**
233
- * Converts a single hex character to its full two-character representation.
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
+ * ```
234
238
  */
235
- function expandHexChar(char) {
236
- return char + char;
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" });
237
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
+
238
261
  /**
239
- * Converts a number to a two-character hex string.
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>
240
272
  */
241
- function toHex(value) {
242
- return clampChannel(value).toString(16).padStart(2, '0').toUpperCase();
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" });
243
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
+
244
305
  /**
245
- * Converts alpha (0-1) to hex (00-FF).
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.
246
318
  */
247
- function alphaToHex(alpha) {
248
- return Math.round(clampAlpha(alpha) * 255).toString(16).padStart(2, '0').toUpperCase();
319
+ class ReplacePipe {
320
+ sanitizer = inject(DomSanitizer);
321
+ transform(value, pattern, replacement, highlightClass, isReplace = true) {
322
+ if (!value)
323
+ return '';
324
+ // handles empty string pattern
325
+ if (!pattern || (typeof pattern === 'string' && pattern.trim() === '')) {
326
+ return value;
327
+ }
328
+ const finalPattern = typeof pattern === 'string' ? new RegExp(pattern, 'gi') : pattern;
329
+ if (!highlightClass) {
330
+ return isReplace ? value.replace(finalPattern, replacement) : value;
331
+ }
332
+ // Sanitize the replacement to prevent XSS
333
+ const sanitizedReplacement = replacement.replace(/</g, '&lt;').replace(/>/g, '&gt;');
334
+ if (isReplace) {
335
+ const highlightedReplacement = `<span class="${highlightClass}">${sanitizedReplacement}</span>`;
336
+ const replaced = value.replace(finalPattern, highlightedReplacement);
337
+ return this.sanitizer.bypassSecurityTrustHtml(replaced);
338
+ }
339
+ const highlightedMatch = `<span class="${highlightClass}">$&</span>`;
340
+ const result = value.replace(finalPattern, highlightedMatch);
341
+ return this.sanitizer.bypassSecurityTrustHtml(result);
342
+ }
343
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
344
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" });
249
345
  }
250
- /**
251
- * Converts hex alpha (00-FF) to decimal (0-1).
252
- */
253
- function hexToAlpha(hex) {
254
- return Math.round((parseInt(hex, 16) / 255) * 100) / 100;
346
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, decorators: [{
347
+ type: Pipe,
348
+ args: [{
349
+ name: 'replace',
350
+ standalone: true
351
+ }]
352
+ }] });
353
+
354
+ /**
355
+ * SnakeCasePipe: Converts text to snake_case (e.g., "hello world" → "hello_world").
356
+ *
357
+ * @param {string} value - The input string to transform.
358
+ * @returns {string} The string in snake_case, or an empty string if input is invalid.
359
+ *
360
+ * @example
361
+ * ```html
362
+ * {{ 'hello world' | snakeCase }} <!-- Outputs: hello_world -->
363
+ * ```
364
+ */
365
+ class SnakeCasePipe {
366
+ transform(value) {
367
+ if (!value || typeof value !== 'string')
368
+ return '';
369
+ return value
370
+ .trim()
371
+ .replace(/([A-Z])/g, '_$1') // Convert camelCase to snake_case (e.g., helloWorld -> hello_World)
372
+ .toLowerCase() // Convert everything to lowercase
373
+ .replace(/[\s-]+/g, '_') // Replace spaces and hyphens with underscores
374
+ .replace(/[^a-z0-9_]+/g, '') // Remove all non-alphanumeric and non-underscore characters
375
+ .replace(/_+/g, '_') // Collapse multiple underscores
376
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
377
+ }
378
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
379
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, isStandalone: true, name: "snakeCase" });
380
+ }
381
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, decorators: [{
382
+ type: Pipe,
383
+ args: [{
384
+ name: 'snakeCase',
385
+ standalone: true
386
+ }]
387
+ }] });
388
+
389
+ /**
390
+ * TitleCasePipe: Capitalizes the first letter of each word in a string.
391
+ *
392
+ * @param {string} value - The input string to transform.
393
+ * @returns {string} The string with each word capitalized, or an empty string if input is invalid.
394
+ *
395
+ * @example
396
+ * ```html
397
+ * {{ 'hello world' | titleCase }} <!-- Outputs: Hello World -->
398
+ * ```
399
+ */
400
+ class TitleCasePipe {
401
+ transform(value) {
402
+ if (!value || typeof value !== 'string')
403
+ return '';
404
+ return value
405
+ .split(' ')
406
+ .filter(word => word.length > 0)
407
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
408
+ .join(' ');
409
+ }
410
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
411
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, isStandalone: true, name: "titleCase" });
412
+ }
413
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, decorators: [{
414
+ type: Pipe,
415
+ args: [{
416
+ name: 'titleCase',
417
+ standalone: true
418
+ }]
419
+ }] });
420
+
421
+ /**
422
+ * TruncatePipe: Truncates a string to a specified maximum length, optionally preserving words.
423
+ *
424
+ * This Angular pipe transforms a string input by truncating it to the given `maxLength`.
425
+ * It provides options to customize the ellipsis and preserve word boundaries.
426
+ *
427
+ * @param {string} value - The input string to be truncated.
428
+ * @param {number} [maxLength=10] - The maximum length of the truncated string. Defaults to 10.
429
+ * @param {string} [ellipsis='...'] - The string to append to the truncated portion. Defaults to '...'.
430
+ * @param {boolean} [preserveWords=false] - If true, truncates at the last space before `maxLength` to avoid cutting words. Defaults to false.
431
+ * @returns {string} - The truncated string. Returns an empty string if the input is null, undefined, or not a string.
432
+ *
433
+ * @example
434
+ * {{ 'This is a long sentence' | truncate }} // Returns 'This is a...'
435
+ * {{ 'This is a long sentence' | truncate: 20 }} // Returns 'This is a long sente...'
436
+ * {{ 'This is a long sentence' | truncate: 15: ' [more]' }} // Returns 'This is a long [more]'
437
+ * {{ 'This is a long sentence' | truncate: 15: '...' : true }} // Returns 'This is a...'
438
+ * {{ 'This is a long sentence' | truncate: 20: '...' : true }} // Returns 'This is a long...'
439
+ * {{ null | truncate }} // Returns ''
440
+ * {{ undefined | truncate }} // Returns ''
441
+ */
442
+ class TruncatePipe {
443
+ transform(value, maxLength = 10, ellipsis = '...', preserveWords = false) {
444
+ if (!value || typeof value !== 'string') {
445
+ return '';
446
+ }
447
+ if (value.length <= maxLength) {
448
+ return value;
449
+ }
450
+ const charsToKeep = maxLength - ellipsis.length;
451
+ // If maxLength is too small to even include the ellipsis, just return the ellipsis.
452
+ if (charsToKeep < 0) {
453
+ return ellipsis;
454
+ }
455
+ let truncated = value.substring(0, charsToKeep);
456
+ if (preserveWords) {
457
+ const lastSpaceIndex = truncated.lastIndexOf(' ');
458
+ // If a space is found and it's not the very beginning of the string
459
+ if (lastSpaceIndex !== -1 && lastSpaceIndex !== 0) {
460
+ truncated = truncated.substring(0, lastSpaceIndex);
461
+ }
462
+ }
463
+ return truncated.trim() + ellipsis;
464
+ }
465
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
466
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, isStandalone: true, name: "truncate" });
467
+ }
468
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, decorators: [{
469
+ type: Pipe,
470
+ args: [{
471
+ name: 'truncate',
472
+ standalone: true
473
+ }]
474
+ }] });
475
+
476
+ /**
477
+ * CreditCardMaskPipe: Masks all but the last four digits of a string, optionally controlled by a boolean flag.
478
+ * By default, masking is applied.
479
+ *
480
+ * @param {string} value - The input string to mask (e.g., credit card number).
481
+ * @param {boolean} shouldMask - (Optional) Determines if masking should be applied. Defaults to true.
482
+ * @returns {string} - The masked string or the original value if `shouldMask` is false or the value is too short.
483
+ *
484
+ * @example
485
+ * {{ '1234567890123456' | creditCardMask }} // Outputs: **** **** **** 3456
486
+ * {{ '1234-5678-9012-3456' | creditCardMask }} // Outputs: **** **** **** 3456
487
+ * {{ '1234567890123456' | creditCardMask: true }} // Outputs: **** **** **** 3456
488
+ * {{ '1234567890123456' | creditCardMask: false }} // Outputs: 1234567890123456
489
+ */
490
+ class CreditCardMaskPipe {
491
+ transform(value, shouldMask = true) {
492
+ if (!value) {
493
+ return value;
494
+ }
495
+ if (shouldMask) {
496
+ const cleanedValue = value.replace(/[\s-]/g, '');
497
+ const cleanedLength = cleanedValue.length;
498
+ if (cleanedLength < 4) {
499
+ return value;
500
+ }
501
+ const visibleDigits = cleanedValue.slice(-4);
502
+ const maskedSection = '*'.repeat(cleanedLength - 4);
503
+ const groupedMask = maskedSection.match(/.{1,4}/g)?.join(' ') ?? '';
504
+ return `${groupedMask} ${visibleDigits}`.trim();
505
+ }
506
+ return value;
507
+ }
508
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
509
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, isStandalone: true, name: "creditCardMask" });
510
+ }
511
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CreditCardMaskPipe, decorators: [{
512
+ type: Pipe,
513
+ args: [{
514
+ name: 'creditCardMask',
515
+ standalone: true,
516
+ }]
517
+ }] });
518
+
519
+ /**
520
+ * EmailMaskPipe: Masks the local part of an email address, revealing only the first and last characters.
521
+ *
522
+ * 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 "***".
523
+ * If the local part is 2 characters or less, it masks all characters except the first.
524
+ *
525
+ * @param {string} value - The email address string to be masked.
526
+ * @returns {string} - The masked email address, or the original value if it's not a valid email or falsy.
527
+ *
528
+ * @example
529
+ * {{ 'test@example.com' | emailMask }} // Returns 't***t@example.com'
530
+ * {{ 'te@example.com' | emailMask }} // Returns 't***@example.com'
531
+ * {{ 't@example.com' | emailMask }} // Returns 't***@example.com'
532
+ * {{ 'example.com' | emailMask }} // Returns 'example.com'
533
+ * {{ null | emailMask }} // Returns ''
534
+ * {{ undefined | emailMask }} // Returns ''
535
+ */
536
+ class EmailMaskPipe {
537
+ transform(value) {
538
+ if (!value || !value.includes('@')) {
539
+ return value || '';
540
+ }
541
+ const [local, domain] = value.split('@');
542
+ if (local.length <= 2) {
543
+ return `${local[0]}***@${domain}`;
544
+ }
545
+ const firstChar = local[0];
546
+ const lastChar = local[local.length - 1];
547
+ return `${firstChar}***${lastChar}@${domain}`;
548
+ }
549
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
550
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, isStandalone: true, name: "emailMask" });
551
+ }
552
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, decorators: [{
553
+ type: Pipe,
554
+ args: [{
555
+ name: 'emailMask',
556
+ standalone: true
557
+ }]
558
+ }] });
559
+
560
+ /**
561
+ * Converts special HTML characters in a string to their corresponding HTML entities.
562
+ * This prevents the browser from interpreting the input as HTML, rendering it as plain text.
563
+ *
564
+ * @param {string} value - The input string containing HTML to escape.
565
+ * @returns {string} The string with special HTML characters escaped, or an empty string if input is invalid.
566
+ *
567
+ * @example
568
+ * ```html
569
+ * {{ '<p>Hello</p>' | htmlEscape }} <!-- Outputs: &lt;p&gt;Hello&lt;/p&gt; -->
570
+ * ```
571
+ */
572
+ class HtmlEscapePipe {
573
+ transform(value) {
574
+ if (!value || typeof value !== 'string')
575
+ return '';
576
+ const escapeMap = {
577
+ '&': '&amp;',
578
+ '<': '&lt;',
579
+ '>': '&gt;',
580
+ '"': '&quot;',
581
+ "'": '&apos;'
582
+ };
583
+ return value.replace(/[&<>"']/g, char => escapeMap[char]);
584
+ }
585
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
586
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, isStandalone: true, name: "htmlEscape" });
587
+ }
588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, decorators: [{
589
+ type: Pipe,
590
+ args: [{
591
+ name: 'htmlEscape',
592
+ standalone: true
593
+ }]
594
+ }] });
595
+
596
+ /**
597
+ * Sanitizes HTML input to remove unsafe elements while allowing safe HTML to be rendered.
598
+ * Uses Angular's DomSanitizer to mark the output as trusted for use in [innerHTML].
599
+ *
600
+ * @param {string} value - The input string containing HTML to sanitize.
601
+ * @returns {SafeHtml} The sanitized HTML marked as safe, or an empty string if input is invalid.
602
+ *
603
+ * @remarks
604
+ * WARNING: Use with caution. Only apply to trusted input to avoid XSS risks.
605
+ * Ensure input is pre-validated or sourced from a secure origin (e.g., a controlled rich-text editor).
606
+ *
607
+ * @example
608
+ * ```html
609
+ * <div [innerHTML]="'<p>Hello</p><script>alert(1)</script>' | htmlSanitize"></div>
610
+ * <!-- Renders: <p>Hello</p> (script tag removed) -->
611
+ * ```
612
+ */
613
+ class HtmlSanitizePipe {
614
+ sanitizer = inject(DomSanitizer);
615
+ transform(value) {
616
+ if (!value || typeof value !== 'string')
617
+ return this.sanitizer.bypassSecurityTrustHtml('');
618
+ return this.sanitizer.sanitize(0, value) || this.sanitizer.bypassSecurityTrustHtml('');
619
+ }
620
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
621
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, isStandalone: true, name: "htmlSanitize" });
622
+ }
623
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, decorators: [{
624
+ type: Pipe,
625
+ args: [{
626
+ name: 'htmlSanitize',
627
+ standalone: true
628
+ }]
629
+ }] });
630
+
631
+ /**
632
+ * IpAddressMaskPipe: Masks the last two octets of an IPv4 address.
633
+ *
634
+ * @param {string} value - The IPv4 address (e.g., 192.168.1.1).
635
+ * @param {boolean} shouldMask - (Optional) Determines if masking should be applied. Defaults to true..
636
+ *
637
+ * @returns {string} - The masked IP address (e.g., 192.168.*.*).
638
+ *
639
+ * @example
640
+ * {{ '192.168.1.1' | ipAddressMask }} // Outputs: 192.168.*.*
641
+ * {{ '10.0.0.255' | ipAddressMask }} // Outputs: 10.0.*.*
642
+ */
643
+ class IpAddressMaskPipe {
644
+ transform(value, shouldMask = true) {
645
+ if (!value || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(value)) {
646
+ return value;
647
+ }
648
+ if (shouldMask) {
649
+ const parts = value.split('.');
650
+ return `${parts[0]}.${parts[1]}.*.*`;
651
+ }
652
+ return value;
653
+ }
654
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
655
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, isStandalone: true, name: "ipAddressMask" });
656
+ }
657
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, decorators: [{
658
+ type: Pipe,
659
+ args: [{
660
+ name: 'ipAddressMask',
661
+ standalone: true
662
+ }]
663
+ }] });
664
+
665
+ /**
666
+ * BarcodePipe: Generates a barcode from a string value.
667
+ *
668
+ * @param {string} value - The value to encode (e.g., '123456789').
669
+ * @param {BarcodeOptions} [options={}] - Configuration options.
670
+ *
671
+ * @returns {Promise<SafeHtml | SafeResourceUrl>} - SVG markup or image data URL.
672
+ *
673
+ * @example
674
+ * <div [innerHTML]="'123456789' | barcode:{elementType:'svg',format:'CODE128'} | async"></div>
675
+ * <img [src]="'123456789' | barcode:{elementType:'img'} | async" />
676
+ */
677
+ class BarcodePipe {
678
+ sanitizer = inject(DomSanitizer);
679
+ async transform(value, options = {}) {
680
+ const { elementType = 'svg', format = 'CODE128', lineColor = '#000000', width = 2, height = 100, displayValue = true, } = options;
681
+ if (!value) {
682
+ return '';
683
+ }
684
+ // Sanitize the value to prevent XSS
685
+ const sanitizedValue = value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
686
+ try {
687
+ const config = { format, lineColor, width, height, displayValue };
688
+ if (elementType === 'svg') {
689
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
690
+ JsBarcode(svg, sanitizedValue, config);
691
+ return this.sanitizer.bypassSecurityTrustHtml(svg.outerHTML);
692
+ }
693
+ else {
694
+ const canvas = document.createElement('canvas');
695
+ JsBarcode(canvas, sanitizedValue, config);
696
+ const dataUrl = canvas.toDataURL('image/png');
697
+ return this.sanitizer.bypassSecurityTrustResourceUrl(dataUrl);
698
+ }
699
+ }
700
+ catch (error) {
701
+ console.error('Barcode generation failed:', error);
702
+ return '';
703
+ }
704
+ }
705
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
706
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, isStandalone: true, name: "barcode" });
707
+ }
708
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BarcodePipe, decorators: [{
709
+ type: Pipe,
710
+ args: [{
711
+ name: 'barcode',
712
+ standalone: true,
713
+ }]
714
+ }] });
715
+
716
+ // Pre-compiled regex patterns for performance
717
+ const HEX_6_PATTERN = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
718
+ const HEX_8_PATTERN = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/;
719
+ const HEX_3_PATTERN = /^#([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/;
720
+ const HEX_4_PATTERN = /^#([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/;
721
+ const RGB_PATTERN = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
722
+ const RGBA_PATTERN = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d.]+)\s*\)$/i;
723
+ /**
724
+ * Clamps a value between 0 and 255 for valid RGB channel values.
725
+ */
726
+ function clampChannel(value) {
727
+ return Math.max(0, Math.min(255, Math.round(value)));
728
+ }
729
+ /**
730
+ * Clamps alpha value between 0 and 1.
731
+ */
732
+ function clampAlpha(value) {
733
+ return Math.max(0, Math.min(1, value));
734
+ }
735
+ /**
736
+ * Converts a single hex character to its full two-character representation.
737
+ */
738
+ function expandHexChar(char) {
739
+ return char + char;
740
+ }
741
+ /**
742
+ * Converts a number to a two-character hex string.
743
+ */
744
+ function toHex(value) {
745
+ return clampChannel(value).toString(16).padStart(2, '0').toUpperCase();
746
+ }
747
+ /**
748
+ * Converts alpha (0-1) to hex (00-FF).
749
+ */
750
+ function alphaToHex(alpha) {
751
+ return Math.round(clampAlpha(alpha) * 255).toString(16).padStart(2, '0').toUpperCase();
752
+ }
753
+ /**
754
+ * Converts hex alpha (00-FF) to decimal (0-1).
755
+ */
756
+ function hexToAlpha(hex) {
757
+ return Math.round((parseInt(hex, 16) / 255) * 100) / 100;
255
758
  }
256
759
  /**
257
760
  * Parses any supported color format into RGBA components.
@@ -361,8 +864,6 @@ function parseColor(value) {
361
864
  * // Alpha channel support
362
865
  * {{ '#FF000080' | colorConvert:'rgba' }} // rgba(255, 0, 0, 0.5)
363
866
  * {{ 'rgba(255, 0, 0, 0.5)' | colorConvert:'hex' }} // #FF000080
364
- *
365
- * @author Mofiro Jean
366
867
  */
367
868
  class ColorConvertPipe {
368
869
  transform(value, target) {
@@ -395,8 +896,66 @@ class ColorConvertPipe {
395
896
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ColorConvertPipe, decorators: [{
396
897
  type: Pipe,
397
898
  args: [{
398
- name: 'colorConvert',
399
- standalone: true,
899
+ name: 'colorConvert',
900
+ standalone: true,
901
+ }]
902
+ }] });
903
+
904
+ /**
905
+ * GravatarPipe: Generates Gravatar URLs from email addresses.
906
+ *
907
+ * @param {string} value - The email address.
908
+ * @param {number} [size=80] - The avatar size in pixels.
909
+ *
910
+ * @returns {string} - The Gravatar URL.
911
+ *
912
+ * @example
913
+ * <img [src]="'user@example.com' | gravatar:100" />
914
+ */
915
+ class GravatarPipe {
916
+ transform(value, size = 80) {
917
+ if (!value)
918
+ return `https://www.gravatar.com/avatar/?s=${size}`;
919
+ const hash = md5(value.trim().toLowerCase());
920
+ return `https://www.gravatar.com/avatar/${hash}?s=${size}`;
921
+ }
922
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
923
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, isStandalone: true, name: "gravatar" });
924
+ }
925
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, decorators: [{
926
+ type: Pipe,
927
+ args: [{
928
+ name: 'gravatar',
929
+ standalone: true
930
+ }]
931
+ }] });
932
+
933
+ /**
934
+ * QrCodePipe: Generates a QR code from a string.
935
+ *
936
+ * @param {string} value - The string to encode.
937
+ * @param {QrCodeOptions} [options] - The QR code options.
938
+ *
939
+ * @returns {Promise<string>} - A promise that resolves with the QR code data URL.
940
+ *
941
+ * @example
942
+ * <img [src]="'Hello, World!' | qrCode | async" />
943
+ */
944
+ class QrCodePipe {
945
+ transform(value, options) {
946
+ if (!value) {
947
+ return Promise.resolve('');
948
+ }
949
+ return QRCode.toDataURL(value, options);
950
+ }
951
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
952
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, isStandalone: true, name: "qrCode" });
953
+ }
954
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, decorators: [{
955
+ type: Pipe,
956
+ args: [{
957
+ name: 'qrCode',
958
+ standalone: true
400
959
  }]
401
960
  }] });
402
961
 
@@ -424,51 +983,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
424
983
  }]
425
984
  }] });
426
985
 
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
986
  /**
473
987
  * DeviceTypePipe: Detects the device type based on the user agent string.
474
988
  *
@@ -505,741 +1019,1088 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
505
1019
  }] });
506
1020
 
507
1021
  /**
508
- * EmailMaskPipe: Masks the local part of an email address, revealing only the first and last characters.
1022
+ * JsonPrettyPipe: Formats JSON data with indentation and syntax highlighting.
509
1023
  *
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.
1024
+ * @param {string | object} value - The JSON string or object to format.
1025
+ * @param {number} [spaces=2] - Number of spaces for indentation.
512
1026
  *
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.
1027
+ * @returns {SafeHtml} - Formatted HTML with color-coded JSON.
515
1028
  *
516
1029
  * @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 ''
1030
+ * {{ '{"name": "John", "age": 30}' | jsonPretty }} // Outputs: Colorful, indented JSON
1031
+ * <pre [innerHTML]="data | jsonPretty:4"></pre> // 4-space indentation
1032
+ */
1033
+ class JsonPrettyPipe {
1034
+ sanitizer = inject(DomSanitizer);
1035
+ transform(value, spaces = 2, highlightProperty) {
1036
+ let jsonString;
1037
+ try {
1038
+ if (typeof value === 'object') {
1039
+ jsonString = JSON.stringify(value, null, spaces);
1040
+ }
1041
+ else if (value && typeof value === 'string') {
1042
+ jsonString = JSON.stringify(JSON.parse(value), null, spaces);
1043
+ }
1044
+ else {
1045
+ throw new Error('Invalid or empty input');
1046
+ }
1047
+ }
1048
+ catch (e) {
1049
+ return this.sanitizer.bypassSecurityTrustHtml('<span class="json-error">Invalid JSON: ' + (e instanceof Error ? e.message : 'Unknown error') + '</span>');
1050
+ }
1051
+ const escapedJson = jsonString
1052
+ .replace(/&/g, '&amp;')
1053
+ .replace(/</g, '&lt;')
1054
+ .replace(/>/g, '&gt;');
1055
+ let finalJson = this.highlightJson(escapedJson);
1056
+ if (highlightProperty) {
1057
+ const lines = finalJson.split('\n');
1058
+ const highlightedLines = lines.map(line => {
1059
+ const searchString = `<span class="json-key">"${highlightProperty}"</span>`;
1060
+ if (line.includes(searchString)) {
1061
+ return `<span class="highlight-line">${line}</span>`;
1062
+ }
1063
+ return line;
1064
+ });
1065
+ finalJson = highlightedLines.join('\n');
1066
+ }
1067
+ return this.sanitizer.bypassSecurityTrustHtml(`<pre class="json-pretty">${finalJson}</pre>`);
1068
+ }
1069
+ highlightJson(json) {
1070
+ let result = json.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, (match, p1, offset) => {
1071
+ const remainingString = json.substring(offset + match.length);
1072
+ const isKey = /^\s*:/.test(remainingString);
1073
+ return isKey
1074
+ ? `<span class="json-key">${match}</span>`
1075
+ : `<span class="json-string">${match}</span>`;
1076
+ });
1077
+ // Highlight numbers
1078
+ result = result.replace(/\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, '<span class="json-number">$&</span>');
1079
+ // Highlight booleans
1080
+ result = result.replace(/\b(true|false)\b/g, '<span class="json-boolean">$&</span>');
1081
+ // Highlight null
1082
+ result = result.replace(/\bnull\b/g, '<span class="json-null">$&</span>');
1083
+ // Highlight punctuation
1084
+ result = result.replace(/[{}[\]]/g, '<span class="json-punctuation">$&</span>');
1085
+ return result;
1086
+ }
1087
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: JsonPrettyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1088
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: JsonPrettyPipe, isStandalone: true, name: "jsonPretty" });
1089
+ }
1090
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: JsonPrettyPipe, decorators: [{
1091
+ type: Pipe,
1092
+ args: [{
1093
+ name: 'jsonPretty',
1094
+ standalone: true
1095
+ }]
1096
+ }] });
1097
+
1098
+ /**
1099
+ * TextToSpeechPipe: Converts text to speech using the Web Speech API.
1100
+ *
1101
+ * @param {string} value - The text to convert to speech.
1102
+ * @param {string} [lang='en-US'] - The language (local) for speech synthesis.
1103
+ *
1104
+ * @returns {void} - Triggers speech synthesis (no return value).
523
1105
  *
524
- * @author Mofiro Jean
1106
+ * @example
1107
+ * <div>{{ Hello World' | textToSpeech }}</div>
1108
+ * <div>{{ 'Bonjour' | textToSpeech:'fr-FR' }}h</div>
525
1109
  */
526
- class EmailMaskPipe {
527
- transform(value) {
528
- if (!value || !value.includes('@')) {
529
- return value || '';
1110
+ class TextToSpeechPipe {
1111
+ transform(value, lang = 'en-US') {
1112
+ if (!value || typeof window === 'undefined' || !window.speechSynthesis)
1113
+ return;
1114
+ const uttrance = new SpeechSynthesisUtterance(value);
1115
+ uttrance.lang = lang;
1116
+ window.speechSynthesis.speak(uttrance);
1117
+ }
1118
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1119
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, isStandalone: true, name: "textToSpeech" });
1120
+ }
1121
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, decorators: [{
1122
+ type: Pipe,
1123
+ args: [{
1124
+ name: 'textToSpeech',
1125
+ standalone: true
1126
+ }]
1127
+ }] });
1128
+
1129
+ /**
1130
+ * TimeAgo: Converts a date into a localized time string.
1131
+ *
1132
+ * Use the in-built Intl.RelativeTimeFormat to convert a date into a localized time string.
1133
+ * It was chosen over moment.js because it's more lightweight and supports more locales.
1134
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
1135
+ *
1136
+ * @param {Date | number | string} value - The date to convert.
1137
+ * @param {string} [local='en'] - BCP 47 local code (e.g., 'en', 'fr', 'es').
1138
+ *
1139
+ * @returns {string} - The localized time string.
1140
+ *
1141
+ * @example
1142
+ * {{ date | timeAgo }} // '5 minutes ago'
1143
+ * {{ date | timeAgo:'fr' }} // 'il y a 5 minutes'
1144
+ *
1145
+ * @note Pure pipe - output won't automatically update as time passes.
1146
+ * Use signals or periodic change detection to re-trigger.
1147
+ * */
1148
+ class TimeAgoPipePipe {
1149
+ static THRESHOLDS = [
1150
+ [60, 1, 'second'],
1151
+ [3600, 60, 'minute'],
1152
+ [86400, 3600, 'hour'],
1153
+ [604800, 86400, 'day'],
1154
+ [2592000, 604800, 'week'],
1155
+ [31536000, 2592000, 'month'],
1156
+ [Infinity, 31536000, 'year'],
1157
+ ];
1158
+ cacheLocal = '';
1159
+ rtf;
1160
+ transform(value, local = 'en') {
1161
+ if (value === null || value === undefined || value === "") {
1162
+ return "";
530
1163
  }
531
- const [local, domain] = value.split('@');
532
- if (local.length <= 2) {
533
- return `${local[0]}***@${domain}`;
1164
+ const date = new Date(value);
1165
+ if (isNaN(date.getTime())) {
1166
+ return "";
534
1167
  }
535
- const firstChar = local[0];
536
- const lastChar = local[local.length - 1];
537
- return `${firstChar}***${lastChar}@${domain}`;
1168
+ if (local !== this.cacheLocal) {
1169
+ this.rtf = new Intl.RelativeTimeFormat(local, { numeric: "auto" });
1170
+ this.cacheLocal = local;
1171
+ }
1172
+ const seconds = Math.floor((date.getTime() - Date.now()) / 1000);
1173
+ for (const [max, divisor, unit] of TimeAgoPipePipe.THRESHOLDS) {
1174
+ if (Math.abs(seconds) < max) {
1175
+ return this.rtf.format(Math.round(seconds / divisor), unit);
1176
+ }
1177
+ }
1178
+ return "";
538
1179
  }
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" });
1180
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1181
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, isStandalone: true, name: "timeAgo" });
541
1182
  }
542
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EmailMaskPipe, decorators: [{
1183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TimeAgoPipePipe, decorators: [{
543
1184
  type: Pipe,
544
1185
  args: [{
545
- name: 'emailMask',
1186
+ name: 'timeAgo',
546
1187
  standalone: true
547
1188
  }]
548
1189
  }] });
549
1190
 
550
1191
  /**
551
- * GravatarPipe: Generates Gravatar URLs from email addresses.
1192
+ * DiffPipe: Returns elements present in the first array but not in the second.
552
1193
  *
553
- * @param {string} value - The email address.
554
- * @param {number} [size=80] - The avatar size in pixels.
1194
+ * Supports primitives and objects by property key with dot notation.
555
1195
  *
556
- * @returns {string} - The Gravatar URL.
1196
+ * @param {unknown[]} value - The source array.
1197
+ * @param {unknown[]} compared - The array to compare against.
1198
+ * @param {string} [key] - Optional property path for object comparison (supports dot notation).
1199
+ *
1200
+ * @returns {unknown[]} - Elements in value that are not in compared.
557
1201
  *
558
1202
  * @example
559
- * <img [src]="'user@example.com' | gravatar:100" />
1203
+ * {{ [1, 2, 3, 4, 5] | diff:[3, 4, 5, 6] }} // [1, 2]
1204
+ * {{ allUsers | diff:activeUsers:'id' }} // inactive users
1205
+ * {{ orders | diff:shipped:'meta.trackingId' }} // unshipped orders
1206
+ */
1207
+ class DiffPipe {
1208
+ transform(value, compared, key) {
1209
+ if (!Array.isArray(value)) {
1210
+ return [];
1211
+ }
1212
+ if (!Array.isArray(compared) || compared.length === 0) {
1213
+ return [...value];
1214
+ }
1215
+ if (!key) {
1216
+ const comparedSet = new Set(compared);
1217
+ return value.filter(item => !comparedSet.has(item));
1218
+ }
1219
+ const comparedSet = new Set(compared.map(item => this.getNestedValue(item, key)));
1220
+ return value.filter(item => {
1221
+ const val = this.getNestedValue(item, key);
1222
+ return !comparedSet.has(val);
1223
+ });
1224
+ }
1225
+ getNestedValue(obj, path) {
1226
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1227
+ }
1228
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DiffPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1229
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: DiffPipe, isStandalone: true, name: "diff" });
1230
+ }
1231
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DiffPipe, decorators: [{
1232
+ type: Pipe,
1233
+ args: [{
1234
+ name: 'diff',
1235
+ standalone: true,
1236
+ }]
1237
+ }] });
1238
+
1239
+ /**
1240
+ * EveryPipe: Checks if all elements in an array satisfy a condition.
1241
+ *
1242
+ * Supports primitives (equality check) and objects by property key with dot notation.
1243
+ *
1244
+ * @param {unknown[]} value - The array to check.
1245
+ * @param {unknown} match - The value to match against.
1246
+ * @param {string} [key] - Optional property path to check (supports dot notation).
1247
+ *
1248
+ * @returns {boolean} - True if all elements match, false otherwise.
560
1249
  *
561
- * @author Mofiro Jean
1250
+ * @example
1251
+ * {{ [true, true, true] | every:true }} // true
1252
+ * {{ users | every:'active':'status' }} // are all users active?
1253
+ * {{ orders | every:'shipped':'meta.state' }} // are all orders shipped?
562
1254
  */
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}`;
1255
+ class EveryPipe {
1256
+ transform(value, match, key) {
1257
+ if (!Array.isArray(value) || value.length === 0) {
1258
+ return false;
1259
+ }
1260
+ if (!key) {
1261
+ return value.every(item => item === match);
1262
+ }
1263
+ return value.every(item => this.getNestedValue(item, key) === match);
1264
+ }
1265
+ getNestedValue(obj, path) {
1266
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1267
+ }
1268
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EveryPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1269
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: EveryPipe, isStandalone: true, name: "every" });
1270
+ }
1271
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EveryPipe, decorators: [{
1272
+ type: Pipe,
1273
+ args: [{
1274
+ name: 'every',
1275
+ standalone: true,
1276
+ }]
1277
+ }] });
1278
+
1279
+ /**
1280
+ * IntersectionPipe: Returns elements common to both arrays.
1281
+ *
1282
+ * Supports primitives and objects by property key with dot notation.
1283
+ *
1284
+ * @param {unknown[]} value - The first array.
1285
+ * @param {unknown[]} compared - The second array.
1286
+ * @param {string} [key] - Optional property path for object comparison (supports dot notation).
1287
+ *
1288
+ * @returns {unknown[]} - Elements present in both arrays.
1289
+ *
1290
+ * @example
1291
+ * {{ [1, 2, 3, 4] | intersection:[3, 4, 5, 6] }} // [3, 4]
1292
+ * {{ teamA | intersection:teamB:'id' }} // shared members
1293
+ * {{ required | intersection:granted:'meta.scope' }} // matched permissions
1294
+ */
1295
+ class IntersectionPipe {
1296
+ transform(value, compared, key) {
1297
+ if (!Array.isArray(value)) {
1298
+ return [];
1299
+ }
1300
+ if (!Array.isArray(compared) || compared.length === 0) {
1301
+ return [];
1302
+ }
1303
+ if (!key) {
1304
+ const comparedSet = new Set(compared);
1305
+ return value.filter(item => comparedSet.has(item));
1306
+ }
1307
+ const comparedSet = new Set(compared.map(item => this.getNestedValue(item, key)));
1308
+ return value.filter(item => {
1309
+ const val = this.getNestedValue(item, key);
1310
+ return comparedSet.has(val);
1311
+ });
1312
+ }
1313
+ getNestedValue(obj, path) {
1314
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1315
+ }
1316
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IntersectionPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1317
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: IntersectionPipe, isStandalone: true, name: "intersection" });
1318
+ }
1319
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IntersectionPipe, decorators: [{
1320
+ type: Pipe,
1321
+ args: [{
1322
+ name: 'intersection',
1323
+ standalone: true,
1324
+ }]
1325
+ }] });
1326
+
1327
+ /**
1328
+ * ChunkPipe: Splits an array into smaller arrays of a specified size.
1329
+ *
1330
+ * @param {unknown[]} value - The array to split.
1331
+ * @param {number} [size=1] - The size of each chunk.
1332
+ *
1333
+ * @returns {unknown[][]} - An array of chunks.
1334
+ *
1335
+ * @example
1336
+ * {{ [1, 2, 3, 4, 5] | chunk:2 }} // [[1, 2], [3, 4], [5]]
1337
+ * {{ [1, 2, 3, 4, 5, 6] | chunk:3 }} // [[1, 2, 3], [4, 5, 6]]
1338
+ */
1339
+ class ChunkPipe {
1340
+ transform(value, size = 1) {
1341
+ if (!Array.isArray(value) || value.length === 0) {
1342
+ return [];
1343
+ }
1344
+ if (size <= 0) {
1345
+ return [];
1346
+ }
1347
+ const result = [];
1348
+ for (let i = 0; i < value.length; i += size) {
1349
+ result.push(value.slice(i, i + size));
1350
+ }
1351
+ return result;
569
1352
  }
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" });
1353
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ChunkPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1354
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ChunkPipe, isStandalone: true, name: "chunk" });
572
1355
  }
573
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GravatarPipe, decorators: [{
1356
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ChunkPipe, decorators: [{
574
1357
  type: Pipe,
575
1358
  args: [{
576
- name: 'gravatar',
1359
+ name: 'chunk',
577
1360
  standalone: true
578
1361
  }]
579
1362
  }] });
580
1363
 
581
1364
  /**
582
- * HighlightPipe: Highlights occurrences of a search term within a string.
1365
+ * FlattenPipe: Flattens nested arrays to a specified depth.
583
1366
  *
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.
1367
+ * @param {unknown[]} value - The nested array to flatten.
1368
+ * @param {number} [depth=Infinity] - How many levels of nesting to flatten.
587
1369
  *
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.
1370
+ * @returns {unknown[]} - A new flattened array.
591
1371
  *
592
1372
  * @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
1373
+ * {{ [[1, 2], [3, 4]] | flatten }} // [1, 2, 3, 4]
1374
+ * {{ [[1, [2, [3]]]] | flatten:1 }} // [1, 2, [3]]
1375
+ * {{ [['a', 'b'], ['c']] | flatten }} // ['a', 'b', 'c']
600
1376
  */
601
- class HighlightPipe {
602
- sanitizer = inject(DomSanitizer);
603
- transform(value, searchTerm) {
604
- if (!value || !searchTerm) {
605
- return this.sanitizer.bypassSecurityTrustHtml(value || '');
1377
+ class Flatten {
1378
+ transform(value, depth = Infinity) {
1379
+ if (!Array.isArray(value)) {
1380
+ return [];
606
1381
  }
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);
1382
+ return value.flat(depth);
611
1383
  }
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" });
1384
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: Flatten, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1385
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: Flatten, isStandalone: true, name: "flatten" });
614
1386
  }
615
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HighlightPipe, decorators: [{
1387
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: Flatten, decorators: [{
616
1388
  type: Pipe,
617
1389
  args: [{
618
- name: 'highlight',
1390
+ name: 'flatten',
619
1391
  standalone: true
620
1392
  }]
621
1393
  }] });
622
1394
 
623
1395
  /**
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.
1396
+ * GroupByPipe: Groups array elements by a property value.
626
1397
  *
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.
1398
+ * @param {unknown[]} value - The array to group.
1399
+ * @param {string} key - Property path to group by (supports dot notation).
1400
+ *
1401
+ * @returns {Record<string, unknown[]>} - An object where keys are group names and values are arrays.
629
1402
  *
630
1403
  * @example
631
- * ```html
632
- * {{ '<p>Hello</p>' | htmlEscape }} <!-- Outputs: &lt;p&gt;Hello&lt;/p&gt; -->
633
- * ```
1404
+ * {{ users | groupBy:'role' }} // { admin: [...], editor: [...] }
1405
+ * {{ orders | groupBy:'customer.city' }} // { 'New York': [...], 'London': [...] }
634
1406
  */
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]);
1407
+ class GroupByPipe {
1408
+ transform(value, key) {
1409
+ if (!Array.isArray(value) || !key) {
1410
+ return {};
1411
+ }
1412
+ return value.reduce((groups, item) => {
1413
+ const groupKey = String(this.getNestedValue(item, key) ?? 'undefined');
1414
+ if (!groups[groupKey]) {
1415
+ groups[groupKey] = [];
1416
+ }
1417
+ groups[groupKey].push(item);
1418
+ return groups;
1419
+ }, {});
647
1420
  }
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" });
1421
+ getNestedValue(obj, path) {
1422
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1423
+ }
1424
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GroupByPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1425
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: GroupByPipe, isStandalone: true, name: "groupBy" });
650
1426
  }
651
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlEscapePipe, decorators: [{
1427
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: GroupByPipe, decorators: [{
652
1428
  type: Pipe,
653
1429
  args: [{
654
- name: 'htmlEscape',
655
- standalone: true
1430
+ name: 'groupBy',
1431
+ standalone: true,
656
1432
  }]
657
1433
  }] });
658
1434
 
659
1435
  /**
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].
1436
+ * InitialPipe: Returns all elements except the last n.
662
1437
  *
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.
1438
+ * @param {unknown[]} value - The array to slice.
1439
+ * @param {number} [n=1] - Number of elements to exclude from the end.
665
1440
  *
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).
1441
+ * @returns {unknown[]} - A new array without the last n elements.
669
1442
  *
670
1443
  * @example
671
- * ```html
672
- * <div [innerHTML]="'<p>Hello</p><script>alert(1)</script>' | htmlSanitize"></div>
673
- * <!-- Renders: <p>Hello</p> (script tag removed) -->
674
- * ```
1444
+ * {{ [1, 2, 3, 4, 5] | initial }} // [1, 2, 3, 4]
1445
+ * {{ [1, 2, 3, 4, 5] | initial:2 }} // [1, 2, 3]
1446
+ * {{ ['a', 'b', 'c'] | initial }} // ['a', 'b']
675
1447
  */
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('');
1448
+ class InitialPipe {
1449
+ transform(value, n = 1) {
1450
+ if (!Array.isArray(value)) {
1451
+ return [];
1452
+ }
1453
+ if (n <= 0) {
1454
+ return [...value];
1455
+ }
1456
+ if (n >= value.length) {
1457
+ return [];
1458
+ }
1459
+ return value.slice(0, -n);
682
1460
  }
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" });
1461
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1462
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: InitialPipe, isStandalone: true, name: "initial" });
685
1463
  }
686
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HtmlSanitizePipe, decorators: [{
1464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialPipe, decorators: [{
687
1465
  type: Pipe,
688
1466
  args: [{
689
- name: 'htmlSanitize',
1467
+ name: 'initial',
690
1468
  standalone: true
691
1469
  }]
692
1470
  }] });
693
1471
 
694
1472
  /**
695
- * InitialsPipe: Extracts initials from a name.
1473
+ * OrderByPipe: Sorts an array by a property value.
696
1474
  *
697
- * @param {string} value - The full name.
1475
+ * @param {unknown[]} value - The array to sort.
1476
+ * @param {string} key - Property path to sort by (supports dot notation).
1477
+ * @param {string} [direction='asc'] - Sort direction: 'asc' or 'desc'.
698
1478
  *
699
- * @returns {string} - The initials (e.g., 'John Doe' → 'JD').
1479
+ * @returns {unknown[]} - A new sorted array.
700
1480
  *
701
1481
  * @example
702
- * {{ 'John Doe' | initials }} // Outputs: JD
703
- * {{ 'Mary Jane Watson' | initials }} // Outputs: MJW
704
- *
705
- * @author Mofiro Jean
1482
+ * {{ users | orderBy:'name' }} // sorted A-Z
1483
+ * {{ users | orderBy:'name':'desc' }} // sorted Z-A
1484
+ * {{ users | orderBy:'age':'asc' }} // sorted by age
706
1485
  */
707
- class InitialsPipe {
708
- transform(value) {
709
- if (!value)
710
- return '';
711
- return value
712
- .trim()
713
- .split(/\s+/)
714
- .map(word => word.charAt(0).toUpperCase())
715
- .join('');
1486
+ class OrderByPipe {
1487
+ transform(value, key, direction = 'asc') {
1488
+ if (!Array.isArray(value) || value.length <= 1 || !key) {
1489
+ return Array.isArray(value) ? [...value] : [];
1490
+ }
1491
+ const dir = direction === 'desc' ? -1 : 1;
1492
+ return [...value].sort((a, b) => {
1493
+ const valA = this.getNestedValue(a, key);
1494
+ const valB = this.getNestedValue(b, key);
1495
+ if (valA === valB)
1496
+ return 0;
1497
+ if (valA === null || valA === undefined)
1498
+ return 1;
1499
+ if (valB === null || valB === undefined)
1500
+ return -1;
1501
+ if (typeof valA === 'string' && typeof valB === 'string') {
1502
+ return valA.localeCompare(valB) * dir;
1503
+ }
1504
+ if (typeof valA === 'number' && typeof valB === 'number') {
1505
+ return (valA - valB) * dir;
1506
+ }
1507
+ return String(valA).localeCompare(String(valB)) * dir;
1508
+ });
716
1509
  }
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" });
1510
+ getNestedValue(obj, path) {
1511
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1512
+ }
1513
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OrderByPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1514
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: OrderByPipe, isStandalone: true, name: "orderBy" });
719
1515
  }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InitialsPipe, decorators: [{
1516
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OrderByPipe, decorators: [{
721
1517
  type: Pipe,
722
1518
  args: [{
723
- name: 'initials',
724
- standalone: true
1519
+ name: 'orderBy',
1520
+ standalone: true,
725
1521
  }]
726
1522
  }] });
727
1523
 
728
1524
  /**
729
- * IpAddressMaskPipe: Masks the last two octets of an IPv4 address.
1525
+ * PluckPipe: Extracts a property value from every object in an array.
730
1526
  *
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..
1527
+ * @param {unknown[]} value - The array of objects.
1528
+ * @param {string} key - Property path to extract (supports dot notation).
733
1529
  *
734
- * @returns {string} - The masked IP address (e.g., 192.168.*.*).
1530
+ * @returns {unknown[]} - An array of the extracted values.
735
1531
  *
736
1532
  * @example
737
- * {{ '192.168.1.1' | ipAddressMask }} // Outputs: 192.168.*.*
738
- * {{ '10.0.0.255' | ipAddressMask }} // Outputs: 10.0.*.*
739
- *
740
- * @author Mofiro Jean
1533
+ * {{ users | pluck:'name' }} // ['Alice', 'Bob', 'Carol']
1534
+ * {{ orders | pluck:'customer.email' }} // ['a@test.com', 'b@test.com']
741
1535
  */
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;
1536
+ class PluckPipe {
1537
+ transform(value, key) {
1538
+ if (!Array.isArray(value) || !key) {
1539
+ return [];
746
1540
  }
747
- if (shouldMask) {
748
- const parts = value.split('.');
749
- return `${parts[0]}.${parts[1]}.*.*`;
750
- }
751
- return value;
1541
+ return value.map(item => this.getNestedValue(item, key));
752
1542
  }
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" });
1543
+ getNestedValue(obj, path) {
1544
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1545
+ }
1546
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PluckPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1547
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PluckPipe, isStandalone: true, name: "pluck" });
755
1548
  }
756
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IpAddressMaskPipe, decorators: [{
1549
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PluckPipe, decorators: [{
757
1550
  type: Pipe,
758
1551
  args: [{
759
- name: 'ipAddressMask',
760
- standalone: true
1552
+ name: 'pluck',
1553
+ standalone: true,
761
1554
  }]
762
1555
  }] });
763
1556
 
764
1557
  /**
765
- * JsonPrettyPipe: Formats JSON data with indentation and syntax highlighting.
1558
+ * RangePipe: Generates a numeric sequence array.
766
1559
  *
767
- * @param {string | object} value - The JSON string or object to format.
768
- * @param {number} [spaces=2] - Number of spaces for indentation.
1560
+ * @param {number} value - The number of items to generate (or the end value when start is provided).
1561
+ * @param {number} [start=0] - The starting number.
1562
+ * @param {number} [step=1] - The increment between each number.
769
1563
  *
770
- * @returns {SafeHtml} - Formatted HTML with color-coded JSON.
1564
+ * @returns {number[]} - An array of sequential numbers.
771
1565
  *
772
1566
  * @example
773
- * {{ '{"name": "John", "age": 30}' | jsonPretty }} // Outputs: Colorful, indented JSON
774
- * <pre [innerHTML]="data | jsonPretty:4"></pre> // 4-space indentation
775
- *
776
- * @author Mofiro Jean
1567
+ * {{ 5 | range }} // [0, 1, 2, 3, 4]
1568
+ * {{ 5 | range:1 }} // [1, 2, 3, 4, 5]
1569
+ * {{ 10 | range:0:2 }} // [0, 2, 4, 6, 8]
777
1570
  */
778
- class JsonPrettyPipe {
779
- sanitizer = inject(DomSanitizer);
780
- transform(value, spaces = 2, highlightProperty) {
781
- let jsonString;
782
- try {
783
- if (typeof value === 'object') {
784
- jsonString = JSON.stringify(value, null, spaces);
785
- }
786
- else if (value && typeof value === 'string') {
787
- jsonString = JSON.stringify(JSON.parse(value), null, spaces);
788
- }
789
- else {
790
- throw new Error('Invalid or empty input');
791
- }
1571
+ class RangePipe {
1572
+ transform(value, start = 0, step = 1) {
1573
+ if (typeof value !== 'number' || isNaN(value) || value <= 0) {
1574
+ return [];
792
1575
  }
793
- catch (e) {
794
- return this.sanitizer.bypassSecurityTrustHtml('<span class="json-error">Invalid JSON: ' + (e instanceof Error ? e.message : 'Unknown error') + '</span>');
1576
+ if (step === 0) {
1577
+ return [];
795
1578
  }
796
- const escapedJson = jsonString
797
- .replace(/&/g, '&amp;')
798
- .replace(/</g, '&lt;')
799
- .replace(/>/g, '&gt;');
800
- let finalJson = this.highlightJson(escapedJson);
801
- if (highlightProperty) {
802
- const lines = finalJson.split('\n');
803
- const highlightedLines = lines.map(line => {
804
- const searchString = `<span class="json-key">"${highlightProperty}"</span>`;
805
- if (line.includes(searchString)) {
806
- return `<span class="highlight-line">${line}</span>`;
807
- }
808
- return line;
809
- });
810
- finalJson = highlightedLines.join('\n');
1579
+ const result = [];
1580
+ for (let i = 0; i < value; i++) {
1581
+ result.push(start + i * step);
811
1582
  }
812
- return this.sanitizer.bypassSecurityTrustHtml(`<pre class="json-pretty">${finalJson}</pre>`);
813
- }
814
- highlightJson(json) {
815
- let result = json.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, (match, p1, offset) => {
816
- const remainingString = json.substring(offset + match.length);
817
- const isKey = /^\s*:/.test(remainingString);
818
- return isKey
819
- ? `<span class="json-key">${match}</span>`
820
- : `<span class="json-string">${match}</span>`;
821
- });
822
- // Highlight numbers
823
- result = result.replace(/\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, '<span class="json-number">$&</span>');
824
- // Highlight booleans
825
- result = result.replace(/\b(true|false)\b/g, '<span class="json-boolean">$&</span>');
826
- // Highlight null
827
- result = result.replace(/\bnull\b/g, '<span class="json-null">$&</span>');
828
- // Highlight punctuation
829
- result = result.replace(/[{}[\]]/g, '<span class="json-punctuation">$&</span>');
830
1583
  return result;
831
1584
  }
832
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: JsonPrettyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
833
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: JsonPrettyPipe, isStandalone: true, name: "jsonPretty" });
1585
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RangePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1586
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: RangePipe, isStandalone: true, name: "range" });
834
1587
  }
835
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: JsonPrettyPipe, decorators: [{
1588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RangePipe, decorators: [{
836
1589
  type: Pipe,
837
1590
  args: [{
838
- name: 'jsonPretty',
839
- standalone: true
1591
+ name: 'range',
1592
+ standalone: true,
840
1593
  }]
841
1594
  }] });
842
1595
 
843
1596
  /**
844
- * KebabCasePipe: Converts text to kebab-case (e.g., "hello world" → "hello-world").
1597
+ * ReversePipe: Reverses the characters in a string.
845
1598
  *
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.
1599
+ * @param {string} value - The string to reverse.
848
1600
  *
849
- * @example
850
- * ```html
851
- * {{ 'hello world' | kebabCase }} <!-- Outputs: hello-world -->
852
- * ```
1601
+ * @returns {string} - The reversed string (e.g., 'hello' → 'olleh').
853
1602
  *
854
- * @author Mofiro Jean
1603
+ * @example
1604
+ * {{ 'hello' | reverse }} // Outputs: 'olleh'
1605
+ * {{ '12345' | reverse }} // Outputs: '54321'
1606
+ * <p>{{ userInput | reverse }}</p>
855
1607
  */
856
- class KebabCasePipe {
1608
+ class ReversePipe {
857
1609
  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
1610
+ if (Array.isArray(value)) {
1611
+ return [...value].reverse();
1612
+ }
1613
+ if (typeof value === 'string') {
1614
+ return value.split('').reverse().join('');
1615
+ }
1616
+ return '';
866
1617
  }
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" });
1618
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1619
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, isStandalone: true, name: "reverse" });
869
1620
  }
870
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: KebabCasePipe, decorators: [{
1621
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, decorators: [{
871
1622
  type: Pipe,
872
1623
  args: [{
873
- name: 'kebabCase',
1624
+ name: 'reverse',
874
1625
  standalone: true
875
1626
  }]
876
1627
  }] });
877
1628
 
878
1629
  /**
879
- * MorseCodePipe: Converts text to Morse code.
1630
+ * SamplePipe: Randomly selects n items from an array.
880
1631
  *
881
- * @param {string} value - The text to convert to Morse code.
1632
+ * Uses Fisher-Yates partial shuffle for unbiased selection.
1633
+ * Returns a single item when n=1 (default), or an array when n>1.
882
1634
  *
883
- * @returns {string} - The Morse code representation (e.g., 'SOS' → '... --- ...').
1635
+ * @param {unknown[]} value - The array to sample from.
1636
+ * @param {number} [n=1] - Number of items to select.
1637
+ *
1638
+ * @returns {unknown | unknown[]} - A single random item (n=1) or array of random items (n>1).
884
1639
  *
885
1640
  * @example
886
- * {{ 'SOS' | morseCode }} // Outputs: '... --- ...'
887
- * {{ 'HELP' | morseCode }} // Outputs: '.... . .-.. .--.'
888
- * <p>{{ userInput | morseCode }}</p>
1641
+ * {{ [1, 2, 3, 4, 5] | sample }} // 3 (random single)
1642
+ * {{ [1, 2, 3, 4, 5] | sample:3 }} // [5, 1, 3] (random 3)
1643
+ * {{ users | sample:5 }} // 5 random users
889
1644
  *
890
- * @author Mofiro Jean
1645
+ * @note Impure pipe — returns different results on each change detection cycle.
1646
+ * Bind the result to a signal to control when it re-samples.
891
1647
  */
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 '';
1648
+ class SamplePipe {
1649
+ transform(value, n = 1) {
1650
+ if (!Array.isArray(value) || value.length === 0) {
1651
+ return n === 1 ? undefined : [];
905
1652
  }
906
- return value
907
- .toUpperCase()
908
- .split('')
909
- .map(char => this.morseCodeMap[char] || '')
910
- .filter(code => code)
911
- .join(' ');
1653
+ if (n <= 0) {
1654
+ return [];
1655
+ }
1656
+ // Clamp n to array length
1657
+ const count = Math.min(n, value.length);
1658
+ // Fisher-Yates partial shuffle — only shuffle `count` positions
1659
+ const arr = [...value];
1660
+ for (let i = arr.length - 1; i > arr.length - 1 - count; i--) {
1661
+ const j = Math.floor(Math.random() * (i + 1));
1662
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1663
+ }
1664
+ const result = arr.slice(arr.length - count);
1665
+ // Return single item for n=1, array otherwise
1666
+ return n === 1 ? result[0] : result;
912
1667
  }
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" });
1668
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SamplePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1669
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: SamplePipe, isStandalone: true, name: "sample", pure: false });
915
1670
  }
916
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MorseCodePipe, decorators: [{
1671
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SamplePipe, decorators: [{
917
1672
  type: Pipe,
918
1673
  args: [{
919
- name: 'morseCode',
920
- standalone: true
1674
+ name: 'sample',
1675
+ standalone: true,
1676
+ pure: false,
921
1677
  }]
922
1678
  }] });
923
1679
 
924
1680
  /**
925
- * QrCodePipe: Generates a QR code from a string.
1681
+ * ShufflePipe: Randomly reorders elements in an array using the Fisher-Yates
1682
+ algorithm.
926
1683
  *
927
- * @param {string} value - The string to encode.
928
- * @param {QrCodeOptions} [options] - The QR code options.
1684
+ * Uses the Fisher-Yates (Knuth) shuffle for unbiased randomization,
1685
+ * guaranteeing every permutation has equal probability.
929
1686
  *
930
- * @returns {Promise<string>} - A promise that resolves with the QR code data URL.
1687
+ * @param {unknown[]} value - The array to shuffle.
1688
+ *
1689
+ * @returns {unknown[]} - A new array with elements in random order.
931
1690
  *
932
1691
  * @example
933
- * <img [src]="'Hello, World!' | qrCode | async" />
1692
+ * {{ [1, 2, 3, 4, 5] | shuffle }} // [3, 1, 5, 2, 4]
1693
+ * {{ ['a', 'b', 'c'] | shuffle }} // ['c', 'a', 'b']
934
1694
  *
935
- * @author Mofiro Jean
1695
+ * @note Impure pipe — runs on every change detection cycle.
1696
+ * Avoid using in performance-critical templates or bind the result
1697
+ * to a signal/variable to control when it re-shuffles.
936
1698
  */
937
- class QrCodePipe {
938
- transform(value, options) {
939
- if (!value) {
940
- return Promise.resolve('');
1699
+ class ShufflePipe {
1700
+ transform(value) {
1701
+ if (!Array.isArray(value)) {
1702
+ return [];
941
1703
  }
942
- return QRCode.toDataURL(value, options);
1704
+ const arr = [...value];
1705
+ for (let i = arr.length - 1; i >= 0; i--) {
1706
+ const j = Math.floor(Math.random() * (i + 1));
1707
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1708
+ }
1709
+ return arr;
943
1710
  }
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" });
1711
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ShufflePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1712
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ShufflePipe, isStandalone: true, name: "shuffle", pure: false });
946
1713
  }
947
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: QrCodePipe, decorators: [{
1714
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ShufflePipe, decorators: [{
948
1715
  type: Pipe,
949
1716
  args: [{
950
- name: 'qrCode',
1717
+ name: 'shuffle',
1718
+ pure: false,
951
1719
  standalone: true
952
1720
  }]
953
1721
  }] });
954
1722
 
955
1723
  /**
956
- * ReplacePipe: A custom Angular pipe that either highlights or replaces text based on a pattern.
1724
+ * TailPipe: Returns all elements except the first n.
957
1725
  *
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.
960
- *
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).
1726
+ * @param {unknown[]} value - The array to slice.
1727
+ * @param {number} [n=1] - Number of elements to exclude from the start.
966
1728
  *
967
- * @returns {string | SafeHtml} - Returns the transformed string or SafeHtml with highlights.
1729
+ * @returns {unknown[]} - A new array without the first n elements.
968
1730
  *
969
1731
  * @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>
1732
+ * {{ [1, 2, 3, 4, 5] | tail }} // [2, 3, 4, 5]
1733
+ * {{ [1, 2, 3, 4, 5] | tail:2 }} // [3, 4, 5]
1734
+ * {{ ['a', 'b', 'c'] | tail }} // ['b', 'c']
1735
+ */
1736
+ class TailPipe {
1737
+ transform(value, n = 1) {
1738
+ if (!Array.isArray(value)) {
1739
+ return [];
1740
+ }
1741
+ // n = 0 means keep everything
1742
+ if (n <= 0)
1743
+ return [...value];
1744
+ // n >= length means remove everything
1745
+ if (n >= value.length)
1746
+ return [];
1747
+ return value.slice(n);
1748
+ }
1749
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TailPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1750
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TailPipe, isStandalone: true, name: "tail" });
1751
+ }
1752
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TailPipe, decorators: [{
1753
+ type: Pipe,
1754
+ args: [{
1755
+ name: 'tail',
1756
+ }]
1757
+ }] });
1758
+
1759
+ /**
1760
+ * TruthifyPipe: Removes all falsy values from an array.
975
1761
  *
976
- * {{ 'Angular is great' | replace:'great':'awesome':'highlight':true }}
977
- * // Output: Angular is <span class="highlight">awesome</span>
1762
+ * Falsy values: false, 0, -0, '', null, undefined, NaN
978
1763
  *
979
- * {{ 'Angular is great' | replace:'great':'awesome':'highlight':false }}
980
- * // Output: Angular is <span class="highlight">great</span>
1764
+ * @param {unknown[]} value - The array to filter.
981
1765
  *
982
- * <div [innerHTML]="'Angular is great' | replace:'great':'awesome':'highlight':false"></div>
983
- * // Renders: Angular is <span class="highlight">great</span>
1766
+ * @returns {unknown[]} - A new array with only truthy values.
984
1767
  *
985
- * @author Mofiro Jean
1768
+ * @example
1769
+ * {{ [0, 1, '', 'hello', null, true] | truthify }} // [1, 'hello', true]
1770
+ * {{ ['', 'a', '', 'b'] | truthify }} // ['a', 'b']
986
1771
  */
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;
995
- }
996
- const finalPattern = typeof pattern === 'string' ? new RegExp(pattern, 'gi') : pattern;
997
- if (!highlightClass) {
998
- return isReplace ? value.replace(finalPattern, replacement) : value;
999
- }
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);
1772
+ class TruthifyPipe {
1773
+ transform(value) {
1774
+ if (!Array.isArray(value)) {
1775
+ return [];
1006
1776
  }
1007
- const highlightedMatch = `<span class="${highlightClass}">$&</span>`;
1008
- const result = value.replace(finalPattern, highlightedMatch);
1009
- return this.sanitizer.bypassSecurityTrustHtml(result);
1777
+ return value.filter(Boolean);
1010
1778
  }
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" });
1779
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruthifyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1780
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: TruthifyPipe, isStandalone: true, name: "truthify" });
1013
1781
  }
1014
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReplacePipe, decorators: [{
1782
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruthifyPipe, decorators: [{
1015
1783
  type: Pipe,
1016
1784
  args: [{
1017
- name: 'replace',
1018
- standalone: true
1785
+ name: 'truthify',
1786
+ standalone: true,
1019
1787
  }]
1020
1788
  }] });
1021
1789
 
1022
1790
  /**
1023
- * ReversePipe: Reverses the characters in a string.
1791
+ * UniquePipe: Removes duplicate values from an array.
1024
1792
  *
1025
- * @param {string} value - The string to reverse.
1793
+ * Supports primitives, objects by property key, and deep nested keys via dot notation.
1026
1794
  *
1027
- * @returns {string} - The reversed string (e.g., 'hello' → 'olleh').
1795
+ * @param {unknown[]} value - The array to deduplicate.
1796
+ * @param {string} [key] - Optional property path to compare objects by (e.g., 'id', 'user.email').
1028
1797
  *
1029
- * @example
1030
- * {{ 'hello' | reverse }} // Outputs: 'olleh'
1031
- * {{ '12345' | reverse }} // Outputs: '54321'
1032
- * <p>{{ userInput | reverse }}</p>
1798
+ * @returns {unknown[]} - A new array with duplicates removed, preserving first occurrence.
1033
1799
  *
1034
- * @author Mofiro Jean
1800
+ * @example
1801
+ * {{ [1, 2, 2, 3] | unique }} // [1, 2, 3]
1802
+ * {{ users | unique:'email' }} // unique by email
1803
+ * {{ orders | unique:'customer.email' }} // unique by nested property
1035
1804
  */
1036
- class ReversePipe {
1037
- transform(value) {
1038
- if (!value || typeof value !== 'string') {
1039
- return '';
1805
+ class UniquePipe {
1806
+ transform(value, key) {
1807
+ if (!Array.isArray(value)) {
1808
+ return [];
1040
1809
  }
1041
- return value.split('').reverse().join('');
1810
+ if (!key) {
1811
+ return [...new Set(value)];
1812
+ }
1813
+ const seen = new Set();
1814
+ return value.filter(item => {
1815
+ const val = this.getNestedValue(item, key);
1816
+ if (seen.has(val))
1817
+ return false;
1818
+ seen.add(val);
1819
+ return true;
1820
+ });
1042
1821
  }
1043
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1044
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, isStandalone: true, name: "reverse" });
1822
+ getNestedValue(obj, path) {
1823
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1824
+ }
1825
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UniquePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1826
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: UniquePipe, isStandalone: true, name: "unique" });
1045
1827
  }
1046
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ReversePipe, decorators: [{
1828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UniquePipe, decorators: [{
1047
1829
  type: Pipe,
1048
1830
  args: [{
1049
- name: 'reverse',
1831
+ name: 'unique',
1050
1832
  standalone: true
1051
1833
  }]
1052
1834
  }] });
1053
1835
 
1054
1836
  /**
1055
- * SnakeCasePipe: Converts text to snake_case (e.g., "hello world" → "hello_world").
1837
+ * WithoutPipe: Excludes specified elements from an array.
1056
1838
  *
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.
1839
+ * Supports primitives and objects by property key with dot notation.
1059
1840
  *
1060
- * @example
1061
- * ```html
1062
- * {{ 'hello world' | snakeCase }} <!-- Outputs: hello_world -->
1063
- * ```
1841
+ * @param {unknown[]} value - The array to filter.
1842
+ * @param {unknown[]} excludes - Values to exclude.
1843
+ * @param {string} [key] - Optional property path for object comparison (supports dot notation).
1064
1844
  *
1065
- * @author Mofiro Jean
1845
+ * @returns {unknown[]} - A new array without the excluded elements.
1846
+ *
1847
+ * @example
1848
+ * {{ [1, 2, 3, 4, 5] | without:[2, 4] }} // [1, 3, 5]
1849
+ * {{ users | without:['banned']:'status' }} // active users
1850
+ * {{ orders | without:['cancelled']:'meta.status' }} // non-cancelled orders
1066
1851
  */
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
1852
+ class WithoutPipe {
1853
+ transform(value, excludes, key) {
1854
+ if (!Array.isArray(value)) {
1855
+ return [];
1856
+ }
1857
+ if (!Array.isArray(excludes) || excludes.length === 0) {
1858
+ return [...value];
1859
+ }
1860
+ const excludeSet = new Set(excludes);
1861
+ if (!key) {
1862
+ return value.filter(item => !excludeSet.has(item));
1863
+ }
1864
+ return value.filter(item => {
1865
+ const val = this.getNestedValue(item, key);
1866
+ return !excludeSet.has(val);
1867
+ });
1079
1868
  }
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" });
1869
+ getNestedValue(obj, path) {
1870
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1871
+ }
1872
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: WithoutPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1873
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: WithoutPipe, isStandalone: true, name: "without" });
1082
1874
  }
1083
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SnakeCasePipe, decorators: [{
1875
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: WithoutPipe, decorators: [{
1084
1876
  type: Pipe,
1085
1877
  args: [{
1086
- name: 'snakeCase',
1087
- standalone: true
1878
+ name: 'without',
1879
+ standalone: true,
1088
1880
  }]
1089
1881
  }] });
1090
1882
 
1091
1883
  /**
1092
- * TextToSpeechPipe: Converts text to speech using the Web Speech API.
1884
+ * SomePipe: Checks if at least one element in an array satisfies a condition.
1093
1885
  *
1094
- * @param {string} value - The text to convert to speech.
1095
- * @param {string} [lang='en-US'] - The language (local) for speech synthesis.
1886
+ * Supports primitives (equality check) and objects by property key with dot notation.
1096
1887
  *
1097
- * @returns {void} - Triggers speech synthesis (no return value).
1888
+ * @param {unknown[]} value - The array to check.
1889
+ * @param {unknown} match - The value to match against.
1890
+ * @param {string} [key] - Optional property path to check (supports dot notation).
1098
1891
  *
1099
- * @example
1100
- * <div>{{ Hello World' | textToSpeech }}</div>
1101
- * <div>{{ 'Bonjour' | textToSpeech:'fr-FR' }}h</div>
1892
+ * @returns {boolean} - True if at least one element matches, false otherwise.
1102
1893
  *
1103
- * @author Mofiro Jean
1894
+ * @example
1895
+ * {{ [false, false, true] | some:true }} // true
1896
+ * {{ users | some:'admin':'role' }} // any admins?
1897
+ * {{ orders | some:'failed':'meta.status' }} // any failures?
1104
1898
  */
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);
1899
+ class SomePipe {
1900
+ transform(value, match, key) {
1901
+ if (!Array.isArray(value) || value.length === 0) {
1902
+ return false;
1903
+ }
1904
+ if (!key) {
1905
+ return value.some(item => item === match);
1906
+ }
1907
+ return value.some(item => this.getNestedValue(item, key) === match);
1112
1908
  }
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" });
1909
+ getNestedValue(obj, path) {
1910
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1911
+ }
1912
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SomePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1913
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: SomePipe, isStandalone: true, name: "some" });
1115
1914
  }
1116
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TextToSpeechPipe, decorators: [{
1915
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SomePipe, decorators: [{
1117
1916
  type: Pipe,
1118
1917
  args: [{
1119
- name: 'textToSpeech',
1120
- standalone: true
1918
+ name: 'some',
1919
+ standalone: true,
1121
1920
  }]
1122
1921
  }] });
1123
1922
 
1124
1923
  /**
1125
- * TitleCasePipe: Capitalizes the first letter of each word in a string.
1924
+ * UnionPipe: Combines two arrays, keeping only unique elements.
1126
1925
  *
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.
1926
+ * Supports primitives and objects by property key with dot notation.
1927
+ *
1928
+ * @param {unknown[]} value - The first array.
1929
+ * @param {unknown[]} other - The second array.
1930
+ * @param {string} [key] - Optional property path for uniqueness check (supports dot notation).
1931
+ *
1932
+ * @returns {unknown[]} - A merged array with duplicates removed.
1129
1933
  *
1130
1934
  * @example
1131
- * ```html
1132
- * {{ 'hello world' | titleCase }} <!-- Outputs: Hello World -->
1133
- * ```
1935
+ * {{ [1, 2, 3] | union:[3, 4, 5] }} // [1, 2, 3, 4, 5]
1936
+ * {{ admins | union:editors:'id' }} // all unique users
1937
+ * {{ local | union:remote:'meta.uuid' }} // merged records
1134
1938
  */
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(' ');
1939
+ class UnionPipe {
1940
+ transform(value, other, key) {
1941
+ if (!Array.isArray(value) && !Array.isArray(other)) {
1942
+ return [];
1943
+ }
1944
+ const first = Array.isArray(value) ? value : [];
1945
+ const second = Array.isArray(other) ? other : [];
1946
+ if (!key) {
1947
+ const seen = new Set();
1948
+ const result = [];
1949
+ for (const item of [...first, ...second]) {
1950
+ if (!seen.has(item)) {
1951
+ seen.add(item);
1952
+ result.push(item);
1953
+ }
1954
+ }
1955
+ return result;
1956
+ }
1957
+ const seen = new Set();
1958
+ const result = [];
1959
+ for (const item of [...first, ...second]) {
1960
+ const val = this.getNestedValue(item, key);
1961
+ if (!seen.has(val)) {
1962
+ seen.add(val);
1963
+ result.push(item);
1964
+ }
1965
+ }
1966
+ return result;
1144
1967
  }
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" });
1968
+ getNestedValue(obj, path) {
1969
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
1970
+ }
1971
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UnionPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1972
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: UnionPipe, isStandalone: true, name: "union" });
1147
1973
  }
1148
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TitleCasePipe, decorators: [{
1974
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UnionPipe, decorators: [{
1149
1975
  type: Pipe,
1150
1976
  args: [{
1151
- name: 'titleCase',
1152
- standalone: true
1977
+ name: 'union',
1978
+ standalone: true,
1153
1979
  }]
1154
1980
  }] });
1155
1981
 
1156
1982
  /**
1157
- * TruncatePipe: Truncates a string to a specified maximum length, optionally preserving words.
1983
+ * FilterByPipe: Filters an array by matching a search term against object
1984
+ properties.
1158
1985
  *
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.
1986
+ * @param {unknown[]} value - The array to filter.
1987
+ * @param {string} search - The search term.
1988
+ * @param {string} [key] - Property to search in. If omitted, searches all string
1989
+ properties.
1161
1990
  *
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.
1991
+ * @returns {unknown[]} - Filtered array with matching items.
1167
1992
  *
1168
1993
  * @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 ''
1176
- *
1177
- * @author Mofiro Jean
1994
+ * {{ users | filterBy:'alice':'name' }} // users with 'alice' in name
1995
+ * {{ users | filterBy:'admin':'role' }} // users with role 'admin'
1996
+ * {{ users | filterBy:'bob' }} // search all properties for 'bob'
1178
1997
  */
1179
- class TruncatePipe {
1180
- transform(value, maxLength = 10, ellipsis = '...', preserveWords = false) {
1181
- if (!value || typeof value !== 'string') {
1182
- return '';
1998
+ class FilterByPipe {
1999
+ transform(value, search, key) {
2000
+ if (!Array.isArray(value) || !search) {
2001
+ return Array.isArray(value) ? [...value] : [];
1183
2002
  }
1184
- if (value.length <= maxLength) {
1185
- return value;
1186
- }
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;
2003
+ const term = String(search).toLowerCase();
2004
+ if (key) {
2005
+ return value.filter(item => {
2006
+ const val = this.getNestedValue(item, key);
2007
+ return val !== undefined && val !== null
2008
+ && String(val).toLowerCase().includes(term);
2009
+ });
1191
2010
  }
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);
2011
+ return value.filter(item => {
2012
+ if (typeof item === 'object' && item !== null) {
2013
+ return this.searchObject(item, term);
1198
2014
  }
1199
- }
1200
- return truncated.trim() + ellipsis;
2015
+ return String(item).toLowerCase().includes(term);
2016
+ });
1201
2017
  }
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" });
2018
+ searchObject(obj, term) {
2019
+ return Object.values(obj).some(val => {
2020
+ if (typeof val === 'string') {
2021
+ return val.toLowerCase().includes(term);
2022
+ }
2023
+ if (typeof val === 'number' || typeof val === 'boolean') {
2024
+ return String(val).toLowerCase().includes(term);
2025
+ }
2026
+ if (typeof val === 'object' && val !== null) {
2027
+ return this.searchObject(val, term);
2028
+ }
2029
+ return false;
2030
+ });
2031
+ }
2032
+ getNestedValue(obj, path) {
2033
+ return path.split('.').reduce((current, segment) => current?.[segment], obj);
2034
+ }
2035
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FilterByPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
2036
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: FilterByPipe, isStandalone: true, name: "filterBy" });
1204
2037
  }
1205
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TruncatePipe, decorators: [{
2038
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FilterByPipe, decorators: [{
1206
2039
  type: Pipe,
1207
2040
  args: [{
1208
- name: 'truncate',
1209
- standalone: true
2041
+ name: 'filterBy',
2042
+ standalone: true,
1210
2043
  }]
1211
2044
  }] });
1212
2045
 
2046
+ // Text
1213
2047
  const ALL_PIPES = [
2048
+ // Text
1214
2049
  AsciiArtPipe,
1215
- BarcodePipe,
1216
2050
  CamelCasePipe,
1217
- ColorConvertPipe,
1218
- CreditCardMaskPipe,
1219
- DeviceTypePipe,
1220
- EmailMaskPipe,
1221
- GravatarPipe,
1222
2051
  HighlightPipe,
1223
- HtmlEscapePipe,
1224
- HtmlSanitizePipe,
1225
2052
  InitialsPipe,
1226
- IpAddressMaskPipe,
1227
- JsonPrettyPipe,
1228
2053
  KebabCasePipe,
1229
2054
  MorseCodePipe,
1230
- QrCodePipe,
1231
2055
  ReplacePipe,
1232
- ReversePipe,
1233
2056
  SnakeCasePipe,
1234
- TextToSpeechPipe,
1235
2057
  TitleCasePipe,
1236
2058
  TruncatePipe,
1237
- CountPipe
2059
+ // Security & Privacy
2060
+ CreditCardMaskPipe,
2061
+ EmailMaskPipe,
2062
+ HtmlEscapePipe,
2063
+ HtmlSanitizePipe,
2064
+ IpAddressMaskPipe,
2065
+ // Media & Visual
2066
+ BarcodePipe,
2067
+ ColorConvertPipe,
2068
+ GravatarPipe,
2069
+ QrCodePipe,
2070
+ // Data & Utility
2071
+ CountPipe,
2072
+ DeviceTypePipe,
2073
+ JsonPrettyPipe,
2074
+ TextToSpeechPipe,
2075
+ TimeAgoPipePipe,
2076
+ // Array
2077
+ ChunkPipe,
2078
+ DiffPipe,
2079
+ EveryPipe,
2080
+ IntersectionPipe,
2081
+ Flatten,
2082
+ GroupByPipe,
2083
+ InitialPipe,
2084
+ OrderByPipe,
2085
+ PluckPipe,
2086
+ RangePipe,
2087
+ ReversePipe,
2088
+ SamplePipe,
2089
+ ShufflePipe,
2090
+ TailPipe,
2091
+ TruthifyPipe,
2092
+ UniquePipe,
2093
+ WithoutPipe,
2094
+ SomePipe,
2095
+ UnionPipe,
2096
+ FilterByPipe
1238
2097
  ];
1239
2098
 
2099
+ // Text
2100
+
1240
2101
  /**
1241
2102
  * Generated bundle index. Do not edit.
1242
2103
  */
1243
2104
 
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 };
2105
+ export { ALL_PIPES, AsciiArtPipe, BarcodePipe, CamelCasePipe, ChunkPipe, ColorConvertPipe, CountPipe, CreditCardMaskPipe, DeviceTypePipe, DiffPipe, EmailMaskPipe, EveryPipe, FilterByPipe, Flatten, GravatarPipe, GroupByPipe, HighlightPipe, HtmlEscapePipe, HtmlSanitizePipe, InitialPipe, InitialsPipe, IntersectionPipe, IpAddressMaskPipe, JsonPrettyPipe, KebabCasePipe, MorseCodePipe, OrderByPipe, PluckPipe, QrCodePipe, RangePipe, ReplacePipe, ReversePipe, SamplePipe, ShufflePipe, SnakeCasePipe, SomePipe, TailPipe, TextToSpeechPipe, TimeAgoPipePipe, TitleCasePipe, TruncatePipe, TruthifyPipe, UnionPipe, UniquePipe, WithoutPipe };
1245
2106
  //# sourceMappingURL=ngx-transforms.mjs.map