2021-05-24 10:25:02 +00:00
|
|
|
import type { IconifyJSON, IconifyOptional } from '@iconify/types';
|
2021-10-12 14:17:51 +00:00
|
|
|
import { iconDefaults, matchName } from '../icon';
|
2021-05-24 10:25:02 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Match character
|
|
|
|
*/
|
|
|
|
export const matchChar = /^[a-f0-9]+(-[a-f0-9]+)*$/;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate icon
|
|
|
|
*
|
|
|
|
* Returns name of property that failed validation or null on success
|
|
|
|
*/
|
|
|
|
function validateIconProps(item: IconifyOptional, fix: boolean): string | null {
|
|
|
|
// Check other properties
|
|
|
|
for (const key in item) {
|
|
|
|
const attr = key as keyof typeof item;
|
|
|
|
const value = item[attr];
|
|
|
|
const type = typeof value;
|
|
|
|
|
|
|
|
if (type === 'undefined') {
|
|
|
|
// Undefined was passed ???
|
|
|
|
delete item[attr];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
case 'body':
|
|
|
|
case 'parent':
|
|
|
|
if (type !== 'string') {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'hFlip':
|
|
|
|
case 'vFlip':
|
|
|
|
case 'hidden':
|
|
|
|
if (type !== 'boolean') {
|
|
|
|
if (fix) {
|
|
|
|
delete item[attr];
|
|
|
|
} else {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'width':
|
|
|
|
case 'height':
|
|
|
|
case 'left':
|
|
|
|
case 'top':
|
|
|
|
case 'rotate':
|
2021-09-16 21:07:22 +00:00
|
|
|
case 'inlineHeight': // Legacy properties
|
2021-05-24 10:25:02 +00:00
|
|
|
case 'inlineTop':
|
|
|
|
case 'verticalAlign':
|
|
|
|
if (type !== 'number') {
|
|
|
|
if (fix) {
|
|
|
|
delete item[attr];
|
|
|
|
} else {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
// Unknown property, make sure its not object
|
|
|
|
if (type === 'object') {
|
|
|
|
if (fix) {
|
|
|
|
delete item[attr];
|
|
|
|
} else {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface IconSetValidationOptions {
|
|
|
|
// If true, validation function will attempt to fix icon set instead of throwing errors.
|
|
|
|
fix?: boolean;
|
|
|
|
|
|
|
|
// Values for provider and prefix. If missing, validation should add them.
|
|
|
|
prefix?: string;
|
|
|
|
provider?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate icon set, return it as IconifyJSON type on success, throw error on failure
|
|
|
|
*/
|
|
|
|
export function validateIconSet(
|
|
|
|
obj: unknown,
|
|
|
|
options?: IconSetValidationOptions
|
|
|
|
): IconifyJSON {
|
|
|
|
const fix = !!options?.fix;
|
|
|
|
|
|
|
|
// Check for object with 'icons' nested object
|
|
|
|
if (
|
|
|
|
typeof obj !== 'object' ||
|
|
|
|
obj === null ||
|
|
|
|
typeof (obj as Record<string, unknown>).icons !== 'object' ||
|
|
|
|
!(obj as Record<string, unknown>).icons
|
|
|
|
) {
|
|
|
|
throw new Error('Bad icon set');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert type
|
|
|
|
const data = obj as IconifyJSON;
|
|
|
|
|
|
|
|
// Set or validate prefix
|
|
|
|
if (typeof options?.prefix === 'string') {
|
|
|
|
data.prefix = options.prefix;
|
|
|
|
} else if (
|
|
|
|
typeof data.prefix !== 'string' ||
|
|
|
|
!data.prefix.match(matchName)
|
|
|
|
) {
|
|
|
|
throw new Error('Invalid prefix');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set or validate provider
|
|
|
|
if (typeof options?.provider === 'string') {
|
|
|
|
data.provider = options.provider;
|
|
|
|
} else if (data.provider !== void 0) {
|
|
|
|
const value = data.provider;
|
|
|
|
if (
|
|
|
|
typeof value !== 'string' ||
|
|
|
|
(value !== '' && !value.match(matchName))
|
|
|
|
) {
|
|
|
|
if (fix) {
|
|
|
|
delete data.provider;
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid provider');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate all icons
|
|
|
|
const icons = data.icons;
|
|
|
|
Object.keys(icons).forEach((name) => {
|
|
|
|
if (!name.match(matchName)) {
|
|
|
|
if (fix) {
|
|
|
|
delete icons[name];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(`Invalid icon name: "${name}"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const item = icons[name];
|
|
|
|
if (
|
|
|
|
typeof item !== 'object' ||
|
|
|
|
item === null ||
|
|
|
|
typeof item.body !== 'string'
|
|
|
|
) {
|
|
|
|
if (fix) {
|
|
|
|
delete icons[name];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(`Invalid icon: "${name}"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check other properties
|
|
|
|
const key =
|
|
|
|
typeof (item as unknown as Record<string, unknown>).parent ===
|
|
|
|
'string'
|
|
|
|
? 'parent'
|
|
|
|
: validateIconProps(item, fix);
|
|
|
|
if (key !== null) {
|
|
|
|
if (fix) {
|
|
|
|
delete icons[name];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(`Invalid property "${key}" in icon "${name}"`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-01-25 22:05:18 +00:00
|
|
|
// Check not_found
|
|
|
|
if (data.not_found !== void 0 && !(data.not_found instanceof Array)) {
|
|
|
|
if (fix) {
|
|
|
|
delete data.not_found;
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid not_found list');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-24 10:25:02 +00:00
|
|
|
// Make sure icons list is not empty
|
2022-01-25 22:05:18 +00:00
|
|
|
if (!Object.keys(data.icons).length && !data.not_found?.length) {
|
2021-05-24 10:25:02 +00:00
|
|
|
throw new Error('Icon set is empty');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate aliases
|
|
|
|
if (data.aliases !== void 0) {
|
|
|
|
if (typeof data.aliases !== 'object' || data.aliases === null) {
|
|
|
|
if (fix) {
|
|
|
|
delete data.aliases;
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid aliases list');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (typeof data.aliases === 'object') {
|
|
|
|
const aliases = data.aliases;
|
|
|
|
const validatedAliases: Set<string> = new Set();
|
|
|
|
const failedAliases: Set<string> = new Set();
|
|
|
|
|
2021-09-16 21:07:22 +00:00
|
|
|
// eslint-disable-next-line no-inner-declarations
|
2021-05-24 10:25:02 +00:00
|
|
|
function validateAlias(name: string, iteration: number): boolean {
|
|
|
|
// Check if alias has already been validated
|
|
|
|
if (validatedAliases.has(name)) {
|
|
|
|
return !failedAliases.has(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
const item = aliases[name];
|
|
|
|
if (
|
|
|
|
// Loop or very long chain: invalidate all aliases
|
|
|
|
iteration > 5 ||
|
|
|
|
// Check if value is a valid object
|
|
|
|
typeof item !== 'object' ||
|
|
|
|
item === null ||
|
|
|
|
typeof item.parent !== 'string' ||
|
|
|
|
// Check if name is valid
|
|
|
|
!name.match(matchName)
|
|
|
|
) {
|
|
|
|
if (fix) {
|
|
|
|
delete aliases[name];
|
|
|
|
failedAliases.add(name);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
throw new Error(`Invalid icon alias: "${name}"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if parent icon/alias exists
|
|
|
|
const parent = item.parent;
|
|
|
|
if (data.icons[parent] === void 0) {
|
|
|
|
// Check for parent alias
|
|
|
|
if (
|
|
|
|
aliases[parent] === void 0 ||
|
|
|
|
!validateAlias(parent, iteration + 1)
|
|
|
|
) {
|
|
|
|
if (fix) {
|
|
|
|
delete aliases[name];
|
|
|
|
failedAliases.add(name);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
throw new Error(`Missing parent icon for alias "${name}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check other properties
|
|
|
|
if (
|
|
|
|
fix &&
|
|
|
|
(item as unknown as Record<string, unknown>).body !== void 0
|
|
|
|
) {
|
|
|
|
delete (item as unknown as Record<string, unknown>).body;
|
|
|
|
}
|
|
|
|
const key =
|
|
|
|
(item as unknown as Record<string, unknown>).body !== void 0
|
|
|
|
? 'body'
|
|
|
|
: validateIconProps(item, fix);
|
|
|
|
if (key !== null) {
|
|
|
|
if (fix) {
|
|
|
|
delete aliases[name];
|
|
|
|
failedAliases.add(name);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
throw new Error(`Invalid property "${key}" in alias "${name}"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
validatedAliases.add(name);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(aliases).forEach((name) => {
|
|
|
|
validateAlias(name, 0);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Delete empty aliases object
|
|
|
|
if (fix && !Object.keys(data.aliases).length) {
|
|
|
|
delete data.aliases;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-12 14:17:51 +00:00
|
|
|
// Validate all properties that can be optimised
|
|
|
|
(Object.keys(iconDefaults) as (keyof typeof iconDefaults)[]).forEach(
|
|
|
|
(prop) => {
|
|
|
|
const expectedType = typeof iconDefaults[prop];
|
|
|
|
const actualType = typeof data[prop as keyof IconifyJSON];
|
|
|
|
if (actualType !== 'undefined' && actualType !== expectedType) {
|
|
|
|
throw new Error(`Invalid value type for "${prop}"`);
|
|
|
|
}
|
2021-05-24 10:25:02 +00:00
|
|
|
}
|
2021-10-12 14:17:51 +00:00
|
|
|
);
|
2021-05-24 10:25:02 +00:00
|
|
|
|
|
|
|
// Validate characters map
|
|
|
|
if (data.chars !== void 0) {
|
|
|
|
if (typeof data.chars !== 'object' || data.chars === null) {
|
|
|
|
if (fix) {
|
|
|
|
delete data.chars;
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid characters map');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof data.chars === 'object') {
|
|
|
|
const chars = data.chars;
|
|
|
|
Object.keys(chars).forEach((char) => {
|
2022-03-04 21:04:14 +00:00
|
|
|
if (!matchChar.exec(char) || typeof chars[char] !== 'string') {
|
2021-05-24 10:25:02 +00:00
|
|
|
if (fix) {
|
|
|
|
delete chars[char];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(`Invalid character "${char}"`);
|
|
|
|
}
|
|
|
|
const target = chars[char];
|
|
|
|
if (
|
|
|
|
data.icons[target] === void 0 &&
|
|
|
|
data.aliases?.[target] === void 0
|
|
|
|
) {
|
|
|
|
if (fix) {
|
|
|
|
delete chars[char];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(
|
|
|
|
`Character "${char}" points to missing icon "${target}"`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Delete empty aliases object
|
|
|
|
if (fix && !Object.keys(data.chars).length) {
|
|
|
|
delete data.chars;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}
|