Skip to content

Commit 4d7596b

Browse files
committed
feat: rewrite all pipes for modern Angular and add tests
1 parent 2f89585 commit 4d7596b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2536
-11
lines changed

libs/ngx-transforms/src/index.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,25 @@
1-
export * from './lib/pipes/count/count.pipe';
2-
export * from './lib/ngx-transforms/ngx-transforms';
1+
export * from './lib/pipes/ascii-art/ascii-art';
2+
export * from './lib/pipes/barcode/barcode';
3+
export * from './lib/pipes/camel-case/camel-case';
4+
export * from './lib/pipes/color-convert/color-convert';
5+
export * from './lib/pipes/count/count';
6+
export * from './lib/pipes/credit-card-mask/credit-card-mask';
7+
export * from './lib/pipes/device-type/device-type';
8+
export * from './lib/pipes/email-mask/email-mask';
9+
export * from './lib/pipes/gravatar/gravatar';
10+
export * from './lib/pipes/highlight/highlight';
11+
export * from './lib/pipes/html-escape/html-escape';
12+
export * from './lib/pipes/html-sanitize/html-sanitize';
13+
export * from './lib/pipes/initials/initials';
14+
export * from './lib/pipes/ip-address-mask/ip-address-mask';
15+
export * from './lib/pipes/json-pretty/json-pretty';
16+
export * from './lib/pipes/kebab-case/kebab-case';
17+
export * from './lib/pipes/morse-code/morse-code';
18+
export * from './lib/pipes/qr-code/qr-code';
19+
export * from './lib/pipes/replace/replace';
20+
export * from './lib/pipes/reverse/reverse';
21+
export * from './lib/pipes/snake-case/snake-case';
22+
export * from './lib/pipes/text-to-speech/text-to-speech';
23+
export * from './lib/pipes/title-case/title-case';
24+
export * from './lib/pipes/truncate/truncate';
25+
export * from './providers/all-pipes.provider';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { AsciiArtPipe } from './ascii-art';
2+
import { describe, it, expect } from 'vitest';
3+
4+
describe('AsciiArtPipe', () => {
5+
it('create an instance', () => {
6+
const pipe = new AsciiArtPipe();
7+
expect(pipe).toBeTruthy();
8+
});
9+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
import figlet from 'figlet';
3+
/**
4+
* Supported figlet fonts for ASCII art.
5+
*/
6+
export type FigletFont = 'Standard' | 'Ghost' | 'Doom' | 'Big' | 'Banner';
7+
8+
/**
9+
* Supported figlet horizaontal layout options.
10+
*/
11+
export type FigletLayout = 'default' | 'fitted' | 'full';
12+
13+
/**
14+
* Options for configuring figlet ASCII art generation
15+
*/
16+
export interface FigletOptions {
17+
font?: FigletFont;
18+
horizontalLayout?: FigletLayout;
19+
width?: number;
20+
whitespaceBreak?: boolean
21+
}
22+
23+
/**
24+
* AsciiArtPipe: Converts text into ASCII art using figlet.js.
25+
*
26+
* @param {string} value - The text to convert to ASCII art.
27+
* @param {FigletOptions} [options] - Configuration options for figlet (font, layout, width, whitespaceBreak).
28+
*
29+
* @returns {string} - The ASCII art wrapped in a <pre> tag with CSS classes.
30+
*
31+
* @remarks
32+
* This pipe returns plain HTML (e.g., `<pre class="ascii-art">...</pre>`). For production,
33+
* use Angular's `DomSanitizer` to sanitize the output and prevent XSS attacks. Example:
34+
* ```typescript
35+
* import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
36+
* constructor(private sanitizer: DomSanitizer) {}
37+
* transform(value: string, options: FigletOptions = {}): SafeHtml {
38+
* const html = this.generateAsciiArt(value, options);
39+
* return this.sanitizer.bypassSecurityTrustHtml(html);
40+
* }
41+
* ```
42+
*
43+
* @example
44+
* {{ 'HI' | asciiArt }} // Outputs ASCII art using default options (Standard font)
45+
* {{ 'HI' | asciiArt:{ font: 'Ghost', horizontalLayout: 'fitted', width: 60 } }} // Outputs ASCII art with custom options
46+
* <div [innerHTML]="userInput | asciiArt:{ font: 'Doom' }"></div>
47+
*
48+
* @author Mofiro Jean
49+
*/
50+
@Pipe({
51+
name: 'asciiArt',
52+
standalone: true
53+
})
54+
export class AsciiArtPipe implements PipeTransform {
55+
56+
transform(value: string, options: FigletOptions = {}): string {
57+
if (!value || typeof value !== 'string') {
58+
return '';
59+
}
60+
61+
const config: FigletOptions = {
62+
font: options.font ?? 'Standard',
63+
horizontalLayout: options.horizontalLayout ?? 'default',
64+
width: options.width ?? 80,
65+
whitespaceBreak: options.whitespaceBreak ?? true
66+
};
67+
68+
try {
69+
const asciiArt = figlet.textSync(value, {
70+
font: config.font,
71+
horizontalLayout: config.horizontalLayout,
72+
verticalLayout: 'default', // Most fonts don't support vertical smushing
73+
width: config.width,
74+
whitespaceBreak: config.whitespaceBreak
75+
});
76+
return `<pre class="ascii-art">${asciiArt}</pre>`;
77+
} catch (error) {
78+
console.log(`AsciiArtPipe: Error generating ASCII art for "${value}" with font "${config.font}"`);
79+
return `<pre class="ascii-art-error">Error: Invalid font or input</pre>`;
80+
}
81+
}
82+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
3+
import { describe, it, expect, beforeEach, beforeAll, vi } from 'vitest';
4+
import { BarcodeElementType, BarcodePipe } from './barcode';
5+
import {
6+
BrowserDynamicTestingModule,
7+
platformBrowserDynamicTesting,
8+
} from '@angular/platform-browser-dynamic/testing';
9+
10+
beforeAll(() => {
11+
TestBed.initTestEnvironment(
12+
BrowserDynamicTestingModule,
13+
platformBrowserDynamicTesting()
14+
);
15+
});
16+
17+
describe('BarcodePipe', () => {
18+
let pipe: BarcodePipe;
19+
20+
beforeEach(() => {
21+
TestBed.configureTestingModule({
22+
imports: [BrowserModule],
23+
providers: [BarcodePipe],
24+
});
25+
pipe = TestBed.inject(BarcodePipe);
26+
});
27+
28+
it('should create an instance', () => {
29+
expect(pipe).toBeTruthy();
30+
});
31+
32+
it('should generate SVG barcode', async () => {
33+
const result = await pipe.transform('123456789', { elementType: 'svg' as BarcodeElementType });
34+
const html = (result as any).changingThisBreaksApplicationSecurity;
35+
expect(html).toContain('<svg');
36+
});
37+
38+
it('should generate image data URL for img', async () => {
39+
const result = await pipe.transform('123456789', { elementType: 'img' as BarcodeElementType });
40+
const url = (result as any).changingThisBreaksApplicationSecurity;
41+
expect(url).toMatch(/^data:image\/png;base64,/);
42+
});
43+
44+
it('should generate image data URL for canvas', async () => {
45+
const result = await pipe.transform('123456789', { elementType: 'canvas' as BarcodeElementType });
46+
const url = (result as any).changingThisBreaksApplicationSecurity;
47+
expect(url).toMatch(/^data:image\/png;base64,/);
48+
});
49+
50+
it('should apply custom options', async () => {
51+
const result = await pipe.transform('123456789', {
52+
elementType: 'svg' as BarcodeElementType,
53+
format: 'CODE128',
54+
width: 3,
55+
height: 150,
56+
lineColor: '#FF0000',
57+
displayValue: false,
58+
});
59+
const html = (result as any).changingThisBreaksApplicationSecurity;
60+
expect(html).toContain('<svg');
61+
expect(html).toContain('width');
62+
});
63+
64+
it('should handle empty input', async () => {
65+
const result = await pipe.transform('');
66+
expect(result).toBe('');
67+
});
68+
69+
it('should handle invalid value', async () => {
70+
const spy = vi.spyOn(console, 'error');
71+
const result = await pipe.transform('invalid', { format: 'EAN13' as any });
72+
expect(result).toBe('');
73+
expect(spy).toHaveBeenCalledWith('Barcode generation failed:', new Error('"invalid" is not a valid input for EAN13'));
74+
});
75+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { inject, Pipe, PipeTransform } from '@angular/core';
2+
import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser';
3+
import JsBarcode from 'jsbarcode';
4+
5+
/**
6+
* BarcodeElementType: Defines the type of element to render the barcode.
7+
*
8+
* @typedef {'svg' | 'img' | 'canvas'} BarcodeElementType
9+
*/
10+
export type BarcodeElementType = 'svg' | 'img' | 'canvas';
11+
12+
/**
13+
* BarcodeFormat: Defines supported barcode formats.
14+
*
15+
* @typedef {'CODE128' | 'EAN13' | 'CODE39'} BarcodeFormat
16+
*/
17+
export type BarcodeFormat = 'CODE128' | 'EAN13' | 'CODE39';
18+
19+
/**
20+
* BarcodeOptions: Configuration options for barcode generation.
21+
*
22+
* @interface BarcodeOptions
23+
* @property {BarcodeElementType} [elementType='svg'] - Output type (svg, img, canvas).
24+
* @property {BarcodeFormat} [format='CODE128'] - Barcode format.
25+
* @property {number} [width=2] - Bar width in pixels.
26+
* @property {number} [height=100] - Barcode height in pixels.
27+
* @property {string} [lineColor='#000000'] - Color of bars.
28+
* @property {boolean} [displayValue=true] - Show value below barcode.
29+
*/
30+
export interface BarcodeOptions {
31+
elementType?: BarcodeElementType;
32+
format?: BarcodeFormat;
33+
width?: number;
34+
height?: number;
35+
lineColor?: string;
36+
displayValue?: boolean;
37+
}
38+
39+
/**
40+
* BarcodePipe: Generates a barcode from a string value.
41+
*
42+
* @param {string} value - The value to encode (e.g., '123456789').
43+
* @param {BarcodeOptions} [options={}] - Configuration options.
44+
*
45+
* @returns {Promise<SafeHtml | SafeResourceUrl>} - SVG markup or image data URL.
46+
*
47+
* @example
48+
* <div [innerHTML]="'123456789' | barcode:{elementType:'svg',format:'CODE128'} | async"></div>
49+
* <img [src]="'123456789' | barcode:{elementType:'img'} | async" />
50+
*
51+
* @author Mofiro Jean
52+
*/
53+
@Pipe({
54+
name: 'barcode',
55+
standalone: true,
56+
})
57+
export class BarcodePipe implements PipeTransform {
58+
private sanitizer = inject(DomSanitizer);
59+
60+
async transform(value: string, options: BarcodeOptions = {}): Promise<SafeHtml | SafeResourceUrl | ''> {
61+
const {
62+
elementType = 'svg',
63+
format = 'CODE128',
64+
lineColor = '#000000',
65+
width = 2,
66+
height = 100,
67+
displayValue = true,
68+
} = options;
69+
70+
if (!value) {
71+
return '';
72+
}
73+
74+
try {
75+
const config = { format, lineColor, width, height, displayValue };
76+
if (elementType === 'svg') {
77+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
78+
JsBarcode(svg, value, config);
79+
return this.sanitizer.bypassSecurityTrustHtml(svg.outerHTML);
80+
} else {
81+
const canvas = document.createElement('canvas');
82+
JsBarcode(canvas, value, config);
83+
const dataUrl = canvas.toDataURL('image/png');
84+
return this.sanitizer.bypassSecurityTrustResourceUrl(dataUrl);
85+
}
86+
} catch (error) {
87+
console.error('Barcode generation failed:', error);
88+
return '';
89+
}
90+
}
91+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { CamelCasePipe } from './camel-case';
2+
import { describe, it, expect, beforeEach } from 'vitest';
3+
4+
describe('CamelCasePipe', () => {
5+
let pipe: CamelCasePipe;
6+
7+
beforeEach(() => {
8+
pipe = new CamelCasePipe();
9+
});
10+
11+
it('should create an instance', () => {
12+
expect(pipe).toBeTruthy();
13+
});
14+
15+
it('should convert "hello world" to "helloWorld"', () => {
16+
expect(pipe.transform('hello world')).toBe('helloWorld');
17+
});
18+
19+
it('should handle single word', () => {
20+
expect(pipe.transform('hello')).toBe('hello');
21+
});
22+
23+
it('should handle multiple spaces and special characters', () => {
24+
expect(pipe.transform('hello world!')).toBe('helloWorld');
25+
});
26+
27+
it('should handle underscore-separated input', () => {
28+
expect(pipe.transform('hello_world')).toBe('helloWorld');
29+
});
30+
31+
it('should return empty string for empty input', () => {
32+
expect(pipe.transform('')).toBe('');
33+
});
34+
35+
it('should return empty string for null input', () => {
36+
expect(pipe.transform(null as any)).toBe('');
37+
});
38+
39+
it('should return empty string for undefined input', () => {
40+
expect(pipe.transform(undefined as any)).toBe('');
41+
});
42+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
3+
/**
4+
* CamelCasePipe: Converts text to camelCase (e.g., "hello world" → "helloWorld").
5+
*
6+
* @param {string} value - The input string to transform.
7+
* @returns {string} The string in camelCase, or an empty string if input is invalid.
8+
*
9+
* @example
10+
* ```html
11+
* {{ 'hello world' | camelCase }} <!-- Outputs: helloWorld -->
12+
* ```
13+
*
14+
* @author Mofiro Jean
15+
*/
16+
@Pipe({
17+
name: 'camelCase',
18+
standalone: true
19+
})
20+
export class CamelCasePipe implements PipeTransform {
21+
22+
transform(value: string): string {
23+
if (!value || typeof value !== 'string') return '';
24+
return value
25+
.toLowerCase()
26+
.replace(/[^a-zA-Z0-9]+/g, ' ')
27+
.trim()
28+
.split(' ')
29+
.filter(word => word.length > 0)
30+
.map((word, index) =>
31+
index === 0
32+
? word.toLowerCase()
33+
: word.charAt(0).toUpperCase() + word.slice(1)
34+
)
35+
.join('');
36+
}
37+
}

0 commit comments

Comments
 (0)