2
0
mirror of https://github.com/iconify/iconify.git synced 2025-01-06 07:20:40 +00:00

Clean up handling icon customisations and transformations

This commit is contained in:
Vjacheslav Trushkin 2022-06-20 23:43:01 +03:00
parent 7b4409665a
commit b2d3accf81
16 changed files with 200 additions and 170 deletions

View File

@ -7,7 +7,7 @@
"declaration": true,
"sourceMap": false,
"strict": true,
"types": ["node", "svelte"],
"types": ["svelte"],
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,

View File

@ -31,8 +31,6 @@ export function buildIcon(
): IconifyIconBuildResult {
return iconToSVG(
{ ...defaultIconProps, ...icon },
customisations
? mergeCustomisations(defaultIconCustomisations, customisations)
: defaultIconCustomisations
mergeCustomisations(defaultIconCustomisations, customisations || {})
);
}

View File

@ -1,4 +1,3 @@
import { mergeIconTransformations } from '../icon/transformations';
import {
defaultIconSizeCustomisations,
FullIconCustomisations,
@ -7,29 +6,35 @@ import {
} from './defaults';
/**
* Convert IconifyIconCustomisations to FullIconCustomisations
* Convert IconifyIconCustomisations to FullIconCustomisations, checking value types
*/
export function mergeCustomisations<T extends FullIconCustomisations>(
defaults: T,
item: IconifyIconCustomisations,
keepOtherProps = true
item: IconifyIconCustomisations
): T {
// Merge transformations
const result = mergeIconTransformations(defaults, item, keepOtherProps);
// Copy default values
const result = {
...defaults,
};
// Merge dimensions
for (const key in defaultIconSizeCustomisations) {
const attr = key as keyof IconifyIconSizeCustomisations;
const value = item[attr];
// Merge all properties
for (const key in item) {
const value = item[key as keyof IconifyIconCustomisations];
const valueType = typeof value;
if (
value === null ||
(value && (valueType === 'string' || valueType === 'number'))
) {
result[attr] = value;
} else {
(result as Record<string, unknown>)[attr] = defaults[attr];
if (key in defaultIconSizeCustomisations) {
// Dimension
if (
value === null ||
(value && (valueType === 'string' || valueType === 'number'))
) {
result[key as keyof IconifyIconSizeCustomisations] =
value as string;
}
} else if (valueType === typeof result[key as keyof T]) {
// Normalise rotation, copy everything else as is
(result as Record<string, unknown>)[key] =
key === 'rotate' ? (value as number) % 4 : value;
}
}

View File

@ -1,6 +1,5 @@
import type { IconifyJSON } from '@iconify/types';
import { defaultIconProps } from '../icon/defaults';
import type { IconifyIcon, FullIconifyIcon } from '../icon/defaults';
import type { ExtendedIconifyIcon, IconifyJSON } from '@iconify/types';
import { defaultIconProps, FullExtendedIconifyIcon } from '../icon/defaults';
import { mergeIconData } from '../icon/merge';
import { getIconsTree } from './tree';
@ -12,30 +11,29 @@ export function internalGetIconData(
name: string,
tree: string[],
full: true
): FullIconifyIcon;
): FullExtendedIconifyIcon;
export function internalGetIconData(
data: IconifyJSON,
name: string,
tree: string[],
full: false
): IconifyIcon;
): ExtendedIconifyIcon;
export function internalGetIconData(
data: IconifyJSON,
name: string,
tree: string[],
full: boolean
): FullIconifyIcon | IconifyIcon {
): FullExtendedIconifyIcon | ExtendedIconifyIcon {
const icons = data.icons;
const aliases = data.aliases || {};
let currentProps = {} as IconifyIcon;
let currentProps = {} as ExtendedIconifyIcon;
// Parse parent item
function parse(name: string) {
currentProps = mergeIconData(
icons[name] || aliases[name],
currentProps,
false
currentProps
);
}
@ -45,9 +43,8 @@ export function internalGetIconData(
// Add default values
currentProps = mergeIconData(
data,
currentProps,
false
) as unknown as IconifyIcon;
currentProps
) as unknown as ExtendedIconifyIcon;
// Return icon
return full
@ -62,17 +59,17 @@ export function getIconData(
data: IconifyJSON,
name: string,
full: true
): FullIconifyIcon | null;
): FullExtendedIconifyIcon | null;
export function getIconData(
data: IconifyJSON,
name: string,
full: false
): IconifyIcon | null;
): ExtendedIconifyIcon | null;
export function getIconData(
data: IconifyJSON,
name: string,
full = false
): FullIconifyIcon | IconifyIcon | null {
): FullExtendedIconifyIcon | ExtendedIconifyIcon | null {
if (data.icons[name]) {
// Parse only icon
return internalGetIconData(data, name, [], full as true);

View File

@ -1,4 +1,4 @@
import type { IconifyAliases, IconifyJSON } from '@iconify/types';
import type { IconifyAliases, IconifyIcons, IconifyJSON } from '@iconify/types';
import { defaultIconDimensions } from '../icon/defaults';
import { getIconsTree } from './tree';
@ -17,7 +17,7 @@ export function getIcons(
names: string[],
not_found?: boolean
): IconifyJSON | null {
const icons = Object.create(null) as IconifyJSON['icons'];
const icons = Object.create(null) as IconifyIcons;
const aliases = Object.create(null) as IconifyAliases;
const result: IconifyJSON = {
prefix: data.prefix,

View File

@ -1,5 +1,5 @@
import type { IconifyJSON } from '@iconify/types';
import type { FullIconifyIcon } from '../icon/defaults';
import type { FullExtendedIconifyIcon } from '../icon/defaults';
import { internalGetIconData } from './get-icon';
import { getIconsTree } from './tree';
@ -10,7 +10,7 @@ import { getIconsTree } from './tree';
*/
export type SplitIconSetCallback = (
name: string,
data: FullIconifyIcon | null
data: FullExtendedIconifyIcon | null
) => void;
/**

View File

@ -43,7 +43,7 @@ export function getIconsTree(
}
// Resolve only required icons
(names || Object.keys(aliases).concat(Object.keys(icons))).forEach(resolve);
(names || Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve);
return resolved;
}

View File

@ -1,6 +1,9 @@
import type { IconifyJSON } from '@iconify/types';
import { matchIconName } from '../icon/name';
import { defaultIconDimensions, defaultIconProps } from '../icon/defaults';
import {
defaultIconDimensions,
defaultExtendedIconProps,
} from '../icon/defaults';
type PropsList = Record<string, unknown>;
@ -65,7 +68,10 @@ export function quicklyValidateIconSet(obj: unknown): IconifyJSON | null {
if (
!name.match(matchIconName) ||
typeof icon.body !== 'string' ||
!checkOptionalProps(icon as unknown as PropsList, defaultIconProps)
!checkOptionalProps(
icon as unknown as PropsList,
defaultExtendedIconProps
)
) {
return null;
}
@ -80,7 +86,10 @@ export function quicklyValidateIconSet(obj: unknown): IconifyJSON | null {
!name.match(matchIconName) ||
typeof parent !== 'string' ||
(!icons[parent] && !aliases[parent]) ||
!checkOptionalProps(icon as unknown as PropsList, defaultIconProps)
!checkOptionalProps(
icon as unknown as PropsList,
defaultExtendedIconProps
)
) {
return null;
}

View File

@ -4,7 +4,7 @@ import type {
IconifyOptional,
} from '@iconify/types';
import { matchIconName } from '../icon/name';
import { defaultIconProps } from '../icon/defaults';
import { defaultExtendedIconProps } from '../icon/defaults';
import { getIconsTree } from './tree';
/**
@ -34,10 +34,9 @@ function validateIconProps(
continue;
}
const expectedType =
key === 'hidden'
? 'boolean'
: typeof (defaultIconProps as Record<string, unknown>)[attr];
const expectedType = typeof (
defaultExtendedIconProps as Record<string, unknown>
)[attr];
if (expectedType !== 'undefined') {
if (type !== expectedType) {

View File

@ -3,12 +3,20 @@ import type {
IconifyTransformations,
IconifyOptional,
IconifyIcon,
ExtendedIconifyIcon,
} from '@iconify/types';
// Export icon and full icon types
export { IconifyIcon };
export type FullIconifyIcon = Required<IconifyIcon>;
// Partial and full extended icon
export type PartialExtendedIconifyIcon = Partial<ExtendedIconifyIcon>;
type IconifyIconExtraProps = Omit<ExtendedIconifyIcon, keyof IconifyIcon>;
export type FullExtendedIconifyIcon = FullIconifyIcon & IconifyIconExtraProps;
/**
* Default values for dimensions
*/
@ -38,3 +46,13 @@ export const defaultIconProps: Required<IconifyOptional> = Object.freeze({
...defaultIconDimensions,
...defaultIconTransformations,
});
/**
* Default values for all properties used in ExtendedIconifyIcon
*/
export const defaultExtendedIconProps: Required<FullExtendedIconifyIcon> =
Object.freeze({
...defaultIconProps,
body: '',
hidden: false,
});

View File

@ -1,43 +1,42 @@
import type {
IconifyDimenisons,
IconifyIcon,
IconifyOptional,
IconifyTransformations,
} from '@iconify/types';
import { defaultIconDimensions } from './defaults';
import type { IconifyTransformations } from '@iconify/types';
import {
defaultExtendedIconProps,
defaultIconTransformations,
PartialExtendedIconifyIcon,
} from './defaults';
import { mergeIconTransformations } from './transformations';
// Props to copy: all icon properties, except transformations
type PropsToCopy = Omit<IconifyIcon, keyof IconifyTransformations>;
const propsToMerge: Required<PropsToCopy> = {
...defaultIconDimensions,
body: '',
};
/**
* Merge icon and alias
*
* Can also be used to merge default values and icon
*/
export function mergeIconData<T extends IconifyOptional>(
export function mergeIconData<T extends PartialExtendedIconifyIcon>(
parent: T,
child: IconifyOptional | IconifyIcon,
keepOtherParentProps = true
child: PartialExtendedIconifyIcon
): T {
// Merge transformations
const result = mergeIconTransformations(
parent,
child,
keepOtherParentProps
);
// Merge transformations and add defaults
const result = mergeIconTransformations(parent, child);
// Merge icon properties that aren't transformations
for (const key in propsToMerge) {
const prop = key as keyof IconifyDimenisons;
if (child[prop] !== void 0) {
result[prop] = child[prop];
} else if (parent[prop] !== void 0) {
result[prop] = parent[prop];
for (const key in defaultExtendedIconProps) {
// Add default transformations if needed
if (
defaultIconTransformations[key as keyof IconifyTransformations] !==
void 0
) {
if (
result[key as 'rotate'] === void 0 &&
parent[key as keyof T] !== void 0
) {
result[key as 'rotate'] =
defaultIconTransformations[key as 'rotate'];
}
// Not transformation
} else if (child[key as 'width'] !== void 0) {
result[key as 'width'] = child[key as 'width'];
} else if (parent[key as 'width'] !== void 0) {
result[key as 'width'] = parent[key as 'width'];
}
}

View File

@ -5,18 +5,18 @@ import type { IconifyTransformations } from '@iconify/types';
*/
export function mergeIconTransformations<T extends IconifyTransformations>(
obj1: T,
obj2: IconifyTransformations,
keepOtherProps = true
obj2: IconifyTransformations
): T {
const result = keepOtherProps ? { ...obj1 } : ({} as T);
if (obj1.hFlip || obj2.hFlip) {
result.hFlip = obj1.hFlip !== obj2.hFlip;
const result = {} as T;
if (!obj1.hFlip !== !obj2.hFlip) {
result.hFlip = true;
}
if (obj1.vFlip || obj2.vFlip) {
result.vFlip = obj1.vFlip !== obj2.vFlip;
if (!obj1.vFlip !== !obj2.vFlip) {
result.vFlip = true;
}
if (obj1.rotate || obj2.rotate) {
result.rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4;
const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4;
if (rotate) {
result.rotate = rotate;
}
return result;
}

View File

@ -19,6 +19,7 @@ export {
// Icon data
export { mergeIconData } from './icon/merge';
export { mergeIconTransformations } from './icon/transformations';
export {
defaultIconProps,
defaultIconDimensions,

View File

@ -160,7 +160,8 @@ export function iconToSVG(
const boxWidth = box.width;
const boxHeight = box.height;
let width, height;
let width: string | number;
let height: string | number;
if (customisationsWidth === null) {
// Width is not set: calculate width from height, default to '1em'
height =
@ -181,15 +182,11 @@ export function iconToSVG(
: customisationsHeight;
}
// Convert to string
width = typeof width === 'string' ? width : width.toString();
height = typeof height === 'string' ? height : height.toString();
// Result
const result: IconifyIconBuildResult = {
attributes: {
width,
height,
width: width.toString(),
height: height.toString(),
viewBox:
box.left.toString() +
' ' +

View File

@ -2,8 +2,7 @@ import type { IconifyIcon } from '@iconify/types';
import { mergeIconData } from '../lib/icon/merge';
describe('Testing merging icon data', () => {
test('Test', () => {
// Nothing to merge
test('Nothing to merge', () => {
const icon: IconifyIcon = {
body: '<g />',
};
@ -13,9 +12,10 @@ describe('Testing merging icon data', () => {
// Check hint manually: supposed to be IconifyIcon
const result = mergeIconData(icon, {});
expect(result).toEqual(expected);
});
// TypeScript full icon test
const icon2: Required<IconifyIcon> = {
test('Full icons', () => {
const icon: Required<IconifyIcon> = {
body: '<g />',
width: 24,
height: 24,
@ -25,7 +25,7 @@ describe('Testing merging icon data', () => {
hFlip: false,
vFlip: false,
};
const expected2: Required<IconifyIcon> = {
const expected: Required<IconifyIcon> = {
body: '<g />',
width: 24,
height: 24,
@ -36,9 +36,11 @@ describe('Testing merging icon data', () => {
vFlip: false,
};
// Check hint manually: supposed to be Required<IconifyIcon>
const result2 = mergeIconData(icon2, {});
expect(result2).toEqual(expected2);
const result = mergeIconData(icon, {});
expect(result).toEqual(expected);
});
test('Copy values', () => {
// Copy values
expect(
mergeIconData(
@ -55,8 +57,9 @@ describe('Testing merging icon data', () => {
width: 24,
height: 32,
});
});
// Override values
test('Override values', () => {
expect(
mergeIconData(
{
@ -74,4 +77,30 @@ describe('Testing merging icon data', () => {
height: 32,
});
});
test('Override transformations', () => {
expect(
mergeIconData(
{
body: '<g />',
width: 24,
height: 24,
hFlip: true,
rotate: 3,
},
{
height: 32,
vFlip: true,
rotate: 2,
}
)
).toEqual({
body: '<g />',
width: 24,
height: 32,
hFlip: true,
vFlip: true,
rotate: 1,
});
});
});

View File

@ -4,7 +4,6 @@ import type { FullIconifyIcon } from '../lib/icon/defaults';
import { defaultIconProps } from '../lib/icon/defaults';
import type { FullIconCustomisations } from '../lib/customisations/defaults';
import { defaultIconCustomisations } from '../lib/customisations/defaults';
import { mergeCustomisations } from '../lib/customisations/merge';
import { iconToHTML } from '../lib/svg/html';
describe('Testing iconToSVG', () => {
@ -31,12 +30,10 @@ describe('Testing iconToSVG', () => {
});
test('Auto size, body', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: 'auto',
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
height: 'auto',
};
const icon: FullIconifyIcon = {
...defaultIconProps,
body: '<path d="" />',
@ -66,12 +63,10 @@ describe('Testing iconToSVG', () => {
});
test('Auto size, body', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: 'auto',
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
height: 'auto',
};
const icon: FullIconifyIcon = {
...defaultIconProps,
body: '<path d="" />',
@ -90,12 +85,10 @@ describe('Testing iconToSVG', () => {
});
test('Custom size', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: 'auto',
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
height: 'auto',
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -116,13 +109,11 @@ describe('Testing iconToSVG', () => {
});
test('Rotation', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: '40px',
rotate: 1,
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
height: '40px',
rotate: 1,
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -143,13 +134,11 @@ describe('Testing iconToSVG', () => {
});
test('Negative rotation', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: '40px',
rotate: -1,
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
height: '40px',
rotate: -1,
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -170,13 +159,11 @@ describe('Testing iconToSVG', () => {
});
test('Flip', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: '32',
hFlip: true,
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
height: '32',
hFlip: true,
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -197,13 +184,11 @@ describe('Testing iconToSVG', () => {
});
test('Flip, rotation', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
hFlip: true,
rotate: 1,
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
hFlip: true,
rotate: 1,
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -224,12 +209,10 @@ describe('Testing iconToSVG', () => {
});
test('Flip icon that is rotated by default', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
hFlip: true,
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
hFlip: true,
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -254,16 +237,14 @@ describe('Testing iconToSVG', () => {
});
test('Flip and rotation canceling eachother', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
width: '1em',
height: 'auto',
hFlip: true,
vFlip: true,
rotate: 2,
}
);
const custom: FullIconCustomisations = {
...defaultIconCustomisations,
width: '1em',
height: 'auto',
hFlip: true,
vFlip: true,
rotate: 2,
};
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
@ -287,10 +268,7 @@ describe('Testing iconToSVG', () => {
const iconBody =
'<g stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" fill="none" fill-rule="evenodd"><path d="M40 64l48-48" class="animation-delay-0 animation-duration-10 animate-stroke stroke-length-102"/><path d="M40 64l48 48" class="animation-delay-0 animation-duration-10 animate-stroke stroke-length-102"/></g>';
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{}
);
const custom: FullIconCustomisations = defaultIconCustomisations;
const icon: FullIconifyIcon = {
...defaultIconProps,
body: iconBody,