feat: working version of API v3

This commit is contained in:
Vjacheslav Trushkin 2022-10-14 17:00:22 +03:00
commit 8ef7f21d74
102 changed files with 63658 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: cyberalien

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.idea
.vscode
.DS_Store
.env
*.map
tsconfig.tsbuildinfo
/node_modules
/lib
/cache

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"trailingComma": "es5",
"singleQuote": true,
"useTabs": true,
"semi": true,
"quoteProps": "consistent",
"endOfLine": "lf",
"printWidth": 120
}

21
license.txt Executable file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Vjacheslav Trushkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "@iconify/api",
"description": "Node.js version of api.iconify.design",
"author": "Vjacheslav Trushkin",
"license": "MIT",
"private": true,
"version": "3.0.0-dev",
"bugs": "https://github.com/iconify/api.js/issues",
"homepage": "https://github.com/iconify/api.js",
"repository": {
"type": "git",
"url": "https://github.com/iconify/api.js.git"
},
"packageManager": "pnpm@7.13.4",
"scripts": {
"build": "tsc -b",
"test": "vitest --config vitest.config.mjs"
},
"dependencies": {
"@iconify/tools": "^2.1.0",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^2.0.0",
"dotenv": "^16.0.2",
"fastify": "^4.6.0"
},
"devDependencies": {
"@types/jest": "^29.0.3",
"@types/node": "^18.7.18",
"typescript": "^4.8.3",
"vitest": "^0.23.2"
}
}

1739
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

58
src/_test.ts Normal file
View File

@ -0,0 +1,58 @@
import { RemoteDownloader } from './downloaders/remote';
import { createJSONCollectionsListImporter } from './importers/collections/collections';
import { createJSONPackageIconSetImporter } from './importers/icon-set/json-package';
import type { RemoteDownloaderOptions } from './types/downloaders/remote';
import type { IconSetImportedData, ImportedData } from './types/importers/common';
(async () => {
const options: RemoteDownloaderOptions = {
downloadType: 'npm',
package: '@iconify/collections',
};
const importer = createJSONCollectionsListImporter(new RemoteDownloader<ImportedData>(options), (prefix) =>
createJSONPackageIconSetImporter(
new RemoteDownloader<IconSetImportedData>({
downloadType: 'npm',
package: `@iconify-json/${prefix}`,
}),
{ prefix }
)
);
const start = Date.now();
await importer.init();
const data = importer.data;
if (!data) {
throw new Error('Something went wrong!');
}
let iconSetsCount = 0;
let visibleIconSetsCount = 0;
let iconsCount = 0;
let visibleIconsCount = 0;
data.prefixes.forEach((prefix) => {
const item = data.iconSets[prefix];
if (!item) {
console.error(`Failed to load: ${prefix}`);
return;
}
const info = item.info;
if (!info) {
console.error(`Missing info in ${prefix}`);
return;
}
iconSetsCount++;
iconsCount += info.total || 0;
if (!info.hidden) {
visibleIconSetsCount++;
visibleIconsCount += info.total || 0;
}
});
console.log('Loaded in', Date.now() - start, 'ms');
console.log(iconSetsCount, 'icon sets,', visibleIconSetsCount, 'visible');
console.log(iconsCount, 'icons,', visibleIconsCount, 'visible)');
})();

55
src/_test2.ts Normal file
View File

@ -0,0 +1,55 @@
import { RemoteDownloader } from './downloaders/remote';
import { createIconSetsPackageImporter } from './importers/full/json';
import type { RemoteDownloaderOptions } from './types/downloaders/remote';
import type { ImportedData } from './types/importers/common';
(async () => {
const startMem = (process.memoryUsage && process.memoryUsage().heapUsed) || 0;
const options: RemoteDownloaderOptions = {
downloadType: 'npm',
package: '@iconify/json',
};
const importer = createIconSetsPackageImporter(new RemoteDownloader<ImportedData>(options));
console.log('Importer type:', importer.type);
const start = Date.now();
await importer.init();
const data = importer.data;
if (!data) {
throw new Error('Something went wrong!');
}
let iconSetsCount = 0;
let visibleIconSetsCount = 0;
let iconsCount = 0;
let visibleIconsCount = 0;
data.prefixes.forEach((prefix) => {
const item = data.iconSets[prefix];
if (!item) {
console.error(`Failed to load: ${prefix}`);
return;
}
const info = item.info;
if (!info) {
console.error(`Missing info in ${prefix}`);
return;
}
iconSetsCount++;
iconsCount += info.total || 0;
if (!info.hidden) {
visibleIconSetsCount++;
visibleIconsCount += info.total || 0;
}
});
const endMem = (process.memoryUsage && process.memoryUsage().heapUsed) || 0;
console.log('Loaded in', Date.now() - start, 'ms');
console.log('Memory usage:', (endMem - startMem) / 1024 / 1024);
console.log(iconSetsCount, 'icon sets,', visibleIconSetsCount, 'visible');
console.log(iconsCount, 'icons,', visibleIconsCount, 'visible)');
})();

67
src/config/app.ts Normal file
View File

@ -0,0 +1,67 @@
import type { AppConfig } from '../types/config/app';
import type { SplitIconSetConfig } from '../types/config/split';
import type { MemoryStorageConfig } from '../types/storage';
/**
* Main configuration
*/
export const appConfig: AppConfig = {
// Index page
redirectIndex: 'https://iconify.design/',
// Region to add to `/version` response. Used to tell which server is responding when running multiple servers
statusRegion: '',
// Cache root directory
cacheRootDir: 'cache',
// HTTP headers to send
headers: [
'Access-Control-Allow-Origin: *',
'Access-Control-Allow-Methods: GET, OPTIONS',
'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding',
'Access-Control-Max-Age: 86400',
'Cross-Origin-Resource-Policy: cross-origin',
'Cache-Control: public, max-age=604800, min-refresh=604800',
],
// Port
// To set custom options for listener, such as IP address, edit `src/http/index.ts`
port: 3000,
// Log stuff
log: true,
};
/**
* Splitting icon sets
*/
export const splitIconSetConfig: SplitIconSetConfig = {
// Average chunk size, in bytes. 0 to disable
chunkSize: 1000000,
// Minimum number of icons in one chunk
minIconsPerChunk: 40,
};
/**
* Storage configuration
*/
export const storageConfig: MemoryStorageConfig = {
// Cache directory, use {cache} to point for relative to cacheRootDir from app config
// Without trailing '/'
cacheDir: '{cache}/storage',
// Maximum number of stored items. 0 to disable
maxCount: 100,
// Minimum delay in milliseconds when data can expire.
// Should be set to at least 10 seconds (10000) to avoid repeated read operations
minExpiration: 20000,
// Timeout in milliseconds to check expired items, > 0 (if disabled, cleanupAfterSec is not ran)
timer: 60000,
// Number of milliseconds to keep item in storage after last use, > minExpiration
cleanupAfter: 0,
};

51
src/config/icon-sets.ts Normal file
View File

@ -0,0 +1,51 @@
import { DirectoryDownloader } from '../downloaders/directory';
import { createJSONDirectoryImporter } from '../importers/full/directory-json';
import { directoryExists } from '../misc/files';
import type { Importer } from '../types/importers';
import type { ImportedData } from '../types/importers/common';
import { fullPackageImporter } from './importers/full-package';
import { splitPackagesImporter } from './importers/split-packages';
/**
* Sources
*
* Change this function to configure sources for your API instance
*/
export async function getImporters(): Promise<Importer[]> {
// Result
const importers: Importer[] = [];
/**
* Import all icon sets from big package
*
* Uses pre-configured importers. See `importers` sub-directory
*/
type IconifyIconSetsOptions = 'full' | 'split' | false;
const iconifyIconSets = 'full' as IconifyIconSetsOptions;
switch (iconifyIconSets) {
case 'full':
importers.push(fullPackageImporter);
break;
case 'split':
importers.push(splitPackagesImporter);
break;
}
/**
* Add custom icons from `json` directory
*/
if (await directoryExists('json')) {
importers.push(
createJSONDirectoryImporter(new DirectoryDownloader<ImportedData>('json'), {
// Filter icon sets. Returns true if icon set should be included, false if not.
filter: (prefix) => {
return true;
},
})
);
}
return importers;
}

View File

@ -0,0 +1,46 @@
import { RemoteDownloader } from '../../downloaders/remote';
import { createIconSetsPackageImporter } from '../../importers/full/json';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote';
import type { ImportedData } from '../../types/importers/common';
/**
* Importer for all icon sets from `@iconify/json` package
*/
// Source options, select one you prefer
// Import from NPM. Does not require any additonal configuration
const npm: RemoteDownloaderOptions = {
downloadType: 'npm',
package: '@iconify/json',
};
// Import from GitHub. Requires setting GitHub API token in environment variable `GITHUB_TOKEN`
const github: RemoteDownloaderOptions = {
downloadType: 'github',
user: 'iconify',
repo: 'icon-sets',
branch: 'master',
token: process.env['GITHUB_TOKEN'] || '',
};
// Import from GitHub using git client. Does not require any additonal configuration
const git: RemoteDownloaderOptions = {
downloadType: 'git',
remote: 'https://github.com/iconify/icon-sets.git',
branch: 'master',
};
export const fullPackageImporter = createIconSetsPackageImporter(
new RemoteDownloader<ImportedData>(
npm,
// Automatically update on startup: boolean
true
),
{
// Filter icon sets. Returns true if icon set should be included, false if not
filter: (prefix) => {
return true;
},
}
);

View File

@ -0,0 +1,41 @@
import { RemoteDownloader } from '../../downloaders/remote';
import { createJSONCollectionsListImporter } from '../../importers/collections/collections';
import { createJSONPackageIconSetImporter } from '../../importers/icon-set/json-package';
import type { IconSetImportedData, ImportedData } from '../../types/importers/common';
// Automatically update on startup: boolean
const autoUpdate = true;
/**
* Importer for all icon sets from `@iconify/collections` and `@iconify-json/*` packages
*
* Differences from full importer in `full-package.ts`:
* - Slower to start because it requires downloading many packages
* - Easier to automatically keep up to date because each package is updated separately, using less storage
*/
export const splitPackagesImporter = createJSONCollectionsListImporter(
new RemoteDownloader<ImportedData>(
{
downloadType: 'npm',
package: '@iconify/collections',
},
autoUpdate
),
(prefix) =>
createJSONPackageIconSetImporter(
new RemoteDownloader<IconSetImportedData>(
{
downloadType: 'npm',
package: `@iconify-json/${prefix}`,
},
autoUpdate
),
{ prefix }
),
{
// Filter icon sets. Returns true if icon set should be included, false if not
filter: (prefix) => {
return true;
},
}
);

View File

@ -0,0 +1,73 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
import { defaultIconDimensions } from '@iconify/utils/lib/icon/defaults';
import type { SplitIconSetConfig } from '../../../types/config/split';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split';
const iconDimensionProps = Object.keys(defaultIconDimensions) as (keyof typeof defaultIconDimensions)[];
const iconSetMainDataProps: (keyof SplitIconifyJSONMainData)[] = [
'prefix',
'lastModified',
'aliases',
...iconDimensionProps,
];
/**
* Get main data
*/
export function splitIconSetMainData(iconSet: IconifyJSON): SplitIconifyJSONMainData {
const result = {} as SplitIconifyJSONMainData;
for (let i = 0; i < iconSetMainDataProps.length; i++) {
const prop = iconSetMainDataProps[i];
if (iconSet[prop]) {
result[prop as 'prefix'] = iconSet[prop as 'prefix'];
} else if (prop === 'aliases') {
result[prop] = Object.create(null);
}
}
if (!iconSet.aliases) {
result.aliases = Object.create(null);
}
return result;
}
/**
* Get size of icons without serialising whole thing, used for splitting icon set
*/
export function getIconSetIconsSize(icons: IconifyIcons): number {
let length = 0;
for (const name in icons) {
length += icons[name].body.length;
}
return length;
}
/**
* Split icon set
*/
export function getIconSetSplitChunksCount(icons: IconifyIcons, config: SplitIconSetConfig): number {
const chunkSize = config.chunkSize;
if (!chunkSize) {
return 1;
}
// Calculate split based on icon count
const numIcons = Object.keys(icons).length;
const resultFromCount = Math.floor(numIcons / config.minIconsPerChunk);
if (resultFromCount < 3) {
// Too few icons: don't split
return 1;
}
// Calculate number of chunks from icons size
const size = getIconSetIconsSize(icons);
const resultFromSize = Math.floor(size / chunkSize);
if (resultFromSize < 3) {
// Too small: don't split
return 1;
}
return Math.min(resultFromCount, resultFromSize);
}

View File

@ -0,0 +1,90 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
import { splitIconSetConfig, storageConfig } from '../../../config/app';
import type { SplitIconSetConfig } from '../../../types/config/split';
import type { StoredIconSet, StoredIconSetDone } from '../../../types/icon-set/storage';
import type { SplitRecord } from '../../../types/split';
import type { MemoryStorage, MemoryStorageItem } from '../../../types/storage';
import { createSplitRecordsTree, splitRecords } from '../../storage/split';
import { createStorage, createStoredItem } from '../../storage/create';
import { getIconSetSplitChunksCount, splitIconSetMainData } from './split';
/**
* Storage
*/
export const iconSetsStorage = createStorage<IconifyIcons>(storageConfig);
/**
* Counter for prefixes
*/
let counter = Date.now();
/**
* Split and store icon set
*/
export function storeLoadedIconSet(
iconSet: IconifyJSON,
done: StoredIconSetDone,
// Optional parameters, can be changed if needed
storage: MemoryStorage<IconifyIcons> = iconSetsStorage,
config: SplitIconSetConfig = splitIconSetConfig
) {
// Get common items
const common = splitIconSetMainData(iconSet);
// Get number of chunks
const chunksCount = getIconSetSplitChunksCount(iconSet.icons, config);
// Stored items
const splitItems: SplitRecord<MemoryStorageItem<IconifyIcons>>[] = [];
const storedItems: MemoryStorageItem<IconifyIcons>[] = [];
// Split
const cachePrefix = `${iconSet.prefix}.${counter++}.`;
splitRecords(
iconSet.icons,
chunksCount,
(splitIcons, next, index) => {
// Store data
createStoredItem<IconifyIcons>(storage, splitIcons.data, cachePrefix + index, true, (storedItem) => {
// Create split record for stored item
const storedSplitItem: SplitRecord<typeof storedItem> = {
keyword: splitIcons.keyword,
data: storedItem,
};
storedItems.push(storedItem);
splitItems.push(storedSplitItem);
next();
});
},
() => {
// Create tree
const tree = createSplitRecordsTree(splitItems);
// Generate result
const result: StoredIconSet = {
common,
storage,
items: storedItems,
tree,
};
if (iconSet.info) {
result.info = iconSet.info;
}
done(result);
}
);
}
/**
* Promise version of storeLoadedIconSet()
*/
export function asyncStoreLoadedIconSet(
iconSet: IconifyJSON,
// Optional parameters, can be changed if needed
storage: MemoryStorage<IconifyIcons> = iconSetsStorage,
config: SplitIconSetConfig = splitIconSetConfig
): Promise<StoredIconSet> {
return new Promise((fulfill) => {
storeLoadedIconSet(iconSet, fulfill, storage, config);
});
}

View File

@ -0,0 +1,43 @@
import type { IconifyJSON } from '@iconify/types';
/**
* Removes bad aliases
*/
export function removeBadAliases(data: IconifyJSON) {
const icons = data.icons;
const aliases = data.aliases || {};
const tested: Set<string> = new Set();
const failed: Set<string> = new Set();
function resolve(name: string): boolean {
if (icons[name]) {
return true;
}
if (!tested.has(name)) {
// Temporary mark as failed if parent alias points to this alias to avoid infinite loop
tested.add(name);
failed.add(name);
// Get parent icon name and resolve it
const parent = aliases[name]?.parent;
if (parent && resolve(parent)) {
failed.delete(name);
}
}
return !failed.has(name);
}
// Resolve aliases
const keys = Object.keys(aliases);
for (let i = 0; i < keys.length; i++) {
resolve(keys[i]);
}
// Remove failed aliases
failed.forEach((name) => {
delete aliases[name];
});
}

View File

@ -0,0 +1,94 @@
import type { ExtendedIconifyAlias, ExtendedIconifyIcon, IconifyIcons, IconifyJSON } from '@iconify/types';
import { mergeIconData } from '@iconify/utils/lib/icon/merge';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split';
import type { StoredIconSet } from '../../../types/icon-set/storage';
import { searchSplitRecordsTree } from '../../storage/split';
import { getStoredItem } from '../../storage/get';
interface PrepareResult {
// Merged properties
props: ExtendedIconifyIcon | ExtendedIconifyAlias;
// Name of icon to merge with
name: string;
}
function prepareAlias(data: SplitIconifyJSONMainData, name: string): PrepareResult {
const aliases = data.aliases;
// Resolve aliases tree
let props: ExtendedIconifyIcon | ExtendedIconifyAlias = aliases[name];
name = props.parent;
while (true) {
const alias = aliases[name];
if (alias) {
// Another alias
props = mergeIconData(alias, props);
name = alias.parent;
} else {
// Icon
return {
props,
name,
};
}
}
}
/**
* Get icon data
*
* Assumes that icon exists and valid. Should validate icon set and load data before running this function
*/
export function getIconData(data: SplitIconifyJSONMainData, name: string, icons: IconifyIcons): ExtendedIconifyIcon {
// Get data
let props: ExtendedIconifyIcon | ExtendedIconifyAlias;
if (icons[name]) {
// Icon: copy as is
props = icons[name];
} else {
// Resolve alias
const result = prepareAlias(data, name);
props = mergeIconData(icons[result.name], result.props);
}
// Add default values
return mergeIconData(data, props) as unknown as ExtendedIconifyIcon;
}
/**
* Get icon data from stored icon set
*/
export function getStoredIconData(
iconSet: StoredIconSet,
name: string,
callback: (data: ExtendedIconifyIcon | null) => void
) {
const common = iconSet.common;
// Get data
let props: ExtendedIconifyIcon | ExtendedIconifyAlias;
if (common.aliases[name]) {
const resolved = prepareAlias(common, name);
props = resolved.props;
name = resolved.name;
} else {
props = {} as ExtendedIconifyIcon;
}
// Load icon
const chunk = searchSplitRecordsTree(iconSet.tree, name);
getStoredItem(iconSet.storage, chunk, (data) => {
if (!data || !data[name]) {
// Failed
callback(null);
return;
}
// Merge icon data with aliases
props = mergeIconData(data[name], props);
// Add default values
callback(mergeIconData(common, props) as unknown as ExtendedIconifyIcon);
});
}

View File

@ -0,0 +1,150 @@
import type { IconifyJSON, IconifyAliases, IconifyIcons } from '@iconify/types';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split';
import type { StoredIconSet } from '../../../types/icon-set/storage';
import { searchSplitRecordsTreeForSet } from '../../storage/split';
import { getStoredItem } from '../../storage/get';
/**
* Get list of icons that must be retrieved
*/
export function getIconsToRetrieve(
iconSetData: SplitIconifyJSONMainData,
names: string[],
copyTo?: IconifyAliases
): Set<string> {
const icons: Set<string> = new Set();
const aliases = iconSetData.aliases || {};
function resolve(name: string) {
if (!aliases[name]) {
// Icon
icons.add(name);
return;
}
// Alias: copy it
const item = aliases[name];
copyTo && (copyTo[name] = item);
// Resolve parent
resolve(item.parent);
}
for (let i = 0; i < names.length; i++) {
resolve(names[i]);
}
return icons;
}
/**
* Extract icons from chunks of icon data
*/
export function getIconsData(
iconSetData: SplitIconifyJSONMainData,
names: string[],
sourceIcons: IconifyIcons[]
): IconifyJSON {
const sourceAliases = iconSetData.aliases;
const icons = Object.create(null) as IconifyJSON['icons'];
const aliases = Object.create(null) as IconifyAliases;
const result: IconifyJSON = {
...iconSetData,
icons,
aliases,
};
function resolve(name: string): boolean {
if (!sourceAliases[name]) {
// Icon
for (let i = 0; i < sourceIcons.length; i++) {
const item = sourceIcons[i];
if (name in item) {
icons[name] = item[name];
return true;
}
}
} else if (name in sourceAliases) {
// Alias
if (name in aliases) {
// Already resolved
return true;
}
const item = sourceAliases[name];
if (resolve(item.parent)) {
aliases[name] = item;
return true;
}
}
// Missing
(result.not_found || (result.not_found = [])).push(name);
return false;
}
for (let i = 0; i < names.length; i++) {
resolve(names[i]);
}
return result;
}
/**
* Get icons from stored icon set
*/
export function getStoredIconsData(iconSet: StoredIconSet, names: string[], callback: (data: IconifyJSON) => void) {
// Get list of icon names
const aliases = Object.create(null) as IconifyAliases;
const iconNames = Array.from(getIconsToRetrieve(iconSet.common, names, aliases));
if (!iconNames.length) {
// Nothing to retrieve
callback({
...iconSet.common,
icons: {},
aliases,
not_found: names,
});
return;
}
// Get map of chunks to load
const chunks = searchSplitRecordsTreeForSet(iconSet.tree, iconNames);
let pending = chunks.size;
let not_found: string[] | undefined;
const icons = Object.create(null) as IconifyIcons;
const storage = iconSet.storage;
chunks.forEach((names, storedItem) => {
getStoredItem(storage, storedItem, (data) => {
// Copy data from chunk
if (!data) {
not_found = names.concat(not_found || []);
} else {
for (let i = 0; i < names.length; i++) {
const name = names[i];
if (data[name]) {
icons[name] = data[name];
} else {
(not_found || (not_found = [])).push(name);
}
}
}
// Check if all chunks have loaded
pending--;
if (!pending) {
const result: IconifyJSON = {
...iconSet.common,
icons,
aliases,
};
if (not_found) {
result.not_found = not_found;
}
callback(result);
}
});
});
}

86
src/data/icon-sets.ts Normal file
View File

@ -0,0 +1,86 @@
import type { StoredIconSet } from '../types/icon-set/storage';
import type { IconSetEntry, Importer } from '../types/importers';
/**
* All importers
*/
let importers: Importer[] | undefined;
export function setImporters(items: Importer[]) {
if (importers) {
throw new Error('Importers can be set only once');
}
importers = items;
}
/**
* All prefixes, sorted
*/
let prefixes: string[] = [];
/**
* Get all prefixes
*/
export function getPrefixes(): string[] {
return prefixes;
}
/**
* All icon sets
*/
export const iconSets = Object.create(null) as Record<string, IconSetEntry>;
/**
* Loaded icon sets
*/
let loadedIconSets: Set<StoredIconSet> = new Set();
/**
* Merge data
*/
export function updateIconSets(): number {
if (!importers) {
return 0;
}
const newLoadedIconSets: Set<StoredIconSet> = new Set();
const newPrefixes: Set<string> = new Set();
importers.forEach((importer, importerIndex) => {
const data = importer.data;
if (!data) {
return;
}
data.prefixes.forEach((prefix) => {
const item = data.iconSets[prefix];
if (!item) {
return;
}
// Add to list of loaded icon sets
newLoadedIconSets.add(item);
loadedIconSets.delete(item);
// Add prefix, but delete it first to keep order
newPrefixes.delete(prefix);
newPrefixes.add(prefix);
// Set data
iconSets[prefix] = {
importer,
item,
};
});
});
// Replace list of icon sets
if (loadedIconSets.size) {
// Got some icon sets to clean up
const cleanup = loadedIconSets;
}
loadedIconSets = newLoadedIconSets;
// Update prefixes
prefixes = Array.from(newPrefixes);
return prefixes.length;
}

34
src/data/loading.ts Normal file
View File

@ -0,0 +1,34 @@
// Status
let loading = true;
// Queue
type Callback = () => void;
const queue: Callback[] = [];
/**
* Loaded: run queue
*/
export function loaded() {
loading = false;
// Run queue
let callback: Callback | undefined;
while ((callback = queue.shift())) {
try {
callback();
} catch (err) {
console.error(err);
}
}
}
/**
* Run when app is ready
*/
export function runWhenLoaded(callback: Callback) {
if (!loading) {
callback();
} else {
queue.push(callback);
}
}

View File

@ -0,0 +1,21 @@
import type { MemoryStorageItem, MemoryStorageCallback } from '../../types/storage';
/**
* Run all callbacks from storage
*/
export function runStorageCallbacks<T>(storedItem: MemoryStorageItem<T>, force = false) {
// Get data
const data = storedItem.data;
if (!data && !force) {
return;
}
// Update time
storedItem.lastUsed = Date.now();
// Run all callbacks
let callback: MemoryStorageCallback<T> | undefined;
while ((callback = storedItem.callbacks.shift())) {
callback(data || null);
}
}

117
src/data/storage/cleanup.ts Normal file
View File

@ -0,0 +1,117 @@
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage';
import { runStorageCallbacks } from './callbacks';
import { writeStoredItem } from './write';
/**
* Stop timer
*/
function stopTimer<T>(storage: MemoryStorage<T>) {
if (storage.timer) {
clearInterval(storage.timer);
delete storage.timer;
}
}
/**
* Clean up stored item
*/
export function cleanupStoredItem<T>(storage: MemoryStorage<T>, storedItem: MemoryStorageItem<T>): boolean {
if (!storedItem.cache?.exists) {
// Cannot be cleaned up
return false;
}
if (storedItem.callbacks.length) {
// Callbacks exist ???
if (storedItem.data) {
runStorageCallbacks(storedItem);
return false;
}
return true;
}
// Cache stored: clean up
delete storedItem.data;
storage.watched.delete(storedItem);
if (!storage.watched.size) {
stopTimer(storage);
}
return true;
}
/**
* Clean up stored items
*/
export function cleanupStorage<T>(storage: MemoryStorage<T>) {
const config = storage.config;
const watched = storage.watched;
// Items with laseUsed > lastUsedLimit cannot be cleaned up
// If not set, allow items to be stored for at least 10 seconds
const lastUsedLimit = Date.now() - (config.minExpiration || 10000);
// Check timer limit
const cleanupAfter = config.cleanupAfter;
if (cleanupAfter) {
const minTimer = Math.min(Date.now() - cleanupAfter, lastUsedLimit);
watched.forEach((item) => {
if (item.lastUsed < minTimer) {
cleanupStoredItem(storage, item);
}
});
}
// Check items limit
const maxCount = config.maxCount;
if (maxCount && watched.size > maxCount) {
// Sort items
const sortedList = Array.from(watched).sort((item1, item2) => item1.lastUsed - item2.lastUsed);
// Delete items, sorted by `lastUsed`
for (let i = 0; i < sortedList.length && watched.size > maxCount; i++) {
// Attempt to remove item
const item = sortedList[i];
if (item.lastUsed < lastUsedLimit) {
cleanupStoredItem(storage, item);
}
}
}
}
/**
* Add storage to cleanup queue
*
* Should be called after writeStoredItem() or loadStoredItem()
*/
export function addStorageToCleanup<T>(storage: MemoryStorage<T>, storedItem: MemoryStorageItem<T>) {
if (!storedItem.data) {
// Nothing to watch
return;
}
const config = storage.config;
const watched = storage.watched;
watched.add(storedItem);
// Set timer
if (!storage.timer) {
const timerDuration = config.timer;
const cleanupAfter = config.cleanupAfter;
if (timerDuration && cleanupAfter) {
storage.timer = setInterval(() => {
// Callback for debugging
config.timerCallback?.();
// Run cleanup
cleanupStorage(storage);
}, timerDuration);
}
}
// Clean up items immediately if there are too many
if (config.maxCount && watched.size >= config.maxCount) {
cleanupStorage(storage);
}
}

View File

@ -0,0 +1,57 @@
import { appConfig } from '../../config/app';
import type { MemoryStorage, MemoryStorageConfig, MemoryStorageItem } from '../../types/storage';
import { cleanupStoredItem } from './cleanup';
import { writeStoredItem } from './write';
/**
* Create storage
*/
export function createStorage<T>(config: MemoryStorageConfig): MemoryStorage<T> {
return {
config,
watched: new Set(),
pendingReads: new Set(),
pendingWrites: new Set(),
};
}
/**
* Create item to store
*/
export function createStoredItem<T>(
storage: MemoryStorage<T>,
data: T,
cacheFile: string,
autoCleanup = true,
done?: (storedItem: MemoryStorageItem<T>, err?: NodeJS.ErrnoException) => void
): MemoryStorageItem<T> {
const filename = storage.config.cacheDir.replace('{cache}', appConfig.cacheRootDir) + '/' + cacheFile;
const storedItem: MemoryStorageItem<T> = {
cache: {
filename,
exists: false,
},
data,
callbacks: [],
lastUsed: autoCleanup ? 0 : Date.now(),
};
// Save cache if cleanup is enabled
const storageConfig = storage.config;
if (storageConfig.maxCount || storageConfig.cleanupAfter) {
writeStoredItem(storage, storedItem, (err) => {
if (autoCleanup && !err) {
// Remove item if not used and not failed
if (!storedItem.lastUsed) {
cleanupStoredItem(storage, storedItem);
}
}
done?.(storedItem, err);
});
} else {
done?.(storedItem);
}
return storedItem;
}

24
src/data/storage/get.ts Normal file
View File

@ -0,0 +1,24 @@
import type { MemoryStorageItem, MemoryStorageCallback, MemoryStorage } from '../../types/storage';
import { loadStoredItem } from './load';
/**
* Get storage data when ready
*/
export function getStoredItem<T>(
storage: MemoryStorage<T>,
storedItem: MemoryStorageItem<T>,
callback: MemoryStorageCallback<T>
) {
if (storedItem.data) {
// Data is already available: run callback
storedItem.lastUsed = Date.now();
callback(storedItem.data);
return;
}
// Add callback to queue
storedItem.callbacks.push(callback);
// Load storage
loadStoredItem(storage, storedItem);
}

41
src/data/storage/load.ts Normal file
View File

@ -0,0 +1,41 @@
import { readFile } from 'node:fs';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage';
import { runStorageCallbacks } from './callbacks';
import { addStorageToCleanup } from './cleanup';
/**
* Load data
*/
export function loadStoredItem<T>(storage: MemoryStorage<T>, storedItem: MemoryStorageItem<T>) {
const pendingReads = storage.pendingReads;
if (storedItem.data || pendingReads.has(storedItem)) {
// Already loaded or loading
return;
}
const config = storedItem.cache;
if (!config?.exists) {
// Cannot load
return;
}
// Load file
pendingReads.add(storedItem);
readFile(config.filename, 'utf8', (err, dataStr) => {
pendingReads.delete(storedItem);
if (err) {
// Failed
console.error(err);
runStorageCallbacks(storedItem, true);
return;
}
// Loaded
storedItem.data = JSON.parse(dataStr) as T;
runStorageCallbacks(storedItem);
// Add to cleanup queue
addStorageToCleanup(storage, storedItem);
});
}

149
src/data/storage/split.ts Normal file
View File

@ -0,0 +1,149 @@
import type { SplitDataTree, SplitRecord, SplitRecordCallback } from '../../types/split';
/**
* Split records into `count` chunks
*
* Calls `callback` for each chunk, which should call `next` param to continue splitting.
* This is done to store data in cache in small chunks when splitting large icon
* set, allowing memory to be collected after each chunk
*
* Calls `done` when done
*/
export function splitRecords<T>(
data: Record<string, T>,
numChunks: number,
callback: SplitRecordCallback<Record<string, T>>,
done: () => void
) {
const keys = Object.keys(data).sort((a, b) => a.localeCompare(b));
const total = keys.length;
let start = 0;
let index = 0;
const next = () => {
if (index === numChunks) {
// Done
done();
return;
}
const end = index === numChunks - 1 ? total : Math.round((total * (index + 1)) / numChunks);
const keywords = keys.slice(start, end);
// Copy data
const itemData = Object.create(null) as typeof data;
for (let j = 0; j < keywords.length; j++) {
const keyword = keywords[j];
itemData[keyword] = data[keyword];
}
const item: SplitRecord<Record<string, T>> = {
keyword: keywords[0],
data: itemData,
};
start = end;
index++;
// Call callback
callback(item, next, index - 1, numChunks);
};
next();
}
/**
* Create tree for searching split records list
*/
export function createSplitRecordsTree<T>(items: SplitRecord<T>[]): SplitDataTree<T> {
const length = items.length;
const midIndex = Math.floor(length / 2);
const midItem = items[midIndex];
const keyword = midItem.keyword;
// Check if item can be split
const hasNext = length > midIndex + 1;
if (!midIndex && !hasNext) {
// Not split
return {
split: false,
match: midItem.data,
};
}
// Add keyword and current item
const tree: SplitDataTree<T> = {
split: true,
keyword,
match: midItem.data,
};
// Add previous items
if (midIndex) {
tree.prev = createSplitRecordsTree(items.slice(0, midIndex));
}
// Next items
if (hasNext) {
tree.next = createSplitRecordsTree(items.slice(midIndex));
}
return tree;
}
/**
* Find item
*/
export function searchSplitRecordsTree<T>(tree: SplitDataTree<T>, keyword: string): T {
if (!tree.split) {
return tree.match;
}
const match = keyword.localeCompare(tree.keyword);
if (match < 0) {
return tree.prev ? searchSplitRecordsTree(tree.prev, keyword) : tree.match;
}
return match > 0 && tree.next ? searchSplitRecordsTree(tree.next, keyword) : tree.match;
}
/**
* Find multiple items
*/
export function searchSplitRecordsTreeForSet<T>(tree: SplitDataTree<T>, keywords: string[]): Map<T, string[]> {
const map: Map<T, string[]> = new Map();
function search(tree: SplitDataTree<T>, keywords: string[]) {
if (!tree.split) {
// Not split
map.set(tree.match, keywords);
return;
}
const prev: string[] = [];
const next: string[] = [];
const matches: string[] = [];
for (let i = 0; i < keywords.length; i++) {
const keyword = keywords[i];
const match = keyword.localeCompare(tree.keyword);
if (match < 0) {
(tree.prev ? prev : matches).push(keyword);
} else {
(match > 0 && tree.next ? next : matches).push(keyword);
}
}
if (tree.prev && prev.length) {
search(tree.prev, prev);
}
if (tree.next && next.length) {
search(tree.next, next);
}
if (matches.length) {
map.set(tree.match, matches);
}
}
search(tree, keywords);
return map;
}

55
src/data/storage/write.ts Normal file
View File

@ -0,0 +1,55 @@
import { writeFile, mkdir } from 'node:fs';
import { dirname } from 'node:path';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage';
import { addStorageToCleanup } from './cleanup';
/**
* Write storage to file
*/
export function writeStoredItem<T>(
storage: MemoryStorage<T>,
storedItem: MemoryStorageItem<T>,
done?: (err?: NodeJS.ErrnoException) => void
) {
const pendingWrites = storage.pendingWrites;
const data = storedItem.data;
const config = storedItem.cache;
if (!data || !config || pendingWrites.has(storedItem)) {
// Missing content or disabled or already writing
done?.();
return;
}
// Serialise and store data
const dataStr = JSON.stringify(data);
pendingWrites.add(storedItem);
// Create directory
const filename = config.filename;
const dir = dirname(filename);
mkdir(
dir,
{
recursive: true,
},
() => {
// Write file
writeFile(filename, dataStr, 'utf8', (err) => {
pendingWrites.delete(storedItem);
if (err) {
// Error
console.error(err);
} else {
// Success
config.exists = true;
// Data is written, storage can be cleaned up when needed
addStorageToCleanup(storage, storedItem);
}
done?.(err || void 0);
});
}
);
}

193
src/downloaders/base.ts Normal file
View File

@ -0,0 +1,193 @@
import type { DownloaderStatus, DownloaderType } from '../types/downloaders/base';
/**
* loadDataFromDirectory()
*/
type DataUpdated<DataType> = (data: DataType) => Promise<unknown>;
/**
* loadDataFromDirectory()
*/
type LoadData<DataType> = () => Promise<DataType | void | undefined>;
/**
* loadDataFromDirectory()
*/
type LoadDataFromDirectory<DataType> = (path: string) => Promise<DataType | void | undefined>;
/**
* Base downloader class, shared with all child classes
*/
export abstract class BaseDownloader<DataType> {
// Downloader type, set in child class
type!: DownloaderType;
// Downloader status
status: DownloaderStatus = 'pending-init';
// Data
data?: DataType;
// Waiting for reload
// Can be reset in _checkForUpdate() function immediately during check for redundancy
// to avoid running same check multiple times that might happen in edge cases
_pendingReload = false;
/**
* Load data from custom source, should be overwrtten by loader
*
* Used by loaders that do not implement _loadDataFromDirectory()
*/
_loadData?: LoadData<DataType>;
/**
* Load data from directory, should be overwritten by loader
*
* Used by loaders that do not implement _loadData()
*/
_loadDataFromDirectory?: LoadDataFromDirectory<DataType>;
/**
* Function to call when data has been updated
*/
_dataUpdated?: DataUpdated<DataType>;
/**
* Load content. Called when content is ready to be loaded, should be overwritten by child classes
*/
async _loadContent() {
throw new Error('_loadContent() not implemented');
}
/**
* Initialise downloader
*
* Returns true on success, false or reject on fatal error.
*/
async _init(): Promise<boolean> {
throw new Error('_init() not implemented');
}
/**
* Initialise downloader
*
* Returns false on error
*/
async init(): Promise<boolean> {
if (this.status === 'pending-init') {
this.status = 'initialising';
let result: boolean;
try {
result = await this._init();
} catch (err) {
// _init() failed
console.error(err);
this.status = false;
return false;
}
if (result) {
// Check for update if reload is pending
if (this._pendingReload) {
await this._checkForUpdateLoop();
}
// Load content
await this._loadContent();
}
// Update status
this.status = result;
return result;
}
return false;
}
/**
* Check for update
*
* Function should update latest version value before calling done(true)
* All errors should be caught and callbac must finish. In case of error, return done(false)
*/
_checkForUpdate(done: (value: boolean) => void) {
throw new Error('_checkForUpdate() not implemented');
}
/**
* Promise wrapper for _checkForUpdate()
*/
_checkForUpdateLoop(): Promise<boolean> {
return new Promise((fulfill, reject) => {
let updated = false;
let changedStatus = false;
// Change status
if (this.status === true) {
this.status = 'updating';
changedStatus = true;
}
const check = (value: boolean) => {
updated = updated || value;
if (value) {
// Successful update: reload data
this._loadContent()
.then(() => {
check(false);
})
.catch((err) => {
// Failed
if (changedStatus) {
this.status = true;
}
reject(err);
});
return;
}
if (this._pendingReload) {
// Run reload
this._pendingReload = false;
this._checkForUpdate(check);
return;
}
// Done
if (changedStatus) {
this.status = true;
}
fulfill(updated);
};
check(false);
});
}
/**
* Check for update
*/
checkForUpdate(): Promise<boolean> {
return new Promise((fulfill, reject) => {
if (this.status === false) {
fulfill(false);
return;
}
if (this._pendingReload) {
// Already pending: should be handled
fulfill(false);
return;
}
this._pendingReload = true;
if (this.status === true) {
// Check immediately
this._checkForUpdateLoop().then(fulfill).catch(reject);
} else {
// Another action is running
fulfill(false);
}
});
}
}

27
src/downloaders/custom.ts Normal file
View File

@ -0,0 +1,27 @@
import { BaseDownloader } from './base';
/**
* Custom downloader
*
* Class extending this downloader must implement:
* - constructor()
* - _init()
* - _checkForUpdate()
* - _loadData()
*/
export class CustomDownloader<DataType> extends BaseDownloader<DataType> {
/**
* Load content
*/
async _loadContent() {
if (!this._loadData) {
throw new Error('Importer does not implement _loadData()');
}
const result = await this._loadData();
if (result) {
this.data = result;
await this._dataUpdated?.(result);
}
}
}

View File

@ -0,0 +1,74 @@
import { directoryExists, hashFiles, listFilesInDirectory } from '../misc/files';
import { BaseDownloader } from './base';
/**
* Directory downloader
*
* Class extending this downloader must implement:
* - _loadDataFromDirectory()
*/
export class DirectoryDownloader<DataType> extends BaseDownloader<DataType> {
// Source directory
path: string;
// Last hash
_lastHash: string = '';
/**
* Constructor
*/
constructor(path: string) {
super();
this.path = path;
}
/**
* Hash content
*/
async _hashContent(): Promise<string> {
const files = await listFilesInDirectory(this.path);
return hashFiles(files);
}
/**
* Init downloader
*/
async _init() {
if (!(await directoryExists(this.path))) {
return false;
}
this._lastHash = await this._hashContent();
return true;
}
/**
* Check if files were changed
*/
_checkForUpdate(done: (value: boolean) => void): void {
this._hashContent()
.then((hash) => {
const changed = this._lastHash !== hash;
this._lastHash = hash;
done(changed);
})
.catch((err) => {
console.error(err);
done(false);
});
}
/**
* Load content
*/
async _loadContent() {
if (!this._loadDataFromDirectory) {
throw new Error('Importer does not implement _loadDataFromDirectory()');
}
const result = await this._loadDataFromDirectory(this.path);
if (result) {
this.data = result;
await this._dataUpdated?.(result);
}
}
}

139
src/downloaders/remote.ts Normal file
View File

@ -0,0 +1,139 @@
import { directoryExists } from '../misc/files';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../types/downloaders/remote';
import { BaseDownloader } from './base';
import { downloadRemoteArchive } from './remote/download';
import { getRemoteDownloaderCacheKey } from './remote/key';
import { getDownloaderVersion, saveDownloaderVersion } from './remote/versions';
/**
* Remote downloader
*
* Class extending this downloader must implement:
* - _loadDataFromDirectory()
*/
export class RemoteDownloader<DataType> extends BaseDownloader<DataType> {
// Params
_downloader: RemoteDownloaderOptions;
_autoUpdate: boolean;
// Source directory
_sourceDir?: string;
// Latest version
_version?: RemoteDownloaderVersion;
/**
* Constructor
*/
constructor(downloader: RemoteDownloaderOptions, autoUpdate?: boolean) {
super();
this._downloader = downloader;
this._autoUpdate = !!autoUpdate;
}
/**
* Init downloader
*/
async _init() {
const downloader = this._downloader;
const cacheKey = getRemoteDownloaderCacheKey(downloader);
// Get last stored version
const lastVersion = await getDownloaderVersion(cacheKey, downloader.downloadType);
if (lastVersion && !this._autoUpdate) {
// Keep last version
const directory = lastVersion.contentsDir;
if (await directoryExists(directory)) {
// Keep old version
this._sourceDir = directory;
this._version = lastVersion;
return true;
}
}
// Missing or need to check for update
const version = await downloadRemoteArchive(
downloader,
lastVersion?.downloadType === downloader.downloadType ? lastVersion : void 0
);
if (version === false) {
if (lastVersion) {
// Keep last version
const directory = lastVersion.contentsDir;
if (await directoryExists(directory)) {
// Keep old version
this._sourceDir = directory;
this._version = lastVersion;
return true;
}
}
// Failed
return false;
}
// Use `version`
const directory = version.contentsDir;
if (await directoryExists(directory)) {
await saveDownloaderVersion(cacheKey, version);
this._sourceDir = directory;
this._version = version;
return true;
}
// Failed
return false;
}
/**
* Check for update
*/
_checkForUpdate(done: (value: boolean) => void): void {
const downloader = this._downloader;
// Promise version of _checkForUpdate()
const check = async () => {
const lastVersion = this._version;
// Check for update
const version = await downloadRemoteArchive(
downloader,
lastVersion?.downloadType === downloader.downloadType ? lastVersion : void 0
);
if (version === false) {
// Nothing to update
return false;
}
// Save new version, use it
await saveDownloaderVersion(getRemoteDownloaderCacheKey(downloader), version);
this._sourceDir = version.contentsDir;
this._version = version;
return true;
};
check()
.then(done)
.catch((err) => {
console.error(err);
done(false);
});
}
/**
* Load content
*/
async _loadContent() {
if (!this._loadDataFromDirectory) {
throw new Error('Importer does not implement _loadDataFromDirectory()');
}
const source = this._sourceDir;
const result = source && (await this._loadDataFromDirectory(source));
if (result) {
this.data = result;
await this._dataUpdated?.(result);
}
}
}

View File

@ -0,0 +1,90 @@
import { execAsync } from '@iconify/tools/lib/misc/exec';
import { getGitHubRepoHash } from '@iconify/tools/lib/download/github/hash';
import { getGitLabRepoHash } from '@iconify/tools/lib/download/gitlab/hash';
import { getNPMVersion, getPackageVersion } from '@iconify/tools/lib/download/npm/version';
import { directoryExists } from '../../misc/files';
import type {
GitDownloaderOptions,
GitDownloaderVersion,
GitHubDownloaderOptions,
GitHubDownloaderVersion,
GitLabDownloaderOptions,
GitLabDownloaderVersion,
NPMDownloaderOptions,
NPMDownloaderVersion,
} from '../../types/downloaders/remote';
/**
* Check git repo for update
*/
export async function isGitUpdateAvailable(
options: GitDownloaderOptions,
oldVersion: GitDownloaderVersion
): Promise<false | GitDownloaderVersion> {
const result = await execAsync(`git ls-remote ${options.remote} --branch ${options.branch}`);
const parts = result.stdout.split(/\s/);
const hash = parts.shift() as string;
if (hash !== oldVersion.hash || !(await directoryExists(oldVersion.contentsDir))) {
const newVerison: GitDownloaderVersion = {
...oldVersion,
hash,
};
return newVerison;
}
return false;
}
/**
* Check GitHub repo for update
*/
export async function isGitHubUpdateAvailable(
options: GitHubDownloaderOptions,
oldVersion: GitHubDownloaderVersion
): Promise<false | GitHubDownloaderVersion> {
const hash = await getGitHubRepoHash(options);
if (hash !== oldVersion.hash || !(await directoryExists(oldVersion.contentsDir))) {
const newVerison: GitHubDownloaderVersion = {
...oldVersion,
hash,
};
return newVerison;
}
return false;
}
/**
* Check GitLab repo for update
*/
export async function isGitLabUpdateAvailable(
options: GitLabDownloaderOptions,
oldVersion: GitLabDownloaderVersion
): Promise<false | GitLabDownloaderVersion> {
const hash = await getGitLabRepoHash(options);
if (hash !== oldVersion.hash || !(await directoryExists(oldVersion.contentsDir))) {
const newVerison: GitLabDownloaderVersion = {
...oldVersion,
hash,
};
return newVerison;
}
return false;
}
/**
* Check NPM package for update
*/
export async function isNPMUpdateAvailable(
options: NPMDownloaderOptions,
oldVersion: NPMDownloaderVersion
): Promise<false | NPMDownloaderVersion> {
const { version } = await getNPMVersion(options);
const dir = oldVersion.contentsDir;
if (version !== oldVersion.version || !(await directoryExists(dir)) || (await getPackageVersion(dir)) !== version) {
const newVerison: NPMDownloaderVersion = {
...oldVersion,
version,
};
return newVerison;
}
return false;
}

View File

@ -0,0 +1,84 @@
import { downloadGitRepo } from '@iconify/tools/lib/download/git';
import { downloadGitHubRepo } from '@iconify/tools/lib/download/github';
import { downloadGitLabRepo } from '@iconify/tools/lib/download/gitlab';
import { downloadNPMPackage } from '@iconify/tools/lib/download/npm';
import { appConfig } from '../../config/app';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../../types/downloaders/remote';
import {
isGitHubUpdateAvailable,
isGitLabUpdateAvailable,
isGitUpdateAvailable,
isNPMUpdateAvailable,
} from './check-update';
import { getDownloadDirectory } from './target';
/**
* Download files from remote archive
*/
export async function downloadRemoteArchive(
options: RemoteDownloaderOptions,
ifModifiedSince?: RemoteDownloaderVersion | null,
key?: string
): Promise<false | RemoteDownloaderVersion> {
const target = getDownloadDirectory(options, key);
switch (options.downloadType) {
case 'git': {
if (ifModifiedSince?.downloadType === 'git' && !(await isGitUpdateAvailable(options, ifModifiedSince))) {
return false;
}
// Download
return await downloadGitRepo({
target,
log: appConfig.log,
...options,
});
}
case 'github': {
if (
ifModifiedSince?.downloadType === 'github' &&
!(await isGitHubUpdateAvailable(options, ifModifiedSince))
) {
return false;
}
// Download
return await downloadGitHubRepo({
target,
log: appConfig.log,
...options,
});
}
case 'gitlab': {
if (
ifModifiedSince?.downloadType === 'gitlab' &&
!(await isGitLabUpdateAvailable(options, ifModifiedSince))
) {
return false;
}
// Download
return await downloadGitLabRepo({
target,
log: appConfig.log,
...options,
});
}
case 'npm': {
if (ifModifiedSince?.downloadType === 'npm' && !(await isNPMUpdateAvailable(options, ifModifiedSince))) {
return false;
}
// Download
return await downloadNPMPackage({
target,
log: appConfig.log,
...options,
});
}
}
}

View File

@ -0,0 +1,21 @@
import { hashString } from '../../misc/hash';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote';
/**
* Get cache key
*/
export function getRemoteDownloaderCacheKey(options: RemoteDownloaderOptions): string {
switch (options.downloadType) {
case 'git':
return hashString(`${options.remote}#${options.branch}`);
case 'github':
return `${options.user}-${options.repo}-${options.branch}`;
case 'gitlab':
return `${options.uri ? hashString(options.uri + options.project) : options.project}-${options.branch}`;
case 'npm':
return options.package + (options.tag ? '-' + options.tag : '');
}
}

View File

@ -0,0 +1,34 @@
import { downloadGitRepo } from '@iconify/tools/lib/download/git';
import { downloadGitHubRepo } from '@iconify/tools/lib/download/github';
import { downloadGitLabRepo } from '@iconify/tools/lib/download/gitlab';
import { downloadNPMPackage } from '@iconify/tools/lib/download/npm';
import { appConfig } from '../../config/app';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../../types/downloaders/remote';
import {
isGitHubUpdateAvailable,
isGitLabUpdateAvailable,
isGitUpdateAvailable,
isNPMUpdateAvailable,
} from './check-update';
import { getRemoteDownloaderCacheKey } from './key';
/**
* Get directory
*/
export function getDownloadDirectory(options: RemoteDownloaderOptions, key?: string): string {
key = key || getRemoteDownloaderCacheKey(options);
switch (options.downloadType) {
case 'git':
return appConfig.cacheRootDir + '/git/' + key;
case 'github':
return appConfig.cacheRootDir + '/github/' + key;
case 'gitlab':
return appConfig.cacheRootDir + '/github/' + key;
case 'npm':
return appConfig.cacheRootDir + '/npm/' + key;
}
}

View File

@ -0,0 +1,68 @@
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import { appConfig } from '../../config/app';
import type {
RemoteDownloaderType,
RemoteDownloaderVersion,
RemoteDownloaderVersionMixin,
} from '../../types/downloaders/remote';
// Storage
type StoredVersions = Record<string, RemoteDownloaderVersion>;
/**
* Get cache file
*/
function getCacheFile(): string {
return appConfig.cacheRootDir + '/versions.json';
}
/**
* Get data
*/
async function getStoredData(): Promise<StoredVersions> {
try {
return JSON.parse(await readFile(getCacheFile(), 'utf8')) as StoredVersions;
} catch {
return {};
}
}
/**
* Get version
*/
export async function getDownloaderVersion<T extends RemoteDownloaderType>(
key: string,
type: T
): Promise<RemoteDownloaderVersionMixin<T> | null> {
const data = await getStoredData();
const value = data[key];
if (value && value.downloadType === type) {
return value as RemoteDownloaderVersionMixin<T>;
}
return null;
}
/**
* Store downloader version in cache
*/
export async function saveDownloaderVersion(key: string, value: RemoteDownloaderVersion) {
const filename = getCacheFile();
// Create directory for cache, if missing
const dir = dirname(filename);
try {
await mkdir(dir, {
recursive: true,
});
} catch {
//
}
// Update data
const data = await getStoredData();
data[key] = value;
// Store file
await writeFile(filename, JSON.stringify(data, null, '\t'), 'utf8');
}

63
src/http/helpers/json.ts Normal file
View File

@ -0,0 +1,63 @@
import type { FastifyReply } from 'fastify';
const callbackMatch = /^[a-z0-9_.]+$/i;
/**
* Check JSONP query
*/
interface JSONPStatus {
wrap: boolean;
callback: string;
}
export function checkJSONPQuery(
query: Record<string, string>,
forceWrap?: boolean,
defaultCallback?: string
): JSONPStatus | false {
const wrap = typeof forceWrap === 'boolean' ? forceWrap : !!query.callback;
if (wrap) {
const customCallback = query.callback;
if (customCallback) {
if (!customCallback.match(callbackMatch)) {
// Invalid callback
return false;
}
return {
wrap: true,
callback: customCallback,
};
}
// No callback provided
return defaultCallback
? {
wrap: true,
callback: defaultCallback,
}
: false;
}
// Do not wrap
return {
wrap: false,
callback: '',
};
}
/**
* Send JSON response
*/
export function sendJSONResponse(data: unknown, query: Record<string, string>, wrap: JSONPStatus, res: FastifyReply) {
// Generate text
const html = query.pretty ? JSON.stringify(data, null, 4) : JSON.stringify(data);
// Check for JSONP callback
if (wrap.wrap) {
res.type('application/javascript; charset=utf-8');
res.send(wrap.callback + '(' + html + ');');
} else {
res.type('application/json; charset=utf-8');
res.send(html);
}
}

142
src/http/index.ts Normal file
View File

@ -0,0 +1,142 @@
import fastify, { FastifyReply } from 'fastify';
import { appConfig } from '../config/app';
import { runWhenLoaded } from '../data/loading';
import { iconNameRoutePartialRegEx, iconNameRouteRegEx, splitIconName } from '../misc/name';
import { generateIconsDataResponse } from './responses/icons';
import { generateSVGResponse } from './responses/svg';
import { initVersionResponse, versionResponse } from './responses/version';
/**
* Start HTTP server
*/
export async function startHTTPServer() {
// Create HTP server
const server = fastify({
caseSensitive: true,
});
// Generate headers to send
interface Header {
key: string;
value: string;
}
const headers: Header[] = [];
appConfig.headers.forEach((item) => {
const parts = item.split(':');
if (parts.length > 1) {
headers.push({
key: parts.shift() as string,
value: parts.join(':').trim(),
});
}
});
server.addHook('preHandler', (req, res, done) => {
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
res.header(header.key, header.value);
}
done();
});
// Init various responses
await initVersionResponse();
// Types for common params
interface PrefixParams {
prefix: string;
}
interface NameParams {
name: string;
}
// SVG: /prefix/icon.svg, /prefix:name.svg, /prefix-name.svg
server.get(
'/:prefix(' + iconNameRoutePartialRegEx + ')/:name(' + iconNameRoutePartialRegEx + ').svg',
(req, res) => {
type Params = PrefixParams & NameParams;
const name = req.params as Params;
runWhenLoaded(() => {
generateSVGResponse(name.prefix, name.name, req.query, res);
});
}
);
// SVG: /prefix:name.svg, /prefix-name.svg
server.get('/:name(' + iconNameRouteRegEx + ').svg', (req, res) => {
const name = splitIconName((req.params as NameParams).name);
if (name) {
runWhenLoaded(() => {
generateSVGResponse(name.prefix, name.name, req.query, res);
});
} else {
res.send(404);
}
});
// Icons data: /prefix/icons.json, /prefix.json
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.json', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
});
});
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').json', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
});
});
// Icons data: /prefix/icons.js, /prefix.js
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.js', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
});
});
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').js', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
});
});
// Options
server.options('/*', (req, res) => {
res.send(200);
});
// Robots
server.get('/robots.txt', (req, res) => {
res.send('User-agent: *\nDisallow: /\n');
});
// Version
server.get('/version', (req, res) => {
res.send(versionResponse(req.query));
});
server.post('/version', (req, res) => {
res.send(versionResponse(req.query));
});
// Redirect
server.get('/', (req, res) => {
res.redirect(301, appConfig.redirectIndex);
});
// Error handling
server.setDefaultRoute((req, res) => {
res.statusCode = 301;
res.setHeader('Location', appConfig.redirectIndex);
// Need to set custom headers because hooks don't work here
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
res.setHeader(header.key, header.value);
}
res.end();
});
// Start it
console.log('Listening on port', appConfig.port);
server.listen({
port: appConfig.port,
});
}

View File

@ -0,0 +1,45 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getStoredIconsData } from '../../data/icon-set/utils/get-icons';
import { iconSets } from '../../data/icon-sets';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
/**
* Generate icons data
*/
export function generateIconsDataResponse(
prefix: string,
wrapJS: boolean,
query: FastifyRequest['query'],
res: FastifyReply
) {
const q = (query || {}) as Record<string, string>;
const names = q.icons?.split(',');
if (!names || !names.length) {
// Missing or invalid icons parameter
res.send(404);
return;
}
// Check for JSONP
const wrap = checkJSONPQuery(q, wrapJS, 'SimpleSVG._loaderCallback');
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Get icon set
const iconSet = iconSets[prefix];
if (!iconSet) {
// No such icon set
res.send(404);
return;
}
// Get icons
getStoredIconsData(iconSet.item, names, (data) => {
// Send data
sendJSONResponse(data, q, wrap, res);
});
}

83
src/http/responses/svg.ts Normal file
View File

@ -0,0 +1,83 @@
import {
defaultIconDimensions,
flipFromString,
iconToHTML,
iconToSVG,
rotateFromString,
stringToColor,
} from '@iconify/utils';
import { defaultIconCustomisations, IconifyIconCustomisations } from '@iconify/utils/lib/customisations/defaults';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getStoredIconData } from '../../data/icon-set/utils/get-icon';
import { iconSets } from '../../data/icon-sets';
/**
* Generate SVG
*/
export function generateSVGResponse(prefix: string, name: string, query: FastifyRequest['query'], res: FastifyReply) {
// Get icon set
const iconSet = iconSets[prefix];
if (!iconSet) {
// No such icon set
res.send(404);
return;
}
// Get icon
getStoredIconData(iconSet.item, name, (data) => {
if (!data) {
// Invalid icon
res.send(404);
return;
}
const q = (query || {}) as Record<string, string>;
// Clean up customisations
const customisations: IconifyIconCustomisations = {};
// Dimensions
customisations.width = q.width || defaultIconCustomisations.width;
customisations.height = q.height || defaultIconCustomisations.height;
// Rotation
customisations.rotate = q.rotate ? rotateFromString(q.rotate, 0) : 0;
// Flip
if (q.flip) {
flipFromString(customisations, q.flip);
}
// Generate SVG
const svg = iconToSVG(data, customisations);
let body = svg.body;
if (q.box) {
// Add bounding box
body =
'<rect x="' +
(data.left || 0) +
'" y="' +
(data.top || 0) +
'" width="' +
(data.width || defaultIconDimensions.width) +
'" height="' +
(data.height || defaultIconDimensions.height) +
'" fill="rgba(255, 255, 255, 0)" />' +
body;
}
let html = iconToHTML(body, svg.attributes);
// Change color
const color = q.color;
if (color && html.indexOf('currentColor') !== -1 && color.indexOf('"') === -1) {
html = html.split('currentColor').join(color);
}
// Send SVG, optionally as attachment
if (q.download) {
res.header('Content-Disposition', 'attachment; filename="' + name + '.svg"');
}
res.type('image/svg+xml; charset=utf-8').send(html);
});
}

View File

@ -0,0 +1,28 @@
import { readFile } from 'node:fs/promises';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { appConfig } from '../../config/app';
let version: string | undefined;
/**
* Get version
*/
export async function initVersionResponse() {
try {
const packageContent = JSON.parse(await readFile('package.json', 'utf8'));
if (typeof packageContent.version === 'string') {
version = packageContent.version;
}
} catch {}
}
/**
* Send response
*/
export function versionResponse(query: FastifyRequest['query']): string {
return (
'Iconify API' +
(version ? ' version ' + version : '') +
(appConfig.statusRegion ? ' (' + appConfig.statusRegion + ')' : '')
);
}

View File

@ -0,0 +1,125 @@
import type { BaseDownloader } from '../../downloaders/base';
import { maybeAwait } from '../../misc/async';
import type {
BaseCollectionsImporter,
CreateIconSetImporter,
CreateIconSetImporterResult,
} from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
/**
* Base collections list importer
*/
export function createBaseCollectionsListImporter<Downloader extends BaseDownloader<ImportedData>>(
instance: Downloader,
createIconSetImporter: CreateIconSetImporter
): Downloader & BaseCollectionsImporter {
const obj = instance as Downloader & BaseCollectionsImporter;
// Importers
const importers: Record<string, CreateIconSetImporterResult> = Object.create(null);
// Import status
let importing = false;
// Import each icon set
const importIconSets = async (prefixes: string[]): Promise<ImportedData> => {
importing = true;
// Reuse old data
const data: ImportedData = obj.data || {
prefixes,
iconSets: Object.create(null),
};
const iconSets = data.iconSets;
// Parse each prefix
for (let i = 0; i < prefixes.length; i++) {
const prefix = prefixes[i];
let importer = importers[prefix];
if (!importer) {
// New item
importer = importers[prefix] = await maybeAwait(createIconSetImporter(prefix));
importer._dataUpdated = async (iconSetData) => {
data.iconSets[prefix] = iconSetData;
if (!importing) {
// Call _dataUpdated() if icon set was updated outside of importIconSets()
obj._dataUpdated?.(data);
}
};
await importer.init();
// Data should have been updated in init()
continue;
}
// Item already exists: check for update
await importer.checkForUpdate();
}
// Change status
importing = false;
return {
prefixes,
iconSets,
};
};
// Import from directory
obj._loadDataFromDirectory = async (path: string) => {
if (!obj._loadCollectionsListFromDirectory) {
throw new Error('Importer does not implement _loadCollectionsListFromDirectory()');
}
const prefixes = await obj._loadCollectionsListFromDirectory(path);
if (prefixes) {
return await importIconSets(prefixes);
}
};
// Custom import
obj._loadData = async () => {
if (!obj._loadCollectionsList) {
throw new Error('Importer does not implement _loadCollectionsList()');
}
const prefixes = await obj._loadCollectionsList();
if (prefixes) {
return await importIconSets(prefixes);
}
};
// Check for update
const checkCollectionsForUpdate = obj.checkForUpdate.bind(obj);
const checkIconSetForUpdate = async (prefix: string): Promise<boolean> => {
const importer = importers[prefix];
if (importer) {
return await importer.checkForUpdate();
}
console.error(`Cannot check "${prefix}" for update: no such icon set`);
return false;
};
// Check everything for update
obj.checkForUpdate = async (): Promise<boolean> => {
let result = await checkCollectionsForUpdate();
const prefixes = obj.data?.prefixes.slice(0) || [];
for (let i = 0; i < prefixes.length; i++) {
const importer = importers[prefixes[i]];
if (importer) {
result = (await importer.checkForUpdate()) || result;
}
}
return result;
};
// Set instance properties
const baseData: BaseCollectionsImporter = {
type: 'collections',
checkCollectionsForUpdate,
checkIconSetForUpdate,
};
Object.assign(obj, baseData);
return obj;
}

View File

@ -0,0 +1,52 @@
import { readFile } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
import { createBaseCollectionsListImporter } from './base';
interface JSONCollectionsListImporterOptions {
// File to load
filename?: string;
// Icon set filter
filter?: (prefix: string) => boolean;
}
/**
* Create importer for `collections.json`
*/
export function createJSONCollectionsListImporter<Downloader extends BaseDownloader<ImportedData>>(
downloader: Downloader,
createIconSetImporter: CreateIconSetImporter,
options?: JSONCollectionsListImporterOptions
): Downloader & BaseCollectionsImporter {
const obj = createBaseCollectionsListImporter(downloader, createIconSetImporter);
// Load data
obj._loadCollectionsListFromDirectory = async (path: string) => {
let prefixes: string[];
try {
const filename = options?.filename || '/collections.json';
const data = JSON.parse(await readFile(path + filename, 'utf8')) as Record<string, unknown>;
prefixes = Object.keys(data).filter((prefix) => matchIconName.test(prefix));
if (!(prefixes instanceof Array)) {
console.error(`Error loading "${filename}": invalid data`);
return;
}
} catch (err) {
console.error(err);
return;
}
// Filter keys
const filter = options?.filter;
if (filter) {
prefixes = prefixes.filter(filter);
}
return prefixes;
};
return obj;
}

View File

@ -0,0 +1,29 @@
import { CustomDownloader } from '../../downloaders/custom';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
import { createBaseCollectionsListImporter } from './base';
/**
* Create importer for hardcoded list of icon sets
*/
export function createHardcodedCollectionsListImporter(
prefixes: string[],
createIconSetImporter: CreateIconSetImporter
): CustomDownloader<ImportedData> & BaseCollectionsImporter {
const obj = createBaseCollectionsListImporter(new CustomDownloader<ImportedData>(), createIconSetImporter);
// Add methods that aren't defined in custom downloader
obj._init = async () => {
return prefixes.length > 0;
};
obj._checkForUpdate = (done: (value: boolean) => void) => {
done(false);
};
obj._loadCollectionsList = async () => {
return prefixes;
};
return obj;
}

View File

@ -0,0 +1,42 @@
import { readFile } from 'node:fs/promises';
import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage';
import type { StoredIconSet } from '../../types/icon-set/storage';
import { prependSlash } from '../../misc/files';
export interface IconSetJSONOptions {
// Ignore bad prefix?
ignoreInvalidPrefix?: boolean;
}
/**
* Reusable function for importing icon set from JSON file
*/
export async function importIconSetFromJSON(
prefix: string,
path: string,
filename: string,
options: IconSetJSONOptions = {}
): Promise<StoredIconSet | undefined> {
try {
const data = quicklyValidateIconSet(JSON.parse(await readFile(path + prependSlash(filename), 'utf8')));
if (!data) {
console.error(`Error loading "${prefix}" icon set: failed to validate`);
return;
}
if (data.prefix !== prefix) {
if (!options.ignoreInvalidPrefix) {
console.error(
`Error loading "${prefix}" icon set: bad prefix (enable ignoreInvalidPrefix option in importer to skip this check)`
);
return;
}
data.prefix = prefix;
}
// TODO: handle metadata from raw icon set data
return await asyncStoreLoadedIconSet(data);
} catch (err) {
console.error(err);
}
}

View File

@ -0,0 +1,55 @@
import { readFile } from 'node:fs/promises';
import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage';
import type { StoredIconSet } from '../../types/icon-set/storage';
import { prependSlash } from '../../misc/files';
export interface IconSetJSONPackageOptions {
// Ignore bad prefix?
ignoreInvalidPrefix?: boolean;
}
/**
* Reusable function for importing icon set from `@iconify-json/*` package
*/
export async function importIconSetFromJSONPackage(
prefix: string,
path: string,
options: IconSetJSONPackageOptions = {}
): Promise<StoredIconSet | undefined> {
try {
const data = quicklyValidateIconSet(JSON.parse(await readFile(path + '/icons.json', 'utf8')));
if (!data) {
console.error(`Error loading "${prefix}" icon set: failed to validate`);
return;
}
if (data.prefix !== prefix) {
if (!options.ignoreInvalidPrefix) {
console.error(
`Error loading "${prefix}" icon set: bad prefix (enable ignoreInvalidPrefix option in importer to skip this check)`
);
return;
}
data.prefix = prefix;
}
const result = await asyncStoreLoadedIconSet(data);
// Check for info
if (!result.info) {
try {
const info = JSON.parse(await readFile(path + '/info.json', 'utf8'));
if (info.prefix === prefix) {
result.info = info;
}
} catch {
//
}
}
// TODO: handle metadata from other .json files
return result;
} catch (err) {
console.error(err);
}
}

View File

@ -0,0 +1,70 @@
import { readdir, stat } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base';
import { DirectoryDownloader } from '../../downloaders/directory';
import type { StoredIconSet } from '../../types/icon-set/storage';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
import { createJSONIconSetImporter } from '../icon-set/json';
import { createBaseCollectionsListImporter } from '../collections/base';
interface JSONDirectoryImporterOptions {
// Icon set filter
filter?: (prefix: string) => boolean;
}
/**
* Create importer for all .json files in directory
*/
export function _createJSONDirectoryImporter<Downloader extends BaseDownloader<ImportedData>>(
downloader: Downloader,
options?: JSONDirectoryImporterOptions
): Downloader & BaseCollectionsImporter {
// Path to import from
let importPath: string | undefined;
// Function to create importer
const createIconSetImporter: CreateIconSetImporter = (prefix) => {
if (!importPath) {
throw new Error('Importer called before path was set');
}
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>(importPath), {
prefix,
filename: `/${prefix}.json`,
});
};
const obj = createBaseCollectionsListImporter(downloader, createIconSetImporter);
// Load data
obj._loadCollectionsListFromDirectory = async (path: string) => {
importPath = path;
let prefixes: string[] = [];
try {
const files = await readdir(path);
for (let i = 0; i < files.length; i++) {
const file = files[i];
const parts = file.split('.');
if (parts.length !== 2 || parts.pop() !== 'json' || !matchIconName.test(parts[0])) {
continue;
}
const data = await stat(path + '/' + file);
if (data.isFile()) {
prefixes.push(parts[0]);
}
}
} catch (err) {
console.error(err);
return;
}
// Filter prefixes
const filter = options?.filter;
if (filter) {
prefixes = prefixes.filter(filter);
}
return prefixes;
};
return obj;
}

View File

@ -0,0 +1,65 @@
import { readFile } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base';
import { DirectoryDownloader } from '../../downloaders/directory';
import type { StoredIconSet } from '../../types/icon-set/storage';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
import { createJSONIconSetImporter } from '../icon-set/json';
import { createBaseCollectionsListImporter } from '../collections/base';
interface IconSetsPackageImporterOptions {
// Icon set filter
filter?: (prefix: string) => boolean;
}
/**
* Create importer for all .json files in directory
*/
export function _createIconSetsPackageImporter<Downloader extends BaseDownloader<ImportedData>>(
downloader: Downloader,
options?: IconSetsPackageImporterOptions
): Downloader & BaseCollectionsImporter {
// Path to import from
let importPath: string | undefined;
// Function to create importer
const createIconSetImporter: CreateIconSetImporter = (prefix) => {
if (!importPath) {
throw new Error('Importer called before path was set');
}
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>(importPath), {
prefix,
filename: `/json/${prefix}.json`,
});
};
const obj = createBaseCollectionsListImporter(downloader, createIconSetImporter);
// Load data
obj._loadCollectionsListFromDirectory = async (path: string) => {
importPath = path;
let prefixes: string[];
try {
const data = JSON.parse(await readFile(path + '/collections.json', 'utf8')) as Record<string, unknown>;
prefixes = Object.keys(data).filter((prefix) => matchIconName.test(prefix));
if (!(prefixes instanceof Array)) {
console.error(`Error loading "collections.json": invalid data`);
return;
}
} catch (err) {
console.error(err);
return;
}
// Filter keys
const filter = options?.filter;
if (filter) {
prefixes = prefixes.filter(filter);
}
return prefixes;
};
return obj;
}

View File

@ -0,0 +1,84 @@
import type { BaseDownloader } from '../../downloaders/base';
import type { StoredIconSet } from '../../types/icon-set/storage';
import type { ImportedData } from '../../types/importers/common';
import type { BaseFullImporter } from '../../types/importers/full';
/**
* Base full importer
*/
export function createBaseImporter<Downloader extends BaseDownloader<ImportedData>>(
instance: Downloader
): Downloader & BaseFullImporter {
const obj = instance as Downloader & BaseFullImporter;
// Import status
let importing = false;
// Import each icon set
type ImportIconSetCallback = (prefix: string) => Promise<StoredIconSet | void | undefined>;
const importIconSets = async (prefixes: string[], callback: ImportIconSetCallback): Promise<ImportedData> => {
importing = true;
// Reuse old data
const data: ImportedData = obj.data || {
prefixes,
iconSets: Object.create(null),
};
const iconSets = data.iconSets;
// Parse each prefix
for (let i = 0; i < prefixes.length; i++) {
const prefix = prefixes[i];
const iconSetData = await callback(prefix);
if (iconSetData) {
data.iconSets[prefix] = iconSetData;
}
}
// Change status
importing = false;
return {
prefixes,
iconSets,
};
};
// Import from directory
obj._loadDataFromDirectory = async (path: string) => {
if (!obj._loadCollectionsListFromDirectory) {
throw new Error('Importer does not implement _loadCollectionsListFromDirectory()');
}
const loader = obj._loadIconSetFromDirectory;
if (!loader) {
throw new Error('Importer does not implement _loadIconSetFromDirectory()');
}
const prefixes = await obj._loadCollectionsListFromDirectory(path);
if (prefixes) {
return await importIconSets(prefixes, (prefix) => loader(prefix, path));
}
};
// Custom import
obj._loadData = async () => {
if (!obj._loadCollectionsList) {
throw new Error('Importer does not implement _loadCollectionsList()');
}
const loader = obj._loadIconSet;
if (!loader) {
throw new Error('Importer does not implement _loadIconSet()');
}
const prefixes = await obj._loadCollectionsList();
if (prefixes) {
return await importIconSets(prefixes, (prefix) => loader(prefix));
}
};
// Set instance properties
const baseData: BaseFullImporter = {
type: 'full',
};
Object.assign(obj, baseData);
return obj;
}

View File

@ -0,0 +1,57 @@
import { readdir, stat } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base';
import type { ImportedData } from '../../types/importers/common';
import type { BaseFullImporter } from '../../types/importers/full';
import { createBaseImporter } from './base';
import { IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json';
interface JSONDirectoryImporterOptions extends IconSetJSONOptions {
// Icon set filter
filter?: (prefix: string) => boolean;
}
/**
* Create importer for all .json files in directory
*/
export function createJSONDirectoryImporter<Downloader extends BaseDownloader<ImportedData>>(
downloader: Downloader,
options: JSONDirectoryImporterOptions = {}
): Downloader & BaseFullImporter {
const obj = createBaseImporter(downloader);
// Load data
obj._loadCollectionsListFromDirectory = async (path: string) => {
let prefixes: string[] = [];
try {
const files = await readdir(path);
for (let i = 0; i < files.length; i++) {
const file = files[i];
const parts = file.split('.');
if (parts.length !== 2 || parts.pop() !== 'json' || !matchIconName.test(parts[0])) {
continue;
}
const data = await stat(path + '/' + file);
if (data.isFile()) {
prefixes.push(parts[0]);
}
}
} catch (err) {
console.error(err);
return;
}
// Filter prefixes
const filter = options?.filter;
if (filter) {
prefixes = prefixes.filter(filter);
}
return prefixes;
};
// Load icon set
obj._loadIconSetFromDirectory = (prefix: string, path: string) =>
importIconSetFromJSON(prefix, path, '/' + prefix + '.json', options);
return obj;
}

View File

@ -0,0 +1,52 @@
import { readFile } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base';
import type { ImportedData } from '../../types/importers/common';
import type { BaseFullImporter } from '../../types/importers/full';
import { createBaseImporter } from './base';
import { IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json';
interface IconSetsPackageImporterOptions extends IconSetJSONOptions {
// Icon set filter
filter?: (prefix: string) => boolean;
}
/**
* Create importer for all .json files in directory
*/
export function createIconSetsPackageImporter<Downloader extends BaseDownloader<ImportedData>>(
downloader: Downloader,
options: IconSetsPackageImporterOptions = {}
): Downloader & BaseFullImporter {
const obj = createBaseImporter(downloader);
// Load collections list
obj._loadCollectionsListFromDirectory = async (path: string) => {
let prefixes: string[];
try {
const data = JSON.parse(await readFile(path + '/collections.json', 'utf8')) as Record<string, unknown>;
prefixes = Object.keys(data).filter((prefix) => matchIconName.test(prefix));
if (!(prefixes instanceof Array)) {
console.error(`Error loading "collections.json": invalid data`);
return;
}
} catch (err) {
console.error(err);
return;
}
// Filter keys
const filter = options?.filter;
if (filter) {
prefixes = prefixes.filter(filter);
}
return prefixes;
};
// Load icon set
obj._loadIconSetFromDirectory = async (prefix: string, path: string) =>
importIconSetFromJSON(prefix, path, '/json/' + prefix + '.json', options);
return obj;
}

View File

@ -0,0 +1,32 @@
import type { BaseDownloader } from '../../downloaders/base';
import type { BaseIconSetImporter } from '../../types/importers/icon-set';
import type { IconSetImportedData } from '../../types/importers/common';
import { IconSetJSONPackageOptions, importIconSetFromJSONPackage } from '../common/json-package';
interface JSONPackageIconSetImporterOptions extends IconSetJSONPackageOptions {
// Icon set prefix
prefix: string;
}
/**
* Create importer for `@iconify-json/*` package
*/
export function createJSONPackageIconSetImporter<Downloader extends BaseDownloader<IconSetImportedData>>(
instance: Downloader,
options: JSONPackageIconSetImporterOptions
): Downloader & BaseIconSetImporter {
const obj = instance as Downloader & BaseIconSetImporter;
const prefix = options.prefix;
// Set static data
const baseData: BaseIconSetImporter = {
type: 'icon-set',
prefix,
};
Object.assign(obj, baseData);
// Load data
obj._loadDataFromDirectory = (path: string) => importIconSetFromJSONPackage(prefix, path, options);
return obj;
}

View File

@ -0,0 +1,35 @@
import type { BaseDownloader } from '../../downloaders/base';
import type { BaseIconSetImporter } from '../../types/importers/icon-set';
import type { IconSetImportedData } from '../../types/importers/common';
import { IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json';
interface JSONIconSetImporterOptions extends IconSetJSONOptions {
// Icon set prefix
prefix: string;
// File to load from
filename: string;
}
/**
* Create importer for .json file
*/
export function createJSONIconSetImporter<Downloader extends BaseDownloader<IconSetImportedData>>(
instance: Downloader,
options: JSONIconSetImporterOptions
): Downloader & BaseIconSetImporter {
const obj = instance as Downloader & BaseIconSetImporter;
const prefix = options.prefix;
// Set instance properties
const baseData: BaseIconSetImporter = {
type: 'icon-set',
prefix,
};
Object.assign(obj, baseData);
// Load data
obj._loadDataFromDirectory = (path: string) => importIconSetFromJSON(prefix, path, options.filename, options);
return obj;
}

30
src/index.ts Normal file
View File

@ -0,0 +1,30 @@
import { config } from 'dotenv';
import { getImporters } from './config/icon-sets';
import { setImporters, updateIconSets } from './data/icon-sets';
import { loaded } from './data/loading';
import { startHTTPServer } from './http';
import { loadEnvConfig } from './misc/load-config';
(async () => {
// Configure environment
config();
loadEnvConfig();
// Start HTTP server
startHTTPServer();
// Get all importers and load data
const importers = await getImporters();
for (let i = 0; i < importers.length; i++) {
await importers[i].init();
}
setImporters(importers);
updateIconSets();
// Loaded
loaded();
})()
.then(() => {
console.log('API startup process complete');
})
.catch(console.error);

11
src/misc/async.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* Handle sync/async code
*/
export async function maybeAwait<T>(value: T | Promise<T>): Promise<T> {
if (value instanceof Promise) {
return value;
}
return new Promise((fulfill) => {
fulfill(value);
});
}

56
src/misc/files.ts Normal file
View File

@ -0,0 +1,56 @@
import { stat } from 'node:fs/promises';
import { scanDirectory } from '@iconify/tools/lib/misc/scan';
import type { FileEntry } from '../types/files';
import { hashString } from './hash';
/**
* List all files in directory
*/
export async function listFilesInDirectory(path: string): Promise<FileEntry[]> {
const files = await scanDirectory(path, (ext, file, subdir, path, stat) => {
const filename = subdir + file + ext;
const item: FileEntry = {
filename,
ext,
file,
path: subdir,
mtime: stat.mtimeMs,
size: stat.size,
};
return item;
});
files.sort((a, b) => a.filename.localeCompare(b.filename));
return files;
}
/**
* Hash files to quickly check if files were changed
*
* Does not check file contents, checking last modification time should be enough
*/
export function hashFiles(files: FileEntry[]): string {
const hashData = files.map(({ filename, mtime, size }) => {
return { filename, mtime, size };
});
return hashString(JSON.stringify(hashData));
}
/**
* Check if directory exists
*/
export async function directoryExists(dir: string): Promise<boolean> {
try {
const stats = await stat(dir);
return stats.isDirectory();
} catch {
return false;
}
}
/**
* Add '/' to start of filename
*/
export function prependSlash(filename: string): string {
return filename.slice(0, 1) === '/' ? filename : '/' + filename;
}

8
src/misc/hash.ts Normal file
View File

@ -0,0 +1,8 @@
import { createHash } from 'crypto';
/**
* Generate unique hash
*/
export function hashString(value: string): string {
return createHash('md5').update(value).digest('hex');
}

46
src/misc/load-config.ts Normal file
View File

@ -0,0 +1,46 @@
import { appConfig } from '../config/app';
/**
* Load config from environment
*/
export function loadEnvConfig(env = process.env) {
[appConfig].forEach((config) => {
const cfg = config as unknown as Record<string, unknown>;
for (const key in cfg) {
const envKey = key.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase());
const value = env[envKey];
if (value !== void 0) {
const defaultValue = cfg[key];
switch (typeof defaultValue) {
case 'boolean': {
const valuelc = value.toLowerCase();
if (valuelc === 'true' || valuelc === '1') {
cfg[key] = true;
} else if (valuelc === 'false' || valuelc === '0') {
cfg[key] = false;
}
break;
}
case 'number': {
const num = parseInt(value);
if (!isNaN(num)) {
cfg[key] = num;
}
break;
}
case 'string':
cfg[key] = value;
break;
case 'object':
if (defaultValue instanceof Array) {
// Append one entry to array
defaultValue.push(value);
}
}
}
}
});
}

31
src/misc/name.ts Normal file
View File

@ -0,0 +1,31 @@
interface SplitIconName {
prefix: string;
name: string;
}
// 2 part icon name
export const iconNameRouteRegEx = '^[a-z0-9-]+:?[a-z0-9-]+$';
// 1 part of icon name
export const iconNameRoutePartialRegEx = '^[a-z0-9-]+$';
/**
* Split icon name
*/
export function splitIconName(value: string): SplitIconName | undefined {
let parts = value.split(/[/:]/);
if (parts.length === 2) {
return {
prefix: parts[0],
name: parts[1],
};
}
parts = value.split('-');
if (parts.length > 1) {
return {
prefix: parts.shift() as string,
name: parts.join('-'),
};
}
}

1
src/types/async.ts Normal file
View File

@ -0,0 +1 @@
export type MaybeAsync<T> = T | Promise<T>;

View File

@ -0,0 +1,12 @@
import type { StoredIconSet } from '../icon-set/storage';
/**
* Generated data
*/
export interface StoredCollectionsList {
// All prefixes
prefixes: string[];
// Available icon sets
iconSets: Record<string, StoredIconSet>;
}

23
src/types/config/app.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* Main configuration
*/
export interface AppConfig {
// Index page
redirectIndex: string;
// Region to add to `/version` response. Used to tell which server is responding when running multiple servers
statusRegion: string;
// Cache root directory
// Without trailing '/'
cacheRootDir: string;
// HTTP headers to send
headers: string[];
// Port
port: number;
// Logging
log: boolean;
}

View File

@ -0,0 +1,7 @@
export interface SplitIconSetConfig {
// Average chunk size, in bytes. 0 to disable
chunkSize: number;
// Minimum number of icons in one chunk
minIconsPerChunk: number;
}

13
src/types/directory.ts Normal file
View File

@ -0,0 +1,13 @@
/**
* Entry for file
*/
export interface ImportDirectoryFileEntry {
// Path to scanned directory, ends with '/'
path: string;
// Sub-directory, ends with '/' (can be empty)
subdir: string;
// Filename without extension
file: string;
// Extension, starts with '.' (can be empty)
ext: string;
}

View File

@ -0,0 +1,20 @@
/**
* Downloader type
*/
export type DownloaderType = 'collections' | 'icon-set' | 'full';
/**
* Status:
*
* 'pending-init' - new instance, waiting for init() to run
* 'initialising' - initialising: _init() is running
* 'updating' - checking for update: _checkForUpdate() is running
* true - ready
* false - fatal error
*/
export type DownloaderStatus = 'pending-init' | 'initialising' | 'updating' | boolean;
/**
* Callback to run after checking for update
*/
export type DownloaderUpdateCallback = (value: boolean) => void;

View File

@ -0,0 +1,116 @@
import type { GitHubAPIOptions } from '@iconify/tools/lib/download/github/types';
import type { GitLabAPIOptions } from '@iconify/tools/lib/download/gitlab/types';
import type { NPMPackageOptions } from '@iconify/tools/lib/download/npm/types';
import type { DownloadGitRepoResult } from '@iconify/tools/lib/download/git';
import type { DownloadGitHubRepoResult } from '@iconify/tools/lib/download/github';
import type { DownloadGitLabRepoResult } from '@iconify/tools/lib/download/gitlab';
import type { DownloadNPMPackageResult } from '@iconify/tools/lib/download/npm';
/**
* Downloaders that download archive that contains files, which can be imported using various importers
*/
export type RemoteDownloaderType =
// Any git repository
| 'git'
// Git repository using GitHub API
| 'github'
// Git repository using GitLab API
| 'gitlab'
// NPM package
| 'npm';
/**
* Options
*/
interface BaseRemoteDownloaderOptions {
downloadType: RemoteDownloaderType;
}
export interface GitDownloaderOptions extends BaseRemoteDownloaderOptions {
downloadType: 'git';
// Repository
remote: string;
// Branch
branch: string;
}
export interface GitHubDownloaderOptions extends BaseRemoteDownloaderOptions, GitHubAPIOptions {
downloadType: 'github';
}
export interface GitLabDownloaderOptions extends BaseRemoteDownloaderOptions, GitLabAPIOptions {
downloadType: 'gitlab';
}
export interface NPMDownloaderOptions extends BaseRemoteDownloaderOptions, NPMPackageOptions {
downloadType: 'npm';
}
export type RemoteDownloaderOptions =
| GitDownloaderOptions
| GitHubDownloaderOptions
| GitLabDownloaderOptions
| NPMDownloaderOptions;
export type RemoteDownloaderOptionsMixin<T extends RemoteDownloaderType> = T extends 'git'
? GitDownloaderOptions
: T extends 'github'
? GitHubDownloaderOptions
: T extends 'gitlab'
? GitLabDownloaderOptions
: T extends 'npm'
? NPMDownloaderOptions
: never;
/**
* Latest version result
*/
interface BaseRemoteDownloaderVersion {
downloadType: RemoteDownloaderType;
}
export interface GitDownloaderVersion extends BaseRemoteDownloaderVersion, DownloadGitRepoResult {
downloadType: 'git';
// `contentsDir` contains full path to uncompressed files
// `hash` contains latest version hash
}
export interface GitHubDownloaderVersion extends BaseRemoteDownloaderVersion, DownloadGitHubRepoResult {
downloadType: 'github';
// `contentsDir` contains full path to uncompressed files
// `hash` contains latest version hash
}
export interface GitLabDownloaderVersion extends BaseRemoteDownloaderVersion, DownloadGitLabRepoResult {
downloadType: 'gitlab';
// `contentsDir` contains full path to uncompressed files
// `hash` contains latest version hash
}
export interface NPMDownloaderVersion extends BaseRemoteDownloaderVersion, DownloadNPMPackageResult {
downloadType: 'npm';
// `contentsDir` contains full path to uncompressed files
// `version` contains latest version
}
export type RemoteDownloaderVersion =
| GitDownloaderVersion
| GitHubDownloaderVersion
| GitLabDownloaderVersion
| NPMDownloaderVersion;
export type RemoteDownloaderVersionMixin<T extends RemoteDownloaderType> = T extends 'git'
? GitDownloaderVersion
: T extends 'github'
? GitHubDownloaderVersion
: T extends 'gitlab'
? GitLabDownloaderVersion
: T extends 'npm'
? NPMDownloaderVersion
: never;

22
src/types/files.ts Normal file
View File

@ -0,0 +1,22 @@
/**
* File
*/
export interface FileEntry {
// Full filename with path
filename: string;
// Extension with dot
ext: string;
// File name without path and extension
file: string;
// Path, relative to scanned directory
path: string;
// Last modification time
mtime: number;
// Size
size: number;
}

View File

@ -0,0 +1,17 @@
import type { IconifyAliases, IconifyJSONIconsData } from '@iconify/types';
/**
* Main data:
*
* prefix
* aliases
* ...optional icon dimensions
* lastModified
*/
export interface SplitIconifyJSONMainData extends Omit<IconifyJSONIconsData, 'provider' | 'icons'> {
// Last modified time
lastModified?: number;
// Aliases, required
aliases: IconifyAliases;
}

View File

@ -0,0 +1,29 @@
import type { IconifyIcons, IconifyInfo, IconifyJSON } from '@iconify/types';
import type { SplitDataTree, SplitRecord } from '../split';
import type { MemoryStorage, MemoryStorageItem } from '../storage';
import type { SplitIconifyJSONMainData } from './split';
/**
* Generated data
*/
export interface StoredIconSet {
// Icon set information
info?: IconifyInfo;
// Common data
common: SplitIconifyJSONMainData;
// Storage reference
storage: MemoryStorage<IconifyIcons>;
// Split chunks, stored in storage
items: MemoryStorageItem<IconifyIcons>[];
tree: SplitDataTree<MemoryStorageItem<IconifyIcons>>;
// TODO: add properties for search data
}
/**
* Callback
*/
export type StoredIconSetDone = (result: StoredIconSet) => void;

19
src/types/importers.ts Normal file
View File

@ -0,0 +1,19 @@
import type { BaseDownloader } from '../downloaders/base';
import type { StoredIconSet } from './icon-set/storage';
import type { ImportedData } from './importers/common';
/**
* Importer
*/
export type Importer = BaseDownloader<ImportedData>;
/**
* Icon set data
*/
export interface IconSetEntry {
// Importer icon set belongs to
importer: Importer;
// Data
item: StoredIconSet;
}

View File

@ -0,0 +1,29 @@
import type { BaseDownloader } from '../../downloaders/base';
import type { MaybeAsync } from '../async';
import type { BaseMainImporter, IconSetImportedData } from './common';
import type { BaseIconSetImporter } from './icon-set';
/**
* Loader for child element
*/
export type CreateIconSetImporterResult = BaseIconSetImporter & BaseDownloader<IconSetImportedData>;
export type CreateIconSetImporter = (prefix: string) => MaybeAsync<CreateIconSetImporterResult>;
/**
* Base collections list importer
*/
export interface BaseCollectionsImporter extends BaseMainImporter {
type: 'collections';
// Load icon sets from directory. Used in importers that implement _loadDataFromDirectory()
_loadCollectionsListFromDirectory?: (path: string) => Promise<string[] | void | undefined>;
// Load icon sets. Used in importers that implement _loadData()
_loadCollectionsList?: () => Promise<string[] | void | undefined>;
// Check only collections list for update (same as checkForUpdate for full importer)
checkCollectionsForUpdate: () => Promise<boolean>;
// Check icon set (same as checkForUpdate for full importer)
checkIconSetForUpdate: (prefix: string) => Promise<boolean>;
}

View File

@ -0,0 +1,37 @@
import type { DownloaderType } from '../downloaders/base';
import type { StoredIconSet } from '../icon-set/storage';
/**
* Base icon set importer interface
*
* Properties/methods should be set in functions that create instances
*/
export interface BaseImporter {
// Downloader type, set in child class
type: DownloaderType;
}
/**
* Imported data
*/
export type IconSetImportedData = StoredIconSet;
export interface ImportedData {
// All prefixes
prefixes: string[];
// Icon sets
iconSets: Record<string, IconSetImportedData | undefined>;
}
/**
* Base main importer, used for full importer and collections list importer
*
* Not used in icon set importer, which is used as a child importer of collections importer
*/
export interface BaseMainImporter extends BaseImporter {
type: Exclude<DownloaderType, 'icon-set'>;
// Check for update, calls _replaceIconSetData() to update data
_updateIconSet?: (prefix: string) => Promise<boolean>;
}

View File

@ -0,0 +1,20 @@
import type { BaseMainImporter, IconSetImportedData } from './common';
/**
* Base full importer
*/
export interface BaseFullImporter extends BaseMainImporter {
type: 'full';
// Load icon sets from directory. Used in importers that implement _loadDataFromDirectory()
_loadCollectionsListFromDirectory?: (path: string) => Promise<string[] | void | undefined>;
// Load icon set from directory. Used in importers that implement _loadDataFromDirectory()
_loadIconSetFromDirectory?: (prefix: string, path: string) => Promise<IconSetImportedData | void | undefined>;
// Load icon sets. Used in importers that implement _loadData()
_loadCollectionsList?: () => Promise<string[] | void | undefined>;
// Load icon set. Used in importers that implement _loadData()
_loadIconSet?: (prefix: string) => Promise<IconSetImportedData | void | undefined>;
}

View File

@ -0,0 +1,16 @@
import type { BaseImporter, IconSetImportedData } from './common';
/**
* Base icon set importer interface
*
* Properties/methods should be set in functions that create instances
*/
export interface BaseIconSetImporter extends BaseImporter {
type: 'icon-set';
// Icon set prefix, set when creating instance
prefix: string;
// Loader for each icon set
_loadIconSet?: () => Promise<IconSetImportedData | void | undefined>;
}

47
src/types/split.ts Normal file
View File

@ -0,0 +1,47 @@
/**
* Split records
*/
export interface SplitRecord<T> {
// Keyword for item
keyword: string;
// Data
data: T;
}
/**
* Callback to call to store record
*/
export type SplitRecordCallback<T> = (data: SplitRecord<T>, next: () => void, index: number, total: number) => void;
/**
* Tree for searching records
*/
interface SplitDataTreeBase<T> {
// Status
split: boolean;
// Matching item
match: T;
}
// Type for item that has no children
interface SplitDataTreeNotSplit<T> extends SplitDataTreeBase<T> {
split: false;
}
// Type for item that has children
interface SplitDataTreeSplit<T> extends SplitDataTreeBase<T> {
split: true;
// Keyword to test
keyword: string;
// Previous items to search if localeCompare returns < 0
prev?: SplitDataTree<T>;
// Next items to search if localeCompare return > 0
next?: SplitDataTree<T>;
}
export type SplitDataTree<T> = SplitDataTreeNotSplit<T> | SplitDataTreeSplit<T>;

75
src/types/storage.ts Normal file
View File

@ -0,0 +1,75 @@
/**
* Cache status
*/
export interface MemoryStorageCache {
// Cache filename
filename: string;
// True if cache exists, false if needs to be written
exists: boolean;
}
/**
* Callback
*/
export type MemoryStorageCallback<T> = (data: T | null) => void;
/**
* Stored item state
*/
export interface MemoryStorageItem<T> {
// Cache, empty if data should not be stored in cache
cache?: MemoryStorageCache;
// Pending callbacks
callbacks: MemoryStorageCallback<T>[];
// Last used time
lastUsed: number;
// Data, if loaded
data?: T;
}
/**
* Storage configuration
*/
export interface MemoryStorageConfig {
// Cache directory, use {cache} to point for relative to cacheRootDir from app config
// Without trailing '/'
cacheDir: string;
// Maximum number of stored items. 0 to disable
maxCount?: number;
// Minimum delay in milliseconds when data can expire.
// Should be set to at least 10 seconds (10000) to avoid repeated read operations
minExpiration?: number;
// Timeout in milliseconds to check expired items, > 0 (if disabled, cleanupAfterSec is not ran)
timer?: number;
// Number of milliseconds to keep item in storage after last use, > minExpiration
cleanupAfter?: number;
// Timer callback, used for debugging and testing. Called before cleanup when its triggered by timer
timerCallback?: () => void;
}
/**
* Storage
*/
export interface MemoryStorage<T> {
// Configuration
config: MemoryStorageConfig;
// Pending writes and reads
pendingWrites: Set<MemoryStorageItem<T>>;
pendingReads: Set<MemoryStorageItem<T>>;
// Timer for cleanup
timer?: ReturnType<typeof setTimeout>;
// Watched items
watched: Set<MemoryStorageItem<T>>;
}

View File

@ -0,0 +1,250 @@
import {
splitRecords,
createSplitRecordsTree,
searchSplitRecordsTree,
searchSplitRecordsTreeForSet,
} from '../../lib/data/storage/split';
import type { SplitRecord } from '../../lib/types/split';
describe('Splitting data', () => {
test('1 chunk', () => {
return new Promise((fulfill, reject) => {
const data: Record<string, number> = {};
for (let i = 0; i < 7; i++) {
data[`test-${i}`] = i;
}
const split: SplitRecord<typeof data>[] = [];
splitRecords(
data,
1,
(item, next) => {
split.push(item);
next();
},
() => {
try {
expect(split).toEqual([
{
keyword: 'test-0',
data: {
'test-0': 0,
'test-1': 1,
'test-2': 2,
'test-3': 3,
'test-4': 4,
'test-5': 5,
'test-6': 6,
},
},
]);
const tree = createSplitRecordsTree(split);
expect(tree.split).toBeFalsy();
// Check all items, including keys that do not exist
for (let i = -10; i < 10; i++) {
expect(searchSplitRecordsTree(tree, `test-${i}`)).toEqual(split[0].data);
}
fulfill(true);
} catch (err) {
reject(err);
}
}
);
});
});
test('2 chunks', () => {
return new Promise((fulfill, reject) => {
const data: Record<string, number> = {};
for (let i = 0; i < 7; i++) {
data[`test-${i}`] = i;
}
const split: SplitRecord<typeof data>[] = [];
splitRecords(
data,
2,
(item, next) => {
split.push(item);
next();
},
() => {
try {
expect(split).toEqual([
{
keyword: 'test-0',
data: {
'test-0': 0,
'test-1': 1,
'test-2': 2,
'test-3': 3,
},
},
{
keyword: 'test-4',
data: {
'test-4': 4,
'test-5': 5,
'test-6': 6,
},
},
]);
const tree = createSplitRecordsTree(split);
expect(tree.split).toBeTruthy();
// Check all items
for (let i = 0; i < 4; i++) {
expect(searchSplitRecordsTree(tree, `test-${i}`)).toEqual(split[0].data);
}
for (let i = 4; i < 7; i++) {
expect(searchSplitRecordsTree(tree, `test-${i}`)).toEqual(split[1].data);
}
// Check items that do not exist. Keys are not checked, only alphabetical match is checked
expect(searchSplitRecordsTree(tree, 'foo')).toEqual(split[0].data);
expect(searchSplitRecordsTree(tree, 'z')).toEqual(split[1].data);
// Search for multiple items
const map1 = new Map();
map1.set(split[0].data, ['test-0', 'test-2', 'test-10']); // '10' is compared as string, so its after 'test-1'
map1.set(split[1].data, ['test-6']);
expect(searchSplitRecordsTreeForSet(tree, ['test-0', 'test-2', 'test-6', 'test-10'])).toEqual(
map1
);
const map2 = new Map();
map2.set(split[1].data, ['z', 'test-4']);
expect(searchSplitRecordsTreeForSet(tree, ['z', 'test-4'])).toEqual(map2);
fulfill(true);
} catch (err) {
reject(err);
}
}
);
});
});
test('3 chunks. async', () => {
return new Promise((fulfill, reject) => {
const data: Record<string, number> = {};
for (let i = 0; i < 7; i++) {
data[`test-${i}`] = i;
}
const split: SplitRecord<typeof data>[] = [];
splitRecords(
data,
3,
(item, next) => {
split.push(item);
setTimeout(next);
},
() => {
try {
expect(split).toEqual([
{
keyword: 'test-0',
data: {
'test-0': 0,
'test-1': 1,
},
},
{
keyword: 'test-2',
data: {
'test-2': 2,
'test-3': 3,
'test-4': 4,
},
},
{
keyword: 'test-5',
data: {
'test-5': 5,
'test-6': 6,
},
},
]);
const tree = createSplitRecordsTree(split);
expect(tree.split).toBeTruthy();
// Check all items
for (let i = 0; i < 2; i++) {
expect(searchSplitRecordsTree(tree, `test-${i}`)).toEqual(split[0].data);
}
for (let i = 2; i < 5; i++) {
expect(searchSplitRecordsTree(tree, `test-${i}`)).toEqual(split[1].data);
}
for (let i = 5; i < 7; i++) {
expect(searchSplitRecordsTree(tree, `test-${i}`)).toEqual(split[2].data);
}
fulfill(true);
} catch (err) {
reject(err);
}
}
);
});
});
test('unsorted list, 2 chunks', () => {
return new Promise((fulfill, reject) => {
const data: Record<string, number> = {};
for (let i = 0; i < 4; i++) {
data[`baz-${i}`] = i;
data[`bar-${i}`] = i;
data[`foo-${i}`] = i;
}
const split: SplitRecord<typeof data>[] = [];
splitRecords(
data,
2,
(item, next) => {
split.push(item);
next();
},
() => {
try {
expect(split).toEqual([
{
keyword: 'bar-0',
data: {
'bar-0': 0,
'bar-1': 1,
'bar-2': 2,
'bar-3': 3,
'baz-0': 0,
'baz-1': 1,
},
},
{
keyword: 'baz-2',
data: {
'baz-2': 2,
'baz-3': 3,
'foo-0': 0,
'foo-1': 1,
'foo-2': 2,
'foo-3': 3,
},
},
]);
fulfill(true);
} catch (err) {
reject(err);
}
}
);
});
});
});

View File

@ -0,0 +1,463 @@
import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { appConfig } from '../../lib/config/app';
import { createStorage, createStoredItem } from '../../lib/data/storage/create';
import { uniqueCacheDir } from '../helpers';
import type { MemoryStorageItem } from '../../lib/types/storage';
describe('Basic data storage tests', () => {
test('Storage with default config', () => {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
});
// Config
expect(storage.config).toEqual({
cacheDir,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
const item = createStoredItem(
storage,
{
test: true,
},
'foo.json'
);
// Nothing should have changed because config doesn't store anything
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: false,
});
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(0);
expect(storage.pendingReads.size).toBe(0);
});
test('Storage with limited number of items', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
maxCount: 2,
});
// Config
expect(storage.config).toEqual({
cacheDir,
maxCount: 2,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
let isSync = true;
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', false, (item) => {
// Async write, wrap in try..catch to reject with error
try {
expect(isSync).toBeFalsy();
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: true,
});
expect(item.data).toEqual(content);
// Expecting no pending writes, 1 watched item, no timer
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(1);
expect(storage.pendingWrites.size).toBe(0);
expect(storage.pendingReads.size).toBe(0);
} catch (err) {
reject(err);
return;
}
// Done
fulfill(true);
});
// Expecting 1 pending write, but no timer
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: false,
});
expect(item.data).toEqual(content);
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(1);
expect(storage.pendingReads.size).toBe(0);
isSync = false;
// Test continues in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
test('Storage with limited number of items, autoCleanup', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
maxCount: 2,
});
// Config
expect(storage.config).toEqual({
cacheDir,
maxCount: 2,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
let isSync = true;
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', true, (item) => {
// Async write, wrap in try..catch to reject with error
try {
expect(isSync).toBeFalsy();
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: true,
});
// Data should be unset
expect(item.data).toBeUndefined();
// Expecting no pending writes, 0 watched items, no timer
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(0);
expect(storage.pendingReads.size).toBe(0);
} catch (err) {
reject(err);
return;
}
// Done
fulfill(true);
});
// Expecting 1 pending write, but no timer
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: false,
});
expect(item.data).toEqual(content);
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(1);
expect(storage.pendingReads.size).toBe(0);
isSync = false;
// Test continues in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
test('Storage with limited number of items, autoCleanup, item with use time', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
maxCount: 2,
});
// Config
expect(storage.config).toEqual({
cacheDir,
maxCount: 2,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
let isSync = true;
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', true, (item) => {
// Async write, wrap in try..catch to reject with error
try {
expect(isSync).toBeFalsy();
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: true,
});
// Data should be set because lastUsed was set
expect(item.data).toBe(content);
// Expecting no pending writes, 1 watched item, no timer
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(1);
expect(storage.pendingWrites.size).toBe(0);
expect(storage.pendingReads.size).toBe(0);
} catch (err) {
reject(err);
return;
}
// Done
fulfill(true);
});
// Expecting 1 pending write, but no timer
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: false,
});
expect(item.data).toEqual(content);
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(1);
expect(storage.pendingReads.size).toBe(0);
// Set last use time
item.lastUsed = Date.now();
isSync = false;
// Test continues in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
test('Storage with timer', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
// Callback for debugging. Because function relies on data provided after creating
// config, it is assigned later, after test item is created
let callback: () => void | undefined;
const timerCallback = () => {
if (!callback) {
reject('Timer was called before timerCallback is set');
} else {
callback();
}
};
// Create storage
const storage = createStorage({
cacheDir,
timer: 50,
cleanupAfter: 150,
minExpiration: 1,
timerCallback,
});
// Config
expect(storage.config).toEqual({
cacheDir,
timer: 50,
cleanupAfter: 150,
minExpiration: 1,
timerCallback,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
let isSync = true;
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', false, (item) => {
// Async write, wrap in try..catch to reject with error
try {
expect(isSync).toBeFalsy();
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: true,
});
// Data should not be unset yet
expect(item.data).toBe(content);
// Expecting no pending writes, 1 watched item and timer
expect(storage.timer).toBeTruthy();
expect(storage.watched.size).toBe(1);
expect(storage.pendingWrites.size).toBe(0);
expect(storage.pendingReads.size).toBe(0);
} catch (err) {
reject(err);
return;
}
// Wait for cleanup
let count = 0;
callback = () => {
if (count++ > 5) {
// Too much waiting!
clearInterval(storage.timer);
reject('Delay is too long');
return;
}
// Data should exist
expect(item.data).toBeTruthy();
// Test on next tick, after cleanup
setTimeout(() => {
if (item.data) {
// Not cleaned up yet
return;
}
// Clear timer before testing
clearInterval(storage.timer);
try {
// Data should be unset
expect(item.data).toBeUndefined();
// Expecting no pending writes, 0 watched items, no timer
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(0);
expect(storage.pendingReads.size).toBe(0);
// Done
fulfill(true);
} catch (err) {
reject(err);
}
});
};
});
// Expecting 1 pending write, no timer
expect(item.cache).toEqual({
filename: 'cache/' + dir + '/foo.json',
exists: false,
});
expect(item.data).toEqual(content);
expect(storage.timer).toBeUndefined();
expect(storage.watched.size).toBe(0);
expect(storage.pendingWrites.size).toBe(1);
expect(storage.pendingReads.size).toBe(0);
isSync = false;
// Test continues in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
test('Error writing cache', () => {
return new Promise((fulfill, reject) => {
try {
interface TestItem {
i: number;
}
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
// Create file instead of directory to fail
const filename = cacheDir.replace('{cache}', appConfig.cacheRootDir);
try {
mkdirSync(dirname(filename), {
recursive: true,
});
} catch {}
try {
writeFileSync(filename, 'test', 'utf8');
} catch (err) {
reject(err);
return;
}
const storage = createStorage<TestItem>({
cacheDir,
maxCount: 1,
});
// Add few items
const numItems = 5;
let counter = numItems;
const items: MemoryStorageItem<TestItem>[] = [];
for (let i = 0; i < numItems; i++) {
createStoredItem<TestItem>(
storage,
{
i,
},
`${i}.json`,
true,
(item, err) => {
try {
items.push(item);
expect(err).toBeTruthy();
expect(item.data).toBeTruthy();
expect(item.cache).toBeTruthy();
expect(item.cache!.exists).toBeFalsy();
counter--;
if (!counter) {
// All items have been created
expect(items.length).toBe(numItems);
for (let i = 0; i < numItems; i++) {
expect(items[i].data).toBeTruthy();
}
fulfill(true);
}
} catch (err) {
reject(err);
}
}
);
}
// Test continues in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
});

View File

@ -0,0 +1,144 @@
import { createStorage, createStoredItem } from '../../lib/data/storage/create';
import { cleanupStorage } from '../../lib/data/storage/cleanup';
import { getStoredItem } from '../../lib/data/storage/get';
import type { MemoryStorageItem } from '../../lib/types/storage';
import { uniqueCacheDir } from '../helpers';
describe('Advanced storage tests', () => {
test('Big set of data, limit to 2', () => {
return new Promise((fulfill, reject) => {
try {
interface Item {
i: number;
title: string;
}
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<Item>({
cacheDir,
maxCount: 2,
});
// Create items
const items: MemoryStorageItem<Item>[] = [];
const limit = 10;
const pending = new Set();
const createdItem = () => {
try {
if (!pending.size) {
// All items have been created and should have been cleaned up. Continue testing...
expect(storage.watched.size).toBe(0);
items.forEach((item, i) => {
expect(item.data).toBeUndefined();
expect(item.cache).toEqual({
filename: `cache/${dir}/item-${i}.json`,
exists: true,
});
});
// Load all even items, which is more than allowed limit
const pendingLoad: Set<MemoryStorageItem<Item>> = new Set();
const loadedItem = (lastItem: MemoryStorageItem<Item>) => {
if (!pendingLoad.size) {
// All items have been loaded
// Last item should not be watched yet: it is added after callbacks are ran, but
// this code is ran inside a callback
expect(storage.watched.size).toBe(Math.floor(limit / 2) - 1);
for (let i = 0; i < limit; i++) {
const item = items[i];
expect(storage.watched.has(item)).toBe(i % 2 === 0 && item !== lastItem);
}
// Wait for next tick to add `lastItem` to watched items
setTimeout(() => {
try {
expect(storage.watched.size).toBe(Math.floor(limit / 2));
expect(lastItem.data).toBeDefined();
// Fake expiration for last item and run cleanup process
lastItem.lastUsed -= 100000;
cleanupStorage(storage);
// Only `lastItem` should have been removed
expect(lastItem.data).toBeUndefined();
expect(storage.watched.size).toBe(Math.floor(limit / 2) - 1);
// Load last item again
getStoredItem(storage, lastItem, (data) => {
try {
expect(data).toBeTruthy();
expect(lastItem.data).toBe(data);
expect(storage.watched.has(lastItem)).toBe(false);
// Should be re-added to watched list on next tick
setTimeout(() => {
try {
expect(storage.watched.has(lastItem)).toBe(true);
fulfill(true);
} catch (err) {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
}
};
for (let i = 0; i < limit; i += 2) {
const item = items[i];
pendingLoad.add(item);
getStoredItem(storage, item, (data) => {
try {
pendingLoad.delete(item);
expect(data).toBeTruthy();
loadedItem(item);
} catch (err) {
reject(err);
}
});
}
// Test continues in loadedItem()...
}
} catch (err) {
reject(err);
}
};
for (let i = 0; i < limit; i++) {
const item = createStoredItem<Item>(
storage,
{
i,
title: `Item ${i}`,
},
`item-${i}.json`,
true,
(item) => {
pending.delete(item);
createdItem();
}
);
pending.add(item);
items.push(item);
}
// All items should be pending, but not watched
expect(pending.size).toBe(limit);
expect(storage.watched.size).toBe(0);
// Test continues in createdItem()...
} catch (err) {
reject(err);
}
});
});
});

View File

@ -0,0 +1,253 @@
import { unlinkSync } from 'node:fs';
import { appConfig } from '../../lib/config/app';
import { createStorage, createStoredItem } from '../../lib/data/storage/create';
import { getStoredItem } from '../../lib/data/storage/get';
import { uniqueCacheDir } from '../helpers';
describe('Reading stored data', () => {
test('Instant callback', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
maxCount: 2,
});
// Config
expect(storage.config).toEqual({
cacheDir,
maxCount: 2,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
let isSync = true;
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', false, () => {
// Data should be set
expect(item.data).toEqual(content);
// Timer should not be set
if (storage.timer) {
clearInterval(storage.timer);
reject('Timer is active');
}
fulfill(true);
});
// Get data
getStoredItem(storage, item, (data) => {
try {
// Should be sync
expect(isSync).toBeTruthy();
expect(data).toEqual(content);
} catch (err) {
reject(err);
}
});
isSync = false;
// Test continues in callback in getStoredItem(), then in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
test('Instant callback, autoCleanup', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
maxCount: 2,
});
// Config
expect(storage.config).toEqual({
cacheDir,
maxCount: 2,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
let isSync = true;
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', true, () => {
// Data should be set, even though autoCleanup is enabled because read was called earlier
expect(item.data).toEqual(content);
// Timer should not be set
if (storage.timer) {
clearInterval(storage.timer);
reject('Timer is active');
}
fulfill(true);
});
// Get data
getStoredItem(storage, item, (data) => {
try {
// Should be sync
expect(isSync).toBeTruthy();
expect(data).toEqual(content);
} catch (err) {
reject(err);
}
});
isSync = false;
// Test continues in callback in getStoredItem(), then in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
test('Delayed callback', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage({
cacheDir,
maxCount: 2,
});
// Config
expect(storage.config).toEqual({
cacheDir,
maxCount: 2,
});
// Timer should not exist
expect(storage.timer).toBeUndefined();
// Add one item
const content = {
test: true,
};
const item = createStoredItem(storage, content, 'foo.json', true, () => {
try {
let isSync = true;
let cb1 = false;
let cb2 = false;
// Data should be unset
expect(item.data).toBeUndefined();
// Get data, attempt #1
getStoredItem(storage, item, (data) => {
try {
// Should be async
expect(isSync).toBeFalsy();
expect(data).toEqual(content);
expect(cb1).toBeFalsy();
cb1 = true;
// Content should be set, but not identical to original data
expect(item.data).toEqual(content);
expect(item.data).not.toBe(content);
} catch (err) {
reject(err);
}
});
// Get data, attempt #2
getStoredItem(storage, item, (data) => {
try {
// Should be async
expect(isSync).toBeFalsy();
expect(data).toEqual(content);
expect(cb2).toBeFalsy();
cb2 = true;
// Attempt #1 should have been done too
expect(cb1).toBeTruthy();
// Timer should not be set
if (storage.timer) {
clearInterval(storage.timer);
reject('Timer is active');
}
// Done
fulfill(true);
} catch (err) {
reject(err);
}
});
isSync = false;
// Test continues in callbacks in getStoredItem()...
} catch (err) {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
});
test('Error reading cache', () => {
return new Promise((fulfill, reject) => {
try {
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const actualCacheDir = cacheDir.replace('{cache}', appConfig.cacheRootDir);
const storage = createStorage({
cacheDir,
maxCount: 1,
});
// Add one item
const content = {
test: true,
};
createStoredItem(storage, content, 'foo.json', true, (item, err) => {
try {
// Data should be written to cache
expect(item.data).toBeUndefined();
// Remove cache file
unlinkSync(actualCacheDir + '/foo.json');
// Attempt to read data
getStoredItem(storage, item, (data) => {
try {
expect(data).toBeFalsy();
fulfill(true);
} catch (err) {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
// Test continues in callback in createStoredItem()...
} catch (err) {
reject(err);
}
});
});
});

View File

@ -0,0 +1,98 @@
import { BaseDownloader } from '../../lib/downloaders/base';
type BooleanCallback = (value: boolean) => void;
type RejectCallback = (value: unknown) => void;
describe('Initialising BaseDownloader class', () => {
class BaseDownloaderTest extends BaseDownloader<unknown> {
/**
* Test init()
*/
initTested = false;
initCalled: ((done: BooleanCallback, reject: RejectCallback) => void) | undefined;
_init(): Promise<boolean> {
this.initTested = true;
return new Promise((fulfill, reject) => {
this.initCalled?.(fulfill, reject);
});
}
/**
* Test _loadContent()
*/
contentLoaded = false;
async _loadContent() {
this.contentLoaded = true;
}
}
test('Initialising', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
expect(test.status).toBe('pending-init');
// Initialise
expect(test.initTested).toBe(false);
expect(test.contentLoaded).toBe(false);
const initResult = await new Promise((fulfill, reject) => {
// Add callback
test.initCalled = (done) => {
// _init() is run
try {
expect(test.initTested).toBe(true);
expect(test.contentLoaded).toBe(false);
expect(test.status).toBe('initialising');
// Finish init()
done(true);
} catch (err) {
reject(err);
}
};
// Run init
test.init().then(fulfill).catch(reject);
// ... continues in initCalled() above
});
// Result
expect(initResult).toBe(true);
expect(test.status).toBe(true);
expect(test.contentLoaded).toBe(true);
});
test('Failing to init', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
// Initialise
const initResult = await new Promise((fulfill, reject) => {
// Add callback
test.initCalled = (done, fail) => {
// _init() is run
try {
expect(test.initTested).toBe(true);
expect(test.status).toBe('initialising');
// Finish init() with error
fail('Expected fatal error in downloader (unit test passes!)');
} catch (err) {
reject(err);
}
};
// Run init
test.init().then(fulfill).catch(reject);
// ... continues in initCalled() above
});
// Result: status should be false, content should not be loaded
expect(initResult).toBe(false);
expect(test.status).toBe(false);
expect(test.contentLoaded).toBe(false);
});
});

View File

@ -0,0 +1,348 @@
import { BaseDownloader } from '../../lib/downloaders/base';
type BooleanCallback = (value: boolean) => void;
type RejectCallback = (value: unknown) => void;
describe('Updating BaseDownloader class', () => {
class BaseDownloaderTest extends BaseDownloader<unknown> {
/**
* Test init()
*/
initTested = false;
initCalled: ((done: BooleanCallback, reject: RejectCallback) => void) | undefined;
_init(): Promise<boolean> {
this.initTested = true;
return new Promise((fulfill, reject) => {
this.initCalled?.(fulfill, reject);
});
}
/**
* Test _loadContent()
*/
contentLoaded = 0;
async _loadContent() {
this.contentLoaded++;
}
/**
* Check for update
*/
updateCalled: ((done: BooleanCallback) => void) | undefined;
_checkForUpdate(done: (value: boolean) => void) {
this.updateCalled?.(done);
}
}
test('Nothing to update after init', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
test.initCalled = (done) => done(true);
await test.init();
expect(test.initTested).toBe(true);
expect(test.status).toBe(true);
expect(test.contentLoaded).toBe(1);
// Reload
let updateCounter = 0;
const updateResult = await new Promise((fulfill, reject) => {
// Setup callback
test.updateCalled = (done) => {
updateCounter++;
try {
expect(test.status).toBe('updating');
expect(updateCounter).toBe(1);
} catch (err) {
reject(err);
return;
}
done(false);
};
// Get result
test.checkForUpdate().then(fulfill).catch(reject);
});
expect(updateResult).toBe(false);
expect(test.status).toBe(true);
expect(updateCounter).toBe(1);
expect(test.contentLoaded).toBe(1);
});
test('Successful update after init', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
test.initCalled = (done) => done(true);
await test.init();
expect(test.initTested).toBe(true);
expect(test.status).toBe(true);
expect(test.contentLoaded).toBe(1);
// Reload
let updateCounter = 0;
const updateResult = await new Promise((fulfill, reject) => {
// Setup callback
test.updateCalled = (done) => {
updateCounter++;
try {
expect(test.status).toBe('updating');
expect(updateCounter).toBe(1);
} catch (err) {
reject(err);
return;
}
done(true);
};
// Get result
test.checkForUpdate().then(fulfill).catch(reject);
});
expect(updateResult).toBe(true);
expect(test.status).toBe(true);
expect(updateCounter).toBe(1);
expect(test.contentLoaded).toBe(2);
});
test('Multiple sequential updates, success on first run', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
test.initCalled = (done) => done(true);
await test.init();
// Reload
let updateCounter = 0;
const updateResult = await new Promise((fulfill, reject) => {
// Setup callback
test.updateCalled = (done) => {
updateCounter++;
// Success only on first reload
done(updateCounter === 1);
};
// Get result
test.checkForUpdate().then(fulfill).catch(reject);
});
expect(updateResult).toBe(true);
expect(test.status).toBe(true);
expect(updateCounter).toBe(1);
expect(test.contentLoaded).toBe(2);
// Another reload
const update2Result = await test.checkForUpdate();
expect(update2Result).toBe(false);
expect(test.status).toBe(true);
expect(updateCounter).toBe(2);
expect(test.contentLoaded).toBe(2);
});
test('Multiple sequential updates, success on last run', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
test.initCalled = (done) => done(true);
await test.init();
// Reload
let updateCounter = 0;
const updateResult = await new Promise((fulfill, reject) => {
// Setup callback
test.updateCalled = (done) => {
updateCounter++;
// Success only on last reload
done(updateCounter === 2);
};
// Get result
test.checkForUpdate().then(fulfill).catch(reject);
});
expect(updateResult).toBe(false);
expect(test.status).toBe(true);
expect(updateCounter).toBe(1);
expect(test.contentLoaded).toBe(1);
// Another reload
const update2Result = await test.checkForUpdate();
expect(update2Result).toBe(true);
expect(test.status).toBe(true);
expect(updateCounter).toBe(2);
expect(test.contentLoaded).toBe(2);
});
test('Multiple updates at once', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
test.initCalled = (done) => done(true);
await test.init();
await new Promise((fulfill, reject) => {
// Setup callback
let updateCounter = 0;
let isSync = true;
let finishUpdate: BooleanCallback | undefined;
test.updateCalled = (done) => {
updateCounter++;
if (updateCounter === 1) {
// First run: complete asynchronously
finishUpdate = done;
return;
}
// Second run: complete immediately
done(false);
};
// Results
let result1: boolean | undefined;
let result2: boolean | undefined;
let result3: boolean | undefined;
let result4: boolean | undefined;
const tested = () => {
if (result1 === void 0 || result2 === void 0 || result3 === void 0 || result4 === void 0) {
// Still waiting
return;
}
try {
expect(result1).toBe(true);
expect(result2).toBe(false);
expect(result3).toBe(false);
expect(result4).toBe(false);
expect(updateCounter).toBe(2);
} catch (err) {
reject(err);
return;
}
fulfill(true);
};
// Run test twice
test.checkForUpdate()
.then((value) => {
result1 = value;
tested();
})
.catch(reject);
test.checkForUpdate()
.then((value) => {
result2 = value;
tested();
})
.catch(reject);
test.checkForUpdate()
.then((value) => {
result3 = value;
tested();
})
.catch(reject);
test.checkForUpdate()
.then((value) => {
result4 = value;
tested();
})
.catch(reject);
isSync = false;
// Finish loading asynchronously
setTimeout(() => {
try {
expect(finishUpdate).toBeDefined();
expect(updateCounter).toBe(1);
finishUpdate?.(true);
} catch (err) {
reject(err);
}
});
});
});
test('Multiple updates at once, resetting pending check', async () => {
// Create new instance, init
const test = new BaseDownloaderTest();
test.initCalled = (done) => done(true);
await test.init();
await new Promise((fulfill, reject) => {
// Setup callback
let updateCounter = 0;
let isSync = true;
let finishUpdate: BooleanCallback | undefined;
test.updateCalled = (done) => {
updateCounter++;
if (updateCounter === 1) {
// First run: complete asynchronously
finishUpdate = done;
return;
}
// Second run: should not be ran!
reject('updateCalled() should not be ran more than once');
};
// Results
let result1: boolean | undefined;
let result2: boolean | undefined;
const tested = () => {
if (result1 === void 0 || result2 === void 0) {
// Still waiting
return;
}
try {
expect(result1).toBe(true);
expect(result2).toBe(false);
expect(updateCounter).toBe(1);
} catch (err) {
reject(err);
return;
}
fulfill(true);
};
// Run test twice
test.checkForUpdate()
.then((value) => {
result1 = value;
tested();
})
.catch(reject);
test.checkForUpdate()
.then((value) => {
result2 = value;
tested();
})
.catch(reject);
isSync = false;
// Finish loading asynchronously
setTimeout(() => {
try {
expect(finishUpdate).toBeDefined();
expect(updateCounter).toBe(1);
test._pendingReload = false;
finishUpdate?.(true);
} catch (err) {
reject(err);
}
});
});
});
});

View File

@ -0,0 +1,114 @@
import { mkdir, readFile, writeFile, rm } from 'node:fs/promises';
import { DirectoryDownloader } from '../../lib/downloaders/directory';
import { uniqueCacheDir } from '../helpers';
describe('Directory downloader', () => {
class TestDownloader extends DirectoryDownloader<unknown> {
/**
* Test _loadContent()
*/
contentLoaded = 0;
async _loadContent() {
this.contentLoaded++;
}
}
test('Existing files', async () => {
// Create new instance
const test = new TestDownloader('tests/fixtures');
expect(test.status).toBe('pending-init');
expect(await test.init()).toBe(true);
expect(test.contentLoaded).toBe(1);
// Nothing to update
expect(await test.checkForUpdate()).toBe(false);
expect(test.contentLoaded).toBe(1);
});
test('Invalid directory', async () => {
// Cache directory
const dir = 'cache/' + uniqueCacheDir();
// Create new instance
const test = new TestDownloader(dir);
expect(test.status).toBe('pending-init');
expect(await test.init()).toBe(false);
expect(test.contentLoaded).toBe(0);
// Nothing to update
expect(await test.checkForUpdate()).toBe(false);
expect(test.contentLoaded).toBe(0);
});
test('Empty directory', async () => {
// Cache directory
const dir = 'cache/' + uniqueCacheDir();
try {
await mkdir(dir, {
recursive: true,
});
} catch {
//
}
// Create new instance
const test = new TestDownloader(dir);
expect(test.status).toBe('pending-init');
expect(await test.init()).toBe(true);
expect(test.contentLoaded).toBe(1);
// Nothing to update
expect(await test.checkForUpdate()).toBe(false);
expect(test.contentLoaded).toBe(1);
});
test('Has content', async () => {
// Cache directory
const dir = 'cache/' + uniqueCacheDir();
try {
await mkdir(dir, {
recursive: true,
});
} catch {
//
}
// Create few files
await writeFile(dir + '/collections.json', await readFile('tests/fixtures/collections.mdi.json'));
await writeFile(dir + '/mdi.json', await readFile('tests/fixtures/json/mdi.json'));
// Create new instance
const test = new TestDownloader(dir);
expect(test.status).toBe('pending-init');
expect(await test.init()).toBe(true);
expect(test.contentLoaded).toBe(1);
// Nothing to update
expect(await test.checkForUpdate()).toBe(false);
expect(test.contentLoaded).toBe(1);
// Replace file
await writeFile(dir + '/mdi.json', await readFile('tests/fixtures/json/mdi-light.json'));
expect(await test.checkForUpdate()).toBe(true);
expect(test.contentLoaded).toBe(2);
// Touch file: should trigger update because file modification time changes
await writeFile(dir + '/mdi.json', await readFile('tests/fixtures/json/mdi-light.json'));
expect(await test.checkForUpdate()).toBe(true);
expect(test.contentLoaded).toBe(3);
// Add new file
await writeFile(dir + '/mdi-light.json', await readFile('tests/fixtures/json/mdi-light.json'));
expect(await test.checkForUpdate()).toBe(true);
expect(test.contentLoaded).toBe(4);
// Delete
await rm(dir + '/mdi-light.json');
expect(await test.checkForUpdate()).toBe(true);
expect(test.contentLoaded).toBe(5);
// Check again: nothing to update
expect(await test.checkForUpdate()).toBe(false);
expect(test.contentLoaded).toBe(5);
});
});

View File

@ -0,0 +1,56 @@
import { readFile, rm, writeFile } from 'node:fs/promises';
import { RemoteDownloader } from '../../lib/downloaders/remote';
import { getDownloadDirectory } from '../../lib/downloaders/remote/target';
import type { RemoteDownloaderOptions } from '../../lib/types/downloaders/remote';
describe('Remote downloader', () => {
class TestDownloader extends RemoteDownloader<unknown> {
/**
* Test _loadContent()
*/
contentLoaded = 0;
async _loadContent() {
this.contentLoaded++;
}
}
test('NPM package', async () => {
// Clean up target directory
const options: RemoteDownloaderOptions = {
downloadType: 'npm',
package: '@iconify-json/mi',
};
try {
await rm(getDownloadDirectory(options), {
recursive: true,
});
} catch {
//
}
// Create new instance
const test = new TestDownloader(options, false);
expect(test.status).toBe('pending-init');
await test.init();
expect(test.contentLoaded).toBe(1);
// Nothing to update
expect(await test.checkForUpdate()).toBe(false);
expect(test.contentLoaded).toBe(1);
// Change version number
const directory = test._sourceDir as string;
const filename = directory + '/package.json';
const data = JSON.parse(await readFile(filename, 'utf8')) as Record<string, unknown>;
data.version = '1.0.0';
await writeFile(filename, JSON.stringify(data, null, '\t'), 'utf8');
// NPM updater checks content, so this should trigger update
expect(await test.checkForUpdate()).toBe(true);
expect(test.contentLoaded).toBe(2);
// Check package.json
const data2 = JSON.parse(await readFile(filename, 'utf8')) as Record<string, unknown>;
expect(data2.version).not.toBe(data.version);
}, 10000);
});

36
tests/fixtures/collections.mdi.json vendored Normal file
View File

@ -0,0 +1,36 @@
{
"mdi": {
"name": "Material Design Icons",
"total": 7134,
"author": {
"name": "Austin Andrews",
"url": "https://github.com/Templarian/MaterialDesign"
},
"license": {
"title": "Apache 2.0",
"spdx": "Apache-2.0",
"url": "https://github.com/Templarian/MaterialDesign/blob/master/LICENSE"
},
"samples": ["account-check", "bell-alert-outline", "calendar-edit"],
"height": 24,
"category": "General",
"palette": false
},
"mdi-light": {
"name": "Material Design Light",
"total": 267,
"author": {
"name": "Austin Andrews",
"url": "https://github.com/Templarian/MaterialDesignLight"
},
"license": {
"title": "Open Font License",
"spdx": "OFL-1.1",
"url": "https://github.com/Templarian/MaterialDesignLight/blob/master/LICENSE.md"
},
"samples": ["cart", "home", "login"],
"height": 24,
"category": "General",
"palette": false
}
}

830
tests/fixtures/json/mdi-light.json vendored Normal file
View File

@ -0,0 +1,830 @@
{
"prefix": "mdi-light",
"info": {
"name": "Material Design Light",
"total": 267,
"author": {
"name": "Austin Andrews",
"url": "https://github.com/Templarian/MaterialDesignLight"
},
"license": {
"title": "Open Font License",
"spdx": "OFL-1.1",
"url": "https://github.com/Templarian/MaterialDesignLight/blob/master/LICENSE.md"
},
"samples": [
"cart",
"home",
"login"
],
"height": 24,
"category": "General",
"palette": false
},
"lastModified": 1656182719,
"icons": {
"account": {
"body": "<path fill=\"currentColor\" d=\"M11.5 14c4.142 0 7.5 1.567 7.5 3.5V20H4v-2.5c0-1.933 3.358-3.5 7.5-3.5Zm6.5 3.5c0-1.38-2.91-2.5-6.5-2.5S5 16.12 5 17.5V19h13v-1.5ZM11.5 5a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7Zm0 1a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5Z\"/>"
},
"account-alert": {
"body": "<path fill=\"currentColor\" d=\"M10.5 14c4.142 0 7.5 1.567 7.5 3.5V20H3v-2.5c0-1.933 3.358-3.5 7.5-3.5Zm6.5 3.5c0-1.38-2.91-2.5-6.5-2.5S4 16.12 4 17.5V19h13v-1.5ZM10.5 5a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7Zm0 1a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5ZM20 16v-1h1v1h-1Zm0-3V7h1v6h-1Z\"/>"
},
"alarm": {
"body": "<path fill=\"currentColor\" d=\"M11.5 6a7.5 7.5 0 1 1 0 15a7.5 7.5 0 0 1 0-15Zm0 1a6.5 6.5 0 1 0 0 13a6.5 6.5 0 0 0 0-13ZM11 9h1v4.363l3.048 1.421l-.423.906L11 14V9Zm4.25-3.75l.643-.766l3.83 3.214l-.643.766l-3.83-3.214Zm-7.5 0L3.92 8.464l-.643-.766l3.83-3.214l.643.766Z\"/>"
},
"alarm-plus": {
"body": "<path fill=\"currentColor\" d=\"M11.5 6a7.5 7.5 0 1 1 0 15a7.5 7.5 0 0 1 0-15Zm0 1a6.5 6.5 0 1 0 0 13a6.5 6.5 0 0 0 0-13Zm3.75-1.75l.643-.766l3.83 3.214l-.643.766l-3.83-3.214Zm-7.5 0L3.92 8.464l-.643-.766l3.83-3.214l.643.766ZM11 11h1v2h2v1h-2v2h-1v-2H9v-1h2v-2Z\"/>"
},
"alert": {
"body": "<path fill=\"currentColor\" d=\"M1 21L11.5 2.813L22 21H1Zm19.268-1L11.5 4.813L2.732 20h17.536ZM11 14v-4h1v4h-1Zm0 2h1v2h-1v-2Z\"/>"
},
"alert-circle": {
"body": "<path fill=\"currentColor\" d=\"M11.5 3a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17ZM11 17v-2h1v2h-1Zm0-4V8h1v5h-1Z\"/>"
},
"alert-octagon": {
"body": "<path fill=\"currentColor\" d=\"M3 16.011V8.98L7.98 4h7.04L20 8.98v7.046L15.025 21H7.99L3 16.011ZM8.393 5L4 9.393v6.204L8.403 20h6.208L19 15.611V9.393L14.607 5H8.393ZM11 8h1v5h-1V8Zm0 7h1v2h-1v-2Z\"/>"
},
"arrange-bring-forward": {
"body": "<path fill=\"currentColor\" d=\"M8 9h4v1H9.707l6.718 6.718l-.708.707L9 10.707V13H8V9ZM3 4h12v9l-1-1V5H4v10h7l1 1H3V4Zm17 5v12H8v-3h1v2h10V10h-2V9h3Z\"/>"
},
"arrange-bring-to-front": {
"body": "<path fill=\"currentColor\" d=\"M9 7V5H4v5h2v1H3V4h7v3H9Zm4 14v-3h1v2h5v-5h-2v-1h3v7h-7ZM8 9h7v7H8V9Zm1 1v5h5v-5H9Z\"/>"
},
"arrange-send-backward": {
"body": "<path fill=\"currentColor\" d=\"M6 7h4v1H7.707l6.718 6.717l-.708.708L7 8.707V11H6V7Zm14 14H8v-9l1 1v7h10V10h-7l-1-1h9v12ZM3 16V4h12v3h-1V5H4v10h2v1H3Z\"/>"
},
"arrange-send-to-back": {
"body": "<path fill=\"currentColor\" d=\"M9 5H4v5h5V5Zm1 6H3V4h7v7Zm3 10v-7h7v7h-7Zm1-1h5v-5h-5v5Zm2-12v4h-1V9h-3V8h4Zm-9 9v-4h1v3h3v1H7Z\"/>"
},
"arrow-down": {
"body": "<path fill=\"currentColor\" d=\"M12 5v12.25L17.25 12l.75.664l-6.5 6.5l-6.5-6.5l.75-.664L11 17.25V5h1Z\"/>"
},
"arrow-down-circle": {
"body": "<path fill=\"currentColor\" d=\"M12.003 7v8.25l3.25-3.25l.75.664l-4.5 4.5l-4.5-4.5l.75-.664l3.25 3.25V7h1Zm-.5 15a9.5 9.5 0 1 1 0-19a9.5 9.5 0 0 1 0 19Zm0-1a8.5 8.5 0 1 0 0-17a8.5 8.5 0 0 0 0 17Z\"/>"
},
"arrow-left": {
"body": "<path fill=\"currentColor\" d=\"M19 13H6.75L12 18.25l-.664.75l-6.5-6.5l6.5-6.5l.664.75L6.75 12H19v1Z\"/>"
},
"arrow-left-circle": {
"body": "<path fill=\"currentColor\" d=\"M17 13H8.75L12 16.25l-.664.75l-4.5-4.5l4.5-4.5l.664.75L8.75 12H17v1Zm-15-.5a9.5 9.5 0 1 1 19 0a9.5 9.5 0 0 1-19 0Zm1 0a8.5 8.5 0 1 0 17 0a8.5 8.5 0 0 0-17 0Z\"/>"
},
"arrow-right": {
"body": "<path fill=\"currentColor\" d=\"M4 12h12.25L11 6.75l.664-.75l6.5 6.5l-6.5 6.5l-.664-.75L16.25 13H4v-1Z\"/>"
},
"arrow-right-circle": {
"body": "<path fill=\"currentColor\" d=\"M6.003 12h8.25l-3.25-3.25l.664-.75l4.5 4.5l-4.5 4.5l-.664-.75l3.25-3.25h-8.25v-1Zm15 .5a9.5 9.5 0 1 1-19 0a9.5 9.5 0 0 1 19 0Zm-1 0a8.5 8.5 0 1 0-17 0a8.5 8.5 0 0 0 17 0Z\"/>"
},
"arrow-up": {
"body": "<path fill=\"currentColor\" d=\"M11 20V7.75L5.75 13L5 12.336l6.5-6.5l6.5 6.5l-.75.664L12 7.75V20h-1Z\"/>"
},
"arrow-up-circle": {
"body": "<path fill=\"currentColor\" d=\"M11 18V9.75L7.75 13L7 12.336l4.5-4.5l4.5 4.5l-.75.664L12 9.75V18h-1Zm.5-15a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17Z\"/>"
},
"bank": {
"body": "<path fill=\"currentColor\" d=\"M11 2.5L20 7v2H2V7l9-4.5Zm4 7.5h4v8h-4v-8ZM2 22v-3h18v3H2Zm7-12h4v8H9v-8Zm-6 0h4v8H3v-8Zm0 10v1h16v-1H3Zm1-9v6h2v-6H4Zm6 0v6h2v-6h-2Zm6 0v6h2v-6h-2ZM3 8h16v-.4l-8-4.019l-8 4.02V8Z\"/>"
},
"bell": {
"body": "<path fill=\"currentColor\" d=\"M12 4.5a.5.5 0 0 0-1 0v1.527A4.5 4.5 0 0 0 7 10.5v5.914L5.414 18h12.172L16 16.414V10.5a4.5 4.5 0 0 0-4-4.473V4.5ZM11.5 3A1.5 1.5 0 0 1 13 4.5v.707c2.309.653 4 2.775 4 5.293V16l3 3H3l3-3v-5.5a5.502 5.502 0 0 1 4-5.293V4.5A1.5 1.5 0 0 1 11.5 3Zm0 19a2.5 2.5 0 0 1-2.45-2h1.035a1.5 1.5 0 0 0 2.83 0h1.035a2.5 2.5 0 0 1-2.45 2Z\"/>"
},
"bell-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75L20.25 20.5l-.707.707L17.336 19H3l3-3v-5.5c0-.83.184-1.617.513-2.323l-3.72-3.72ZM12 4.5a.5.5 0 0 0-1 0v1.527a4.485 4.485 0 0 0-2.599 1.21l-.707-.707A5.494 5.494 0 0 1 10 5.207V4.5a1.5 1.5 0 0 1 3 0v.707c2.309.653 4 2.775 4 5.293v5.336l-1-1V10.5a4.5 4.5 0 0 0-4-4.473V4.5Zm-5 6v5.914L5.414 18h10.922L7.277 8.941A4.49 4.49 0 0 0 7 10.5ZM11.5 22a2.5 2.5 0 0 1-2.45-2h1.035a1.5 1.5 0 0 0 2.83 0h1.035a2.5 2.5 0 0 1-2.45 2Z\"/>"
},
"bell-plus": {
"body": "<path fill=\"currentColor\" d=\"M11.5 3A1.5 1.5 0 0 0 10 4.5v.707A5.503 5.503 0 0 0 6 10.5V16l-3 3h17l-3-3v-5.5a5.503 5.503 0 0 0-4-5.293V4.5A1.5 1.5 0 0 0 11.5 3Zm0 1a.5.5 0 0 1 .5.5v1.527a4.5 4.5 0 0 1 4 4.473v5.914L17.586 18H5.414L7 16.414V10.5a4.5 4.5 0 0 1 4-4.473V4.5a.5.5 0 0 1 .5-.5Zm-.5 6v2H9v1h2v2h1v-2h2v-1h-2v-2h-1ZM9.05 20a2.5 2.5 0 0 0 4.9 0h-1.036a1.5 1.5 0 0 1-2.828 0H9.05Z\"/>"
},
"bluetooth": {
"body": "<path fill=\"currentColor\" d=\"M11 3h1l4.854 4.854l-4.647 4.646l4.647 4.646L12 22h-1v-8.293l-4.45 4.45l-.707-.707l4.95-4.95l-4.95-4.95l.707-.707l4.45 4.45V3Zm1 1.414v6.879l3.44-3.44L12 4.415Zm0 16.172l3.44-3.44L12 13.707v6.879Z\"/>"
},
"book": {
"body": "<path fill=\"currentColor\" d=\"M7 3h9a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-3v6.7l-3-2.1l-3 2.1V4Zm5 0H8v4.78l2-1.401l2 1.4V4Z\"/>"
},
"book-multiple": {
"body": "<path fill=\"currentColor\" d=\"M8 3h9a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-3v6.7l-3-2.1l-3 2.1V4Zm5 0H9v4.78l2-1.401l2 1.4V4ZM8 24a5 5 0 0 1-5-5V7h1v12a4 4 0 0 0 4 4h8v1H8Z\"/>"
},
"book-plus": {
"body": "<path fill=\"currentColor\" d=\"M7 3h9a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-3v6.7l-3-2.1l-3 2.1V4Zm5 0H8v4.78l2-1.401l2 1.4V4ZM9 19v-2H7v-1h2v-2h1v2h2v1h-2v2H9Z\"/>"
},
"bookmark": {
"body": "<path fill=\"currentColor\" d=\"M8 3h8a3 3 0 0 1 3 3v15l-7-3l-7 3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v13.49l6-2.548l6 2.547V6a2 2 0 0 0-2-2H8Z\"/>"
},
"border-all": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v17H3V4Zm1 1v7h7V5H4Zm15 7V5h-7v7h7ZM4 20h7v-7H4v7Zm15 0v-7h-7v7h7Z\"/>"
},
"border-bottom": {
"body": "<path fill=\"currentColor\" d=\"M3 20h8v-1h1v1h8v1H3v-1Zm17-2h-1v-2h1v2ZM4 14H3v-3h1v1h1v1H4v1Zm0 4H3v-2h1v2Zm8-5h-1v-1h1v1ZM3 5V4h1v1H3Zm0 2h1v2H3V7Zm12-2V4h2v1h-2ZM6 5V4h2v1H6Zm4 0V4h3v1h-1v1h-1V5h-1Zm9 9v-1h-1v-1h1v-1h1v3h-1Zm1-5h-1V7h1v2Zm-1-4V4h1v1h-1Zm-5 8v-1h2v1h-2Zm-2 4h-1v-2h1v2Zm0-7h-1V8h1v2Zm-5 3v-1h2v1H7Z\"/>"
},
"border-horizontal": {
"body": "<path fill=\"currentColor\" d=\"M20 18h-1v-2h1v2ZM4 14H3v-3h1v1h15v-1h1v3h-1v-1H4v1Zm0 4H3v-2h1v2ZM3 5V4h1v1H3Zm0 2h1v2H3V7Zm12-2V4h2v1h-2ZM6 5V4h2v1H6Zm4 0V4h3v1h-1v1h-1V5h-1Zm10 4h-1V7h1v2Zm-1-4V4h1v1h-1Zm-7 12h-1v-2h1v2Zm0-7h-1V8h1v2ZM3 20h1v1H3v-1Zm12 0h2v1h-2v-1Zm-9 0h2v1H6v-1Zm4 0h1v-1h1v1h1v1h-3v-1Zm9 0h1v1h-1v-1Z\"/>"
},
"border-inside": {
"body": "<path fill=\"currentColor\" d=\"M17 4v1h-2V4h2Zm-4 16v1h-3v-1h1v-7H4v1H3v-3h1v1h7V5h-1V4h3v1h-1v7h7v-1h1v3h-1v-1h-7v7h1Zm4 0v1h-2v-1h2ZM4 21H3v-1h1v1Zm2 0v-1h2v1H6ZM4 9H3V7h1v2Zm0 9H3v-2h1v2ZM8 4v1H6V4h2ZM4 5H3V4h1v1Zm15 16v-1h1v1h-1Zm0-12V7h1v2h-1Zm0 9v-2h1v2h-1Zm0-13V4h1v1h-1Z\"/>"
},
"border-left": {
"body": "<path fill=\"currentColor\" d=\"M4 4v8h1v1H4v8H3V4h1Zm2 17v-1h2v1H6Zm4-16V4h3v1h-1v1h-1V5h-1ZM6 5V4h2v1H6Zm5 8v-1h1v1h-1Zm8-9h1v1h-1V4Zm-2 0v1h-2V4h2Zm2 12h1v2h-1v-2Zm0-9h1v2h-1V7Zm0 4h1v3h-1v-1h-1v-1h1v-1Zm-9 9h1v-1h1v1h1v1h-3v-1Zm5 1v-1h2v1h-2Zm4-1h1v1h-1v-1Zm-8-5h1v2h-1v-2Zm-4-2v-1h2v1H7Zm7 0v-1h2v1h-2Zm-3-5h1v2h-1V8Z\"/>"
},
"border-none": {
"body": "<path fill=\"currentColor\" d=\"M20 18h-1v-2h1v2ZM4 14H3v-3h1v1h1v1H4v1Zm15-2v-1h1v3h-1v-1h-1v-1h1Zm-5 1v-1h2v1h-2Zm-3 0v-1h1v1h-1Zm-4 0v-1h2v1H7Zm-3 5H3v-2h1v2ZM3 5V4h1v1H3Zm0 2h1v2H3V7Zm12-2V4h2v1h-2ZM6 5V4h2v1H6Zm4 0V4h3v1h-1v1h-1V5h-1Zm10 4h-1V7h1v2Zm-1-4V4h1v1h-1Zm-7 12h-1v-2h1v2Zm0-7h-1V8h1v2ZM3 20h1v1H3v-1Zm12 0h2v1h-2v-1Zm-9 0h2v1H6v-1Zm4 0h1v-1h1v1h1v1h-3v-1Zm9 0h1v1h-1v-1Z\"/>"
},
"border-outside": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v17H3V4Zm1 1v7h1v1H4v7h7v-1h1v1h7v-7h-1v-1h1V5h-7v1h-1V5H4Zm3 7h2v1H7v-1Zm4 0h1v1h-1v-1Zm0 3h1v2h-1v-2Zm3-3h2v1h-2v-1Zm-3-2V8h1v2h-1Z\"/>"
},
"border-right": {
"body": "<path fill=\"currentColor\" d=\"M19 21v-8h-1v-1h1V4h1v17h-1ZM17 4v1h-2V4h2Zm-4 16v1h-3v-1h1v-1h1v1h1Zm4 0v1h-2v-1h2Zm-5-8v1h-1v-1h1Zm-8 9H3v-1h1v1Zm2 0v-1h2v1H6ZM4 9H3V7h1v2Zm0 9H3v-2h1v2Zm0-4H3v-3h1v1h1v1H4v1Zm9-9h-1v1h-1V5h-1V4h3v1ZM8 4v1H6V4h2ZM4 5H3V4h1v1Zm8 5h-1V8h1v2Zm4 2v1h-2v-1h2Zm-7 0v1H7v-1h2Zm3 5h-1v-2h1v2Z\"/>"
},
"border-top": {
"body": "<path fill=\"currentColor\" d=\"M20 5h-8v1h-1V5H3V4h17v1ZM3 7h1v2H3V7Zm16 4h1v3h-1v-1h-1v-1h1v-1Zm0-4h1v2h-1V7Zm-8 5h1v1h-1v-1Zm9 8v1h-1v-1h1Zm0-2h-1v-2h1v2ZM8 20v1H6v-1h2Zm9 0v1h-2v-1h2Zm-4 0v1h-3v-1h1v-1h1v1h1Zm-9-9v1h1v1H4v1H3v-3h1Zm-1 5h1v2H3v-2Zm1 4v1H3v-1h1Zm5-8v1H7v-1h2Zm2-4h1v2h-1V8Zm0 7h1v2h-1v-2Zm5-3v1h-2v-1h2Z\"/>"
},
"border-vertical": {
"body": "<path fill=\"currentColor\" d=\"M17 4v1h-2V4h2Zm-4 16v1h-3v-1h1V5h-1V4h3v1h-1v15h1Zm4 0v1h-2v-1h2ZM4 21H3v-1h1v1Zm2 0v-1h2v1H6ZM4 9H3V7h1v2Zm0 9H3v-2h1v2Zm0-4H3v-3h1v1h1v1H4v1ZM8 4v1H6V4h2ZM4 5H3V4h1v1Zm12 7v1h-2v-1h2Zm-7 0v1H7v-1h2Zm10 9v-1h1v1h-1Zm0-12V7h1v2h-1Zm0 9v-2h1v2h-1Zm0-4v-1h-1v-1h1v-1h1v3h-1Zm0-9V4h1v1h-1Z\"/>"
},
"briefcase": {
"body": "<path fill=\"currentColor\" d=\"M5 7h3V5l2-2h3l2 2v2h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3Zm5.414-3L9 5.414V7h5V5.414L12.586 4h-2.172ZM5 8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2H5Z\"/>"
},
"bullhorn": {
"body": "<path fill=\"currentColor\" d=\"m14 4l-3 3H3a3 3 0 0 0-3 3v3a3 3 0 0 0 3 3v3a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3h3l3 3h2V4h-2Zm.414 1H15v13h-.586l-3-3H3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h8.414l3-3ZM18 7v1.008a3.501 3.501 0 0 1 0 6.93v1.007A4.5 4.5 0 0 0 18 7Zm0 3.059v2.828a1.5 1.5 0 0 0 0-2.828ZM4 16h3v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3Z\"/>"
},
"calendar": {
"body": "<path fill=\"currentColor\" d=\"M7 2h1a1 1 0 0 1 1 1v1h5V3a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3V3a1 1 0 0 1 1-1Zm8 2h1V3h-1v1ZM8 4V3H7v1h1ZM6 5a2 2 0 0 0-2 2v1h15V7a2 2 0 0 0-2-2H6ZM4 18a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V9H4v9Zm8-5h5v5h-5v-5Zm1 1v3h3v-3h-3Z\"/>"
},
"camcorder": {
"body": "<path fill=\"currentColor\" d=\"M5 7h9a2 2 0 0 1 2 2v2.5l4-4v10l-4-4V16a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2Zm0 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1H5Zm14 1.914L16.414 12.5L19 15.086V9.914Z\"/>"
},
"camera": {
"body": "<path fill=\"currentColor\" d=\"M11.5 8a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9Zm0 1a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM5 5h2l2-2h5l2 2h2a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm4.414-1l-2 2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-2.414l-2-2H9.414Z\"/>"
},
"cancel": {
"body": "<path fill=\"currentColor\" d=\"M11.503 22a9.5 9.5 0 1 1 0-19a9.5 9.5 0 0 1 0 19Zm0-1a8.5 8.5 0 0 0 6.352-14.148l-12 12A8.468 8.468 0 0 0 11.503 21Zm0-17a8.5 8.5 0 0 0-6.355 14.145l12-12A8.468 8.468 0 0 0 11.503 4Z\"/>"
},
"cart": {
"body": "<path fill=\"currentColor\" d=\"M16 18a2 2 0 1 1 0 4a2 2 0 0 1 0-4Zm0 1a1 1 0 1 0 0 2a1 1 0 0 0 0-2Zm-9-1a2 2 0 1 1 0 4a2 2 0 0 1 0-4Zm0 1a1 1 0 1 0 0 2a1 1 0 0 0 0-2ZM18 6H4.273l2.547 6H15a.994.994 0 0 0 .8-.402l3-4h.001A1 1 0 0 0 18 6Zm-3 7H6.866L6.1 14.56L6 15a1 1 0 0 0 1 1h11v1H7a2 2 0 0 1-1.75-2.97l.72-1.474L2.338 4H1V3h2l.849 2H18a2 2 0 0 1 1.553 3.26l-2.914 3.886A1.998 1.998 0 0 1 15 13Z\"/>"
},
"chart-areaspline": {
"body": "<path fill=\"currentColor\" d=\"M3 4h1v13.979L9.57 8.33l6.01 3.47l3.615-6.263l.866.5l-4.115 7.129l-6.01-3.47L4 19.98V20h2.297l3.872-6.706l.5-.866l.865.5l5.144 2.97L20 10.144V21H3V4Zm14.044 13.264l-6.01-3.47L7.452 20H19v-6.124l-1.956 3.388Z\"/>"
},
"chart-bar": {
"body": "<path fill=\"currentColor\" d=\"M2 4h1v16h2V10h4v10h2V6h4v14h2v-6h4v7H2V4Zm16 11v5h2v-5h-2Zm-6-8v13h2V7h-2Zm-6 4v9h2v-9H6Z\"/>"
},
"chart-histogram": {
"body": "<path fill=\"currentColor\" d=\"M3 4h1v9h3V7h5v4h4v4h4v6H3V4Zm13 12v4h3v-4h-3Zm-4-4v8h3v-8h-3ZM8 8v12h3V8H8Zm-4 6v6h3v-6H4Z\"/>"
},
"chart-line": {
"body": "<path fill=\"currentColor\" d=\"M3 4h1v14l5.581-9.667l6.01 3.47l3.615-6.263l.866.5l-4.116 7.129l-6.009-3.47L4 20h16v1H3V4Z\"/>"
},
"chart-pie": {
"body": "<path fill=\"currentColor\" d=\"M12 3h1a8 8 0 0 1 8 8v1h-9V3Zm1 8h7a7 7 0 0 0-7-7v7Zm-3 3h8a8 8 0 1 1-8-8v8Zm-1 1V7.07A7.002 7.002 0 0 0 10 21a7.001 7.001 0 0 0 6.93-6H9Z\"/>"
},
"check": {
"body": "<path fill=\"currentColor\" d=\"M18.9 8.1L9 18l-4.95-4.95l.707-.707L9 16.586l9.192-9.193l.707.708Z\"/>"
},
"check-bold": {
"body": "<path fill=\"currentColor\" d=\"m9 19l-5.657-5.657l2.121-2.121L9 14.757l8.485-8.485l2.122 2.121L9 19Zm-3.536-6.364l-.707.707L9 17.586l9.192-9.193l-.707-.707L9 16.172l-3.536-3.536Z\"/>"
},
"chevron-double-down": {
"body": "<path fill=\"currentColor\" d=\"M17.157 7.593L11.5 13.25L5.843 7.593l.707-.707l4.95 4.95l4.95-4.95l.707.707Zm0 4L11.5 17.25l-5.657-5.657l.707-.707l4.95 4.95l4.95-4.95l.707.707Z\"/>"
},
"chevron-double-left": {
"body": "<path fill=\"currentColor\" d=\"M16.407 18.157L10.75 12.5l5.657-5.657l.707.707l-4.95 4.95l4.95 4.95l-.707.707Zm-4 0L6.75 12.5l5.657-5.657l.707.707l-4.95 4.95l4.95 4.95l-.707.707Z\"/>"
},
"chevron-double-right": {
"body": "<path fill=\"currentColor\" d=\"M6.593 6.843L12.25 12.5l-5.657 5.657l-.707-.707l4.95-4.95l-4.95-4.95l.707-.707Zm4 0L16.25 12.5l-5.657 5.657l-.707-.707l4.95-4.95l-4.95-4.95l.707-.707Z\"/>"
},
"chevron-double-up": {
"body": "<path fill=\"currentColor\" d=\"M5.843 17.407L11.5 11.75l5.657 5.657l-.707.707l-4.95-4.95l-4.95 4.95l-.707-.707Zm0-4L11.5 7.75l5.657 5.657l-.707.707l-4.95-4.95l-4.95 4.95l-.707-.707Z\"/>"
},
"chevron-down": {
"body": "<path fill=\"currentColor\" d=\"M5.843 9.593L11.5 15.25l5.657-5.657l-.707-.707l-4.95 4.95l-4.95-4.95l-.707.707Z\"/>"
},
"chevron-left": {
"body": "<path fill=\"currentColor\" d=\"M14.407 18.157L8.75 12.5l5.657-5.657l.707.707l-4.95 4.95l4.95 4.95l-.707.707Z\"/>"
},
"chevron-right": {
"body": "<path fill=\"currentColor\" d=\"M8.593 18.157L14.25 12.5L8.593 6.843l-.707.707l4.95 4.95l-4.95 4.95l.707.707Z\"/>"
},
"chevron-up": {
"body": "<path fill=\"currentColor\" d=\"M5.843 15.407L11.5 9.75l5.657 5.657l-.707.707l-4.95-4.95l-4.95 4.95l-.707-.707Z\"/>"
},
"clipboard": {
"body": "<path fill=\"currentColor\" d=\"M6 5h2.5a3 3 0 1 1 6 0H17a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-1v3H7V6H6Zm2 2h7V6H8v2Zm3.5-5a2 2 0 0 0-2 2h4a2 2 0 0 0-2-2Z\"/>"
},
"clipboard-check": {
"body": "<path fill=\"currentColor\" d=\"M6 5h2.5a3 3 0 1 1 6 0H17a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-1v3H7V6H6Zm2 2h7V6H8v2Zm3.5-5a2 2 0 0 0-2 2h4a2 2 0 0 0-2-2Zm5.65 8.6L10 18.75l-3.2-3.2l.707-.707L10 17.336l6.442-6.443l.707.707Z\"/>"
},
"clipboard-plus": {
"body": "<path fill=\"currentColor\" d=\"M6 5h2.5a3 3 0 1 1 6 0H17a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-1v3H7V6H6Zm2 2h7V6H8v2Zm3.5-5a2 2 0 0 0-2 2h4a2 2 0 0 0-2-2ZM8 19v-2H6v-1h2v-2h1v2h2v1H9v2H8Z\"/>"
},
"clipboard-text": {
"body": "<path fill=\"currentColor\" d=\"M6 5h2.5a3 3 0 1 1 6 0H17a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-1v3H7V6H6Zm2 2h7V6H8v2Zm3.5-5a2 2 0 0 0-2 2h4a2 2 0 0 0-2-2ZM6 11h11v1H6v-1Zm0 3h11v1H6v-1Zm0 3h9v1H6v-1Z\"/>"
},
"clock": {
"body": "<path fill=\"currentColor\" d=\"M11.5 3a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17ZM11 7h1v5.423l4.696 2.711l-.5.866L11 13V7Z\"/>"
},
"closed-caption": {
"body": "<path fill=\"currentColor\" d=\"M4 5h15a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h15a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H4Zm4.5 2c.99 0 1.904.32 2.647.86l-.588.81a3.5 3.5 0 1 0 0 5.662l.588.808A4.5 4.5 0 1 1 8.5 8Zm7.853 0c.99 0 1.904.32 2.647.86l-.588.81a3.5 3.5 0 1 0 0 5.662l.588.808A4.5 4.5 0 1 1 16.353 8Z\"/>"
},
"cloud": {
"body": "<path fill=\"currentColor\" d=\"M5.5 20a5.5 5.5 0 0 1-.002-11a6.502 6.502 0 0 1 12.485 2.03L18.5 11a4.5 4.5 0 1 1 0 9h-13Zm0-10a4.5 4.5 0 1 0 0 9h13a3.5 3.5 0 1 0-1.569-6.63a5.5 5.5 0 0 0-10.74-2.317L5.501 10Z\"/>"
},
"cloud-download": {
"body": "<path fill=\"currentColor\" d=\"M5.5 20a5.5 5.5 0 0 1-.002-11a6.502 6.502 0 0 1 12.485 2.03L18.5 11a4.5 4.5 0 1 1 0 9h-13Zm0-10a4.5 4.5 0 1 0 0 9h13a3.5 3.5 0 1 0-1.569-6.63L17 11.5a5.5 5.5 0 0 0-10.808-1.447L5.5 10Zm6.5 0v5.25L14.25 13l.75.664l-3.5 3.5l-3.5-3.5l.75-.664L11 15.25V10h1Z\"/>"
},
"cloud-upload": {
"body": "<path fill=\"currentColor\" d=\"M5.5 20a5.5 5.5 0 0 1-.002-11a6.502 6.502 0 0 1 12.485 2.03L18.5 11a4.5 4.5 0 1 1 0 9h-13Zm0-10a4.5 4.5 0 1 0 0 9h13a3.5 3.5 0 1 0-1.569-6.63L17 11.5a5.5 5.5 0 0 0-10.808-1.447L5.5 10Zm6.5 7v-5.25L14.25 14l.75-.664l-3.5-3.5l-3.5 3.5l.75.664L11 11.75V17h1Z\"/>"
},
"cog": {
"body": "<path fill=\"currentColor\" d=\"m19.588 15.492l-1.814-1.29a6.483 6.483 0 0 0-.005-3.421l1.82-1.274l-1.453-2.514l-2.024.926a6.484 6.484 0 0 0-2.966-1.706L12.953 4h-2.906l-.193 2.213A6.483 6.483 0 0 0 6.889 7.92l-2.025-.926l-1.452 2.514l1.82 1.274a6.483 6.483 0 0 0-.006 3.42l-1.814 1.29l1.452 2.502l2.025-.927a6.483 6.483 0 0 0 2.965 1.706l.193 2.213h2.906l.193-2.213a6.484 6.484 0 0 0 2.965-1.706l2.025.927l1.453-2.501ZM13.505 2.985a.5.5 0 0 1 .5.477l.178 2.035a7.45 7.45 0 0 1 2.043 1.178l1.85-.863a.5.5 0 0 1 .662.195l2.005 3.47a.5.5 0 0 1-.162.671l-1.674 1.172c.128.798.124 1.593.001 2.359l1.673 1.17a.5.5 0 0 1 .162.672l-2.005 3.457a.5.5 0 0 1-.662.195l-1.85-.863c-.602.49-1.288.89-2.043 1.179l-.178 2.035a.5.5 0 0 1-.5.476h-4.01a.5.5 0 0 1-.5-.476l-.178-2.035a7.453 7.453 0 0 1-2.043-1.179l-1.85.863a.5.5 0 0 1-.663-.194L2.257 15.52a.5.5 0 0 1 .162-.671l1.673-1.171a7.45 7.45 0 0 1 0-2.359L2.42 10.148a.5.5 0 0 1-.162-.67L4.26 6.007a.5.5 0 0 1 .663-.195l1.85.863a7.45 7.45 0 0 1 2.043-1.178l.178-2.035a.5.5 0 0 1 .5-.477h4.01ZM11.5 9a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7Zm0 1a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5Z\"/>"
},
"comment": {
"body": "<path fill=\"currentColor\" d=\"M5 3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-4.586l-3.707 3.707A1 1 0 0 1 8 21v-3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm13 1H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h4v4l4-4h5a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z\"/>"
},
"comment-alert": {
"body": "<path fill=\"currentColor\" d=\"M5 3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-4.586l-3.707 3.707A1 1 0 0 1 8 21v-3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm13 1H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h4v4l4-4h5a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm-7 2h1v5h-1V6Zm0 7h1v2h-1v-2Z\"/>"
},
"comment-text": {
"body": "<path fill=\"currentColor\" d=\"M5 3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-4.586l-3.707 3.707A1 1 0 0 1 8 21v-3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm13 1H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h4v4l4-4h5a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM5 7h13v1H5V7Zm0 3h12v1H5v-1Zm0 3h8v1H5v-1Z\"/>"
},
"console": {
"body": "<path fill=\"currentColor\" d=\"M5 4h13a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2h17a2 2 0 0 0-2-2H5ZM3 18a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V8H3v10Zm14 0h-5v-1h5v1ZM6 10.5l.707-.707L10.914 14l-4.207 4.207L6 17.5L9.5 14L6 10.5Z\"/>"
},
"content-cut": {
"body": "<path fill=\"currentColor\" d=\"M9 6.5c0 .786-.26 1.512-.697 2.096L20 20.293V21h-.707L11.5 13.207l-3.197 3.197a3.5 3.5 0 1 1-.707-.707l3.197-3.197l-3.197-3.197A3.5 3.5 0 1 1 9 6.5Zm-1 0a2.5 2.5 0 1 0-5 0a2.5 2.5 0 0 0 5 0ZM19.293 4H20v.707l-7.146 7.147l-.708-.707L19.293 4ZM5.5 16a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5Z\"/>"
},
"content-duplicate": {
"body": "<path fill=\"currentColor\" d=\"M9 6h8a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3v-1h1v1a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v5H6V9a3 3 0 0 1 3-3ZM5 2h10v1H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h7.25L10 13.75l.664-.75l3.5 3.5l-3.5 3.5l-.664-.75L12.25 17H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3Z\"/>"
},
"content-paste": {
"body": "<path fill=\"currentColor\" d=\"M11.5 1a2.5 2.5 0 0 1 2.45 2H17a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h3.05a2.5 2.5 0 0 1 2.45-2Zm1.415 2a1.5 1.5 0 0 0-2.83 0h2.83ZM6 4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1v3H7V4H6Zm2 0v2h7V4H8Z\"/>"
},
"content-save": {
"body": "<path fill=\"currentColor\" d=\"M6 4h10.586L20 7.414V18a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7.914L16.086 5H15v5H6V5Zm1 0v4h7V5H7Zm5 7a3 3 0 1 1 0 6a3 3 0 0 1 0-6Zm0 1a2 2 0 1 0 0 4a2 2 0 0 0 0-4Z\"/>"
},
"content-save-all": {
"body": "<path fill=\"currentColor\" d=\"M6 3h10.586L20 6.414V17a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V6.914L16.086 4H15v5H6V4Zm1 0v4h7V4H7Zm5 7a3 3 0 1 1 0 6a3 3 0 0 1 0-6Zm0 1a2 2 0 1 0 0 4a2 2 0 0 0 0-4ZM6 22a5 5 0 0 1-5-5V7h1v10a4 4 0 0 0 4 4h10v1H6Z\"/>"
},
"credit-card": {
"body": "<path fill=\"currentColor\" d=\"M5 5h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v1h17V8a2 2 0 0 0-2-2H5ZM3 17a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-5H3v5Zm2-1h4v1H5v-1Zm6 0h3v1h-3v-1Zm-8-6v1h17v-1H3Z\"/>"
},
"crop": {
"body": "<path fill=\"currentColor\" d=\"M8 6h7a3 3 0 0 1 3 3v7h-1V9a2 2 0 0 0-2-2H8V6Zm0 13a3 3 0 0 1-3-3V7H1V6h4V2h1v14a2 2 0 0 0 2 2h13v1h-3v4h-1v-4H8Z\"/>"
},
"crop-free": {
"body": "<path fill=\"currentColor\" d=\"M6 4h2v1H6a2 2 0 0 0-2 2v2H3V7a3 3 0 0 1 3-3ZM4 18a2 2 0 0 0 2 2h2v1H6a3 3 0 0 1-3-3v-2h1v2ZM17 4a3 3 0 0 1 3 3v2h-1V7a2 2 0 0 0-2-2h-2V4h2Zm3 14a3 3 0 0 1-3 3h-2v-1h2a2 2 0 0 0 2-2v-2h1v2Z\"/>"
},
"currency-eur": {
"body": "<path fill=\"currentColor\" d=\"m2 11l.5-1h2.874a8.504 8.504 0 0 1 13.695-3.922l-.397.99A7.503 7.503 0 0 0 6.426 10l11.074.001l-.4 1H6.15a7.535 7.535 0 0 0 0 3h9.75l-.4 1H6.427A7.503 7.503 0 0 0 19 17.6v1.381A8.504 8.504 0 0 1 5.374 15H2l.5-1h2.632a8.552 8.552 0 0 1 0-3H2Z\"/>"
},
"currency-gbp": {
"body": "<path fill=\"currentColor\" d=\"M7 13v-1h2.823c-.076-.474-.166-.9-.251-1.303c-.187-.88-.363-1.712-.32-2.718c.074-1.801.671-3.05 1.773-3.713c1.686-1.011 4.028-.285 4.998.094l-.168 1.012c-.649-.272-2.896-1.104-4.317-.247c-.795.478-1.228 1.452-1.288 2.896c-.037.88.12 1.616.3 2.468c.098.462.202.954.285 1.511H15v1h-4.05c.032.383.05.798.05 1.251c0 3.177-1.473 4.733-2.654 5.749H17v1H6.5v-1l.805-.432C8.443 18.624 10 17.332 10 14.25c0-.454-.02-.868-.053-1.251H7Z\"/>"
},
"currency-rub": {
"body": "<path fill=\"currentColor\" d=\"M7 21v-5H6v-1h1v-3H6v-1h1V4h7a4 4 0 0 1 0 8H8v3h6v1H8v5H7Zm1-10h6a3 3 0 0 0 0-6H8v6Z\"/>"
},
"currency-usd": {
"body": "<path fill=\"currentColor\" d=\"M11 4h1v2.01c3.29.141 4 1.685 4 2.99h-1c0-1.327-1.178-2-3.5-2c-.82 0-3.5.163-3.5 2.249c0 .872 0 1.86 3.621 2.766l1.606.485C15.76 13.435 16 14.572 16 15.751c0 1.881-1.518 3.093-4 3.235V21h-1v-2.01c-3.29-.141-4-1.685-4-2.99h1c0 1.327 1.178 2 3.5 2c.82 0 3.5-.163 3.5-2.249c0-.872 0-1.86-3.621-2.766L9.773 12.5C7.24 11.565 7 10.428 7 9.249c0-1.881 1.518-3.093 4-3.235V4Z\"/>"
},
"delete": {
"body": "<path fill=\"currentColor\" d=\"M18 19a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V7H4V4h4.5l1-1h4l1 1H19v3h-1v12ZM6 7v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V7H6Zm12-1V5h-4l-1-1h-3L9 5H5v1h13ZM8 9h1v10H8V9Zm6 0h1v10h-1V9Z\"/>"
},
"diamond": {
"body": "<path fill=\"currentColor\" d=\"M6 3h11l3.902 5.573L11.5 22L2.098 8.573L6 3Zm4.162 1L8.464 8h6.072l-1.698-4h-2.676ZM8.327 9l3.173 9.764L14.672 9H8.328ZM3.72 8h3.658l1.698-4H6.52l-2.8 4Zm-.102 1l6.825 9.747L7.276 9H3.618ZM19.28 8l-2.8-4h-2.556l1.698 4h3.658Zm.102 1h-3.658l-3.167 9.747L19.382 9Z\"/>"
},
"dots-horizontal": {
"body": "<path fill=\"currentColor\" d=\"M16 12a2 2 0 1 1 4 0a2 2 0 0 1-4 0Zm-6 0a2 2 0 1 1 4 0a2 2 0 0 1-4 0Zm-6 0a2 2 0 1 1 4 0a2 2 0 0 1-4 0Zm2-1a1 1 0 1 0 0 2a1 1 0 0 0 0-2Zm6 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2Zm6 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2Z\"/>"
},
"dots-vertical": {
"body": "<path fill=\"currentColor\" d=\"M12 16a2 2 0 1 1 0 4a2 2 0 0 1 0-4Zm0-6a2 2 0 1 1 0 4a2 2 0 0 1 0-4Zm0-6a2 2 0 1 1 0 4a2 2 0 0 1 0-4Zm0 1a1 1 0 1 0 0 2a1 1 0 0 0 0-2Zm0 6a1 1 0 1 0 0 2a1 1 0 0 0 0-2Zm0 6a1 1 0 1 0 0 2a1 1 0 0 0 0-2Z\"/>"
},
"download": {
"body": "<path fill=\"currentColor\" d=\"M12 4v12.25L17.25 11l.75.664l-6.5 6.5l-6.5-6.5l.75-.664L11 16.25V4h1ZM3 19h1v2h15v-2h1v3H3v-3Z\"/>"
},
"eject": {
"body": "<path fill=\"currentColor\" d=\"m5.33 15l6.17-9.25L17.67 15H5.33ZM5 18h13v1H5v-1Zm1.98-3.975h9.04L11.5 7.25l-4.52 6.776Z\"/>"
},
"email": {
"body": "<path fill=\"currentColor\" d=\"M5 5h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a1.99 1.99 0 0 0-1.283.466L11.5 11.52l7.783-5.054A1.992 1.992 0 0 0 18 6H5Zm6.5 6.712L3.134 7.28A1.995 1.995 0 0 0 3 8v9a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V8c0-.254-.047-.497-.134-.72L11.5 12.711Z\"/>"
},
"email-open": {
"body": "<path fill=\"currentColor\" d=\"M21 9v9a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V9c0-1.11.603-2.08 1.5-2.598l-.003-.004l8.001-4.62l8.007 4.623l-.001.003A2.999 2.999 0 0 1 21 9ZM3.717 7.466L11.5 12.52l7.783-5.054l-7.785-4.533l-7.781 4.533Zm7.783 6.246L3.134 8.28A1.995 1.995 0 0 0 3 9v9a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V9c0-.254-.047-.497-.134-.72L11.5 13.711Z\"/>"
},
"ereader": {
"body": "<path fill=\"currentColor\" d=\"M7 3h9a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H7Zm0 1h9a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm0 1v12h9V6H7Zm2 2h5v1H9V8Zm0 3h5v1H9v-1Zm0 3h3v1H9v-1Z\"/>"
},
"eye": {
"body": "<path fill=\"currentColor\" d=\"M11.5 18c3.989 0 7.458-2.224 9.235-5.5A10.498 10.498 0 0 0 11.5 7a10.498 10.498 0 0 0-9.235 5.5A10.498 10.498 0 0 0 11.5 18Zm0-12a11.5 11.5 0 0 1 10.36 6.5A11.5 11.5 0 0 1 11.5 19a11.5 11.5 0 0 1-10.36-6.5A11.5 11.5 0 0 1 11.5 6Zm0 2a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9Zm0 1a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7Z\"/>"
},
"eye-off": {
"body": "<path fill=\"currentColor\" d=\"M2.543 4.707L3.25 4L20 20.75l-.707.707l-3.348-3.348c-1.367.574-2.87.891-4.445.891a11.5 11.5 0 0 1-10.36-6.5a11.55 11.55 0 0 1 4.374-4.821L2.543 4.707ZM11.5 18c1.293 0 2.531-.234 3.675-.661l-1.129-1.128A4.5 4.5 0 0 1 7.79 9.954L6.244 8.408a10.55 10.55 0 0 0-3.98 4.092A10.498 10.498 0 0 0 11.5 18Zm9.235-5.5A10.498 10.498 0 0 0 11.5 7a10.49 10.49 0 0 0-3.305.53l-.783-.782A11.474 11.474 0 0 1 11.5 6a11.5 11.5 0 0 1 10.36 6.5a11.55 11.55 0 0 1-4.068 4.628l-.724-.724a10.552 10.552 0 0 0 3.667-3.904ZM11.5 8a4.5 4.5 0 0 1 3.904 6.74l-.74-.74A3.5 3.5 0 0 0 10 9.336l-.74-.74A4.48 4.48 0 0 1 11.5 8ZM8 12.5a3.5 3.5 0 0 0 5.324 2.988l-4.812-4.812A3.484 3.484 0 0 0 8 12.5Z\"/>"
},
"factory": {
"body": "<path fill=\"currentColor\" d=\"m2 8l7 4.041V8l7 4V3h5l.002 9H21v10H2V8Zm15 6l-7-4.268v4.042L3 9.732V21h17v-9h.002L20 4h-3v10ZM5 15h3v1H5v-1Zm0 3h5v1H5v-1Zm7-3h3v1h-3v-1Zm0 3h6v1h-6v-1Z\"/>"
},
"fast-forward": {
"body": "<path fill=\"currentColor\" d=\"M21.402 12.5L12 18.375L11 19v-5.624l-8 5L2 19V6l9 5.624V6l10.402 6.5Zm-1.887 0L12 7.804v9.392l7.515-4.696Zm-9 0L3 7.804v9.392l7.515-4.696Z\"/>"
},
"file": {
"body": "<path fill=\"currentColor\" d=\"M14 11a3 3 0 0 1-3-3V4H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8h-4Zm-2-3a2 2 0 0 0 2 2h3.586L12 4.414V8ZM7 3h5l7 7v9a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Z\"/>"
},
"file-alert": {
"body": "<path fill=\"currentColor\" d=\"M7 3a3 3 0 0 0-3 3v13a3 3 0 0 0 3 3h9a3 3 0 0 0 3-3v-9l-7-7H7Zm0 1h4v4a3 3 0 0 0 3 3h4v8a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm5 .414L17.586 10H14a2 2 0 0 1-2-2V4.414ZM7.5 10v5h1v-5h-1Zm0 7v2h1v-2h-1Z\"/>"
},
"file-multiple": {
"body": "<path fill=\"currentColor\" d=\"M15 11a3 3 0 0 1-3-3V4H8a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8h-4Zm-2-3a2 2 0 0 0 2 2h3.586L13 4.414V8ZM8 3h5l7 7v9a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 21a5 5 0 0 1-5-5V7h1v12a4 4 0 0 0 4 4h8v1H8Z\"/>"
},
"file-plus": {
"body": "<path fill=\"currentColor\" d=\"M14 11a3 3 0 0 1-3-3V4H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8h-4Zm-2-3a2 2 0 0 0 2 2h3.586L12 4.414V8ZM7 3h5l7 7v9a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm2 16v-2H7v-1h2v-2h1v2h2v1h-2v2H9Z\"/>"
},
"filmstrip": {
"body": "<path fill=\"currentColor\" d=\"M4 4h1v2h2V4h9v2h2V4h1v17h-1v-2h-2v2H7v-2H5v2H4V4Zm3 3H5v3h2V7Zm0 4H5v3h2v-3Zm0 4H5v3h2v-3Zm9 3h2v-3h-2v3Zm0-4h2v-3h-2v3Zm0-4h2V7h-2v3ZM8 5v7h7V5H8Zm0 8v7h7v-7H8Z\"/>"
},
"flag": {
"body": "<path fill=\"currentColor\" d=\"M5 5h8.423l1.154 2H19v9h-6l-1.155-2H6v7H5V5Zm13 10V8h-4l-1.155-2H6v7h6.423l1.154 2H18Z\"/>"
},
"flash": {
"body": "<path fill=\"currentColor\" d=\"m16 3l-3.49 7H16l-6 12.034V14H7V3h9Zm-5.106 8l3.49-7H8v9h3v4.787L14.384 11h-3.49Z\"/>"
},
"flask": {
"body": "<path fill=\"currentColor\" d=\"M13 6h1V5a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1v1h1v2.072L4.285 17.97A2 2 0 0 0 6 21h11a2 2 0 0 0 1.715-3.03L13 8.072V6ZM6 22a3 3 0 0 1-2.516-4.635L9 7.811V7a1 1 0 0 1-1-1V5a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v.811l5.516 9.554A3 3 0 0 1 17 22H6Zm6.294-6.708l1.626-1.626L17 19H6l3.66-6.34l2.634 2.632Zm0 1.414l-2.419-2.418L7.732 18h7.536l-1.562-2.706l-1.412 1.412ZM12 10a1 1 0 1 1 0 2a1 1 0 0 1 0-2Z\"/>"
},
"flask-empty": {
"body": "<path fill=\"currentColor\" d=\"M13 6h1V5a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1v1h1v2.072L4.285 17.97A2 2 0 0 0 6 21h11a2 2 0 0 0 1.715-3.03L13 8.072V6ZM6 22a3 3 0 0 1-2.516-4.635L9 7.811V7a1 1 0 0 1-1-1V5a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v.811l5.516 9.554A3 3 0 0 1 17 22H6Z\"/>"
},
"folder": {
"body": "<path fill=\"currentColor\" d=\"M5 5h4l3 3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-6.414l-3-3H5Z\"/>"
},
"folder-multiple": {
"body": "<path fill=\"currentColor\" d=\"M6 5h3l3 3h7a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-7.414l-3-3H6Zm0 16a5 5 0 0 1-5-5V9h1v8a4 4 0 0 0 4 4h12v1H6Z\"/>"
},
"folder-plus": {
"body": "<path fill=\"currentColor\" fill-opacity=\".824\" d=\"M5 5h4l3 3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-6.414l-3-3H5Zm2 11v-2H5v-1h2v-2h1v2h2v1H8v2H7Z\"/>"
},
"format-align-bottom": {
"body": "<path fill=\"currentColor\" d=\"M3 21v-1h13v1H3Zm0-4v-1h17v1H3Zm8-13h1v8.25L15.25 9l.75.664l-4.5 4.5l-4.5-4.5L7.75 9L11 12.25V4Z\"/>"
},
"format-align-center": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v1H3V4Zm4 4h9v1H7V8Zm-4 4h17v1H3v-1Zm4 4h9v1H7v-1Zm-4 4h17v1H3v-1Z\"/>"
},
"format-align-justify": {
"body": "<path fill=\"currentColor\" d=\"M3 5V4h17v1H3Zm0 4V8h17v1H3Zm0 4v-1h17v1H3Zm0 4v-1h17v1H3Zm0 4v-1h17v1H3Z\"/>"
},
"format-align-left": {
"body": "<path fill=\"currentColor\" d=\"M3 21v-1h17v1H3Zm0-4v-1h11v1H3Zm0-4v-1h17v1H3Zm0-4V8h11v1H3Zm0-4V4h17v1H3Z\"/>"
},
"format-align-middle": {
"body": "<path fill=\"currentColor\" d=\"M3 13v-1h17v1H3Zm8-9h1v4.25L14.25 6l.75.664l-3.5 3.5l-3.5-3.5L8.75 6L11 8.25V4Zm0 17v-4.25L8.75 19L8 18.336l3.5-3.5l3.5 3.5l-.75.664L12 16.75V21h-1Z\"/>"
},
"format-align-right": {
"body": "<path fill=\"currentColor\" d=\"M20 4v1H3V4h17Zm0 4v1H9V8h11Zm0 4v1H3v-1h17Zm0 4v1H9v-1h11Zm0 4v1H3v-1h17Z\"/>"
},
"format-align-top": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v1H3V4Zm0 4h13v1H3V8Zm8 13v-8.25L7.75 16L7 15.336l4.5-4.5l4.5 4.5l-.75.664L12 12.75V21h-1Z\"/>"
},
"format-bold": {
"body": "<path fill=\"currentColor\" d=\"M16 14.5a3.5 3.5 0 0 1-3.5 3.5H7V5h4.5a3.5 3.5 0 0 1 2.21 6.215A3.501 3.501 0 0 1 16 14.5ZM11.5 6H8v5h3.5a2.5 2.5 0 0 0 0-5Zm1 6H8v5h4.5a2.5 2.5 0 0 0 0-5Z\"/>"
},
"format-clear": {
"body": "<path fill=\"currentColor\" d=\"M8 4h9v1h-3.989L8.507 17h-1L12.01 5H8V4ZM5 21v-1h8v1H5Zm11.793-3.5L14 14.707l.707-.707l2.793 2.793L20.293 14l.707.707l-2.793 2.793L21 20.293l-.707.707l-2.793-2.793L14.707 21L14 20.293l2.793-2.793Z\"/>"
},
"format-float-center": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v1H3V4Zm0 12h17v1H3v-1Zm13 4v1H3v-1h13ZM8 7h7v7H8V7Zm6 1H9v5h5V8Z\"/>"
},
"format-float-left": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v1H3V4Zm9 4h8v1h-8V8Zm0 4h8v1h-8v-1Zm-9 4h13v1H3v-1Zm17 4v1H3v-1h17ZM3 7h7v7H3V7Zm6 1H4v5h5V8Z\"/>"
},
"format-float-none": {
"body": "<path fill=\"currentColor\" d=\"M3 4h17v1H3V4Zm9 9v-1h8v1h-8ZM3 7h7v7H3V7Zm1 1v5h5V8H4Zm-1 8h13v1H3v-1Zm0 4h17v1H3v-1Z\"/>"
},
"format-float-right": {
"body": "<path fill=\"currentColor\" d=\"M20 4v1H3V4h17Zm-9 4v1H3V8h8Zm-8 4h5v1H3v-1Zm0 4h13v1H3v-1Zm0 4h17v1H3v-1ZM20 7v7h-7V7h7Zm-1 1h-5v5h5V8Z\"/>"
},
"format-indent-decrease": {
"body": "<path fill=\"currentColor\" d=\"M3 21v-1h17v1H3Zm8-4v-1h9v1h-9Zm0-4v-1h9v1h-9Zm0-4V8h9v1h-9ZM3 5V4h17v1H3Zm0 7.5L8.5 18V7L3 12.5Zm1.414 0L7.5 9.414v6.172L4.414 12.5Z\"/>"
},
"format-indent-increase": {
"body": "<path fill=\"currentColor\" d=\"M3 21v-1h17v1H3Zm8-4v-1h9v1h-9Zm0-4v-1h9v1h-9Zm0-4V8h9v1h-9ZM3 5V4h17v1H3Zm5.5 7.5L3 18V7l5.5 5.5Zm-1.414 0L4 9.414v6.172L7.086 12.5Z\"/>"
},
"format-italic": {
"body": "<path fill=\"currentColor\" d=\"M6 17v-1h3l4.01-11H10V4h7v1h-2.99L10 16h3v1H6Z\"/>"
},
"format-line-spacing": {
"body": "<path fill=\"currentColor\" d=\"M21 6v1H10V6h11Zm0 6v1H10v-1h11Zm0 6v1H10v-1h11ZM5 19.25L7.25 17l.75.664l-3.5 3.5l-3.5-3.5l.75-.664L4 19.25V5.75L1.75 8L1 7.336l3.5-3.5l3.5 3.5L7.25 8L5 5.75v13.5Z\"/>"
},
"format-list-bulleted": {
"body": "<path fill=\"currentColor\" d=\"M20 18v1H7v-1h13Zm-16.5-.5a1 1 0 1 1 0 2a1 1 0 0 1 0-2ZM20 12v1H7v-1h13Zm-16.5-.5a1 1 0 1 1 0 2a1 1 0 0 1 0-2ZM20 6v1H7V6h13ZM3.5 5.5a1 1 0 1 1 0 2a1 1 0 0 1 0-2Z\"/>"
},
"format-list-checks": {
"body": "<path fill=\"currentColor\" d=\"M20 18v1H7v-1h13Zm0-6v1H7v-1h13Zm0-6v1H7V6h13ZM2 5h3v3H2V5Zm1 1v1h1V6H3Zm-1 5h3v3H2v-3Zm1 1v1h1v-1H3Zm-1 5h3v3H2v-3Zm1 1v1h1v-1H3Z\"/>"
},
"format-list-numbers": {
"body": "<path fill=\"currentColor\" d=\"M2 10.998v-1h3v.9l-1.8 2.1H5v1H2v-.9l1.8-2.1H2Zm1-3v-3H2v-1h2v4H3Zm-1 9v-1h3v4H2v-1h2v-.5H3v-1h1v-.5H2ZM20 6v1H7V6h13Zm0 6v1H7v-1h13Zm0 6v1H7v-1h13Z\"/>"
},
"format-quote-close": {
"body": "<path fill=\"currentColor\" d=\"M18 6v8l-1.95 4h-4.201l1.95-4H12V6h6Zm-1 7.77V7h-4v6h2.4l-1.951 4h1.975L17 13.77ZM11 6v8l-1.95 4H4.848l1.95-4H5V6h6Zm-1 7.77V7H6v6h2.4l-1.951 4h1.975L10 13.77Z\"/>"
},
"format-quote-open": {
"body": "<path fill=\"currentColor\" d=\"M5 18v-8l1.95-4h4.201l-1.95 4H11v8H5Zm1-7.77V17h4v-6H7.6l1.951-4H7.576L6 10.23ZM12 18v-8l1.95-4h4.201l-1.95 4H18v8h-6Zm1-7.77V17h4v-6h-2.4l1.951-4h-1.975L13 10.23Z\"/>"
},
"format-underline": {
"body": "<path fill=\"currentColor\" d=\"M17 11.5a5.5 5.5 0 1 1-11 0V4h1v7.5a4.5 4.5 0 1 0 9 0V4h1v7.5ZM5 21v-1h13v1H5Z\"/>"
},
"format-wrap-inline": {
"body": "<path fill=\"currentColor\" d=\"M20 4v1H3V4h17Zm0 12v1h-6v-1h6Zm0 4v1H3v-1h17ZM7.5 8l4.5 9H3l4.5-9Zm0 2.241L4.616 16h5.768L7.5 10.241Z\"/>"
},
"format-wrap-square": {
"body": "<path fill=\"currentColor\" d=\"m11.5 8l4.5 9H7l4.5-9Zm0 2.241L8.616 16h5.768L11.5 10.241ZM3 16h2v1H3v-1Zm0-4h2v1H3v-1Zm0-4h2v1H3V8Zm15 0h2v1h-2V8Zm0 4h2v1h-2v-1Zm0 4h2v1h-2v-1Zm2 4v1H3v-1h17ZM3 4h17v1H3V4Z\"/>"
},
"format-wrap-tight": {
"body": "<path fill=\"currentColor\" d=\"m11.5 8l4.5 9H7l4.5-9Zm0 2.241L8.616 16h5.768L11.5 10.241ZM3 4h17v1H3V4Zm11 4h6v1h-6V8ZM3 8h6v1H3V8Zm0 4h4v1H3v-1Zm0 4h2v1H3v-1Zm15 0h2v1h-2v-1Zm-2-4h4v1h-4v-1ZM3 20h17v1H3v-1Z\"/>"
},
"format-wrap-top-bottom": {
"body": "<path fill=\"currentColor\" d=\"m11.5 8l4.5 9H7l4.5-9Zm0 2.241L8.616 16h5.768L11.5 10.241ZM3 4h17v1H3V4Zm17 16v1H3v-1h17Z\"/>"
},
"forum": {
"body": "<path fill=\"currentColor\" d=\"M2 16.586L5.586 13H15a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10.586ZM2 18H1V6a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H6l-4 4Zm19 2.586V10a2 2 0 0 0-2-2V7a3 3 0 0 1 3 3v12h-1l-4-4H8a3 3 0 0 1-2.761-1.825l.797-.797A2 2 0 0 0 8 17h9.414L21 20.586Z\"/>"
},
"fullscreen": {
"body": "<path fill=\"currentColor\" d=\"M5 4h13a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5Zm5 3h7v7h-1V9.707l-7.146 7.146l-.708-.707L15.293 9H10V8Z\"/>"
},
"fullscreen-close": {
"body": "<path fill=\"currentColor\" d=\"M18 21H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3Zm0-1a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h13Zm-5-3H6v-7h1v5.293l7.146-7.147l.708.708L7.707 16H13v1Z\"/>"
},
"gift": {
"body": "<path fill=\"currentColor\" d=\"M4 13v8h7v-8H4Zm8 0v8h7v-8h-7Zm8 0v9H3v-9H2V7h3.035L5 6.5a3.5 3.5 0 0 1 6.5-1.804A3.5 3.5 0 0 1 18 6.5l-.035.5H21v6h-1ZM3 8v4h8V8H3Zm17 4V8h-8v4h8Zm-3.05-5l.05-.5a2.5 2.5 0 0 0-5 0V7h4.95ZM11 7v-.5a2.5 2.5 0 0 0-5 0l.05.5H11Z\"/>"
},
"grid": {
"body": "<path fill=\"currentColor\" d=\"M5 3h13a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v3h5V4H5ZM3 19a2 2 0 0 0 2 2h3v-5H3v3Zm5-9H3v5h5v-5Zm10 11a2 2 0 0 0 2-2v-3h-5v5h3Zm2-11h-5v5h5v-5Zm0-4a2 2 0 0 0-2-2h-3v5h5V6ZM9 4v5h5V4H9Zm0 17h5v-5H9v5Zm5-11H9v5h5v-5Z\"/>"
},
"grid-large": {
"body": "<path fill=\"currentColor\" d=\"M12 4v8h8V6a2 2 0 0 0-2-2h-6Zm8 9h-8v8h6a2 2 0 0 0 2-2v-6Zm-9 8v-8H3v6a2 2 0 0 0 2 2h6Zm-8-9h8V4H5a2 2 0 0 0-2 2v6Zm2-9h13a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Z\"/>"
},
"grid-off": {
"body": "<path fill=\"currentColor\" d=\"M.793 2.457L1.5 1.75L22.25 22.5l-.707.707l-1.78-1.78A2.986 2.986 0 0 1 18 22H5a3 3 0 0 1-3-3V6c0-.659.212-1.268.573-1.763l-1.78-1.78ZM5 3h13a3 3 0 0 1 3 3v13l-.093.743l-.907-.907V16h-2.836l-1-1H20v-5h-5v3.836l-1-1V10h-2.836l-1-1H14V4H9v3.836l-1-1V4H5.164l-.907-.907L5 3ZM3 6v3h4.336L3.293 4.957A1.99 1.99 0 0 0 3 6Zm6 9h4.336L9 10.664V15Zm6 6h3c.382 0 .74-.107 1.043-.293L15 16.664V21ZM3 19a2 2 0 0 0 2 2h3v-5H3v3Zm5-9H3v5h5v-5Zm12-4a2 2 0 0 0-2-2h-3v5h5V6ZM9 21h5v-5H9v5Z\"/>"
},
"group": {
"body": "<path fill=\"currentColor\" d=\"M7 8v5h6V8H7ZM2 3h3v1h13V3h3v3h-1v13h1v3h-3v-1H5v1H2v-3h1V6H2V3Zm3 16v1h13v-1h1V6h-1V5H5v1H4v13h1ZM6 7h8v4h3v7H8v-4H6V7Zm8 7H9v3h7v-5h-2v2ZM3 4v1h1V4H3Zm16 0v1h1V4h-1Zm0 16v1h1v-1h-1ZM3 20v1h1v-1H3Z\"/>"
},
"hamburger": {
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M7 4h9a5 5 0 0 1 5 5H2a5 5 0 0 1 5-5Zm9 1H7a4.002 4.002 0 0 0-3.874 3h16.748A4.002 4.002 0 0 0 16 5Zm5 11a5 5 0 0 1-5 5H7a5 5 0 0 1-5-5h19ZM7 20h9a4.002 4.002 0 0 0 3.874-3H3.126c.444 1.725 2.01 3 3.874 3Zm5.5-10l2 2l2-2H19a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1a2 2 0 0 1 2-2h8.5Zm2 3.414L12.086 11H4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h15a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1h-2.086L14.5 13.414Z\"/>"
},
"heart": {
"body": "<path fill=\"currentColor\" d=\"M4.244 12.252a4.25 4.25 0 1 1 6.697-5.111h1.118a4.25 4.25 0 1 1 6.697 5.111L11.5 19.51l-7.256-7.257Zm15.218.71A5.25 5.25 0 1 0 11.5 6.167a5.25 5.25 0 1 0-7.962 6.795l7.962 7.961l7.962-7.96Z\"/>"
},
"heart-half": {
"body": "<path fill=\"currentColor\" d=\"M4.244 12.252L11.5 19.51v1.414l-7.962-7.96A5.25 5.25 0 1 1 11.5 6.167v.973h-.56a4.25 4.25 0 1 0-6.697 5.111Z\"/>"
},
"heart-off": {
"body": "<path fill=\"currentColor\" d=\"M1.793 3.457L2.5 2.75L19.25 19.5l-.707.707l-3.163-3.163l-3.88 3.88l-7.962-7.962A5.234 5.234 0 0 1 2 9.25c0-1.535.659-2.916 1.71-3.876L1.792 3.457Zm2.45 8.796l7.257 7.256l3.172-3.173L4.417 6.082A4.24 4.24 0 0 0 3 9.25a4.23 4.23 0 0 0 1.244 3.002Zm14.513 0a4.25 4.25 0 1 0-6.697-5.111h-1.118A4.248 4.248 0 0 0 7.25 5l-.974.112l-.803-.804A5.24 5.24 0 0 1 7.25 4c1.748 0 3.296.854 4.25 2.167a5.25 5.25 0 1 1 7.962 6.795l-2.668 2.668l-.708-.708l2.67-2.67Z\"/>"
},
"help": {
"body": "<path fill=\"currentColor\" d=\"M11 22v-2h1v2h-1Zm0-4.5c0-4.5 5-6 5-9A4.5 4.5 0 0 0 7.027 8H6.022A5.5 5.5 0 0 1 17 8.5c0 3.5-5 4.5-5 9v.5h-1v-.5Z\"/>"
},
"help-circle": {
"body": "<path fill=\"currentColor\" d=\"M11.5 4a8.5 8.5 0 1 0 .001 17.001A8.5 8.5 0 0 0 11.5 4Zm0-1a9.5 9.5 0 0 1 9.5 9.5a9.5 9.5 0 0 1-9.5 9.5A9.5 9.5 0 0 1 2 12.5A9.5 9.5 0 0 1 11.5 3ZM11 17h1v2h-1v-2Zm.5-11A3.5 3.5 0 0 1 15 9.5c0 .897-.699 1.519-1.439 2.176l-.935.903c-.588.674-.652 1.955-.627 2.392V15H11c-.004-.052-.102-1.964.874-3.079l1.022-.992C13.488 10.403 14 9.948 14 9.5a2.5 2.5 0 1 0-5 0H8A3.5 3.5 0 0 1 11.5 6Z\"/>"
},
"home": {
"body": "<path fill=\"currentColor\" d=\"m16 8.414l-4.5-4.5L4.414 11H6v8h3v-6h5v6h3v-8h1.586L17 9.414V6h-1v2.414ZM2 12l9.5-9.5L15 6V5h3v4l3 3h-3v7.998h-5v-6h-3v6H5V12H2Z\"/>"
},
"inbox": {
"body": "<path fill=\"currentColor\" d=\"M6 4h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v8h5v.5a2.5 2.5 0 0 0 5 0V15h5V7a2 2 0 0 0-2-2H6ZM4 18a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-2h-4.035a3.501 3.501 0 0 1-6.93 0H4v2Z\"/>"
},
"information": {
"body": "<path fill=\"currentColor\" d=\"M11.5 3a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17ZM11 8v2h1V8h-1Zm0 4v5h1v-5h-1Z\"/>"
},
"label": {
"body": "<path fill=\"currentColor\" d=\"M6 6h8c.971 0 1.834.461 2.383 1.177l4.159 5.323l-4.16 5.323A2.995 2.995 0 0 1 14 19H6a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Zm9.578 11.23l3.695-4.73l-3.695-4.73l-.002-.001A1.996 1.996 0 0 0 14 7H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8c.64 0 1.21-.3 1.576-.769l.002-.002Z\"/>"
},
"layers": {
"body": "<path fill=\"currentColor\" d=\"m2.7 11.02l8.667-6.772l8.934 6.98L11.633 18l-8.934-6.98Zm16 .189l-7.309-5.71l-7.068 5.521l7.31 5.711l7.068-5.522ZM11.634 21L2.7 14.02l.788-.615l8.122 6.345l7.88-6.157l.812.635L11.633 21Z\"/>"
},
"lightbulb": {
"body": "<path fill=\"currentColor\" d=\"M14 20a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1h1Zm1-3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-2.022a6.5 6.5 0 1 1 7 0V17Zm-6 0a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-2.6a5.5 5.5 0 1 0-5 0V17Z\"/>"
},
"lightbulb-on": {
"body": "<path fill=\"currentColor\" d=\"M14 20a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1h1Zm1-3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-2.022a6.5 6.5 0 1 1 7 0V17Zm-6 0a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-2.6a5.5 5.5 0 1 0-5 0V17Zm-.871-6.879L10.5 7.75l2 2L14.25 8l.707.707l-2.457 2.457l-2-2l-1.664 1.664l-.707-.707Z\"/>"
},
"link": {
"body": "<path fill=\"currentColor\" d=\"M8 13v-1h7v1H8Zm7.5-6a5.5 5.5 0 1 1 0 11H13v-1h2.5a4.5 4.5 0 1 0 0-9H13V7h2.5Zm-8 11a5.5 5.5 0 1 1 0-11H10v1H7.5a4.5 4.5 0 1 0 0 9H10v1H7.5Z\"/>"
},
"link-variant": {
"body": "<path fill=\"currentColor\" d=\"M10.728 14.965a.5.5 0 1 1-.423.906v.001a4.5 4.5 0 0 1-1.28-7.261l3.536-3.536a4.5 4.5 0 1 1 6.364 6.364l-1.634 1.633l-.15-1.264l1.077-1.076a3.5 3.5 0 0 0-4.95-4.95L9.732 9.318a3.5 3.5 0 0 0 .995 5.648v-.001Zm-6.653 4.96a4.5 4.5 0 0 1 0-6.364l1.634-1.633l.15 1.264l-1.077 1.076a3.5 3.5 0 1 0 4.95 4.95l3.536-3.536a3.5 3.5 0 0 0-.995-5.648v.001a.5.5 0 1 1 .422-.906v-.001a4.5 4.5 0 0 1 1.28 7.261l-3.536 3.536a4.5 4.5 0 0 1-6.364 0Z\"/>"
},
"loading": {
"body": "<path fill=\"currentColor\" d=\"M11.5 4A8.5 8.5 0 0 0 3 12.5H2A9.5 9.5 0 0 1 11.5 3v1Z\"/>"
},
"lock": {
"body": "<path fill=\"currentColor\" fill-opacity=\".886\" d=\"M16 8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3V6.5a4.5 4.5 0 1 1 9 0V8ZM7 9a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2H7Zm8-1V6.5a3.5 3.5 0 0 0-7 0V8h7Zm-3.5 6a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3Zm0-1a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5Z\"/>"
},
"lock-unlocked": {
"body": "<path fill=\"currentColor\" fill-opacity=\".886\" d=\"M16 8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h8V6.5A3.5 3.5 0 0 0 8.035 6H7.027A4.5 4.5 0 0 1 16 6.5V8ZM7 9a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2H7Zm4.5 5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3Zm0-1a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5Z\"/>"
},
"login": {
"body": "<path fill=\"currentColor\" d=\"M15 3H9a3 3 0 0 0-3 3v4h1V6a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2v-4H6v4a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3ZM3 12h10.25L10 8.75l.664-.75l4.5 4.5l-4.5 4.5l-.664-.75L13.25 13H3v-1Z\"/>"
},
"logout": {
"body": "<path fill=\"currentColor\" d=\"M5 3h6a3 3 0 0 1 3 3v4h-1V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-4h1v4a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm3 9h11.25L16 8.75l.664-.75l4.5 4.5l-4.5 4.5l-.664-.75L19.25 13H8v-1Z\"/>"
},
"magnify": {
"body": "<path fill=\"currentColor\" d=\"M9.5 4a6.5 6.5 0 0 1 4.932 10.734l5.644 5.644l-.707.707l-5.645-5.645A6.5 6.5 0 1 1 9.5 4Zm0 1a5.5 5.5 0 1 0 0 11a5.5 5.5 0 0 0 0-11Z\"/>"
},
"magnify-minus": {
"body": "<path fill=\"currentColor\" d=\"M9.5 4a6.5 6.5 0 0 1 4.932 10.734l5.644 5.644l-.707.707l-5.645-5.645A6.5 6.5 0 1 1 9.5 4Zm0 1a5.5 5.5 0 1 0 0 11a5.5 5.5 0 0 0 0-11ZM7 10h5v1H7v-1Z\"/>"
},
"magnify-plus": {
"body": "<path fill=\"currentColor\" d=\"M9.5 4a6.5 6.5 0 0 1 4.932 10.734l5.644 5.644l-.707.707l-5.645-5.645A6.5 6.5 0 1 1 9.5 4Zm0 1a5.5 5.5 0 1 0 0 11a5.5 5.5 0 0 0 0-11ZM7 10h2V8h1v2h2v1h-2v2H9v-2H7v-1Z\"/>"
},
"map-marker": {
"body": "<path fill=\"currentColor\" d=\"M11.5 7a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5Zm0 1a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3Zm-4.7 4.357l4.7 7.73l4.7-7.73a5.5 5.5 0 1 0-9.4 0Zm10.254.52L11.5 22.012l-5.554-9.135a6.5 6.5 0 1 1 11.11 0h-.002Z\"/>"
},
"memory": {
"body": "<path fill=\"currentColor\" d=\"M8 19a3 3 0 0 1-3-3v-1H3v-1h2v-3H3v-1h2V9a3 3 0 0 1 3-3h1V4h1v2h3V4h1v2h1a3 3 0 0 1 3 3v1h2v1h-2v3h2v1h-2v1a3 3 0 0 1-3 3h-1v2h-1v-2h-3v2H9v-2H8ZM8 7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2H8Zm1 8v-5h5v5H9Zm1-4v3h3v-3h-3Z\"/>"
},
"menu": {
"body": "<path fill=\"currentColor\" d=\"M3 8V7h17v1H3Zm17 4v1H3v-1h17ZM3 17h17v1H3v-1Z\"/>"
},
"message": {
"body": "<path fill=\"currentColor\" d=\"M3 20.586L6.586 17H18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14.586ZM3 22H2V6a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H7l-4 4Z\"/>"
},
"message-alert": {
"body": "<path fill=\"currentColor\" d=\"M3 20.586L6.586 17H18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14.586ZM3 22H2V6a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H7l-4 4Zm8-16h1v5h-1V6Zm1 9h-1v-2h1v2Z\"/>"
},
"message-photo": {
"body": "<path fill=\"currentColor\" d=\"M3 20.586L6.586 17H18a2 2 0 0 0 2-2v-.086l-5.207-5.207l-5 5l-2.5-2.5L3 16.5v4.086ZM20 6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9.086l4.293-4.293l2.5 2.5l5-5L20 13.5V6ZM3 22H2V6a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H7l-4 4ZM9 6a2 2 0 1 1 0 4a2 2 0 0 1 0-4Zm0 1a1 1 0 1 0 0 2a1 1 0 0 0 0-2Z\"/>"
},
"message-processing": {
"body": "<path fill=\"currentColor\" d=\"M3 20.586L6.586 17H18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14.586ZM3 22H2V6a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H7l-4 4ZM6.5 9a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm0 1a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1Zm5-1a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm0 1a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1Zm5-1a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm0 1a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1Z\"/>"
},
"message-reply": {
"body": "<path fill=\"currentColor\" d=\"M20 20.586L16.414 17H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v14.586ZM20 22h1V6a3 3 0 0 0-3-3H5a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h11l4 4Z\"/>"
},
"message-text": {
"body": "<path fill=\"currentColor\" d=\"M3 20.586L6.586 17H18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14.586ZM3 22H2V6a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H7l-4 4ZM6 7h11v1H6V7Zm0 3h11v1H6v-1Zm0 3h8v1H6v-1Z\"/>"
},
"message-video": {
"body": "<path fill=\"currentColor\" d=\"M3 20.586L6.586 17H18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14.586ZM3 22H2V6a3 3 0 0 1 3-3h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H7l-4 4ZM17 7v7h-1l-2-2v2H6V7h8v2l2-2h1Zm-3 3.414v.172l2 2V8.414l-2 2ZM7 8v5h6V8H7Z\"/>"
},
"microphone": {
"body": "<path fill=\"currentColor\" d=\"M11 21v-3.019A6.5 6.5 0 0 1 5 11.5V11h1v.5a5.5 5.5 0 1 0 11 0V11h1v.5a6.5 6.5 0 0 1-6 6.481V21h-1Zm.5-18A2.5 2.5 0 0 1 14 5.5v6a2.5 2.5 0 0 1-5 0v-6A2.5 2.5 0 0 1 11.5 3Zm0 1A1.5 1.5 0 0 0 10 5.5v6a1.5 1.5 0 0 0 3 0v-6A1.5 1.5 0 0 0 11.5 4Z\"/>"
},
"microphone-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75l11.762 11.762l.707.707L20.25 20.5l-.707.707l-4.354-4.354A6.465 6.465 0 0 1 12 17.98V21h-1v-3.019A6.5 6.5 0 0 1 5 11.5V11h1v.5a5.5 5.5 0 0 0 8.467 4.632l-2.239-2.24A2.5 2.5 0 0 1 9 11.5v-.836L2.793 4.457ZM17 11.5V11h1v.5a6.472 6.472 0 0 1-1.358 3.977l-.714-.714A5.475 5.475 0 0 0 17 11.5ZM11.5 3A2.5 2.5 0 0 1 14 5.5v6c0 .39-.09.759-.248 1.088l-.783-.783L13 11.5v-6a1.5 1.5 0 0 0-3 0v3.336l-1-1V5.5A2.5 2.5 0 0 1 11.5 3Zm-1.49 8.674a1.5 1.5 0 0 0 1.316 1.316l-1.316-1.316Z\"/>"
},
"minus": {
"body": "<path fill=\"currentColor\" d=\"M5 13v-1h13.01L18 13H5Z\"/>"
},
"minus-box": {
"body": "<path fill=\"currentColor\" d=\"M7 12h9v1H7v-1ZM6 4h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H6Z\"/>"
},
"minus-circle": {
"body": "<path fill=\"currentColor\" d=\"M7 12h9v1H7v-1Zm4.5-9a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17Z\"/>"
},
"monitor": {
"body": "<path fill=\"currentColor\" d=\"M5 4h13a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-4.508L14 20h1v1H8v-1h1l.508-3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm5.508 13L10 20h3l-.508-3h-1.984ZM5 5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5Z\"/>"
},
"monitor-multiple": {
"body": "<path fill=\"currentColor\" d=\"M6 5h13a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-4.508L15 21h1v1H9v-1h1l.508-3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Zm5.508 13L11 21h3l-.508-3h-1.984ZM6 6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H6ZM1 8a5 5 0 0 1 5-5h12v1H6a4 4 0 0 0-4 4v6H1V8Z\"/>"
},
"music": {
"body": "<path fill=\"currentColor\" d=\"m8 6.104l11-1.117V16a3 3 0 1 1-1-2.236v-4.72l-9 .993V17a3 3 0 1 1-1-2.236v-8.66Zm1 .904V9.03l9-.993V6.094l-9 .914ZM8 17a2 2 0 1 0-4 0a2 2 0 0 0 4 0Zm10-1a2 2 0 1 0-4 0a2 2 0 0 0 4 0Z\"/>"
},
"music-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75L20.25 20.5l-.707.707l-2.423-2.423a3 3 0 0 1-3.904-3.904L9 10.664V17a3 3 0 1 1-1-2.236v-5.1L2.793 4.457ZM8 6.104l11-1.117V16a2.98 2.98 0 0 1-.378 1.458l-.75-.75a2 2 0 0 0-2.579-2.579l-.751-.751A2.977 2.977 0 0 1 16 13c.768 0 1.47.289 2 .764v-4.72l-7.017.774l-.907-.906L18 8.038V6.094l-9 .914v.828l-1-1v-.732ZM14 16a2 2 0 0 0 2.312 1.976l-2.288-2.288A2.015 2.015 0 0 0 14 16Zm-6 1a2 2 0 1 0-4 0a2 2 0 0 0 4 0Z\"/>"
},
"nfc": {
"body": "<path fill=\"currentColor\" d=\"M5 3h13a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H5Zm8 8.5a1.5 1.5 0 1 1-2-1.415V8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2v1H7v9h9V8h-4v3.085a1.5 1.5 0 0 1 1 1.415Zm-1.5-.5a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1Z\"/>"
},
"note": {
"body": "<path fill=\"currentColor\" d=\"M16 12a3 3 0 0 1-3-3V5H5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6h-4Zm-2-3a2 2 0 0 0 2 2h3.586L14 5.414V9ZM5 4h9l7 7v7a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Z\"/>"
},
"note-multiple": {
"body": "<path fill=\"currentColor\" d=\"M17 12a3 3 0 0 1-3-3V5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6h-4Zm-2-3a2 2 0 0 0 2 2h3.586L15 5.414V9ZM6 4h9l7 7v7a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 19a5 5 0 0 1-5-5V8h1v10a4 4 0 0 0 4 4h12v1H6Z\"/>"
},
"note-plus": {
"body": "<path fill=\"currentColor\" d=\"M16 12a3 3 0 0 1-3-3V5H5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6h-4Zm-2-3a2 2 0 0 0 2 2h3.586L14 5.414V9ZM5 4h9l7 7v7a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm2 14v-2H5v-1h2v-2h1v2h2v1H8v2H7Z\"/>"
},
"note-text": {
"body": "<path fill=\"currentColor\" d=\"M16 12a3 3 0 0 1-3-3V5H5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-6h-4Zm-2-3a2 2 0 0 0 2 2h3.586L14 5.414V9ZM5 4h9l7 7v7a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 4h6v1H5V8Zm0 4h6v1H5v-1Zm0 4h13v1H5v-1Z\"/>"
},
"paperclip": {
"body": "<path fill=\"currentColor\" d=\"M17 7v10.5c0 3.04-2.46 5.5-5.5 5.5S6 20.54 6 17.5V6c0-2.21 1.79-4 4-4s4 1.79 4 4v10.5a2.5 2.5 0 0 1-5 0V7h1v9.5a1.5 1.5 0 0 0 3 0V6a3 3 0 0 0-6 0v11.5a4.5 4.5 0 1 0 9 0V7h1Z\"/>"
},
"pause": {
"body": "<path fill=\"currentColor\" d=\"M13 19V6h4v13h-4Zm-7 0V6h4v13H6ZM7 7v11h2V7H7Zm7 0v11h2V7h-2Z\"/>"
},
"pencil": {
"body": "<path fill=\"currentColor\" d=\"m19.706 8.042l-2.332 2.332l-3.75-3.75l2.332-2.332a.999.999 0 0 1 1.414 0l2.336 2.336a.999.999 0 0 1 0 1.414ZM2.999 17.248L13.064 7.184l3.75 3.75L6.749 20.998H3v-3.75ZM16.621 5.044l-1.54 1.539l2.337 2.335l1.538-1.539l-2.335-2.335Zm-1.264 5.935l-2.335-2.336L4 17.664V20h2.336l9.021-9.021Z\"/>"
},
"phone": {
"body": "<path fill=\"currentColor\" d=\"M19.5 22c.827 0 1.5-.673 1.5-1.5V17c0-.827-.673-1.5-1.5-1.5c-1.17 0-2.32-.184-3.42-.547a1.523 1.523 0 0 0-1.523.363l-1.44 1.44a14.655 14.655 0 0 1-5.885-5.883L8.66 9.436c.412-.382.56-.963.384-1.522A10.872 10.872 0 0 1 8.5 4.5C8.5 3.673 7.827 3 7 3H3.5C2.673 3 2 3.673 2 4.5C2 14.15 9.85 22 19.5 22ZM3.5 4H7a.5.5 0 0 1 .5.5c0 1.277.2 2.531.593 3.72a.473.473 0 0 1-.127.497L6.01 10.683c1.637 3.228 4.055 5.646 7.298 7.297l1.949-1.95a.516.516 0 0 1 .516-.126c1.196.396 2.45.596 3.727.596c.275 0 .5.225.5.5v3.5c0 .275-.225.5-.5.5C10.402 21 3 13.598 3 4.5a.5.5 0 0 1 .5-.5Z\"/>"
},
"picture": {
"body": "<path fill=\"currentColor\" d=\"M5 3h13a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11.586l4.293-4.293l2.5 2.5l5-5L20 16V6a2 2 0 0 0-2-2H5Zm4.793 13.207l-2.5-2.5L3 19a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-1.586l-5.207-5.207l-5 5ZM7.5 6a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5Zm0 1a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3Z\"/>"
},
"pin": {
"body": "<path fill=\"currentColor\" d=\"M14 12.415V5h1V4H8v1h1v7.414l-2 2V15h9v-.586l-2-2Zm3 1.583v2L12 16v4.5l-.5 1.5l-.5-1.5V16l-5-.002v-2h.002L8 12V5.998H7v-3h8.999v3h-1V12l2 1.998Z\"/>"
},
"pin-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75L20.25 20.5l-.707.707L14.335 16H12v4.5l-.5 1.5l-.5-1.5V16l-5-.002v-2h.002L8 12V9.664L2.793 4.457ZM14 12.415V5h1V4H8v1h1v2.836l-1-1v-.838h-.838L7 5.835V2.998h9v3h-1V12l2 1.998v1.837l-1-1v-.42l-2-2Zm-5-.001l-2.001 2V15h6.336L9 10.665v1.749Z\"/>"
},
"play": {
"body": "<path fill=\"currentColor\" d=\"M18.402 12.5L9 18.375L8 19V6l10.402 6.5Zm-1.887 0L9 7.804v9.392l7.515-4.696Z\"/>"
},
"plus": {
"body": "<path fill=\"currentColor\" d=\"M5 13v-1h6V6h1v6h6v1h-6v6h-1v-6H5Z\"/>"
},
"plus-box": {
"body": "<path fill=\"currentColor\" d=\"M7 12h4V8h1v4h4v1h-4v4h-1v-4H7v-1ZM6 4h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H6Z\"/>"
},
"plus-circle": {
"body": "<path fill=\"currentColor\" d=\"M7 12h4V8h1v4h4v1h-4v4h-1v-4H7v-1Zm4.5-9a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17Z\"/>"
},
"power": {
"body": "<path fill=\"currentColor\" d=\"M11 13V4h1v9h-1Zm8-.5A7.5 7.5 0 1 1 7.595 6.095l.731.731a6.5 6.5 0 1 0 6.348 0l.73-.73A7.496 7.496 0 0 1 19 12.5Z\"/>"
},
"presentation": {
"body": "<path fill=\"currentColor\" d=\"M2 4h8a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1h8v1h-1v11h-5.732l1.608 6H14.84l-1.607-6H9.767L8.16 22H7.124l1.608-6H3V5H2V4Zm17 11V5H4v10h15Z\"/>"
},
"presentation-play": {
"body": "<path fill=\"currentColor\" d=\"M2 4h8a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1h8v1h-1v11h-5.732l1.608 6H14.84l-1.607-6H9.767L8.16 22H7.124l1.608-6H3V5H2V4Zm17 11V5H4v10h15Zm-9-8h1l3 3l-3 3h-1V7Zm1 1.414v3.172L12.586 10L11 8.414Z\"/>"
},
"printer": {
"body": "<path fill=\"currentColor\" d=\"M17 3.998v5h1a3 3 0 0 1 3 3v5h-4v4H6v-4H2v-5a3 3 0 0 1 3-3h1v-5h11Zm1 9a1 1 0 1 1 0-2a1 1 0 1 1 0 2ZM3 12v4h3v-2h11v2h3v-4a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Zm13 8v-5H7v5h9ZM7 5v4h9V5H7Z\"/>"
},
"redo-variant": {
"body": "<path fill=\"currentColor\" d=\"M17 20v-1h-1v1h1ZM10 8a6 6 0 1 0 0 12h4v-1h-4a5 5 0 0 1 0-10h7.086l-3.036 3.036l.707.707L19 8.5l-4.243-4.243l-.707.707L17.086 8H10Z\"/>"
},
"refresh": {
"body": "<path fill=\"currentColor\" d=\"M4.996 5h5v5h-1V6.493a6.502 6.502 0 0 0 2.504 12.5a6.5 6.5 0 0 0 1.496-12.827V5.142A7.5 7.5 0 1 1 7.744 6H4.996V5Z\"/>"
},
"repeat": {
"body": "<path fill=\"currentColor\" d=\"m20 7.5l-3.535 3.536l-.708-.708L18.086 8H6v4H5V7h13.086l-2.329-2.328l.708-.708L20 7.5ZM17 17v-4h1v5H4.914l2.329 2.328l-.707.707L3 17.5l3.536-3.536l.707.708L4.914 17H17Z\"/>"
},
"repeat-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75L20.25 20.5l-.707.707L16.336 18H4.914l2.329 2.328l-.707.707L3 17.5l3.536-3.536l.707.708L4.914 17h10.422l-9-9H6v4H5V7h.336L2.793 4.457ZM20 7.5l-3.535 3.536l-.708-.708L18.086 8H9.164l-1-1h9.922l-2.329-2.328l.708-.708L20 7.5ZM17 13h1v3.836l-1-1V13Z\"/>"
},
"repeat-once": {
"body": "<path fill=\"currentColor\" d=\"m20 7.5l-3.535 3.536l-.708-.708L18.086 8H6v4H5V7h13.086l-2.329-2.328l.708-.708L20 7.5ZM17 17v-4h1v5H4.914l2.329 2.328l-.707.707L3 17.5l3.536-3.536l.707.708L4.914 17H17Zm-7-3h1v-3h-1v-1h2v4h1v1h-3v-1Z\"/>"
},
"rewind": {
"body": "<path fill=\"currentColor\" d=\"m1 12.5l9.402 5.875l1 .625v-5.624l8 5l1 .624V6l-9 5.624V6L1 12.5Zm1.887 0l7.515-4.696v9.392L2.887 12.5Zm9 0l7.515-4.696v9.392L11.887 12.5Z\"/>"
},
"rss": {
"body": "<path fill=\"currentColor\" d=\"M5 16a3 3 0 1 1 0 6a3 3 0 0 1 0-6Zm0 1a2 2 0 1 0 0 4a2 2 0 0 0 0-4Zm-3-6c6.075 0 11 4.925 11 11h-1c0-5.523-4.477-10-10-10v-1Zm0-4c8.284 0 15 6.716 15 15h-1C16 14.268 9.732 8 2 8V7Zm0-4c10.493 0 19 8.507 19 19h-1c0-9.941-8.059-18-18-18V3Z\"/>"
},
"script": {
"body": "<path fill=\"currentColor\" d=\"M3 19a2 2 0 0 0 2 2h6.764A2.989 2.989 0 0 1 11 19H3Zm11 2a2 2 0 0 0 2-2V6c0-.768.289-1.47.764-2H8a2 2 0 0 0-2 2v12h6v1a2 2 0 0 0 2 2ZM5 6a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v2h-5v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-1h3V6Zm16 1V6a2 2 0 0 0-4 0v1h4Z\"/>"
},
"seek-next": {
"body": "<path fill=\"currentColor\" d=\"M15.402 12.5L5 6v13l1-.625l9.402-5.875Zm-1.887 0L6 17.196V7.804l7.515 4.696ZM18 6h-1v13h1V6Z\"/>"
},
"seek-previous": {
"body": "<path fill=\"currentColor\" d=\"M7.598 12.5L18 6v13l-1-.625L7.598 12.5Zm1.887 0L17 17.196V7.804L9.485 12.5ZM5 6h1v13H5V6Z\"/>"
},
"shape-circle": {
"body": "<path fill=\"currentColor\" d=\"M11.5 3a9.5 9.5 0 1 1 0 19a9.5 9.5 0 0 1 0-19Zm0 1a8.5 8.5 0 1 0 0 17a8.5 8.5 0 0 0 0-17Z\"/>"
},
"shape-hexagon": {
"body": "<path fill=\"currentColor\" d=\"m6.593 21l-4.905-8.494L6.6 4h9.808l4.908 8.5l-4.908 8.5H6.593ZM15.83 5H7.177l-4.334 7.506L7.17 20h8.66l4.33-7.5L15.83 5Z\"/>"
},
"shape-octagon": {
"body": "<path fill=\"currentColor\" d=\"M3 16.011V8.98L7.98 4h7.04L20 8.98v7.046L15.025 21H7.99L3 16.011ZM8.393 5L4 9.393v6.204L8.403 20h6.208L19 15.611V9.393L14.607 5H8.393Z\"/>"
},
"shape-rhombus": {
"body": "<path fill=\"currentColor\" d=\"M2.586 12.5L11.5 3.586l8.914 8.914l-8.914 8.914L2.586 12.5ZM11.5 5L4 12.5l7.5 7.5l7.5-7.5L11.5 5Z\"/>"
},
"shape-square": {
"body": "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3 4h17v17H3V4Zm1 1v15h15V5H4Z\"/>"
},
"shape-triangle": {
"body": "<path fill=\"currentColor\" d=\"M1 21L11.5 2.813L22 21H1Zm19.268-1L11.5 4.813L2.732 20h17.536Z\"/>"
},
"shield": {
"body": "<path fill=\"currentColor\" d=\"M11.5 3.108L19 6.63v5.013c0 4.81-3.216 9.252-7.5 10.392C7.216 20.896 4 16.453 4 11.644V6.631m7.5 16.438c4.898-1.227 8.5-6.125 8.5-11.425V6l-8.5-4L3 6v5.644c0 5.3 3.602 10.198 8.5 11.425Z\"/>"
},
"shuffle": {
"body": "<path fill=\"currentColor\" d=\"M13 5h6v6h-1V6.707L4.711 19.996l-.707-.707L17.293 6H13V5Zm0 14h4.293l-4.586-4.586l.707-.707L18 18.293V14h1v6h-6v-1ZM4.004 5.711l.707-.707l5.582 5.582l-.707.707L4.004 5.71Z\"/>"
},
"signal": {
"body": "<path fill=\"currentColor\" d=\"M2 21v-5h4v5H2Zm1-4v3h2v-3H3Zm4 4v-9h4v9H7Zm1-8v7h2v-7H8Zm4 8V8h4v13h-4Zm1-12v11h2V9h-2Zm4 12V4h4v17h-4Zm1-16v15h2V5h-2Z\"/>"
},
"sim": {
"body": "<path fill=\"currentColor\" d=\"m11 3l-7 7v9a3 3 0 0 0 3 3h9a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3h-5Zm.414 1H16a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-8.586L11.414 4ZM8 11v4h1v-4H8Zm3 0v2h1v-2h-1Zm3 0v4h1v-4h-1Zm-3 4v4h1v-4h-1Zm-3 2v2h1v-2H8Zm6 0v2h1v-2h-1Z\"/>"
},
"sim-alert": {
"body": "<path fill=\"currentColor\" d=\"m11 3l-7 7v9a3 3 0 0 0 3 3h9a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3h-5Zm.414 1H16a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-8.586L11.414 4ZM11 9v5h1V9h-1Zm0 7v2h1v-2h-1Z\"/>"
},
"sim-off": {
"body": "<path fill=\"currentColor\" d=\"m2.5 2.75l-.707.707l4.365 4.385L4 10v9a3 3 0 0 0 3 3h9a2.971 2.971 0 0 0 2.643-1.615l1.814 1.822l.793-.707L7.582 7.832l-.707-.707L2.5 2.75ZM11 3L7.582 6.418l.707.707L11.414 4H16a2 2 0 0 1 2 2v10.836l1 1V6a3 3 0 0 0-3-3h-5ZM6.863 8.55l11.024 11.075A1.99 1.99 0 0 1 16 21H7a2 2 0 0 1-2-2v-8.586l1.863-1.863Z\"/>"
},
"sitemap": {
"body": "<path fill=\"currentColor\" d=\"M9 3h5v5h-2v4h5a3 3 0 0 1 3 3v2h2v5h-5v-5h2v-2a2 2 0 0 0-2-2h-5v4h2v5H9v-5h2v-4H6a2 2 0 0 0-2 2v2h2v5H1v-5h2v-2a3 3 0 0 1 3-3h5V8H9V3Zm4 4V4h-3v3h3ZM5 21v-3H2v3h3Zm8 0v-3h-3v3h3Zm8 0v-3h-3v3h3Z\"/>"
},
"sleep": {
"body": "<path fill=\"currentColor\" d=\"M2 13h5v1l-3.74 5H7v1H2v-1l3.75-5H2v-1Zm7-4h5v1l-3.74 5H14v1H9v-1l3.75-5H9V9Zm7-4h5v1l-3.74 5H21v1h-5v-1l3.75-5H16V5Z\"/>"
},
"sleep-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75L20.25 20.5l-.707.707L14 15.664V16H9v-1l1.858-2.478l-8.065-8.065ZM2 13h5v1l-3.74 5H7v1H2v-1l3.75-5H2v-1Zm12-4v1l-1.214 1.622l-.716-.716l.68-.906h-1.586l-1-1H14Zm-3.74 6h3.076l-1.76-1.76L10.259 15ZM16 5h5v1l-3.74 5H21v1h-5v-1l3.75-5H16V5Z\"/>"
},
"spellcheck": {
"body": "<path fill=\"currentColor\" d=\"m8.5 17.5l.707-.707l3.5 3.5L20.5 12.5l.707.707l-8.5 8.5L8.5 17.5ZM4.712 13L3.5 16h-1L7.348 4H8.75l4.848 12h-1l-1.212-3H4.712Zm.404-1h5.866L8.05 4.74L5.116 12Z\"/>"
},
"star": {
"body": "<path fill=\"currentColor\" d=\"M12.86 10.442L11 6.06l-1.862 4.387l-4.743.415l3.596 3.127l-1.07 4.64l4.085-2.455l4.08 2.452l-1.071-4.644l3.593-3.123l-4.748-.416Zm3.731 10.253l-5.585-3.356l-5.59 3.359l1.466-6.35L1.96 10.07l6.491-.567L11 3.5l2.546 5.998l6.496.569l-4.918 4.275l1.467 6.353Z\"/>"
},
"star-half": {
"body": "<path fill=\"currentColor\" d=\"m11 6.06l-1.862 4.386l-4.743.415l3.596 3.127l-1.07 4.64L11 16.175v1.167l-5.584 3.355l1.466-6.35L1.96 10.07l6.491-.567L11 3.5v2.56Z\"/>"
},
"stop": {
"body": "<path fill=\"currentColor\" d=\"M17 18H6V7h11v11ZM7 8v9h9V8H7Z\"/>"
},
"tab": {
"body": "<path fill=\"currentColor\" d=\"M6 4h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-8h-8V5H6Zm13 2a2 2 0 0 0-2-2h-5v4h7V7Z\"/>"
},
"tab-plus": {
"body": "<path fill=\"currentColor\" d=\"M6 4h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-8h-8V5H6Zm13 2a2 2 0 0 0-2-2h-5v4h7V7ZM8 18v-2H6v-1h2v-2h1v2h2v1H9v2H8Z\"/>"
},
"table": {
"body": "<path fill=\"currentColor\" d=\"M6 5h11a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3ZM4 17a2 2 0 0 0 2 2h5v-3H4v1Zm7-5H4v3h7v-3Zm6 7a2 2 0 0 0 2-2v-1h-7v3h5Zm2-7h-7v3h7v-3ZM4 11h7V8H4v3Zm8 0h7V8h-7v3Z\"/>"
},
"taco": {
"body": "<path fill=\"currentColor\" d=\"M3.5 18A2.5 2.5 0 0 1 1 15.5C1 11.358 4.806 8 9.5 8c.689 0 1.359.072 2 .209a9.606 9.606 0 0 1 2-.209c4.694 0 8.5 3.358 8.5 7.5a2.5 2.5 0 0 1-2.5 2.5h-16ZM2 15.5a1.5 1.5 0 0 0 3 0c0-2.776 1.71-5.2 4.25-6.496C5.223 9.118 2 11.983 2 15.5ZM19.5 17a1.5 1.5 0 0 0 1.5-1.5c0-3.59-3.358-6.5-7.5-6.5C9.358 9 6 11.91 6 15.5c0 .563-.186 1.082-.5 1.5h14Z\"/>"
},
"tag": {
"body": "<path fill=\"currentColor\" d=\"M15.62 21.119a3 3 0 0 1-4.243 0l-8.323-8.123C2.45 12.448 2 11.63 2 10.75V6a3 3 0 0 1 3-3h4.75c.88 0 1.698.451 2.246 1.054l8.073 8.373a3 3 0 0 1 0 4.242l-4.45 4.45Zm-.708-.707l4.45-4.45a2 2 0 0 0 0-2.828l-8.25-8.55C10.776 4.197 10.302 4 9.75 4l-4.777-.027C3.87 3.973 3 4.895 3 6v4.75c0 .552.197 1.026.584 1.362l8.5 8.3a2 2 0 0 0 2.828 0ZM6.5 5a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5Zm0 1a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3Z\"/>"
},
"television": {
"body": "<path fill=\"currentColor\" d=\"M8 21v-1h1v-1H4a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h15a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-5v1h1v1H8Zm2-2v1h3v-1h-3ZM4 5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h15a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H4Z\"/>"
},
"thumb-down": {
"body": "<path fill=\"currentColor\" d=\"M21 15h-3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1Zm0-1V5h-3v9h3ZM5.284 5.975l-3.001 5c-.18.3-.283.65-.283 1.025v1a2 2 0 0 0 2 2h5.61l-1.458 5.44l-.004.018a.999.999 0 0 0 .258.966l6.009-6.009c.361-.363.585-.863.585-1.415V7a2 2 0 0 0-2-2H7c-.73 0-1.368.39-1.716.975ZM1 12c0-.594.172-1.147.47-1.612l2.906-4.843A3 3 0 0 1 7 4h6a3 3 0 0 1 3 3v7a2.99 2.99 0 0 1-.877 2.12l-6.717 6.718l-.707-.707a2 2 0 0 1-.507-1.97L8.307 16H4a3 3 0 0 1-3-3v-1Z\"/>"
},
"thumb-up": {
"body": "<path fill=\"currentColor\" d=\"M2 10h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1Zm0 1v9h3v-9H2Zm15.716 8.025l3.001-5c.18-.3.283-.65.283-1.025v-1a2 2 0 0 0-2-2h-5.61l1.457-5.44l.005-.018a1 1 0 0 0-.258-.966L8.585 9.585A1.994 1.994 0 0 0 8 11v7a2 2 0 0 0 2 2h6c.73 0 1.368-.39 1.716-.975ZM22 13c0 .594-.172 1.147-.47 1.612l-2.906 4.843A3 3 0 0 1 16 21h-6a3 3 0 0 1-3-3v-7c0-.828.335-1.577.877-2.12l6.717-6.718l.707.707a2 2 0 0 1 .507 1.97L14.693 9H19a3 3 0 0 1 3 3v1Z\"/>"
},
"thumbs-up-down": {
"body": "<path fill=\"currentColor\" d=\"M10.904 8.428L11 8a1 1 0 0 0-1-1H4.926l.921-3.44l.005-.018a1 1 0 0 0-.258-.965L1.585 6.585A1.994 1.994 0 0 0 1 8v4a2 2 0 0 0 2 2h4a2 2 0 0 0 1.807-1.142h-.002l2.1-4.43ZM7 15H3a3 3 0 0 1-3-3V8c0-.827.335-1.577.877-2.12l4.717-4.718l.707.707a2 2 0 0 1 .507 1.97L6.23 6H10a2 2 0 0 1 1.765 2.941L9.764 13.17A3 3 0 0 1 7 15Zm5.096 1.572L12 17a1 1 0 0 0 1 1h5.074l-.922 3.44l-.004.018a1 1 0 0 0 .258.966l4.009-4.01c.361-.362.585-.862.585-1.414v-4a2 2 0 0 0-2-2h-4a2 2 0 0 0-1.807 1.142h.002l-2.1 4.43ZM16 10h4a3 3 0 0 1 3 3v4a2.99 2.99 0 0 1-.877 2.12l-4.717 4.718l-.707-.707a2 2 0 0 1-.507-1.97L16.77 19H13a2 2 0 0 1-1.765-2.941l2.001-4.228A3 3 0 0 1 16 10Z\"/>"
},
"tooltip": {
"body": "<path fill=\"currentColor\" d=\"M5 4h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-3.5l-3 3l-3-3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3.914l2.586 2.586L14.086 18H18a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5Z\"/>"
},
"tooltip-text": {
"body": "<path fill=\"currentColor\" d=\"M5 4h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-3.5l-3 3l-3-3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 1a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3.914l2.586 2.586L14.086 18H18a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5Zm0 3h13v1H5V8Zm0 3h12v1H5v-1Zm0 3h8v1H5v-1Z\"/>"
},
"trophy": {
"body": "<path fill=\"currentColor\" d=\"M7 22a4 4 0 0 1 4-4v-1a5.002 5.002 0 0 1-4.9-4H5a3 3 0 0 1-3-3V5h2c.768 0 1.47.289 2 .764V3h11v2.764A2.989 2.989 0 0 1 19 5h2v5a3 3 0 0 1-3 3h-1.1a5.002 5.002 0 0 1-4.9 4v1a4 4 0 0 1 4 4H7Zm5-3h-1a3.001 3.001 0 0 0-2.83 2h6.66A3.001 3.001 0 0 0 12 19Zm4-15H7v8a4 4 0 0 0 4 4h1a4 4 0 0 0 4-4V4Zm4 6V6h-1a2 2 0 0 0-2 2v4h1a2 2 0 0 0 2-2ZM3 10a2 2 0 0 0 2 2h1V8a2 2 0 0 0-2-2H3v4Z\"/>"
},
"truck": {
"body": "<path fill=\"currentColor\" d=\"M5.5 14a2.5 2.5 0 0 1 2.45 2H15V6H4a2 2 0 0 0-2 2v8h1.05a2.5 2.5 0 0 1 2.45-2Zm0 5a2.5 2.5 0 0 1-2.45-2H1V8a3 3 0 0 1 3-3h11a1 1 0 0 1 1 1v2h3l3 3.981V17h-2.05a2.5 2.5 0 0 1-4.9 0h-7.1a2.5 2.5 0 0 1-2.45 2Zm0-4a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3Zm12-1a2.5 2.5 0 0 1 2.45 2H21v-3.684L20.762 12H16v2.5a2.49 2.49 0 0 1 1.5-.5Zm0 1a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3ZM16 9v2h4.009L18.5 9H16Z\"/>"
},
"undo-variant": {
"body": "<path fill=\"currentColor\" d=\"M6 20v-1h1v1H6Zm7-12a6 6 0 1 1 0 12H9v-1h4a5 5 0 0 0 0-10H5.914l3.036 3.036l-.707.707L4 8.5l4.243-4.243l.707.707L5.914 8H13Z\"/>"
},
"unfold-less-horizontal": {
"body": "<path fill=\"currentColor\" d=\"M15.743 5.293L11.5 9.536L7.257 5.293l.707-.707L11.5 8.12l3.536-3.535l.707.707Zm0 14.414l-.707.707L11.5 16.88l-3.536 3.535l-.707-.707l4.243-4.243l4.243 4.243Z\"/>"
},
"unfold-less-vertical": {
"body": "<path fill=\"currentColor\" d=\"M18.707 16.743L14.464 12.5l4.243-4.243l.707.707L15.88 12.5l3.535 3.535l-.707.708Zm-14.414 0l-.707-.707L7.12 12.5L3.586 8.964l.707-.707L8.536 12.5l-4.243 4.243Z\"/>"
},
"unfold-more-horizontal": {
"body": "<path fill=\"currentColor\" d=\"m15.743 8.828l-.707.708L11.5 6L7.964 9.536l-.707-.708L11.5 4.586l4.243 4.242Zm0 7.344L11.5 20.414l-4.243-4.242l.707-.708L11.5 19l3.536-3.536l.707.708Z\"/>"
},
"unfold-more-vertical": {
"body": "<path fill=\"currentColor\" d=\"m15.172 16.743l-.708-.707L18 12.5l-3.536-3.536l.708-.707l4.242 4.243l-4.242 4.243Zm-7.344 0L3.586 12.5l4.242-4.243l.708.707L5 12.5l3.536 3.535l-.708.708Z\"/>"
},
"ungroup": {
"body": "<path fill=\"currentColor\" d=\"M2 3h3v1h8V3h3v3h-1v4h3V9h3v3h-1v7h1v3h-3v-1h-7v1H8v-3h1v-3H5v1H2v-3h1V6H2V3Zm16 9v-1h-3v3h1v3h-3v-1h-3v3h1v1h7v-1h1v-7h-1Zm-5-6V5H5v1H4v8h1v1h4v-3H8V9h3v1h3V6h-1Zm-2 6h-1v3h3v-1h1v-3h-3v1ZM3 5h1V4H3v1Zm11 0h1V4h-1v1Zm-5 6h1v-1H9v1Zm10 0h1v-1h-1v1ZM9 21h1v-1H9v1Zm10 0h1v-1h-1v1ZM3 16h1v-1H3v1Zm11 0h1v-1h-1v1Z\"/>"
},
"upload": {
"body": "<path fill=\"currentColor\" d=\"M12 18.164V5.914l5.25 5.25l.75-.664L11.5 4L5 10.5l.75.664L11 5.914v12.25h1ZM3 19h1v2h15v-2h1v3H3v-3Z\"/>"
},
"view-dashboard": {
"body": "<path fill=\"currentColor\" d=\"M12 4h8v6h-8V4Zm0 17V11h8v10h-8Zm-9 0v-6h8v6H3Zm0-7V4h8v10H3Zm1-9v8h6V5H4Zm9 0v4h6V5h-6Zm0 7v8h6v-8h-6Zm-9 4v4h6v-4H4Z\"/>"
},
"view-module": {
"body": "<path fill=\"currentColor\" d=\"M15 6h5v6h-5V6Zm-6 6V6h5v6H9Zm6 7v-6h5v6h-5Zm-6 0v-6h5v6H9Zm-6 0v-6h5v6H3Zm0-7V6h5v6H3Zm1-5v4h3V7H4Zm6 0v4h3V7h-3Zm6 0v4h3V7h-3ZM4 14v4h3v-4H4Zm6 0v4h3v-4h-3Zm6 0v4h3v-4h-3Z\"/>"
},
"volume": {
"body": "<path fill=\"currentColor\" d=\"M21 12.5a7.5 7.5 0 0 1-7 7.484V18.98a6.5 6.5 0 0 0 0-12.96V5.016a7.5 7.5 0 0 1 7 7.484Zm-3 0a4.5 4.5 0 0 1-4 4.473v-1.008a3.501 3.501 0 0 0 0-6.93V8.027a4.5 4.5 0 0 1 4 4.473Zm-3 0a1.5 1.5 0 0 1-1 1.415v-2.83a1.5 1.5 0 0 1 1 1.415ZM2 9h4l4-4h2v15h-2l-4-4H2V9Zm1 6h3.414l4 4H11V6h-.586l-4 4H3v5Z\"/>"
},
"volume-minus": {
"body": "<path fill=\"currentColor\" d=\"M2 9h4l4-4h2v15h-2l-4-4H2V9Zm1 6h3.414l4 4H11V6h-.586l-4 4H3v5Zm11-2v-1h7v1h-7Z\"/>"
},
"volume-mute": {
"body": "<path fill=\"currentColor\" d=\"M2 9h4l4-4h2v15h-2l-4-4H2V9Zm1 6h3.414l4 4H11V6h-.586l-4 4H3v5Zm10.964.328l2.829-2.828l-2.829-2.828l.708-.708l2.828 2.829l2.828-2.829l.707.708l-2.828 2.828l2.828 2.828l-.707.707l-2.828-2.828l-2.828 2.828l-.708-.707Z\"/>"
},
"volume-off": {
"body": "<path fill=\"currentColor\" d=\"M2.793 4.457L3.5 3.75L20.25 20.5l-.707.707l-2.241-2.24A7.455 7.455 0 0 1 14 19.983V18.98a6.463 6.463 0 0 0 2.568-.749l-1.51-1.51c-.335.125-.69.21-1.058.25v-1.007a4.21 4.21 0 0 0 .254-.046L12 13.663V20h-2l-4-4H2V9h4l.668-.668l-3.875-3.875ZM21 12.5c0 2.03-.806 3.87-2.115 5.22l-.707-.707A6.5 6.5 0 0 0 14 6.02V5.016a7.5 7.5 0 0 1 7 7.484Zm-3 0c0 1.2-.47 2.292-1.237 3.099l-.707-.708A3.5 3.5 0 0 0 14 9.035V8.027a4.5 4.5 0 0 1 4 4.473Zm-3 0c0 .372-.136.713-.36.975l-.64-.64v-1.75a1.5 1.5 0 0 1 1 1.415ZM6.414 10H3v5h3.414l4 4H11v-6.336L7.375 9.04l-.96.961ZM10 5h2v5.836l-1-1V6h-.586L8.79 7.625l-.707-.707L10 5Z\"/>"
},
"volume-plus": {
"body": "<path fill=\"currentColor\" d=\"M2 9h4l4-4h2v15h-2l-4-4H2V9Zm1 6h3.414l4 4H11V6h-.586l-4 4H3v5Zm14 1v-3h-3v-1h3V9h1v3h3v1h-3v3h-1Z\"/>"
},
"wallet": {
"body": "<path fill=\"currentColor\" d=\"M4 3a3 3 0 0 0-3 3v13a3 3 0 0 0 3 3h13a3 3 0 0 0 3-3v-1.77A3 3 0 0 0 21 15v-5a3 3 0 0 0-1-2.23V6a3 3 0 0 0-3-3H4Zm0 1h13a2 2 0 0 1 2 2v1.174A3 3 0 0 0 18 7h-6a3 3 0 0 0-3 3v5a3 3 0 0 0 3 3h6a3 3 0 0 0 1-.174V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm8 4h6a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2Zm2.5 2a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5Zm0 1a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>"
},
"wifi": {
"body": "<path fill=\"currentColor\" d=\"m11.5 20.5l-4.514-5.99A7.467 7.467 0 0 1 11.5 13c1.695 0 3.258.562 4.514 1.51L11.5 20.5Zm0-1.662l3.067-4.07A6.472 6.472 0 0 0 11.5 14c-1.11 0-2.154.278-3.067.768l3.067 4.07ZM11.5 4a16.42 16.42 0 0 1 9.93 3.322l-.601.798A15.432 15.432 0 0 0 11.5 5a15.432 15.432 0 0 0-9.329 3.12l-.602-.798A16.427 16.427 0 0 1 11.5 4Zm0 6c2.372 0 4.561.787 6.32 2.114l-.602.798A9.458 9.458 0 0 0 11.5 11a9.458 9.458 0 0 0-5.718 1.912l-.601-.798A10.454 10.454 0 0 1 11.5 10Zm0-3a13.44 13.44 0 0 1 8.125 2.718l-.602.798A12.445 12.445 0 0 0 11.5 8c-2.824 0-5.43.937-7.523 2.517l-.602-.8A13.44 13.44 0 0 1 11.5 7Z\"/>"
},
"xml": {
"body": "<path fill=\"currentColor\" d=\"m16.172 17.743l-.708-.707L20 12.5l-4.536-4.536l.708-.707l5.242 5.243l-5.242 5.243Zm-9.344 0L1.586 12.5l5.242-5.243l.708.707L3 12.5l4.536 4.535l-.708.708ZM12.732 5h1l-3.463 15h-1l3.463-15Z\"/>"
}
},
"width": 24,
"height": 24
}

54325
tests/fixtures/json/mdi.json vendored Normal file

File diff suppressed because it is too large Load Diff

47
tests/helpers.ts Normal file
View File

@ -0,0 +1,47 @@
import { readFile } from 'node:fs/promises';
import { parse } from 'dotenv';
/**
* Load fixture
*/
export async function loadFixture(file: string): Promise<string> {
return await readFile('tests/fixtures/' + file, 'utf8');
}
// Counter
let uniqueDirCounter = Date.now();
/**
* Get unique cache directory
*/
export function uniqueCacheDir(): string {
// Return unique dir
return 'tests/dir-' + (uniqueDirCounter++).toString();
}
/**
* Await
*/
export function awaitTick(): Promise<undefined> {
return new Promise((fulfill, reject) => {
setTimeout(() => {
fulfill(void 0);
});
});
}
/**
* Get env variable
*/
export async function getEnv(key: string): Promise<string | undefined> {
const files = ['.env.test', '.env.dev', '.env'];
for (let i = 0; i < files.length; i++) {
try {
const contents = await readFile(files[i]);
const env = parse(contents);
if (env[key] !== void 0) {
return env[key];
}
} catch {}
}
}

View File

@ -0,0 +1,63 @@
import type { IconifyAliases } from '@iconify/types';
import { getIconsToRetrieve, getIconsData } from '../../lib/data/icon-set/utils/get-icons';
import { splitIconSetMainData } from '../../lib/data/icon-set/store/split';
import { loadFixture } from '../helpers';
describe('Getting icons data', () => {
test('Getting icon names to retrieve', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi.json'));
const data = splitIconSetMainData(iconSet);
// Icons without aliases
const aliases1 = {} as IconifyAliases;
expect(getIconsToRetrieve(data, ['account-multiple-minus', 'math-log'], aliases1)).toEqual(
new Set(['account-multiple-minus', 'math-log'])
);
expect(aliases1).toEqual({});
// Icons with aliases
const aliases2 = {} as IconifyAliases;
expect(getIconsToRetrieve(data, ['account-multiple-minus', '123', '1-2-3', '4k'], aliases2)).toEqual(
new Set(['account-multiple-minus', 'numeric', 'video-4k-box'])
);
expect(aliases2).toEqual({
'123': {
parent: 'numeric',
},
'1-2-3': {
parent: 'numeric',
},
'4k': {
parent: 'video-4k-box',
},
});
});
test('Getting icon data from one object', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi.json'));
const data = splitIconSetMainData(iconSet);
const icons = iconSet.icons;
expect(getIconsData(data, ['123', 'windsock'], [icons])).toEqual({
prefix: 'mdi',
icons: {
numeric: {
body: '<path fill="currentColor" d="M4 17V9H2V7h4v10H4m18-2a2 2 0 0 1-2 2h-4v-2h4v-2h-2v-2h2V9h-4V7h4a2 2 0 0 1 2 2v1.5a1.5 1.5 0 0 1-1.5 1.5a1.5 1.5 0 0 1 1.5 1.5V15m-8 0v2H8v-4a2 2 0 0 1 2-2h2V9H8V7h4a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-2v2h4Z"/>',
},
windsock: {
body: '<path fill="currentColor" d="M7 5v8l15-2V7L7 5m3 1.91l3 .4v3.38l-3 .4V6.91m6 .8l3 .4v1.78l-3 .4V7.71M5 10v1h1v1H5v9H3V4c0-.55.45-1 1-1s1 .45 1 1v2h1v1H5v3Z"/>',
},
},
aliases: {
'123': {
parent: 'numeric',
},
},
width: 24,
height: 24,
lastModified: 1663305505,
});
});
});

View File

@ -0,0 +1,141 @@
import type { ExtendedIconifyIcon, IconifyIcons, IconifyJSON } from '@iconify/types';
import { storeLoadedIconSet } from '../../lib/data/icon-set/store/storage';
import { getStoredIconData } from '../../lib/data/icon-set/utils/get-icon';
import { createStorage } from '../../lib/data/storage/create';
import type { StoredIconSet } from '../../lib/types/icon-set/storage';
import { loadFixture, uniqueCacheDir } from '../helpers';
describe('Loading icon data from storage', () => {
test('Testing mdi', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi.json')) as IconifyJSON;
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
maxCount: 2,
});
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 5000,
minIconsPerChunk: 10,
});
});
}
const storedIconSet = await store();
function getIcon(name: string): Promise<ExtendedIconifyIcon | null> {
return new Promise((fulfill, reject) => {
getStoredIconData(storedIconSet, name, (data) => {
try {
fulfill(data);
} catch (err) {
reject(err);
}
});
});
}
// Icons
expect(await getIcon('abacus')).toEqual({
body: iconSet.icons['abacus'].body,
width: 24,
height: 24,
});
expect(await getIcon('account-off')).toEqual({
body: iconSet.icons['account-off'].body,
width: 24,
height: 24,
});
// Aliases
expect(await getIcon('123')).toEqual({
body: iconSet.icons['numeric'].body,
width: 24,
height: 24,
});
// Missing icons
expect(await getIcon('foo')).toBeNull();
});
test('Testing complex aliases', async () => {
const iconSet: IconifyJSON = {
prefix: 'test',
icons: {
foo: {
body: '<g id="main" />',
width: 16,
height: 16,
},
},
aliases: {
'bar': {
parent: 'foo',
hFlip: true,
},
'bar-wide': {
parent: 'bar',
width: 24,
left: -4,
},
},
width: 24,
};
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
maxCount: 2,
});
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 0,
minIconsPerChunk: 10,
});
});
}
const storedIconSet = await store();
function getIcon(name: string): Promise<ExtendedIconifyIcon | null> {
return new Promise((fulfill, reject) => {
getStoredIconData(storedIconSet, name, (data) => {
try {
fulfill(data);
} catch (err) {
reject(err);
}
});
});
}
// Icons
expect(await getIcon('foo')).toEqual({
body: '<g id="main" />',
width: 16,
height: 16,
});
// Aliases
expect(await getIcon('bar-wide')).toEqual({
body: '<g id="main" />',
left: -4,
width: 24,
height: 16,
hFlip: true,
});
// Missing icons
expect(await getIcon('invalid-icon')).toBeNull();
});
});

View File

@ -0,0 +1,157 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
import { storeLoadedIconSet } from '../../lib/data/icon-set/store/storage';
import { getStoredIconsData } from '../../lib/data/icon-set/utils/get-icons';
import { createStorage } from '../../lib/data/storage/create';
import type { StoredIconSet } from '../../lib/types/icon-set/storage';
import { loadFixture, uniqueCacheDir } from '../helpers';
describe('Loading icons from storage', () => {
test('Get existing icons', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi-light.json')) as IconifyJSON;
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
maxCount: 2,
});
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 5000,
minIconsPerChunk: 10,
});
});
}
const storedIconSet = await store();
function getIcons(): Promise<IconifyJSON> {
return new Promise((fulfill, reject) => {
getStoredIconsData(
storedIconSet,
[
// Icons that exist
'account',
'camcorder',
'camera',
'monitor',
'note',
'paperclip',
'wallet',
'xml',
],
fulfill
);
});
}
const data = await getIcons();
expect(data).toEqual({
prefix: 'mdi-light',
lastModified: iconSet.lastModified,
icons: {
account: iconSet.icons['account'],
camcorder: iconSet.icons['camcorder'],
camera: iconSet.icons['camera'],
monitor: iconSet.icons['monitor'],
note: iconSet.icons['note'],
paperclip: iconSet.icons['paperclip'],
wallet: iconSet.icons['wallet'],
xml: iconSet.icons['xml'],
},
aliases: {},
width: 24,
height: 24,
});
});
test('Aliases, missing icons', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi.json')) as IconifyJSON;
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
maxCount: 2,
});
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 100000,
minIconsPerChunk: 50,
});
});
}
const storedIconSet = await store();
function getIcons(): Promise<IconifyJSON> {
return new Promise((fulfill, reject) => {
getStoredIconsData(
storedIconSet,
[
// Icons that exist
'abacus',
'abjad-arabic',
'abjad-hebrew',
'floor-1',
'folder-swap',
'folder-swap-outline',
// Missing icons
'no-such-icon',
'foo',
// Aliases
'123',
'1-2-3',
'1up',
'accessible',
],
fulfill
);
});
}
const data = await getIcons();
// Sort missing icons: they might be in any order
const not_found = data.not_found?.sort((a, b) => a.localeCompare(b));
expect(not_found).toEqual(['foo', 'no-such-icon']);
expect(data).toEqual({
prefix: 'mdi',
lastModified: iconSet.lastModified,
icons: {
'abacus': iconSet.icons['abacus'],
'abjad-arabic': iconSet.icons['abjad-arabic'],
'abjad-hebrew': iconSet.icons['abjad-hebrew'],
'floor-1': iconSet.icons['floor-1'],
'folder-swap': iconSet.icons['folder-swap'],
'folder-swap-outline': iconSet.icons['folder-swap-outline'],
'numeric': iconSet.icons['numeric'],
'one-up': iconSet.icons['one-up'],
'wheelchair': iconSet.icons['wheelchair'],
},
aliases: {
'123': {
parent: 'numeric',
},
'1-2-3': {
parent: 'numeric',
},
'1up': {
parent: 'one-up',
},
'accessible': {
parent: 'wheelchair',
},
},
not_found,
width: 24,
height: 24,
});
});
});

View File

@ -0,0 +1,71 @@
import type { IconifyJSON } from '@iconify/types';
import { getIconSetSplitChunksCount } from '../../lib/data/icon-set/store/split';
import { loadFixture } from '../helpers';
describe('Splitting icon set', () => {
test('Testing config with small icon set', async () => {
// 267 icons, 63104 bytes
const { icons } = JSON.parse(await loadFixture('json/mdi-light.json')) as IconifyJSON;
// Disabled
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 0,
minIconsPerChunk: 10,
})
).toBe(1);
// Chunk size is more than icon set size
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 100000,
minIconsPerChunk: 10,
})
).toBe(1);
// Chunk size is 6.3 times less than icon set
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 10000,
minIconsPerChunk: 10,
})
).toBe(6);
// Chunk size is 63 times less than icon set, number of icons is 10 times less than in icon set
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 1000,
minIconsPerChunk: 25,
})
).toBe(10);
});
test('Testing config with big icon set', async () => {
// 7328 icons, 2308927 bytes
const { icons } = JSON.parse(await loadFixture('json/mdi.json')) as IconifyJSON;
// Chunk size is 2.3 times less than icon set
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 1000000,
minIconsPerChunk: 40,
})
).toBe(1);
// Chunk size is 23 times less than icon set
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 100000,
minIconsPerChunk: 40,
})
).toBe(23);
// Icons count per chunk is exactly 16 less than number of icons
expect(
getIconSetSplitChunksCount(icons, {
chunkSize: 10000,
minIconsPerChunk: 7328 / 16,
})
).toBe(16);
});
});

View File

@ -0,0 +1,189 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
import { storeLoadedIconSet } from '../../lib/data/icon-set/store/storage';
import { searchSplitRecordsTree } from '../../lib/data/storage/split';
import { createStorage } from '../../lib/data/storage/create';
import { getStoredItem } from '../../lib/data/storage/get';
import type { StoredIconSet } from '../../lib/types/icon-set/storage';
import type { MemoryStorageItem } from '../../lib/types/storage';
import { awaitTick, loadFixture, uniqueCacheDir } from '../helpers';
describe('Storing loaded icon set', () => {
test('No storage, no splitting', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi-light.json')) as IconifyJSON;
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
});
// Split icon set
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 0,
minIconsPerChunk: 100,
});
});
}
const storedIconSet = await store();
// Simple test
expect(storedIconSet.storage).toBe(storage);
expect(storedIconSet.items.length).toBe(1);
// Get item
const storedItem = searchSplitRecordsTree(storedIconSet.tree, 'calendar');
expect(storedItem).toBe(storedIconSet.tree.match);
// Load it
function getItem(): Promise<IconifyIcons | null> {
return new Promise((fulfill, reject) => {
getStoredItem(storage, storedItem, fulfill);
});
}
const data = await getItem();
expect(data).toBeTruthy();
// Data should be identical because storage is disabled
expect(data!['calendar']).toBe(iconSet.icons['calendar']);
});
test('Split icon set', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi-light.json')) as IconifyJSON;
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
});
// Split icon set
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 10000,
minIconsPerChunk: 10,
});
});
}
const storedIconSet = await store();
// Simple test
expect(storedIconSet.storage).toBe(storage);
expect(storedIconSet.items.length).toBe(6);
// Get item from middle
const storedItem = searchSplitRecordsTree(storedIconSet.tree, 'grid');
expect(storedItem).toBe(storedIconSet.tree.match);
// Get item from first tree item
const firstStoredItem = searchSplitRecordsTree(storedIconSet.tree, 'alert');
expect(firstStoredItem).not.toBe(storedIconSet.tree.match);
expect(searchSplitRecordsTree(storedIconSet.tree, 'account')).toBe(firstStoredItem);
// Load it
function getItem(): Promise<IconifyIcons | null> {
return new Promise((fulfill, reject) => {
getStoredItem(storage, storedItem, fulfill);
});
}
const data = await getItem();
expect(data).toBeTruthy();
// Data should be identical because storage is disabled
expect(data!['grid']).toBe(iconSet.icons['grid']);
// Icons from other chunks should not exist
expect(data!['alert']).toBeUndefined();
expect(data!['account']).toBeUndefined();
expect(data!['repeat']).toBeUndefined();
});
test('Split and store icon set', async () => {
const iconSet = JSON.parse(await loadFixture('json/mdi-light.json')) as IconifyJSON;
// Create storage
const dir = uniqueCacheDir();
const cacheDir = '{cache}/' + dir;
const storage = createStorage<IconifyIcons>({
cacheDir,
maxCount: 2,
});
// Split icon set
function store(): Promise<StoredIconSet> {
return new Promise((fulfill, reject) => {
// Split icon set
storeLoadedIconSet(iconSet, fulfill, storage, {
chunkSize: 10000,
minIconsPerChunk: 10,
});
});
}
const storedIconSet = await store();
// Simple test
expect(storedIconSet.storage).toBe(storage);
expect(storedIconSet.items.length).toBe(6);
// Get item from middle
const storedItem = searchSplitRecordsTree(storedIconSet.tree, 'grid');
expect(storedItem).toBe(storedIconSet.tree.match);
// Get item from first tree item
const firstStoredItem = searchSplitRecordsTree(storedIconSet.tree, 'alert');
expect(firstStoredItem).not.toBe(storedIconSet.tree.match);
expect(searchSplitRecordsTree(storedIconSet.tree, 'account')).toBe(firstStoredItem);
// Load icon from middle
function getItem(item: MemoryStorageItem<IconifyIcons>): Promise<IconifyIcons | null> {
return new Promise((fulfill, reject) => {
getStoredItem(storage, item, fulfill);
});
}
const testItemData = await getItem(storedItem);
expect(testItemData).toBeTruthy();
// Data should be different because it is loaded from cache
expect(testItemData!['grid']).toEqual(iconSet.icons['grid']);
expect(testItemData!['grid']).not.toBe(iconSet.icons['grid']);
// Icons from other chunks should not exist
expect(testItemData!['alert']).toBeUndefined();
expect(testItemData!['account']).toBeUndefined();
expect(testItemData!['repeat']).toBeUndefined();
// Load icon from first chunk
const item1Data = await getItem(firstStoredItem);
expect(item1Data).toBeTruthy();
// Data should be different because it is loaded from cache
expect(item1Data!['alert']).toEqual(iconSet.icons['alert']);
expect(item1Data!['alert']).not.toBe(iconSet.icons['alert']);
// Check storage on next tick: watched items list is updated after this callback
await awaitTick();
// Only 2 items loaded in this test should be loaded and watched
expect(storage.watched.size).toBe(2);
expect(storage.watched.has(firstStoredItem)).toBe(true);
expect(storage.watched.has(storedItem)).toBe(true);
// Test all items
expect(storedIconSet.items.length).toBe(6);
expect(storedIconSet.items[0].data).toBe(item1Data);
expect(storedIconSet.items[1].data).toBeUndefined();
expect(storedIconSet.items[2].data).toBeUndefined();
expect(storedIconSet.items[3].data).toBe(testItemData);
expect(storedIconSet.items[4].data).toBeUndefined();
expect(storedIconSet.items[5].data).toBeUndefined();
});
});

View File

@ -0,0 +1,63 @@
import { removeBadAliases } from '../../lib/data/icon-set/store/validate';
describe('Validating icon set', () => {
test('Long chain of aliases, bad aliases', () => {
const body = '<g />';
const iconSet = {
prefix: 'foo',
icons: {
foo: {
body,
},
bar: {
body,
},
},
aliases: {
baz: {
parent: 'bar',
},
// Will be parsed before parent
baz2: {
parent: 'baz3',
},
// Will be parsed when already resolved
baz3: {
parent: 'baz',
},
baz4: {
parent: 'baz3',
},
baz5: {
parent: 'baz4',
},
baz6: {
parent: 'baz5',
},
bazz5: {
parent: 'baz4',
hFlip: true,
},
// Bad alias
bad: {
parent: 'good',
},
// Loop
loop1: {
parent: 'loop3',
},
loop2: {
parent: 'loop1',
},
loop3: {
parent: 'loop1',
},
},
};
removeBadAliases(iconSet);
// Check aliases
expect(Object.keys(iconSet.aliases)).toEqual(['baz', 'baz2', 'baz3', 'baz4', 'baz5', 'baz6', 'bazz5']);
});
});

View File

@ -0,0 +1,35 @@
import { DirectoryDownloader } from '../../lib/downloaders/directory';
import { createJSONDirectoryImporter } from '../../lib/importers/full/directory-json';
import type { ImportedData } from '../../lib/types/importers/common';
describe('JSON files from directory importer', () => {
test('Scan directory', async () => {
// Create importer for collections list
const importer = createJSONDirectoryImporter(new DirectoryDownloader<ImportedData>('tests/fixtures/json'));
// Track changes
let updateCounter = 0;
importer._dataUpdated = async () => {
updateCounter++;
};
// Initial data
expect(importer.data).toBeUndefined();
expect(updateCounter).toBe(0);
// Wait for import
await importer.init();
expect(updateCounter).toBe(1);
// Check data
expect(importer.data).toBeDefined();
const data = importer.data!;
expect(data.prefixes).toEqual(['mdi-light', 'mdi']);
expect(data.iconSets['mdi']).toBeDefined();
expect(data.iconSets['mdi-light']).toBeDefined();
// Check for update
expect(await importer.checkForUpdate()).toBeFalsy();
expect(updateCounter).toBe(1);
}, 5000);
});

View File

@ -0,0 +1,78 @@
import { DirectoryDownloader } from '../../lib/downloaders/directory';
import { createHardcodedCollectionsListImporter } from '../../lib/importers/collections/list';
import { createJSONIconSetImporter } from '../../lib/importers/icon-set/json';
import type { StoredIconSet } from '../../lib/types/icon-set/storage';
describe('Hardcoded collections list importer', () => {
test('Import from JSON files', async () => {
// Create importer for collections list
const importer = createHardcodedCollectionsListImporter(['mdi-light', 'mdi'], (prefix) => {
// Create downloader and importer for icon set
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>('tests/fixtures/json'), {
prefix,
filename: `/${prefix}.json`,
});
});
// Track changes
let updateCounter = 0;
importer._dataUpdated = async () => {
updateCounter++;
};
// Initial data
expect(importer.data).toBeUndefined();
expect(updateCounter).toBe(0);
// Wait for import
await importer.init();
expect(updateCounter).toBe(1);
// Check data
expect(importer.data).toBeDefined();
const data = importer.data!;
expect(data.prefixes).toEqual(['mdi-light', 'mdi']);
expect(data.iconSets['mdi']).toBeDefined();
expect(data.iconSets['mdi-light']).toBeDefined();
// Check for update
expect(await importer.checkForUpdate()).toBeFalsy();
expect(updateCounter).toBe(1);
}, 5000);
test('Invalid files', async () => {
// Create importer for collections list
const importer = createHardcodedCollectionsListImporter(['foo', 'bar'], (prefix) => {
// Create downloader and importer for icon set
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>('tests/fixtures/json'), {
prefix,
filename: `/${prefix}.json`,
});
});
// Track changes
let updateCounter = 0;
importer._dataUpdated = async () => {
updateCounter++;
};
// Initial data
expect(importer.data).toBeUndefined();
expect(updateCounter).toBe(0);
// Wait for import
await importer.init();
expect(updateCounter).toBe(1);
// Check data
expect(importer.data).toBeDefined();
const data = importer.data!;
expect(data.prefixes).toEqual(['foo', 'bar']);
expect(data.iconSets['foo']).toBeUndefined();
expect(data.iconSets['bar']).toBeUndefined();
// Check for update
expect(await importer.checkForUpdate()).toBeFalsy();
expect(updateCounter).toBe(1);
}, 5000);
});

View File

@ -0,0 +1,128 @@
import { DirectoryDownloader } from '../../lib/downloaders/directory';
import { createJSONCollectionsListImporter } from '../../lib/importers/collections/collections';
import { createJSONIconSetImporter } from '../../lib/importers/icon-set/json';
import type { StoredIconSet } from '../../lib/types/icon-set/storage';
import type { ImportedData } from '../../lib/types/importers/common';
describe('Icon collections.json importer', () => {
test('Import from JSON files', async () => {
// Create importer for collections list
const downloader = new DirectoryDownloader<ImportedData>('tests/fixtures');
const importer = createJSONCollectionsListImporter(
downloader,
(prefix) => {
// Create downloader and importer for icon set
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>('tests/fixtures/json'), {
prefix,
filename: `/${prefix}.json`,
});
},
{
filename: '/collections.mdi.json',
}
);
// Track changes
let updateCounter = 0;
importer._dataUpdated = async () => {
updateCounter++;
};
// Initial data
expect(importer.data).toBeUndefined();
expect(updateCounter).toBe(0);
// Wait for import
await importer.init();
expect(updateCounter).toBe(1);
// Check data
expect(importer.data).toBeDefined();
const data = importer.data!;
expect(data.prefixes).toEqual(['mdi', 'mdi-light']);
expect(data.iconSets['mdi']).toBeDefined();
expect(data.iconSets['mdi-light']).toBeDefined();
// Check for update
expect(await importer.checkForUpdate()).toBeFalsy();
expect(updateCounter).toBe(1);
}, 5000);
test('Bad file', async () => {
// Create importer for collections list
const downloader = new DirectoryDownloader<ImportedData>('tests/fixtures');
const importer = createJSONCollectionsListImporter(
downloader,
(prefix) => {
// Create downloader and importer for icon set
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>('tests/fixtures/json'), {
prefix,
filename: `/${prefix}.json`,
});
},
{
filename: '/collections.whatever.json',
}
);
// Track changes
let updateCounter = 0;
importer._dataUpdated = async () => {
updateCounter++;
};
// Initial data
expect(importer.data).toBeUndefined();
expect(updateCounter).toBe(0);
// Wait for import
await importer.init();
expect(updateCounter).toBe(0);
// Check data
expect(importer.data).toBeUndefined();
}, 5000);
test('Bad icon set importers', async () => {
// Create importer for collections list
const downloader = new DirectoryDownloader<ImportedData>('tests/fixtures');
const importer = createJSONCollectionsListImporter(
downloader,
(prefix) => {
// Create downloader and importer for icon set
return createJSONIconSetImporter(new DirectoryDownloader<StoredIconSet>('tests/fixtures/json'), {
prefix,
filename: `/mdi-light.json`,
});
},
{
filename: '/collections.mdi.json',
}
);
// Track changes
let updateCounter = 0;
importer._dataUpdated = async () => {
updateCounter++;
};
// Initial data
expect(importer.data).toBeUndefined();
expect(updateCounter).toBe(0);
// Wait for import
await importer.init();
expect(updateCounter).toBe(1);
// Check data
expect(importer.data).toBeDefined();
const data = importer.data!;
expect(data.prefixes).toEqual(['mdi', 'mdi-light']);
expect(data.iconSets['mdi']).toBeUndefined();
expect(data.iconSets['mdi-light']).toBeDefined();
// Check for update
expect(await importer.checkForUpdate()).toBeFalsy();
expect(updateCounter).toBe(1);
}, 5000);
});

View File

@ -0,0 +1,71 @@
import { RemoteDownloader } from '../../lib/downloaders/remote';
import { DirectoryDownloader } from '../../lib/downloaders/directory';
import { createJSONIconSetImporter } from '../../lib/importers/icon-set/json';
import { createJSONPackageIconSetImporter } from '../../lib/importers/icon-set/json-package';
import type { StoredIconSet } from '../../lib/types/icon-set/storage';
describe('Icon set IconifyJSON importer', () => {
test('Import from NPM, nothing to update', async () => {
// Create downloader and importer
const downloader = new RemoteDownloader<StoredIconSet>({
downloadType: 'npm',
package: '@iconify-json/topcoat',
});
const importer = createJSONPackageIconSetImporter(downloader, {
prefix: 'topcoat',
});
let iconSet: StoredIconSet | undefined;
let updateCounter = 0;
// Add callback
expect(importer._dataUpdated).toBeUndefined();
importer._dataUpdated = async (data) => {
updateCounter++;
iconSet = data;
};
// Init
expect(await importer.init()).toBe(true);
expect(iconSet).toBeDefined();
expect(updateCounter).toBe(1);
// Info should be set
expect(iconSet?.info?.name).toBe('TopCoat Icons');
// Check for update
expect(await importer.checkForUpdate()).toBe(false);
expect(updateCounter).toBe(1);
}, 5000);
test('Import from JSON file', async () => {
// Create downloader and importer
const downloader = new DirectoryDownloader<StoredIconSet>('tests/fixtures/json');
const importer = createJSONIconSetImporter(downloader, {
prefix: 'mdi-light',
filename: '/mdi-light.json',
});
let iconSet: StoredIconSet | undefined;
let updateCounter = 0;
// Add callback
expect(importer._dataUpdated).toBeUndefined();
importer._dataUpdated = async (data) => {
updateCounter++;
iconSet = data;
};
// Init
expect(await importer.init()).toBe(true);
expect(iconSet).toBeDefined();
expect(updateCounter).toBe(1);
// Info should be set
expect(iconSet?.info?.name).toBe('Material Design Light');
// Check for update
expect(await importer.checkForUpdate()).toBe(false);
expect(updateCounter).toBe(1);
}, 5000);
});

9
tests/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"types": ["node", "jest", "cheerio"],
"rootDir": ".",
"sourceMap": true,
"mapRoot": "tests/"
}
}

14
tsconfig-base.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "CommonJS",
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error",
"resolveJsonModule": true,
"declaration": true
}
}

Some files were not shown because too many files have changed in this diff Show More