From 3ae86d5f0d0020f87f9d970b0dfa455e937e1c38 Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Sat, 2 Nov 2024 09:02:07 +0200 Subject: [PATCH] feat(utils): do not force naming convension when validating icon sets and icon names --- packages/utils/src/icon-set/validate-basic.ts | 11 +- packages/utils/src/icon-set/validate.ts | 11 +- packages/utils/src/icon/name.ts | 15 ++- packages/utils/tests/icon-name-test.ts | 20 ++-- packages/utils/tests/validate-basic-test.ts | 32 ++++-- packages/utils/tests/validate-test.ts | 105 +++++++++++++++--- 6 files changed, 147 insertions(+), 47 deletions(-) diff --git a/packages/utils/src/icon-set/validate-basic.ts b/packages/utils/src/icon-set/validate-basic.ts index c898bab..a2de37c 100644 --- a/packages/utils/src/icon-set/validate-basic.ts +++ b/packages/utils/src/icon-set/validate-basic.ts @@ -1,5 +1,4 @@ import type { IconifyAliases, IconifyJSON } from '@iconify/types'; -import { matchIconName } from '../icon/name'; import { defaultIconDimensions, defaultExtendedIconProps, @@ -63,8 +62,11 @@ export function quicklyValidateIconSet(obj: unknown): IconifyJSON | null { for (const name in icons) { const icon = icons[name]; if ( - !name.match(matchIconName) || + // Name cannot be empty + !name || + // Must have body typeof icon.body !== 'string' || + // Check other props !checkOptionalProps( icon as unknown as PropsList, defaultExtendedIconProps @@ -80,9 +82,12 @@ export function quicklyValidateIconSet(obj: unknown): IconifyJSON | null { const icon = aliases[name]; const parent = icon.parent; if ( - !name.match(matchIconName) || + // Name cannot be empty + !name || + // Parent must be set and point to existing icon typeof parent !== 'string' || (!icons[parent] && !aliases[parent]) || + // Check other props !checkOptionalProps( icon as unknown as PropsList, defaultExtendedIconProps diff --git a/packages/utils/src/icon-set/validate.ts b/packages/utils/src/icon-set/validate.ts index b6ac7bb..e8359c8 100644 --- a/packages/utils/src/icon-set/validate.ts +++ b/packages/utils/src/icon-set/validate.ts @@ -4,7 +4,6 @@ import type { IconifyJSON, IconifyOptional, } from '@iconify/types'; -import { matchIconName } from '../icon/name'; import { defaultExtendedIconProps } from '../icon/defaults'; import { getIconsTree } from './tree'; @@ -99,8 +98,9 @@ export function validateIconSet( if (options && typeof options.prefix === 'string') { data.prefix = options.prefix; } else if ( + // Prefix must be a string and not empty typeof data.prefix !== 'string' || - !data.prefix.match(matchIconName) + !data.prefix ) { throw new Error('Invalid prefix'); } @@ -110,10 +110,7 @@ export function validateIconSet( data.provider = options.provider; } else if (data.provider !== void 0) { const value = data.provider; - if ( - typeof value !== 'string' || - (value !== '' && !value.match(matchIconName)) - ) { + if (typeof value !== 'string') { if (fix) { delete data.provider; } else { @@ -150,7 +147,7 @@ export function validateIconSet( throw new Error(`Invalid alias: ${name}`); } - if (!name.match(matchIconName)) { + if (!name) { if (fix) { delete parentObj[name]; continue; diff --git a/packages/utils/src/icon/name.ts b/packages/utils/src/icon/name.ts index e04aa45..b23834b 100644 --- a/packages/utils/src/icon/name.ts +++ b/packages/utils/src/icon/name.ts @@ -14,11 +14,14 @@ export type IconifyIconSource = Omit; /** * Expression to test part of icon name. + * + * Used when loading icons from Iconify API due to project naming convension. + * Ignored when using custom icon sets - convension does not apply. */ export const matchIconName = /^[a-z0-9]+(-[a-z0-9]+)*$/; /** - * Convert string to Icon object. + * Convert string icon name to IconifyIconName object. */ export const stringToIcon = ( value: string, @@ -96,9 +99,11 @@ export const validateIconName = ( } return !!( - (icon.provider === '' || icon.provider.match(matchIconName)) && - ((allowSimpleName && icon.prefix === '') || - icon.prefix.match(matchIconName)) && - icon.name.match(matchIconName) + // Check prefix: cannot be empty, unless allowSimpleName is enabled + // Check name: cannot be empty + ( + ((allowSimpleName && icon.prefix === '') || !!icon.prefix) && + !!icon.name + ) ); }; diff --git a/packages/utils/tests/icon-name-test.ts b/packages/utils/tests/icon-name-test.ts index 1ab79d1..0a985f5 100644 --- a/packages/utils/tests/icon-name-test.ts +++ b/packages/utils/tests/icon-name-test.ts @@ -58,7 +58,7 @@ describe('Testing icon name', () => { expect(validateIconName(icon)).toBe(false); expect(validateIconName(icon, true)).toBe(false); - // Underscore is not an acceptable separator + // No prefix icon = stringToIcon('fa_home'); expect(icon).toEqual(null); expect(validateIconName(icon)).toBe(false); @@ -71,30 +71,30 @@ describe('Testing icon name', () => { name: 'fa_home', }); expect(validateIconName(icon)).toBe(false); - expect(validateIconName(icon, true)).toBe(false); + expect(validateIconName(icon, true)).toBe(true); - // Invalid character '_': fail validateIcon + // Underscore is allowed now icon = stringToIcon('fa:home_outline') as IconifyIconName; expect(icon).toEqual({ provider: '', prefix: 'fa', name: 'home_outline', }); - expect(validateIconName(icon)).toBe(false); + expect(validateIconName(icon)).toBe(true); // Too many colons: fail stringToIcon icon = stringToIcon('mdi:light:home:outline'); expect(icon).toEqual(null); expect(validateIconName(icon)).toBe(false); - // Upper case: fail validateIcon + // Upper case: allowed now icon = stringToIcon('MD:Home') as IconifyIconName; expect(icon).toEqual({ provider: '', prefix: 'MD', name: 'Home', }); - expect(validateIconName(icon)).toBe(false); + expect(validateIconName(icon)).toBe(true); // Numbers: pass icon = stringToIcon('1:foo') as IconifyIconName; @@ -105,14 +105,14 @@ describe('Testing icon name', () => { }); expect(validateIconName(icon)).toBe(true); - // Accented letters: fail validateIcon + // Accented letters: allowed now icon = stringToIcon('md-fõö') as IconifyIconName; expect(icon).toEqual({ provider: '', prefix: 'md', name: 'fõö', }); - expect(validateIconName(icon)).toBe(false); + expect(validateIconName(icon)).toBe(true); }); it('Providers', () => { @@ -167,13 +167,13 @@ describe('Testing icon name', () => { icon = stringToIcon('@mdi:light:home:outline', false, true); expect(icon).toEqual(null); - // Upper case: fail validateIcon + // Upper case: allowed now icon = stringToIcon('@MD:home-outline') as IconifyIconName; expect(icon).toEqual({ provider: 'MD', prefix: 'home', name: 'outline', }); - expect(validateIconName(icon)).toBe(false); + expect(validateIconName(icon)).toBe(true); }); }); diff --git a/packages/utils/tests/validate-basic-test.ts b/packages/utils/tests/validate-basic-test.ts index 5f57855..721d93a 100644 --- a/packages/utils/tests/validate-basic-test.ts +++ b/packages/utils/tests/validate-basic-test.ts @@ -33,9 +33,11 @@ describe('Testing validation', () => { expect( quicklyValidateIconSet({ - prefix: 'foo', + // Characters that used to be invalid + prefix: 'fòó_bār', icons: { - bar: { + // Characters that used to be invalid + bär: { body: '', width: 32, height: 32, @@ -47,8 +49,9 @@ describe('Testing validation', () => { }, }, aliases: { - baz: { - parent: 'bar', + // Characters that used to be invalid + Bär_Bāz: { + parent: 'bär', hFlip: true, }, }, @@ -56,9 +59,9 @@ describe('Testing validation', () => { height: 24, }) ).toEqual({ - prefix: 'foo', + prefix: 'fòó_bār', icons: { - bar: { + bär: { body: '', width: 32, height: 32, @@ -69,8 +72,8 @@ describe('Testing validation', () => { }, }, aliases: { - baz: { - parent: 'bar', + Bär_Bāz: { + parent: 'bär', hFlip: true, }, }, @@ -104,6 +107,19 @@ describe('Testing validation', () => { ).toBe(null); }); + test('Empty icon name', () => { + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: { + '': { + body: '', + }, + }, + }) + ).toBe(null); + }); + test('Invalid optional properties', () => { expect( quicklyValidateIconSet({ diff --git a/packages/utils/tests/validate-test.ts b/packages/utils/tests/validate-test.ts index 9a0a1ca..c6d5cf7 100644 --- a/packages/utils/tests/validate-test.ts +++ b/packages/utils/tests/validate-test.ts @@ -5,7 +5,7 @@ describe('Testing validation', () => { return new Promise((fulfill, reject) => { try { validateIconSet(void 0); - reject('Expected to throw error on undefined'); + reject(new Error('Expected to throw error on undefined')); return; } catch { // @@ -13,7 +13,7 @@ describe('Testing validation', () => { try { validateIconSet({}); - reject('Expected to throw error on empty object'); + reject(new Error('Expected to throw error on empty object')); return; } catch { // @@ -21,7 +21,7 @@ describe('Testing validation', () => { try { validateIconSet(null); - reject('Expected to throw error on null'); + reject(new Error('Expected to throw error on null')); return; } catch { // @@ -29,7 +29,7 @@ describe('Testing validation', () => { try { validateIconSet([]); - reject('Expected to throw error on array'); + reject(new Error('Expected to throw error on array')); return; } catch { // @@ -42,19 +42,74 @@ describe('Testing validation', () => { test('Valid set', () => { expect( validateIconSet({ - prefix: 'foo', + prefix: 'fòó_bār', icons: { bar: { body: '', }, + // Characters that used to be invalid + fòó_: { + body: '', + }, }, }) ).toEqual({ - prefix: 'foo', + prefix: 'fòó_bār', icons: { bar: { body: '', }, + fòó_: { + body: '', + }, + }, + }); + }); + + test('Bad icon names', () => { + // Empty name, not fixed + let threw = false; + try { + validateIconSet({ + prefix: 'foo', + icons: { + // Cannot be empty + '': { + body: '', + }, + }, + }); + } catch { + threw = true; + } + expect(threw).toBeTruthy(); + + // Empty name, fixed + expect( + validateIconSet( + { + prefix: 'foo', + icons: { + // Empty name + '': { + body: '', + }, + // Characters that used to be invalid + 'fòó_': { + body: '', + }, + }, + }, + { + fix: true, + } + ) + ).toEqual({ + prefix: 'foo', + icons: { + fòó_: { + body: '', + }, }, }); }); @@ -65,7 +120,9 @@ describe('Testing validation', () => { validateIconSet({ prefix: 'foo', }); - reject('Expected to throw error when icons are missing'); + reject( + new Error('Expected to throw error when icons are missing') + ); return; } catch { // @@ -76,7 +133,9 @@ describe('Testing validation', () => { prefix: 'foo', icons: {}, }); - reject('Expected to throw error when icons are empty'); + reject( + new Error('Expected to throw error when icons are empty') + ); return; } catch { // @@ -84,7 +143,7 @@ describe('Testing validation', () => { try { validateIconSet([]); - reject('Expected to throw error on array'); + reject(new Error('Expected to throw error on array')); return; } catch { // @@ -149,7 +208,9 @@ describe('Testing validation', () => { }, }); reject( - 'Expected to throw error when character points to missing icon' + new Error( + 'Expected to throw error when character points to missing icon' + ) ); return; } catch { @@ -193,7 +254,11 @@ describe('Testing validation', () => { test: 'bar', }, }); - reject('Expected to throw error when character is invalid'); + reject( + new Error( + 'Expected to throw error when character is invalid' + ) + ); return; } catch { // @@ -253,7 +318,11 @@ describe('Testing validation', () => { }, { fix: true } ); - reject('Expected to throw error for bad default properties'); + reject( + new Error( + 'Expected to throw error for bad default properties' + ) + ); return; } catch { // @@ -274,7 +343,11 @@ describe('Testing validation', () => { }, { fix: true } ); - reject('Expected to throw error for bad default properties'); + reject( + new Error( + 'Expected to throw error for bad default properties' + ) + ); return; } catch { // @@ -295,7 +368,11 @@ describe('Testing validation', () => { }, { fix: true } ); - reject('Expected to throw error for bad default properties'); + reject( + new Error( + 'Expected to throw error for bad default properties' + ) + ); return; } catch { //