Skip to content

Commit

Permalink
Implement custom icons feature
Browse files Browse the repository at this point in the history
  • Loading branch information
imikailoby committed Aug 19, 2024
1 parent 34a8875 commit 2883deb
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 46 deletions.
1 change: 1 addition & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import type { MarineCodeConfig } from '../types/config';
export const DEFAULT_CONFIG: Required<MarineCodeConfig> = {
orientation: 'horizontal',
offset: 16, // 1/3 of the flag size
customIcons: {},
};
28 changes: 28 additions & 0 deletions src/constants/defaultIcons.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Alphabet } from '../types/config';
import { extractElementSizes } from '../utils/extractElementSizes';
import { defaultIcons } from './defaultIcons';

const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

describe('defaultIcons()', () => {
it('contains all 26 letters of the alphabet', () => {
expect(Object.keys(defaultIcons).length).toBe(letters.length);

for (let i = 0; i < letters.length; i++) {
const letter = letters[i];
expect(defaultIcons[letter as Alphabet]).toBeDefined();
}
});

it('each icon has width and height', () => {
for (let i = 0; i < letters.length; i++) {
const letter = letters[i];
const svg = defaultIcons[letter as Alphabet];

const { width, height } = extractElementSizes(svg);

expect(width).toBeGreaterThan(0);
expect(height).toBeGreaterThan(0);
}
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const svgFlagsByLetter: { [key: string]: string } = {
import type { Alphabet } from '../types/config';

export const defaultIcons: Record<Alphabet, string> = {
A: `<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M0 0h24v48H0z"/><path d="M24 0h24L36 24l12 24H24V0Z" fill="#002CFF"/><mask id="a" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M24 0H0v48h48L36 24 48 0H24Z"/></mask><path d="M0 0v-1h-1v1h1Zm0 48h-1v1h1v-1Zm48 0v1h1.618l-.724-1.447L48 48ZM36 24l-.894-.447-.224.447.224.447L36 24ZM48 0l.894.447L49.618-1H48v1ZM0 1h24v-2H0v2Zm1 47V0h-2v48h2Zm23-1H0v2h24v-2Zm24 0H24v2h24v-2ZM35.106 24.447l12 24 1.788-.894-12-24-1.788.894Zm12-24.894-12 24 1.788.894 12-24-1.788-.894ZM24 1h24v-2H24v2Z" fill="#000" mask="url(#a)"/></svg>`,
B: `<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#FF1000" d="M0 0h24v48H0zM24 0h24L36 24l12 24H24V0Z"/><mask id="a" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M24 0H0v48h48L36 24 48 0H24Z"/></mask><path d="M0 0v-1h-1v1h1Zm0 48h-1v1h1v-1Zm48 0v1h1.618l-.724-1.447L48 48ZM36 24l-.894-.447-.224.447.224.447L36 24ZM48 0l.894.447L49.618-1H48v1ZM0 1h24v-2H0v2Zm1 47V0h-2v48h2Zm23-1H0v2h24v-2Zm24 0H24v2h24v-2ZM35.106 24.447l12 24 1.788-.894-12-24-1.788.894Zm12-24.894-12 24 1.788.894 12-24-1.788-.894ZM24 1h24v-2H24v2Z" fill="#000" mask="url(#a)"/></svg>`,
C: `<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#002CFF" d="M0 0h48v12H0z"/><path fill="#fff" d="M0 12h48v8H0zM0 28h48v8H0z"/><path fill="#FF1000" d="M0 20h48v8H0z"/><path fill="#002BFD" d="M0 36h48v12H0z"/><path stroke="#000" d="M.5.5h47v47H.5z"/></svg>`,
Expand Down
14 changes: 0 additions & 14 deletions src/constants/svgFlagsByLetter.test.ts

This file was deleted.

29 changes: 29 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
export interface MarineCodeConfig {
orientation?: Orientation;
offset?: number;
customIcons?: Partial<Record<Alphabet, string>>;
}

export type Orientation = 'horizontal' | 'vertical';

export type Alphabet =
| 'A'
| 'B'
| 'C'
| 'D'
| 'E'
| 'F'
| 'G'
| 'H'
| 'I'
| 'J'
| 'K'
| 'L'
| 'M'
| 'N'
| 'O'
| 'P'
| 'Q'
| 'R'
| 'S'
| 'T'
| 'U'
| 'V'
| 'W'
| 'X'
| 'Y'
| 'Z';
10 changes: 5 additions & 5 deletions src/utils/buildSvg.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { DEFAULT_CONFIG } from '../constants/config';
import { svgFlagsByLetter } from '../constants/svgFlagsByLetter';
import { defaultIcons } from '../constants/defaultIcons';
import { buildSvg } from './buildSvg';

describe('buildSvg()', () => {
it('returns a string with an SVG element with default configuration', () => {
expect(buildSvg([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], DEFAULT_CONFIG)).toBe(
expect(buildSvg([defaultIcons.A, defaultIcons.B, defaultIcons.C], DEFAULT_CONFIG)).toBe(
'<svg xmlns="http://www.w3.org/2000/svg" width="176px" height="48px"><g transform="translate(0, 0)"><svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M0 0h24v48H0z"/><path d="M24 0h24L36 24l12 24H24V0Z" fill="#002CFF"/><mask id="a" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M24 0H0v48h48L36 24 48 0H24Z"/></mask><path d="M0 0v-1h-1v1h1Zm0 48h-1v1h1v-1Zm48 0v1h1.618l-.724-1.447L48 48ZM36 24l-.894-.447-.224.447.224.447L36 24ZM48 0l.894.447L49.618-1H48v1ZM0 1h24v-2H0v2Zm1 47V0h-2v48h2Zm23-1H0v2h24v-2Zm24 0H24v2h24v-2ZM35.106 24.447l12 24 1.788-.894-12-24-1.788.894Zm12-24.894-12 24 1.788.894 12-24-1.788-.894ZM24 1h24v-2H24v2Z" fill="#000" mask="url(#a)"/></svg></g><g transform="translate(64, 0)"><svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#FF1000" d="M0 0h24v48H0zM24 0h24L36 24l12 24H24V0Z"/><mask id="a" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M24 0H0v48h48L36 24 48 0H24Z"/></mask><path d="M0 0v-1h-1v1h1Zm0 48h-1v1h1v-1Zm48 0v1h1.618l-.724-1.447L48 48ZM36 24l-.894-.447-.224.447.224.447L36 24ZM48 0l.894.447L49.618-1H48v1ZM0 1h24v-2H0v2Zm1 47V0h-2v48h2Zm23-1H0v2h24v-2Zm24 0H24v2h24v-2ZM35.106 24.447l12 24 1.788-.894-12-24-1.788.894Zm12-24.894-12 24 1.788.894 12-24-1.788-.894ZM24 1h24v-2H24v2Z" fill="#000" mask="url(#a)"/></svg></g><g transform="translate(128, 0)"><svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#002CFF" d="M0 0h48v12H0z"/><path fill="#fff" d="M0 12h48v8H0zM0 28h48v8H0z"/><path fill="#FF1000" d="M0 20h48v8H0z"/><path fill="#002BFD" d="M0 36h48v12H0z"/><path stroke="#000" d="M.5.5h47v47H.5z"/></svg></g></svg>',
);
});

describe('orientation', () => {
it('returns a string with an SVG element for horizontal orientation', () => {
expect(
buildSvg([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], {
buildSvg([defaultIcons.A, defaultIcons.B, defaultIcons.C], {
...DEFAULT_CONFIG,
orientation: 'horizontal',
}),
Expand All @@ -23,7 +23,7 @@ describe('buildSvg()', () => {

it('returns a string with an SVG element for vertical orientation', () => {
expect(
buildSvg([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], {
buildSvg([defaultIcons.A, defaultIcons.B, defaultIcons.C], {
...DEFAULT_CONFIG,
orientation: 'vertical',
}),
Expand All @@ -36,7 +36,7 @@ describe('buildSvg()', () => {
describe('offset', () => {
it('returns a string with an SVG element with custom offset', () => {
expect(
buildSvg([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], {
buildSvg([defaultIcons.A, defaultIcons.B, defaultIcons.C], {
...DEFAULT_CONFIG,
offset: 0,
}),
Expand Down
2 changes: 1 addition & 1 deletion src/utils/generateMarineCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function generateMarineCode(text: string, config?: MarineCodeConfig): str
const preparedString = prepareString(text);
if (!preparedString) return '';

const svgFlagsArray = getSvgArrayFromName(preparedString);
const normalizedConfig = normalizeConfig(config);
const svgFlagsArray = getSvgArrayFromName(preparedString, normalizedConfig.customIcons);
return buildSvg(svgFlagsArray, normalizedConfig);
}
13 changes: 7 additions & 6 deletions src/utils/getSvgArrayFromName.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { svgFlagsByLetter } from '../constants/svgFlagsByLetter';
import { defaultIcons } from '../constants/defaultIcons';
import type { Alphabet } from '../types/config';
import { getSvgArrayFromName } from './getSvgArrayFromName';

describe('getSvgArrayFromName()', () => {
it('returns an array with SVG elements for a given name', () => {
const result = getSvgArrayFromName(Object.keys(svgFlagsByLetter).join('').toLowerCase());
expect(result).toHaveLength(Object.keys(svgFlagsByLetter).length);
const result = getSvgArrayFromName(Object.keys(defaultIcons).join('').toLowerCase(), defaultIcons);
expect(result).toHaveLength(Object.keys(defaultIcons).length);
result.forEach((svgElement, index) => {
expect(svgElement).toBe(svgFlagsByLetter[Object.keys(svgFlagsByLetter)[index]]);
expect(svgElement).toBe(defaultIcons[Object.keys(defaultIcons)[index] as Alphabet]);
});
});

it('returns an empty array for an empty name', () => {
expect(getSvgArrayFromName('')).toEqual([]);
expect(getSvgArrayFromName('', defaultIcons)).toEqual([]);
});

it('returns an empty array for a name with no SVG letters', () => {
expect(getSvgArrayFromName('123!@$423423,123842391234')).toEqual([]);
expect(getSvgArrayFromName('123!@$423423,123842391234', defaultIcons)).toEqual([]);
});
});
16 changes: 9 additions & 7 deletions src/utils/getSvgArrayFromName.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { svgFlagsByLetter } from '../constants/svgFlagsByLetter';
import type { Alphabet } from '../types/config';

export function getSvgArrayFromName(name: string): string[] {
export function getSvgArrayFromName(name: string, icons: Record<Alphabet, string>): string[] {
if (!name) return [];

return name.toUpperCase().split('').filter(isSvgLetter).map(getSvgForLetter);
return (name.toUpperCase().split('') as Alphabet[])
.filter((l) => isSvgLetter(l, icons))
.map((l) => getSvgForLetter(l, icons));
}

function isSvgLetter(letter: string): boolean {
return letter in svgFlagsByLetter;
function isSvgLetter(letter: string, icons: Record<Alphabet, string>): boolean {
return letter in icons;
}

function getSvgForLetter(letter: string): string {
return svgFlagsByLetter[letter];
function getSvgForLetter(letter: Alphabet, icons: Record<Alphabet, string>): string {
return icons[letter];
}
14 changes: 5 additions & 9 deletions src/utils/getSvgSizes.test.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import { DEFAULT_CONFIG } from '../constants/config';
import { svgFlagsByLetter } from '../constants/svgFlagsByLetter';
import { defaultIcons } from '../constants/defaultIcons';
import { getSvgSizes } from './getSvgSizes';

describe('getSvgSizes()', () => {
it('returns correct sizes for horizontal orientation', () => {
expect(
getSvgSizes([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], 'horizontal', DEFAULT_CONFIG.offset),
).toEqual({
expect(getSvgSizes([defaultIcons.A, defaultIcons.B, defaultIcons.C], 'horizontal', DEFAULT_CONFIG.offset)).toEqual({
width: 48 * 3 + DEFAULT_CONFIG.offset * 2,
height: 48,
});
});

it('returns correct sizes for vertical orientation', () => {
expect(
getSvgSizes([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], 'vertical', DEFAULT_CONFIG.offset),
).toEqual({
expect(getSvgSizes([defaultIcons.A, defaultIcons.B, defaultIcons.C], 'vertical', DEFAULT_CONFIG.offset)).toEqual({
width: 48,
height: 48 * 3 + DEFAULT_CONFIG.offset * 2,
});
});

it('returns correct sizes for custom offset', () => {
expect(getSvgSizes([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], 'horizontal', 0)).toEqual({
expect(getSvgSizes([defaultIcons.A, defaultIcons.B, defaultIcons.C], 'horizontal', 0)).toEqual({
width: 48 * 3,
height: 48,
});

expect(getSvgSizes([svgFlagsByLetter.A, svgFlagsByLetter.B, svgFlagsByLetter.C], 'vertical', 64)).toEqual({
expect(getSvgSizes([defaultIcons.A, defaultIcons.B, defaultIcons.C], 'vertical', 64)).toEqual({
width: 48,
height: 48 * 3 + 64 * 2,
});
Expand Down
39 changes: 38 additions & 1 deletion src/utils/normalizeConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { Orientation } from '../types/config';
import { DEFAULT_CONFIG } from '../constants/config';
import { defaultIcons } from '../constants/defaultIcons';
import type { Alphabet, Orientation } from '../types/config';
import { normalizeConfig } from './normalizeConfig';

describe('normalizeConfig()', () => {
it('returns default config when no config is provided', () => {
expect(normalizeConfig()).toEqual({
orientation: 'horizontal',
offset: 16,
customIcons: defaultIcons,
});
});

Expand Down Expand Up @@ -33,11 +36,45 @@ describe('normalizeConfig()', () => {

it('returns default offset when invalid offset is provided', () => {
expect(normalizeConfig({ offset: -1 }).offset).toBe(16);
expect(normalizeConfig({ offset: '1' as unknown as number }).offset).toBe(16);
});

it('returns provided offset', () => {
expect(normalizeConfig({ offset: 32 }).offset).toBe(32);
expect(normalizeConfig({ offset: 0 }).offset).toBe(0);
});
});

describe('custom icons', () => {
it('returns default icons when no custom icons are provided', () => {
expect(normalizeConfig({}).customIcons).toEqual(defaultIcons);
expect(normalizeConfig(undefined).customIcons).toEqual(defaultIcons);
expect(normalizeConfig({ ...DEFAULT_CONFIG, customIcons: undefined }).customIcons).toEqual(defaultIcons);
expect(normalizeConfig({ ...DEFAULT_CONFIG, customIcons: {} }).customIcons).toEqual(defaultIcons);
});

it('replaces invalid icons with default icons', () => {
expect(normalizeConfig({ customIcons: { A: 'invalid' } }).customIcons.A).toEqual(defaultIcons.A);
expect(normalizeConfig({ customIcons: { A: '<svg></svg>' } }).customIcons.A).toEqual(defaultIcons.A);
expect(normalizeConfig({ customIcons: { A: '<svg width="0" height="10"></svg>' } }).customIcons.A).toEqual(
defaultIcons.A,
);
expect(normalizeConfig({ customIcons: { A: '<svg width="10" height="0"></svg>' } }).customIcons.A).toEqual(
defaultIcons.A,
);
expect(normalizeConfig({ customIcons: { A: '<svg width="10"></svg>' } }).customIcons.A).toEqual(defaultIcons.A);
expect(normalizeConfig({ customIcons: { A: '<svg height="10"></svg>' } }).customIcons.A).toEqual(defaultIcons.A);
});

it('returns provided custom icons', () => {
const customIcons = { A: '<svg width="10" height="10"></svg>' };
const result = normalizeConfig({ customIcons });
expect(result.customIcons.A).toEqual(customIcons.A);

for (let i = 1; i < Object.keys(defaultIcons).length; i++) {
const letter = Object.keys(defaultIcons)[i];
expect(result.customIcons[letter as Alphabet]).toEqual(defaultIcons[letter as Alphabet]);
}
});
});
});
36 changes: 34 additions & 2 deletions src/utils/normalizeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { DEFAULT_CONFIG } from '../constants/config';
import type { MarineCodeConfig } from '../types/config';
import { defaultIcons } from '../constants/defaultIcons';
import type { Alphabet, MarineCodeConfig } from '../types/config';
import { extractElementSizes } from './extractElementSizes';

export function normalizeConfig(config?: MarineCodeConfig): Required<MarineCodeConfig> {
export function normalizeConfig(
config?: MarineCodeConfig,
): Omit<Required<MarineCodeConfig>, 'customIcons'> & { customIcons: Record<Alphabet, string> } {
return {
orientation: getValidValue(config?.orientation, ['horizontal', 'vertical'], DEFAULT_CONFIG.orientation),
offset: getValidNumber(config?.offset, DEFAULT_CONFIG.offset),
customIcons: getValidIcons(config?.customIcons),
};
}

Expand All @@ -15,3 +20,30 @@ function getValidValue<T>(value: T | undefined, validValues: T[], defaultValue:
function getValidNumber(value: number | undefined, defaultValue: number): number {
return typeof value === 'number' && value >= 0 ? value : defaultValue;
}

function getValidIcons(value?: Partial<Record<Alphabet, string>>): Record<Alphabet, string> {
if (!value || Object.keys(value).length === 0) return defaultIcons;

return Object.keys(defaultIcons).reduce(
(acc, key) => {
if (key.length === 1 && key.toUpperCase() === key) {
const svg = value?.[key as Alphabet];
const defaultSvg = defaultIcons[key as Alphabet];

if (!svg) {
acc[key as Alphabet] = defaultSvg;
return acc;
}

const { width, height } = extractElementSizes(svg);
if (!!width && !!height) {
acc[key as Alphabet] = svg;
} else {
acc[key as Alphabet] = defaultSvg;
}
}
return acc;
},
{} as Record<Alphabet, string>,
);
}

0 comments on commit 2883deb

Please sign in to comment.