mirror of https://github.com/iconify/api.git
feat: working version of API v3
This commit is contained in:
commit
8ef7f21d74
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
github: cyberalien
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
*.map
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
/node_modules
|
||||||
|
/lib
|
||||||
|
/cache
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)');
|
||||||
|
})();
|
||||||
|
|
@ -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)');
|
||||||
|
})();
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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 + ')' : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate unique hash
|
||||||
|
*/
|
||||||
|
export function hashString(value: string): string {
|
||||||
|
return createHash('md5').update(value).digest('hex');
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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('-'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export type MaybeAsync<T> = T | Promise<T>;
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
|
@ -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>>;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node", "jest", "cheerio"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"sourceMap": true,
|
||||||
|
"mapRoot": "tests/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
Loading…
Reference in New Issue