2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-19 00:39:02 +00:00

In IconifyJSON do not allow default transformations, remove inline from customisations, restructure utils

This commit is contained in:
Vjacheslav Trushkin 2022-06-19 17:12:26 +03:00
parent 99ddeeae47
commit ad29f6df20
28 changed files with 530 additions and 496 deletions

View File

@ -30,7 +30,6 @@ export interface IconifyDimenisons {
* Used in:
* icon (as is)
* alias (merged with icon's properties)
* root of JSON file (default values)
*/
export interface IconifyTransformations {
// Number of 90 degrees rotations.
@ -223,7 +222,7 @@ export interface IconifyMetaData {
/**
* JSON structure, contains only icon data
*/
export interface IconifyJSONIconsData extends IconifyOptional {
export interface IconifyJSONIconsData extends IconifyDimenisons {
// Prefix for icons in JSON file, required.
prefix: string;
@ -236,8 +235,8 @@ export interface IconifyJSONIconsData extends IconifyOptional {
// Optional aliases.
aliases?: IconifyAliases;
// IconifyOptional properties that are used as default values for icons when icon is missing value.
// If property exists in both icon and root, use value from icon.
// IconifyDimenisons properties that are used as default viewbox for icons when icon is missing value.
// If viewbox exists in both icon and root, use value from icon.
// This is used to reduce duplication.
}

View File

@ -55,21 +55,17 @@
"require": "./lib/customisations/bool.cjs",
"import": "./lib/customisations/bool.mjs"
},
"./lib/customisations/compare": {
"require": "./lib/customisations/compare.cjs",
"import": "./lib/customisations/compare.mjs"
},
"./lib/customisations": {
"require": "./lib/customisations/index.cjs",
"import": "./lib/customisations/index.mjs"
"./lib/customisations/defaults": {
"require": "./lib/customisations/defaults.cjs",
"import": "./lib/customisations/defaults.mjs"
},
"./lib/customisations/flip": {
"require": "./lib/customisations/flip.cjs",
"import": "./lib/customisations/flip.mjs"
},
"./lib/customisations/index": {
"require": "./lib/customisations/index.cjs",
"import": "./lib/customisations/index.mjs"
"./lib/customisations/merge": {
"require": "./lib/customisations/merge.cjs",
"import": "./lib/customisations/merge.mjs"
},
"./lib/customisations/rotate": {
"require": "./lib/customisations/rotate.cjs",
@ -107,13 +103,9 @@
"require": "./lib/icon-set/validate-basic.cjs",
"import": "./lib/icon-set/validate-basic.mjs"
},
"./lib/icon": {
"require": "./lib/icon/index.cjs",
"import": "./lib/icon/index.mjs"
},
"./lib/icon/index": {
"require": "./lib/icon/index.cjs",
"import": "./lib/icon/index.mjs"
"./lib/icon/defaults": {
"require": "./lib/icon/defaults.cjs",
"import": "./lib/icon/defaults.mjs"
},
"./lib/icon/merge": {
"require": "./lib/icon/merge.cjs",
@ -123,6 +115,10 @@
"require": "./lib/icon/name.cjs",
"import": "./lib/icon/name.mjs"
},
"./lib/icon/transformations": {
"require": "./lib/icon/transformations.cjs",
"import": "./lib/icon/transformations.mjs"
},
"./lib": {
"require": "./lib/index.cjs",
"import": "./lib/index.mjs"

View File

@ -1,32 +0,0 @@
import type { FullIconCustomisations } from './index';
import { defaults } from './index';
// Get all keys
const allKeys: (keyof FullIconCustomisations)[] = Object.keys(
defaults
) as (keyof FullIconCustomisations)[];
// All keys without width/height
const filteredKeys = allKeys.filter(
(key) => key !== 'width' && key !== 'height'
);
/**
* Compare sets of cusotmisations, return false if they are different, true if the same
*
* If dimensions are derived from props1 or props2, do not compare them.
*/
export function compare(
item1: FullIconCustomisations,
item2: FullIconCustomisations,
compareDimensions = true
): boolean {
const keys = compareDimensions ? allKeys : filteredKeys;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (item1[key] !== item2[key]) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,41 @@
import type { IconifyTransformations } from '@iconify/types';
import { defaultIconTransformations } from '../icon/defaults';
/**
* Icon size
*/
export type IconifyIconSize = null | string | number;
/**
* Dimensions
*/
export interface IconifyIconSizeCustomisations {
width?: IconifyIconSize;
height?: IconifyIconSize;
}
/**
* Icon customisations
*/
export interface IconifyIconCustomisations
extends IconifyTransformations,
IconifyIconSizeCustomisations {}
export type FullIconCustomisations = Required<IconifyIconCustomisations>;
/**
* Default icon customisations values
*/
export const defaultIconSizeCustomisations: Required<IconifyIconSizeCustomisations> =
Object.freeze({
width: null,
height: null,
});
export const defaultIconCustomisations: FullIconCustomisations = Object.freeze({
// Dimensions
...defaultIconSizeCustomisations,
// Transformations
...defaultIconTransformations,
});

View File

@ -1,4 +1,4 @@
import type { IconifyIconCustomisations } from './index';
import type { IconifyIconCustomisations } from './defaults';
const separator = /[\s,]+/;

View File

@ -1,110 +0,0 @@
/**
* Icon size
*/
export type IconifyIconSize = null | string | number;
/**
* Icon customisations
*/
export interface IconifyIconCustomisations {
// Display mode
inline?: boolean;
// Dimensions
width?: IconifyIconSize;
height?: IconifyIconSize;
// Transformations
hFlip?: boolean;
vFlip?: boolean;
rotate?: number;
}
export type FullIconCustomisations = Required<IconifyIconCustomisations>;
/**
* Default icon customisations values
*/
export const defaults: FullIconCustomisations = Object.freeze({
// Display mode
inline: false,
// Dimensions
width: null,
height: null,
// Transformations
hFlip: false,
vFlip: false,
rotate: 0,
});
/**
* TypeScript
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function assertNever(v: never) {
//
}
/**
* Convert IconifyIconCustomisations to FullIconCustomisations
*/
export function mergeCustomisations(
defaults: FullIconCustomisations,
item: IconifyIconCustomisations
): FullIconCustomisations {
const result: FullIconCustomisations = {} as FullIconCustomisations;
for (const key in defaults) {
const attr = key as keyof FullIconCustomisations;
// Copy old value
(result as Record<string, unknown>)[attr] = defaults[attr];
if (item[attr] === void 0) {
continue;
}
// Validate new value
const value = item[attr];
switch (attr) {
// Boolean attributes that override old value
case 'inline':
if (typeof value === 'boolean') {
result[attr] = value;
}
break;
// Boolean attributes that are merged
case 'hFlip':
case 'vFlip':
if (value === true) {
result[attr] = !result[attr];
}
break;
// Non-empty string / non-zero number / null
case 'width':
case 'height':
if (
(typeof value === 'string' && value !== '') ||
(typeof value === 'number' && value) ||
value === null
) {
result[attr] = value as IconifyIconSize;
}
break;
// Rotation
case 'rotate':
if (typeof value === 'number') {
result[attr] += value;
}
break;
default:
assertNever(attr);
}
}
return result;
}

View File

@ -0,0 +1,36 @@
import { mergeIconTransformations } from '../icon/transformations';
import {
defaultIconSizeCustomisations,
FullIconCustomisations,
IconifyIconCustomisations,
IconifyIconSizeCustomisations,
} from './defaults';
/**
* Convert IconifyIconCustomisations to FullIconCustomisations
*/
export function mergeCustomisations(
defaults: FullIconCustomisations,
item: IconifyIconCustomisations
): FullIconCustomisations {
// Merge transformations
const result = mergeIconTransformations(defaults, item);
// Merge dimensions
for (const key in defaultIconSizeCustomisations) {
const attr = key as keyof IconifyIconSizeCustomisations;
const value = item[attr];
const valueType = typeof value;
if (
value === null ||
(value && (valueType === 'string' || valueType === 'number'))
) {
result[attr] = value;
} else {
(result as Record<string, unknown>)[attr] = defaults[attr];
}
}
return result;
}

View File

@ -1,5 +1,5 @@
import type { IconifyJSON } from '@iconify/types';
import { iconDefaults } from '../icon';
import { defaultIconDimensions } from '../icon/defaults';
/**
* Expand minified icon set
@ -9,21 +9,23 @@ import { iconDefaults } from '../icon';
export function expandIconSet(data: IconifyJSON): void {
const icons = Object.keys(data.icons);
(Object.keys(iconDefaults) as (keyof typeof iconDefaults)[]).forEach(
(prop) => {
if (typeof data[prop] !== typeof iconDefaults[prop]) {
return;
}
const value = data[prop];
icons.forEach((name) => {
const item = data.icons[name];
if (item[prop] === void 0) {
item[prop as 'height'] = value as number;
}
});
delete data[prop];
(
Object.keys(
defaultIconDimensions
) as (keyof typeof defaultIconDimensions)[]
).forEach((prop) => {
if (typeof data[prop] !== typeof defaultIconDimensions[prop]) {
return;
}
);
const value = data[prop];
icons.forEach((name) => {
const item = data.icons[name];
if (item[prop] === void 0) {
item[prop] = value;
}
});
delete data[prop];
});
}

View File

@ -1,6 +1,6 @@
import type { IconifyJSON, IconifyOptional } from '@iconify/types';
import { fullIcon, IconifyIcon, iconDefaults } from '../icon';
import type { FullIconifyIcon } from '../icon';
import type { IconifyDimenisons, IconifyJSON } from '@iconify/types';
import { defaultIconProps, defaultIconDimensions } from '../icon/defaults';
import type { IconifyIcon, FullIconifyIcon } from '../icon/defaults';
import { mergeIconData } from '../icon/merge';
/**
@ -56,17 +56,19 @@ export function getIconData(
// Add default properties
if (result) {
for (const key in iconDefaults) {
for (const key in defaultIconDimensions) {
if (
result[key as keyof IconifyOptional] === void 0 &&
data[key as keyof IconifyOptional] !== void 0
result[key as keyof IconifyDimenisons] === void 0 &&
data[key as keyof IconifyDimenisons] !== void 0
) {
(result as unknown as Record<string, unknown>)[key] =
data[key as keyof IconifyOptional];
data[key as keyof IconifyDimenisons];
}
}
}
// Return icon
return result && full ? fullIcon(result) : result;
return result && full
? Object.assign({}, defaultIconProps, result)
: result;
}

View File

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { IconifyJSON } from '@iconify/types';
import { iconDefaults } from '../icon';
import { defaultIconProps } from '../icon/defaults';
/**
* Optional properties that must be copied when copying icon set
*/
export const propsToCopy = Object.keys(iconDefaults).concat([
export const propsToCopy = Object.keys(defaultIconProps).concat([
'provider',
]) as (keyof IconifyJSON)[];

View File

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { IconifyJSON } from '@iconify/types';
import { iconDefaults } from '../icon';
import { defaultIconDimensions } from '../icon/defaults';
/**
* Minify icon set
*
* Function finds common values for few numeric properties, such as 'width' and 'height' (see iconDefaults keys for list of properties),
* Function finds common values for few numeric properties, such as 'width' and 'height' (see defaultIconDimensions keys for list of properties),
* removes entries from icons and sets default entry in root of icon set object.
*
* For example, this:
@ -45,99 +45,101 @@ import { iconDefaults } from '../icon';
export function minifyIconSet(data: IconifyJSON): void {
const icons = Object.keys(data.icons);
(Object.keys(iconDefaults) as (keyof typeof iconDefaults)[]).forEach(
(prop) => {
// Check for default value for property
if (data[prop] === iconDefaults[prop]) {
delete data[prop];
}
const defaultValue = iconDefaults[prop];
const propType = typeof defaultValue;
// Check for previously minified value
const hasMinifiedDefault =
typeof data[prop] === propType && data[prop] !== defaultValue;
// Find value that is used by most icons
let maxCount = 0;
let maxValue: typeof defaultValue | null = null;
const counters: Map<typeof defaultValue, number> = new Map();
for (let i = 0; i < icons.length; i++) {
const item = data.icons[icons[i]];
let value: typeof defaultValue;
if (typeof item[prop] === propType) {
value = item[prop]!;
} else if (hasMinifiedDefault) {
value = data[prop]!;
} else {
value = iconDefaults[prop];
}
if (i === 0) {
// First item
maxCount = 1;
maxValue = value;
counters.set(value, 1);
continue;
}
if (!counters.has(value)) {
// First entry for new value
counters.set(value, 1);
continue;
}
const count = counters.get(value)! + 1;
counters.set(value, count);
if (count > maxCount) {
maxCount = count;
maxValue = value;
}
}
const canMinify = maxValue !== null && maxCount > 1;
// Get default value
const oldDefault = hasMinifiedDefault ? data[prop] : null;
const newDefault = canMinify ? maxValue : oldDefault;
// console.log(
// `Prop: ${prop}, oldDefault: ${oldDefault}, canMinify: ${canMinify}, maxValue: ${maxValue}`
// );
// Change global value
if (newDefault === defaultValue) {
delete data[prop];
} else if (canMinify) {
data[prop as 'height'] = newDefault as number;
}
// Update all icons
icons.forEach((key) => {
const item = data.icons[key];
const value =
item[prop] === void 0
? hasMinifiedDefault
? oldDefault
: defaultValue
: item[prop];
if (
value === newDefault ||
(newDefault === null && value === defaultValue)
) {
// Property matches minified value
// Property matches default value and there is no minified value
delete item[prop];
return;
}
if (canMinify && item[prop] === void 0) {
// Value matches old minified value
item[prop as 'height'] = value as number;
}
});
(
Object.keys(
defaultIconDimensions
) as (keyof typeof defaultIconDimensions)[]
).forEach((prop) => {
// Check for default value for property
if (data[prop] === defaultIconDimensions[prop]) {
delete data[prop];
}
);
const defaultValue = defaultIconDimensions[prop];
const propType = typeof defaultValue;
// Check for previously minified value
const hasMinifiedDefault =
typeof data[prop] === propType && data[prop] !== defaultValue;
// Find value that is used by most icons
let maxCount = 0;
let maxValue: typeof defaultValue | null = null;
const counters: Map<typeof defaultValue, number> = new Map();
for (let i = 0; i < icons.length; i++) {
const item = data.icons[icons[i]];
let value: typeof defaultValue;
if (typeof item[prop] === propType) {
value = item[prop]!;
} else if (hasMinifiedDefault) {
value = data[prop]!;
} else {
value = defaultIconDimensions[prop];
}
if (i === 0) {
// First item
maxCount = 1;
maxValue = value;
counters.set(value, 1);
continue;
}
if (!counters.has(value)) {
// First entry for new value
counters.set(value, 1);
continue;
}
const count = counters.get(value)! + 1;
counters.set(value, count);
if (count > maxCount) {
maxCount = count;
maxValue = value;
}
}
const canMinify = maxValue !== null && maxCount > 1;
// Get default value
const oldDefault = hasMinifiedDefault ? data[prop] : null;
const newDefault = canMinify ? maxValue : oldDefault;
// console.log(
// `Prop: ${prop}, oldDefault: ${oldDefault}, canMinify: ${canMinify}, maxValue: ${maxValue}`
// );
// Change global value
if (newDefault === defaultValue) {
delete data[prop];
} else if (canMinify) {
data[prop as 'height'] = newDefault as number;
}
// Update all icons
icons.forEach((key) => {
const item = data.icons[key];
const value =
item[prop] === void 0
? hasMinifiedDefault
? oldDefault
: defaultValue
: item[prop];
if (
value === newDefault ||
(newDefault === null && value === defaultValue)
) {
// Property matches minified value
// Property matches default value and there is no minified value
delete item[prop];
return;
}
if (canMinify && item[prop] === void 0) {
// Value matches old minified value
item[prop as 'height'] = value as number;
}
});
});
}

View File

@ -1,5 +1,5 @@
import type { IconifyJSON } from '@iconify/types';
import type { FullIconifyIcon } from '../icon';
import type { FullIconifyIcon } from '../icon/defaults';
import { getIconData } from './get-icon';
/**

View File

@ -1,18 +1,32 @@
import type { IconifyJSON } from '@iconify/types';
import { iconDefaults, matchName } from '../icon';
import { matchIconName } from '../icon/name';
import { defaultIconDimensions, defaultIconProps } from '../icon/defaults';
type PropsList = Record<string, unknown>;
/**
* Optional properties
*/
const optionalProperties = {
provider: 'string',
aliases: 'object',
not_found: 'object',
} as Record<string, string>;
const optionalPropertyDefaults = {
provider: '',
aliases: {},
not_found: {},
...defaultIconDimensions,
} as PropsList;
for (const prop in iconDefaults) {
optionalProperties[prop] =
typeof iconDefaults[prop as keyof typeof iconDefaults];
/**
* Check props
*/
function checkOptionalProps(item: PropsList, defaults: PropsList): boolean {
for (const prop in defaults) {
if (
item[prop] !== void 0 &&
typeof item[prop] !== typeof defaults[prop]
) {
return false;
}
}
return true;
}
/**
@ -40,58 +54,35 @@ export function quicklyValidateIconSet(obj: unknown): IconifyJSON | null {
}
// Check for optional properties
for (const prop in optionalProperties) {
if (
(obj as Record<string, unknown>)[prop] !== void 0 &&
typeof (obj as Record<string, unknown>)[prop] !==
optionalProperties[prop]
) {
return null;
}
if (!checkOptionalProps(obj as PropsList, optionalPropertyDefaults)) {
return null;
}
// Check all icons
const icons = data.icons;
for (const name in icons) {
const icon = icons[name];
if (!name.match(matchName) || typeof icon.body !== 'string') {
if (
!name.match(matchIconName) ||
typeof icon.body !== 'string' ||
!checkOptionalProps(icon as unknown as PropsList, defaultIconProps)
) {
return null;
}
for (const prop in iconDefaults) {
if (
icon[prop as keyof typeof icon] !== void 0 &&
typeof icon[prop as keyof typeof icon] !==
typeof iconDefaults[prop as keyof typeof iconDefaults]
) {
return null;
}
}
}
// Check all aliases
const aliases = data.aliases;
if (aliases) {
for (const name in aliases) {
const icon = aliases[name];
const parent = icon.parent;
if (
!name.match(matchName) ||
typeof parent !== 'string' ||
(!icons[parent] && !aliases[parent])
) {
return null;
}
for (const prop in iconDefaults) {
if (
icon[prop as keyof typeof icon] !== void 0 &&
typeof icon[prop as keyof typeof icon] !==
typeof iconDefaults[prop as keyof typeof iconDefaults]
) {
return null;
}
}
const aliases = data.aliases || {};
for (const name in aliases) {
const icon = aliases[name];
const parent = icon.parent;
if (
!name.match(matchIconName) ||
typeof parent !== 'string' ||
(!icons[parent] && !aliases[parent]) ||
!checkOptionalProps(icon as unknown as PropsList, defaultIconProps)
) {
return null;
}
}

View File

@ -1,5 +1,6 @@
import type { IconifyJSON, IconifyOptional } from '@iconify/types';
import { iconDefaults, matchName } from '../icon';
import { matchIconName } from '../icon/name';
import { defaultIconDimensions } from '../icon/defaults';
/**
* Match character
@ -112,7 +113,7 @@ export function validateIconSet(
data.prefix = options.prefix;
} else if (
typeof data.prefix !== 'string' ||
!data.prefix.match(matchName)
!data.prefix.match(matchIconName)
) {
throw new Error('Invalid prefix');
}
@ -124,7 +125,7 @@ export function validateIconSet(
const value = data.provider;
if (
typeof value !== 'string' ||
(value !== '' && !value.match(matchName))
(value !== '' && !value.match(matchIconName))
) {
if (fix) {
delete data.provider;
@ -137,7 +138,7 @@ export function validateIconSet(
// Validate all icons
const icons = data.icons;
Object.keys(icons).forEach((name) => {
if (!name.match(matchName)) {
if (!name.match(matchIconName)) {
if (fix) {
delete icons[name];
return;
@ -221,7 +222,7 @@ export function validateIconSet(
item === null ||
typeof item.parent !== 'string' ||
// Check if name is valid
!name.match(matchName)
!name.match(matchIconName)
) {
if (fix) {
delete aliases[name];
@ -283,15 +284,17 @@ export function validateIconSet(
}
// 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}"`);
}
(
Object.keys(
defaultIconDimensions
) as (keyof typeof defaultIconDimensions)[]
).forEach((prop) => {
const expectedType = typeof defaultIconDimensions[prop];
const actualType = typeof data[prop as keyof IconifyJSON];
if (actualType !== 'undefined' && actualType !== expectedType) {
throw new Error(`Invalid value type for "${prop}"`);
}
);
});
// Validate characters map
if (data.chars !== void 0) {

View File

@ -9,11 +9,6 @@ import type {
export { IconifyIcon };
export type FullIconifyIcon = Required<IconifyIcon>;
/**
* Expression to test part of icon name.
*/
export const matchName = /^[a-z0-9]+(-[a-z0-9]+)*$/;
/**
* Default values for dimensions
*/
@ -39,14 +34,7 @@ export const defaultIconTransformations: Required<IconifyTransformations> =
/**
* Default values for all optional IconifyIcon properties
*/
export const iconDefaults: Required<IconifyOptional> = Object.freeze({
export const defaultIconProps: Required<IconifyOptional> = Object.freeze({
...defaultIconDimensions,
...defaultIconTransformations,
});
/**
* Add optional properties to icon
*/
export function fullIcon(data: IconifyIcon): FullIconifyIcon {
return { ...iconDefaults, ...data };
}

View File

@ -1,5 +1,6 @@
import type { IconifyOptional } from '@iconify/types';
import { iconDefaults } from './index';
import type { IconifyDimenisons, IconifyOptional } from '@iconify/types';
import { defaultIconDimensions } from './defaults';
import { mergeIconTransformations } from './transformations';
/**
* Merge icon and alias
@ -8,35 +9,16 @@ export function mergeIconData<T extends IconifyOptional>(
icon: T,
alias: IconifyOptional
): T {
const result = { ...icon };
for (const key in iconDefaults) {
const prop = key as keyof IconifyOptional;
// Merge transformations, while keeping other props
const result = mergeIconTransformations(icon, alias);
// Merge dimensions
for (const key in defaultIconDimensions) {
const prop = key as keyof IconifyDimenisons;
if (alias[prop] !== void 0) {
const value = alias[prop];
if (result[prop] === void 0) {
// Missing value
(result as unknown as Record<string, unknown>)[prop] = value;
continue;
}
switch (prop) {
case 'rotate':
(result[prop] as number) =
((result[prop] as number) + (value as number)) % 4;
break;
case 'hFlip':
case 'vFlip':
result[prop] = value !== result[prop];
break;
default:
// Overwrite value
(result as unknown as Record<string, unknown>)[prop] =
value;
}
result[prop] = alias[prop];
}
}
return result;
}

View File

@ -1,5 +1,3 @@
import { matchName } from './index';
/**
* Icon name
*/
@ -14,6 +12,11 @@ export interface IconifyIconName {
*/
export type IconifyIconSource = Omit<IconifyIconName, 'name'>;
/**
* Expression to test part of icon name.
*/
export const matchIconName = /^[a-z0-9]+(-[a-z0-9]+)*$/;
/**
* Convert string to Icon object.
*/
@ -93,9 +96,9 @@ export const validateIcon = (
}
return !!(
(icon.provider === '' || icon.provider.match(matchName)) &&
(icon.provider === '' || icon.provider.match(matchIconName)) &&
((allowSimpleName && icon.prefix === '') ||
icon.prefix.match(matchName)) &&
icon.name.match(matchName)
icon.prefix.match(matchIconName)) &&
icon.name.match(matchIconName)
);
};

View File

@ -0,0 +1,23 @@
import type { IconifyTransformations } from '@iconify/types';
/**
* Merge transformations. Also copies other properties from first parameter
*/
export function mergeIconTransformations<T extends IconifyTransformations>(
obj1: T,
obj2: IconifyTransformations
): T {
const result = {
...obj1,
};
if (obj2.hFlip) {
result.hFlip = !result.hFlip;
}
if (obj2.vFlip) {
result.vFlip = !result.vFlip;
}
if (obj2.rotate) {
result.rotate = ((result.rotate || 0) + obj2.rotate) % 4;
}
return result;
}

View File

@ -1,9 +1,9 @@
// Customisations
export { compare as compareCustomisations } from './customisations/compare';
export {
defaults as defaultCustomisations,
mergeCustomisations,
} from './customisations/index';
defaultIconCustomisations,
defaultIconSizeCustomisations,
} from './customisations/defaults';
export { mergeCustomisations } from './customisations/merge';
// Customisations: converting attributes in components
export { toBoolean } from './customisations/bool';
@ -11,17 +11,19 @@ export { flipFromString } from './customisations/flip';
export { rotateFromString } from './customisations/rotate';
// Icon names
export { stringToIcon, validateIcon as validateIconName } from './icon/name';
export { matchName as matchIconName } from './icon/index';
export {
matchIconName,
stringToIcon,
validateIcon as validateIconName,
} from './icon/name';
// Icon data
export { mergeIconData } from './icon/merge';
export {
iconDefaults as defaultIconData,
fullIcon as fullIconData,
defaultIconProps,
defaultIconDimensions,
defaultIconTransformations,
} from './icon/index';
} from './icon/defaults';
// Icon set functions
export { parseIconSet } from './icon-set/parse';

View File

@ -1,10 +1,10 @@
import type { IconifyJSON } from '@iconify/types';
import type { FullIconifyIcon } from '../icon';
import type { FullIconifyIcon } from '../icon/defaults';
import { iconToSVG } from '../svg/build';
import { getIconData } from '../icon-set/get-icon';
import { mergeIconProps } from './utils';
import createDebugger from 'debug';
import { defaults as DefaultIconCustomizations } from '../customisations';
import { defaultIconCustomisations } from '../customisations/defaults';
import type { IconifyLoaderOptions } from './types';
const debug = createDebugger('@iconify-loader:icon');
@ -21,7 +21,7 @@ export async function searchForIcon(
iconData = getIconData(iconSet, id, true);
if (iconData) {
debug(`${collection}:${id}`);
let defaultCustomizations = { ...DefaultIconCustomizations };
let defaultCustomizations = { ...defaultIconCustomisations };
if (typeof customize === 'function')
defaultCustomizations = customize(defaultCustomizations);

View File

@ -1,5 +1,5 @@
import type { Awaitable } from '@antfu/utils';
import type { FullIconCustomisations } from '../customisations';
import type { FullIconCustomisations } from '../customisations/defaults';
import type { IconifyJSON } from '@iconify/types';
/**

View File

@ -0,0 +1,35 @@
/**
* Compares two objects, returns true if identical
*
* Reference object contains keys
*/
export function compareObjects<T extends Record<string, unknown>>(
obj1: T,
obj2: T,
ref: T = obj1
): boolean {
for (const key in ref) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
return Object.keys(obj1).length === Object.keys(obj2).length;
}
/**
* Unmerge objects, removing items that match in both objects
*/
export function unmergeObjects<T extends Record<string, unknown>>(
obj1: T,
obj2: T
): T {
const result = {
...obj1,
};
for (const key in obj2) {
if (result[key] === obj2[key]) {
delete result[key];
}
}
return result;
}

View File

@ -1,5 +1,5 @@
import type { FullIconifyIcon } from '../icon';
import type { FullIconCustomisations } from '../customisations';
import type { FullIconifyIcon } from '../icon/defaults';
import type { FullIconCustomisations } from '../customisations/defaults';
import { calculateSize } from './size';
/**
@ -12,10 +12,9 @@ export interface IconifyIconBuildResult {
height: string;
viewBox: string;
};
// Content
body: string;
// True if 'vertical-align: -0.125em' or equivalent should be added by implementation
inline?: boolean;
}
/**
@ -203,9 +202,5 @@ export function iconToSVG(
body,
};
if (customisations.inline) {
result.inline = true;
}
return result;
}

View File

@ -0,0 +1,45 @@
import { defaultIconCustomisations } from '../lib/customisations/defaults';
import { mergeCustomisations } from '../lib/customisations/merge';
describe('Testing mergeCustomisations', () => {
test('Empty', () => {
expect(mergeCustomisations(defaultIconCustomisations, {})).toEqual(
defaultIconCustomisations
);
});
test('Flip', () => {
expect(
mergeCustomisations(defaultIconCustomisations, {
hFlip: true,
})
).toEqual({
...defaultIconCustomisations,
hFlip: true,
});
});
test('Excessive rotation', () => {
expect(
mergeCustomisations(defaultIconCustomisations, {
rotate: 10,
})
).toEqual({
...defaultIconCustomisations,
rotate: 2,
});
});
test('Dimensions', () => {
expect(
mergeCustomisations(defaultIconCustomisations, {
width: '1em',
height: 20,
})
).toEqual({
...defaultIconCustomisations,
width: '1em',
height: 20,
});
});
});

View File

@ -1,5 +1,5 @@
import { parseIconSet } from '../lib/icon-set/parse';
import type { FullIconifyIcon } from '../lib/icon';
import type { FullIconifyIcon } from '../lib/icon/defaults';
describe('Testing parsing icon set', () => {
test('Simple icon set', () => {

View File

@ -1,15 +1,16 @@
import type { IconifyIconBuildResult } from '../lib/svg/build';
import { iconToSVG } from '../lib/svg/build';
import type { FullIconifyIcon } from '../lib/icon';
import { fullIcon, iconDefaults } from '../lib/icon';
import type { FullIconCustomisations } from '../lib/customisations';
import { defaults, mergeCustomisations } from '../lib/customisations';
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', () => {
test('Empty icon', () => {
const custom: FullIconCustomisations = defaults;
const icon: FullIconifyIcon = { ...iconDefaults, body: '' };
const custom: FullIconCustomisations = defaultIconCustomisations;
const icon: FullIconifyIcon = { ...defaultIconProps, body: '' };
const expected: IconifyIconBuildResult = {
attributes: {
width: '1em',
@ -29,14 +30,17 @@ describe('Testing iconToSVG', () => {
);
});
test('Auto size, inline, body', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
inline: true,
height: 'auto',
});
const icon: FullIconifyIcon = fullIcon({
test('Auto size, body', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: 'auto',
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
body: '<path d="" />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '16',
@ -44,7 +48,6 @@ describe('Testing iconToSVG', () => {
viewBox: '0 0 16 16',
},
body: '<path d="" />',
inline: true,
};
const result = iconToSVG(icon, custom);
@ -56,23 +59,23 @@ describe('Testing iconToSVG', () => {
'role': 'img',
...result.attributes,
};
if (result.inline) {
htmlProps['style'] = 'vertical-align: -0.125em;';
}
const html = iconToHTML(result.body, htmlProps);
expect(html).toBe(
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="16" height="16" viewBox="0 0 16 16" style="vertical-align: -0.125em;"><path d="" /></svg>'
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="16" height="16" viewBox="0 0 16 16"><path d="" /></svg>'
);
});
test('Auto size, inline, body', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
inline: true,
height: 'auto',
});
const icon: FullIconifyIcon = fullIcon({
test('Auto size, body', () => {
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: 'auto',
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
body: '<path d="" />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '16',
@ -80,7 +83,6 @@ describe('Testing iconToSVG', () => {
viewBox: '0 0 16 16',
},
body: '<path d="" />',
inline: true,
};
const result = iconToSVG(icon, custom);
@ -88,14 +90,18 @@ describe('Testing iconToSVG', () => {
});
test('Custom size', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
height: 'auto',
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: 'auto',
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '20',
@ -110,15 +116,19 @@ describe('Testing iconToSVG', () => {
});
test('Rotation', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
height: '40px',
rotate: 1,
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: '40px',
rotate: 1,
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '32px',
@ -133,15 +143,19 @@ describe('Testing iconToSVG', () => {
});
test('Negative rotation', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
height: '40px',
rotate: -1,
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: '40px',
rotate: -1,
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '32px',
@ -156,15 +170,19 @@ describe('Testing iconToSVG', () => {
});
test('Flip', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
height: '32',
hFlip: true,
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
height: '32',
hFlip: true,
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '40',
@ -179,15 +197,19 @@ describe('Testing iconToSVG', () => {
});
test('Flip, rotation', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
hFlip: true,
rotate: 1,
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
hFlip: true,
rotate: 1,
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '0.8em',
@ -202,15 +224,19 @@ describe('Testing iconToSVG', () => {
});
test('Flip icon that is rotated by default', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
hFlip: true,
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
hFlip: true,
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
rotate: 1,
});
};
// Horizontally flipping icon that has 90 or 270 degrees rotation will result in vertical flip.
// Therefore result should be rotation + vertical flip to visually match horizontal flip on normal icon.
@ -228,18 +254,22 @@ describe('Testing iconToSVG', () => {
});
test('Flip and rotation canceling eachother', () => {
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
width: '1em',
height: 'auto',
hFlip: true,
vFlip: true,
rotate: 2,
});
const icon: FullIconifyIcon = fullIcon({
const custom: FullIconCustomisations = mergeCustomisations(
defaultIconCustomisations,
{
width: '1em',
height: 'auto',
hFlip: true,
vFlip: true,
rotate: 2,
}
);
const icon: FullIconifyIcon = {
...defaultIconProps,
width: 20,
height: 16,
body: '<path d="..." />',
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '1em',
@ -258,15 +288,16 @@ describe('Testing iconToSVG', () => {
'<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(
defaults,
defaultIconCustomisations,
{}
);
const icon: FullIconifyIcon = fullIcon({
const icon: FullIconifyIcon = {
...defaultIconProps,
body: iconBody,
width: 128,
height: 128,
hFlip: true,
});
};
const expected: IconifyIconBuildResult = {
attributes: {
width: '1em',

View File

@ -115,7 +115,7 @@ describe('Testing validation', () => {
},
height: 24,
// Object
rotate: {
width: {
foo: 1,
},
})
@ -131,7 +131,7 @@ describe('Testing validation', () => {
},
height: 24,
// Object
hFlip: null,
left: null,
})
).toBe(null);

View File

@ -231,7 +231,7 @@ describe('Testing validation', () => {
},
height: 24,
// Object
rotate: {
width: {
foo: 1,
},
},
@ -253,7 +253,7 @@ describe('Testing validation', () => {
},
height: 24,
// Object
hFlip: null,
left: null,
},
{ fix: true }
);