mirror of
https://github.com/iconify/iconify.git
synced 2024-12-13 22:18:24 +00:00
Merge remote-tracking branch 'origin/userquin/feat-split-modern-loader' into next
This commit is contained in:
commit
65dfecf963
19
packages/utils/package-lock.json
generated
19
packages/utils/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"local-pkg": "^0.4.0"
|
"local-pkg": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/flat-color-icons": "^1.0.2",
|
||||||
"@iconify/library-builder": "^1.0.5",
|
"@iconify/library-builder": "^1.0.5",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
@ -740,6 +741,15 @@
|
|||||||
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
|
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify-json/flat-color-icons": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/flat-color-icons/-/flat-color-icons-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-UWbgi+4T0wkOQHNKxfxfGiDevGPrgD369kzC/dHGaz6WJxlwqxxRQvb6L8tTVCLdwIcmcNCV1Wy0SKgX0R/pHw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "^1.0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@iconify/library-builder": {
|
"node_modules/@iconify/library-builder": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify/library-builder/-/library-builder-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify/library-builder/-/library-builder-1.0.5.tgz",
|
||||||
@ -5951,6 +5961,15 @@
|
|||||||
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
|
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@iconify-json/flat-color-icons": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/flat-color-icons/-/flat-color-icons-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-UWbgi+4T0wkOQHNKxfxfGiDevGPrgD369kzC/dHGaz6WJxlwqxxRQvb6L8tTVCLdwIcmcNCV1Wy0SKgX0R/pHw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@iconify/types": "^1.0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@iconify/library-builder": {
|
"@iconify/library-builder": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify/library-builder/-/library-builder-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify/library-builder/-/library-builder-1.0.5.tgz",
|
||||||
|
@ -124,6 +124,18 @@
|
|||||||
"require": "./lib/loader/custom.js",
|
"require": "./lib/loader/custom.js",
|
||||||
"import": "./lib/loader/custom.mjs"
|
"import": "./lib/loader/custom.mjs"
|
||||||
},
|
},
|
||||||
|
"./lib/loader/fs": {
|
||||||
|
"require": "./lib/loader/fs.js",
|
||||||
|
"import": "./lib/loader/fs.mjs"
|
||||||
|
},
|
||||||
|
"./lib/loader/install-pkg": {
|
||||||
|
"require": "./lib/loader/install-pkg.js",
|
||||||
|
"import": "./lib/loader/install-pkg.mjs"
|
||||||
|
},
|
||||||
|
"./lib/loader/loader": {
|
||||||
|
"require": "./lib/loader/loader.js",
|
||||||
|
"import": "./lib/loader/loader.mjs"
|
||||||
|
},
|
||||||
"./lib/loader/loaders": {
|
"./lib/loader/loaders": {
|
||||||
"require": "./lib/loader/loaders.js",
|
"require": "./lib/loader/loaders.js",
|
||||||
"import": "./lib/loader/loaders.mjs"
|
"import": "./lib/loader/loaders.mjs"
|
||||||
@ -148,6 +160,10 @@
|
|||||||
"require": "./lib/svg/build.js",
|
"require": "./lib/svg/build.js",
|
||||||
"import": "./lib/svg/build.mjs"
|
"import": "./lib/svg/build.mjs"
|
||||||
},
|
},
|
||||||
|
"./lib/svg/encode-svg-for-css": {
|
||||||
|
"require": "./lib/svg/encode-svg-for-css.js",
|
||||||
|
"import": "./lib/svg/encode-svg-for-css.mjs"
|
||||||
|
},
|
||||||
"./lib/svg/id": {
|
"./lib/svg/id": {
|
||||||
"require": "./lib/svg/id.js",
|
"require": "./lib/svg/id.js",
|
||||||
"import": "./lib/svg/id.mjs"
|
"import": "./lib/svg/id.mjs"
|
||||||
@ -166,6 +182,7 @@
|
|||||||
"local-pkg": "^0.4.0"
|
"local-pkg": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/flat-color-icons": "^1.0.2",
|
||||||
"@iconify/library-builder": "^1.0.5",
|
"@iconify/library-builder": "^1.0.5",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
|
@ -39,6 +39,7 @@ export { convertIconSetInfo } from './icon-set/convert-info';
|
|||||||
export { iconToSVG } from './svg/build';
|
export { iconToSVG } from './svg/build';
|
||||||
export { replaceIDs } from './svg/id';
|
export { replaceIDs } from './svg/id';
|
||||||
export { calculateSize } from './svg/size';
|
export { calculateSize } from './svg/size';
|
||||||
|
export { encodeSvgForCss } from './svg/encode-svg-for-css';
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
export { colorKeywords } from './colors/keywords';
|
export { colorKeywords } from './colors/keywords';
|
||||||
@ -50,12 +51,14 @@ export type {
|
|||||||
CustomCollections,
|
CustomCollections,
|
||||||
IconCustomizer,
|
IconCustomizer,
|
||||||
IconCustomizations,
|
IconCustomizations,
|
||||||
|
IconifyLoaderOptions,
|
||||||
InlineCollection,
|
InlineCollection,
|
||||||
} from './loader/types';
|
} from './loader/types';
|
||||||
export { tryInstallPkg, mergeIconProps } from './loader/utils';
|
export { mergeIconProps } from './loader/utils';
|
||||||
|
export { isNode, loadIcon } from './loader/loader';
|
||||||
export { FileSystemIconLoader } from './loader/loaders';
|
export { FileSystemIconLoader } from './loader/loaders';
|
||||||
export { getCustomIcon } from './loader/custom';
|
export { getCustomIcon } from './loader/custom';
|
||||||
export { loadCollection, searchForIcon } from './loader/modern';
|
export { searchForIcon } from './loader/modern';
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
export { camelize, camelToKebab, pascalize } from './misc/strings';
|
export { camelize, camelToKebab, pascalize } from './misc/strings';
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import createDebugger from 'debug';
|
import createDebugger from 'debug';
|
||||||
import type {
|
import type { CustomIconLoader, IconifyLoaderOptions, InlineCollection } from './types';
|
||||||
CustomIconLoader,
|
|
||||||
IconCustomizations,
|
|
||||||
InlineCollection,
|
|
||||||
} from './types';
|
|
||||||
import { mergeIconProps } from './utils';
|
import { mergeIconProps } from './utils';
|
||||||
|
|
||||||
const debug = createDebugger('@iconify-loader:custom');
|
const debug = createDebugger('@iconify-loader:custom');
|
||||||
@ -15,7 +11,7 @@ export async function getCustomIcon(
|
|||||||
custom: CustomIconLoader | InlineCollection,
|
custom: CustomIconLoader | InlineCollection,
|
||||||
collection: string,
|
collection: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
iconsCustomizations?: IconCustomizations
|
options?: IconifyLoaderOptions,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
let result: string | undefined | null;
|
let result: string | undefined | null;
|
||||||
|
|
||||||
@ -29,24 +25,19 @@ export async function getCustomIcon(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
if (!result.startsWith('<svg ')) {
|
if (!result.startsWith('<svg')) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Custom icon "${icon}" in "${collection}" is not a valid SVG`
|
`Custom icon "${icon}" in "${collection}" is not a valid SVG`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const {
|
const { transform } = options?.customizations ?? {};
|
||||||
transform,
|
|
||||||
additionalProps = {},
|
|
||||||
iconCustomizer,
|
|
||||||
} = iconsCustomizations || {};
|
|
||||||
return await mergeIconProps(
|
return await mergeIconProps(
|
||||||
transform ? await transform(result) : result,
|
typeof transform === 'function' ? await transform(result) : result,
|
||||||
collection,
|
collection,
|
||||||
icon,
|
icon,
|
||||||
additionalProps,
|
options,
|
||||||
undefined,
|
undefined,
|
||||||
iconCustomizer
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
packages/utils/src/loader/fs.ts
Normal file
39
packages/utils/src/loader/fs.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { promises as fs, Stats } from 'fs';
|
||||||
|
import { isPackageExists, resolveModule } from 'local-pkg'
|
||||||
|
import type { IconifyJSON } from '@iconify/types'
|
||||||
|
import { tryInstallPkg } from './install-pkg';
|
||||||
|
|
||||||
|
const _collections: Record<string, Promise<IconifyJSON | undefined>> = {};
|
||||||
|
const isLegacyExists = isPackageExists('@iconify/json');
|
||||||
|
|
||||||
|
export async function loadCollectionFromFS(name: string, autoInstall = false): Promise<IconifyJSON | undefined> {
|
||||||
|
if (!_collections[name]) {
|
||||||
|
_collections[name] = task();
|
||||||
|
}
|
||||||
|
return _collections[name];
|
||||||
|
|
||||||
|
async function task() {
|
||||||
|
let jsonPath = resolveModule(`@iconify-json/${name}/icons.json`);
|
||||||
|
if (!jsonPath && isLegacyExists) {
|
||||||
|
jsonPath = resolveModule(`@iconify/json/json/${name}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonPath && !isLegacyExists && autoInstall) {
|
||||||
|
await tryInstallPkg(`@iconify-json/${name}`);
|
||||||
|
jsonPath = resolveModule(`@iconify-json/${name}/icons.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stat: Stats | undefined;
|
||||||
|
try {
|
||||||
|
stat = jsonPath ? await fs.lstat(jsonPath) : undefined;
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (stat && stat.isFile()) {
|
||||||
|
return JSON.parse(await fs.readFile(jsonPath as string, 'utf8')) as IconifyJSON;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
packages/utils/src/loader/install-pkg.ts
Normal file
42
packages/utils/src/loader/install-pkg.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { installPackage } from '@antfu/install-pkg';
|
||||||
|
import { sleep } from '@antfu/utils';
|
||||||
|
import { cyan, yellow } from 'kolorist';
|
||||||
|
|
||||||
|
const warned = new Set<string>();
|
||||||
|
|
||||||
|
export function warnOnce(msg: string): void {
|
||||||
|
if (!warned.has(msg)) {
|
||||||
|
warned.add(msg);
|
||||||
|
console.warn(yellow(`[@iconify-loader] ${msg}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pending: Promise<void> | undefined;
|
||||||
|
const tasks: Record<string, Promise<void> | undefined> = {};
|
||||||
|
|
||||||
|
export async function tryInstallPkg(name: string): Promise<void | undefined> {
|
||||||
|
if (pending) {
|
||||||
|
await pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tasks[name]) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(cyan(`Installing ${name}...`));
|
||||||
|
tasks[name] = pending = installPackage(name, {
|
||||||
|
dev: true,
|
||||||
|
preferOffline: true,
|
||||||
|
})
|
||||||
|
.then(() => sleep(300))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
.catch((e: any) => {
|
||||||
|
warnOnce(`Failed to install ${name}`);
|
||||||
|
console.error(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pending = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return tasks[name]!;
|
||||||
|
}
|
67
packages/utils/src/loader/loader.ts
Normal file
67
packages/utils/src/loader/loader.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { getCustomIcon } from './custom';
|
||||||
|
import { searchForIcon } from './modern';
|
||||||
|
import { warnOnce } from './install-pkg';
|
||||||
|
import type { IconifyLoaderOptions } from './types';
|
||||||
|
|
||||||
|
export const isNode = typeof process < 'u' && typeof process.stdout < 'u'
|
||||||
|
|
||||||
|
export async function loadIcon(
|
||||||
|
collection: string,
|
||||||
|
icon: string,
|
||||||
|
options?: IconifyLoaderOptions
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const custom = options?.customCollections?.[collection];
|
||||||
|
|
||||||
|
if (custom) {
|
||||||
|
const result = await getCustomIcon(custom, collection, icon, options);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await loadNodeBuiltinIcon(collection, icon, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importFsModule(): Promise<typeof import('./fs') | undefined> {
|
||||||
|
try {
|
||||||
|
return await import('./fs');
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
// cjs environments
|
||||||
|
return require('./fs.js');
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadNodeBuiltinIcon(
|
||||||
|
collection: string,
|
||||||
|
icon: string,
|
||||||
|
options?: IconifyLoaderOptions,
|
||||||
|
warn = true,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { loadCollectionFromFS } = await importFsModule();
|
||||||
|
const iconSet = await loadCollectionFromFS(collection, options?.autoInstall);
|
||||||
|
if (iconSet) {
|
||||||
|
// possible icon names
|
||||||
|
const ids = [
|
||||||
|
icon,
|
||||||
|
icon.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(),
|
||||||
|
icon.replace(/([a-z])(\d+)/g, '$1-$2'),
|
||||||
|
];
|
||||||
|
return await searchForIcon(iconSet, collection, ids, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warn) {
|
||||||
|
warnOnce(`failed to load \`@iconify-json/${collection}\`, have you installed it?`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,70 +1,22 @@
|
|||||||
import { promises as fs } from 'fs';
|
|
||||||
import type { IconifyJSON } from '@iconify/types';
|
import type { IconifyJSON } from '@iconify/types';
|
||||||
import type { FullIconifyIcon } from '../icon';
|
import type { FullIconifyIcon } from '../icon';
|
||||||
import { iconToSVG } from '../svg/build';
|
import { iconToSVG } from '../svg/build';
|
||||||
import { getIconData } from '../icon-set/get-icon';
|
import { getIconData } from '../icon-set/get-icon';
|
||||||
import { mergeIconProps, tryInstallPkg } from './utils';
|
import { mergeIconProps } from './utils';
|
||||||
import createDebugger from 'debug';
|
import createDebugger from 'debug';
|
||||||
import { isPackageExists, resolveModule } from 'local-pkg';
|
|
||||||
import { defaults as DefaultIconCustomizations } from '../customisations';
|
import { defaults as DefaultIconCustomizations } from '../customisations';
|
||||||
import type { IconCustomizations } from './types';
|
import type { IconifyLoaderOptions } from './types';
|
||||||
|
|
||||||
const debug = createDebugger('@iconify-loader:icon');
|
const debug = createDebugger('@iconify-loader:icon');
|
||||||
const debugModern = createDebugger('@iconify-loader:modern');
|
|
||||||
const debugLegacy = createDebugger('@iconify-loader:legacy');
|
|
||||||
|
|
||||||
const _collections: Record<string, Promise<IconifyJSON | undefined>> = {};
|
|
||||||
const isLegacyExists = isPackageExists('@iconify/json');
|
|
||||||
|
|
||||||
export async function loadCollection(
|
|
||||||
name: string,
|
|
||||||
autoInstall = false
|
|
||||||
): Promise<IconifyJSON | undefined> {
|
|
||||||
if (!_collections[name]) {
|
|
||||||
_collections[name] = task();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _collections[name];
|
|
||||||
|
|
||||||
async function task(): Promise<IconifyJSON | undefined> {
|
|
||||||
let jsonPath = resolveModule(`@iconify-json/${name}/icons.json`);
|
|
||||||
if (jsonPath) {
|
|
||||||
debugModern(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!jsonPath && isLegacyExists) {
|
|
||||||
jsonPath = resolveModule(`@iconify/json/json/${name}.json`);
|
|
||||||
if (jsonPath) {
|
|
||||||
debugLegacy(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!jsonPath && !isLegacyExists && autoInstall) {
|
|
||||||
await tryInstallPkg(`@iconify-json/${name}`);
|
|
||||||
jsonPath = resolveModule(`@iconify-json/${name}/icons.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonPath) {
|
|
||||||
return JSON.parse(await fs.readFile(jsonPath, 'utf8'));
|
|
||||||
} else {
|
|
||||||
debugModern(`failed to load ${name}`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchForIcon(
|
export async function searchForIcon(
|
||||||
iconSet: IconifyJSON,
|
iconSet: IconifyJSON,
|
||||||
collection: string,
|
collection: string,
|
||||||
ids: string[],
|
ids: string[],
|
||||||
iconCustomizations?: IconCustomizations
|
options?: IconifyLoaderOptions,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
let iconData: FullIconifyIcon | null;
|
let iconData: FullIconifyIcon | null;
|
||||||
const {
|
const { customize } = options?.customizations ?? {};
|
||||||
customize,
|
|
||||||
additionalProps = {},
|
|
||||||
iconCustomizer,
|
|
||||||
} = iconCustomizations || {};
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
iconData = getIconData(iconSet, id, true);
|
iconData = getIconData(iconSet, id, true);
|
||||||
if (iconData) {
|
if (iconData) {
|
||||||
@ -77,12 +29,12 @@ export async function searchForIcon(
|
|||||||
: defaultCustomizations
|
: defaultCustomizations
|
||||||
);
|
);
|
||||||
return await mergeIconProps(
|
return await mergeIconProps(
|
||||||
`<svg>${body}</svg>`,
|
// DON'T remove space on <svg >
|
||||||
|
`<svg >${body}</svg>`,
|
||||||
collection,
|
collection,
|
||||||
id,
|
id,
|
||||||
additionalProps,
|
options,
|
||||||
() => attributes,
|
() => attributes,
|
||||||
iconCustomizer
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ export type IconCustomizations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of icons as object. Key is icon name, value is icon data or callback (can be async) to get icon data
|
* List of icons as object. Key is the icon name, the value is the icon data or callback (can be async) to get icon data
|
||||||
*/
|
*/
|
||||||
export type InlineCollection = Record<
|
export type InlineCollection = Record<
|
||||||
string,
|
string,
|
||||||
@ -64,9 +64,59 @@ export type InlineCollection = Record<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of custom icons. Key is collection name, value is loader or InlineCollection object
|
* Collection of custom icons. Key is the collection name, the value is the loader or InlineCollection object
|
||||||
*/
|
*/
|
||||||
export type CustomCollections = Record<
|
export type CustomCollections = Record<
|
||||||
string,
|
string,
|
||||||
CustomIconLoader | InlineCollection
|
CustomIconLoader | InlineCollection
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to use with the modern loader.
|
||||||
|
*/
|
||||||
|
export type IconifyLoaderOptions = {
|
||||||
|
/**
|
||||||
|
* Add svg and xlink xml namespace when necessary.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
addXmlNs?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale of icons against 1em
|
||||||
|
*/
|
||||||
|
scale?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style to apply to icons by default
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
defaultStyle?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class names to apply to icons by default
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
defaultClass?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loader for custom loaders
|
||||||
|
*/
|
||||||
|
customCollections?: Record<string, CustomIconLoader | InlineCollection>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon customizer
|
||||||
|
*/
|
||||||
|
customizations?: IconCustomizations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto install icon sources package when the usages is detected
|
||||||
|
*
|
||||||
|
* **WARNING**: only on `node` environment, on `browser` this option will be ignored
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
autoInstall?: boolean
|
||||||
|
}
|
||||||
|
@ -1,66 +1,59 @@
|
|||||||
import { installPackage } from '@antfu/install-pkg';
|
import type { Awaitable } from '@antfu/utils';
|
||||||
import { Awaitable, sleep } from '@antfu/utils';
|
import type { IconifyLoaderOptions } from './types';
|
||||||
import { cyan, yellow } from 'kolorist';
|
|
||||||
import type { IconCustomizer } from './types';
|
|
||||||
|
|
||||||
const warned = new Set<string>();
|
|
||||||
|
|
||||||
export function warnOnce(msg: string): void {
|
|
||||||
if (!warned.has(msg)) {
|
|
||||||
warned.add(msg);
|
|
||||||
console.warn(yellow(`[@iconify-loader] ${msg}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pending: Promise<void> | undefined;
|
|
||||||
const tasks: Record<string, Promise<void> | undefined> = {};
|
|
||||||
|
|
||||||
export async function mergeIconProps(
|
export async function mergeIconProps(
|
||||||
svg: string,
|
svg: string,
|
||||||
collection: string,
|
collection: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
additionalProps: Record<string, string | undefined>,
|
options?: IconifyLoaderOptions,
|
||||||
propsProvider?: () => Awaitable<Record<string, string>>,
|
propsProvider?: () => Awaitable<Record<string, string>>,
|
||||||
iconCustomizer?: IconCustomizer
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const { scale, addXmlNs = false } = options ?? {}
|
||||||
|
const {
|
||||||
|
additionalProps = {},
|
||||||
|
iconCustomizer,
|
||||||
|
} = options?.customizations ?? {};
|
||||||
const props: Record<string, string> = (await propsProvider?.()) ?? {};
|
const props: Record<string, string> = (await propsProvider?.()) ?? {};
|
||||||
|
if (!svg.includes(" width=") && !svg.includes(" height=") && typeof scale === 'number') {
|
||||||
|
if ((typeof props.width === 'undefined' || props.width === null) && (typeof props.height === 'undefined' || props.height === null)) {
|
||||||
|
props.width = `${scale}em`;
|
||||||
|
props.height = `${scale}em`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await iconCustomizer?.(collection, icon, props);
|
await iconCustomizer?.(collection, icon, props);
|
||||||
Object.keys(additionalProps).forEach((p) => {
|
Object.keys(additionalProps).forEach((p) => {
|
||||||
const v = additionalProps[p];
|
const v = additionalProps[p];
|
||||||
if (v !== undefined && v !== null) props[p] = v;
|
if (v !== undefined && v !== null) props[p] = v;
|
||||||
});
|
});
|
||||||
const replacement = svg.startsWith('<svg ') ? '<svg ' : '<svg';
|
// add xml namespaces if necessary
|
||||||
return svg.replace(
|
if (addXmlNs) {
|
||||||
replacement,
|
// add svg xmlns if missing
|
||||||
`${replacement}${Object.keys(props)
|
if (!svg.includes(' xmlns=') && !props['xmlns']) {
|
||||||
.map((p) => `${p}="${props[p]}"`)
|
props['xmlns'] = 'http://www.w3.org/2000/svg';
|
||||||
.join(' ')}`
|
}
|
||||||
|
// add xmlns:xlink if xlink present and the xmlns missing
|
||||||
|
if (!svg.includes(' xmlns:xlink=') && svg.includes('xlink:') && !props['xmlns:xlink']) {
|
||||||
|
props['xmlns:xlink'] = 'http://www.w3.org/1999/xlink';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg = svg.replace(
|
||||||
|
'<svg ',
|
||||||
|
`<svg ${Object.keys(props).map((p) => `${p}="${props[p]}"`).join(' ')}`
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
export async function tryInstallPkg(name: string): Promise<void | undefined> {
|
if (svg && options) {
|
||||||
if (pending) {
|
const { defaultStyle, defaultClass } = options
|
||||||
await pending;
|
// additional props and iconCustomizer takes precedence
|
||||||
|
if (defaultClass && !svg.includes(' class=')) {
|
||||||
|
svg = svg.replace('<svg ', `<svg class="${defaultClass}" `);
|
||||||
|
}
|
||||||
|
// additional props and iconCustomizer takes precedence
|
||||||
|
if (defaultStyle && !svg.includes(' style=')) {
|
||||||
|
svg = svg.replace('<svg ', `<svg style="${defaultStyle}" `);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tasks[name]) {
|
return svg;
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(cyan(`Installing ${name}...`));
|
|
||||||
tasks[name] = pending = installPackage(name, {
|
|
||||||
dev: true,
|
|
||||||
preferOffline: true,
|
|
||||||
})
|
|
||||||
.then(() => sleep(300))
|
|
||||||
// eslint-disable-next-line
|
|
||||||
.catch((e: any) => {
|
|
||||||
warnOnce(`Failed to install ${name}`);
|
|
||||||
console.error(e);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
pending = undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return tasks[name]!;
|
|
||||||
}
|
}
|
||||||
|
17
packages/utils/src/svg/encode-svg-for-css.ts
Normal file
17
packages/utils/src/svg/encode-svg-for-css.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
|
||||||
|
export function encodeSvgForCss(svg: string): string {
|
||||||
|
let useSvg = svg.startsWith('<svg>') ? svg.replace('<svg>', '<svg >') : svg;
|
||||||
|
if (!useSvg.includes(' xmlns:xlink=') && useSvg.includes(' xlink:')) {
|
||||||
|
useSvg = useSvg.replace('<svg ', '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ');
|
||||||
|
}
|
||||||
|
if (!useSvg.includes(' xmlns=')) {
|
||||||
|
useSvg = useSvg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
|
||||||
|
}
|
||||||
|
return useSvg.replace(/"/g, '\'')
|
||||||
|
.replace(/%/g, '%25')
|
||||||
|
.replace(/#/g, '%23')
|
||||||
|
.replace(/{/g, '%7B')
|
||||||
|
.replace(/}/g, '%7D')
|
||||||
|
.replace(/</g, '%3C')
|
||||||
|
.replace(/>/g, '%3E');
|
||||||
|
}
|
@ -13,9 +13,11 @@ describe('Testing getCustomIcon', () => {
|
|||||||
test('CustomIconLoader with transform', async () => {
|
test('CustomIconLoader with transform', async () => {
|
||||||
const svg = await fs.readFile(fixturesDir + '/circle.svg', 'utf8');
|
const svg = await fs.readFile(fixturesDir + '/circle.svg', 'utf8');
|
||||||
const result = await getCustomIcon(() => svg, 'a', 'b', {
|
const result = await getCustomIcon(() => svg, 'a', 'b', {
|
||||||
transform(icon) {
|
customizations: {
|
||||||
return icon.replace('<svg ', '<svg width="1em" height="1em" ');
|
transform(icon) {
|
||||||
},
|
return icon.replace('<svg ', '<svg width="1em" height="1em" ');
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(result && result.indexOf('width="1em"') > -1).toBeTruthy();
|
expect(result && result.indexOf('width="1em"') > -1).toBeTruthy();
|
||||||
expect(result && result.indexOf('height="1em"') > -1).toBeTruthy();
|
expect(result && result.indexOf('height="1em"') > -1).toBeTruthy();
|
||||||
|
59
packages/utils/tests/iconify-icon-test.ts
Normal file
59
packages/utils/tests/iconify-icon-test.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { loadIcon } from '../lib';
|
||||||
|
|
||||||
|
describe('Testing loadIcon with @iconify-json/flat-color-icons>', () => {
|
||||||
|
|
||||||
|
test('loadIcon works', async () => {
|
||||||
|
const result = await loadIcon('flat-color-icons', 'up-right');
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadIcon adds xmlns:xlink', async () => {
|
||||||
|
const result = await loadIcon('flat-color-icons', 'up-right', { addXmlNs: true });
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('xmlns:xlink=') > - 1).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadIcon with customize with default style and class', async () => {
|
||||||
|
const result = await loadIcon('flat-color-icons', 'up-right', {
|
||||||
|
defaultStyle: 'margin-top: 1rem;',
|
||||||
|
defaultClass: 'clazz',
|
||||||
|
customizations: {
|
||||||
|
customize(props) {
|
||||||
|
props.width = '2em';
|
||||||
|
props.height = '2em';
|
||||||
|
return props;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('margin-top: 1rem;') > - 1).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('class="clazz"') > - 1).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('width="2em"') > - 1).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('height="2em"') > - 1).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadIcon preserves customizations order', async () => {
|
||||||
|
const result = await loadIcon('flat-color-icons', 'up-right', {
|
||||||
|
scale: 1,
|
||||||
|
defaultStyle: 'color: red;',
|
||||||
|
defaultClass: 'clazz1',
|
||||||
|
customizations: {
|
||||||
|
additionalProps: {
|
||||||
|
'width': '2em',
|
||||||
|
'height': '2em',
|
||||||
|
'style': 'color: blue;',
|
||||||
|
'class': 'clazz2',
|
||||||
|
},
|
||||||
|
// it will never be called, it is not a custom icon
|
||||||
|
transform(icon) {
|
||||||
|
return icon.replace('<svg ', '<svg width="4em" height="4em" ');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result && result.includes('style="color: blue;"')).toBeTruthy();
|
||||||
|
expect(result && result.includes('class="clazz2"')).toBeTruthy();
|
||||||
|
expect(result && result.includes('width="2em"')).toBeTruthy();
|
||||||
|
expect(result && result.includes('height="2em"')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
69
packages/utils/tests/load-icon-test.ts
Normal file
69
packages/utils/tests/load-icon-test.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { CustomIconLoader, loadIcon } from '../lib';
|
||||||
|
|
||||||
|
const fixturesDir = __dirname + '/fixtures';
|
||||||
|
|
||||||
|
const loader: CustomIconLoader = async(name) => {
|
||||||
|
return await fs.readFile(`${fixturesDir}/${name}.svg`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Testing loadIcon', () => {
|
||||||
|
test('CustomCollection', async () => {
|
||||||
|
const svg = await loader('circle');
|
||||||
|
expect(svg).toBeTruthy();
|
||||||
|
const result = await loadIcon('a', 'circle', {
|
||||||
|
customCollections: {
|
||||||
|
'a': {
|
||||||
|
'circle': svg as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(svg).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CustomCollection with transform', async () => {
|
||||||
|
const svg = await loader('circle');
|
||||||
|
expect(svg).toBeTruthy();
|
||||||
|
const result = await loadIcon('a', 'circle', {
|
||||||
|
customCollections: {
|
||||||
|
'a': {
|
||||||
|
'circle': svg as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customizations: {
|
||||||
|
transform(icon) {
|
||||||
|
return icon.replace('<svg ', '<svg width="1em" height="1em" ');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('width="1em"') > -1).toBeTruthy();
|
||||||
|
expect(result && result.indexOf('height="1em"') > -1).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CustomCollection Icon with XML heading', async () => {
|
||||||
|
const svg = await loader('1f3eb');
|
||||||
|
expect(svg).toBeTruthy();
|
||||||
|
// Intercept console.warn
|
||||||
|
let warned = false;
|
||||||
|
const warn = console.warn;
|
||||||
|
console.warn = (/*...args*/) => {
|
||||||
|
// warn.apply(this, args);
|
||||||
|
warned = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await loadIcon('a', '1f3eb', {
|
||||||
|
customCollections: {
|
||||||
|
'a': {
|
||||||
|
'1f3eb': svg as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Restore console.warn
|
||||||
|
console.warn = warn;
|
||||||
|
|
||||||
|
expect(svg).toEqual(result);
|
||||||
|
expect(warned).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user