Compare commits

..

No commits in common. "main" and "3.0.1" have entirely different histories.
main ... 3.0.1

91 changed files with 4353 additions and 3797 deletions

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

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

View File

@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 'latest'
node-version: 18
- name: 📦 Install dependencies
run: npm ci

View File

@ -1,18 +0,0 @@
/.idea
/.vscode
.DS_Store
/.env
/.editorconfig
/.prettierrc
*.map
/docker.sh
/Dockerfile
/tsconfig*.*
/vitest.config.*
/.github
/src
/node_modules
/cache
/tmp
/icons
/tests

View File

@ -1,11 +1,11 @@
ARG ARCH=amd64
ARG NODE_VERSION=22
ARG NODE_VERSION=18
ARG OS=bullseye-slim
ARG ICONIFY_API_VERSION=3.2.0
ARG ICONIFY_API_VERSION=3.0.0
ARG SRC_PATH=./
#### Stage BASE ########################################################################################################
FROM --platform=${ARCH} node:${NODE_VERSION}-${OS} AS base
FROM ${ARCH}/node:${NODE_VERSION}-${OS} AS base
# This gives node.js apps access to the OS CAs
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
@ -53,8 +53,8 @@ COPY ${SRC_PATH}icons/ /data/iconify-api/icons/
# Build API
RUN npm run build
#### Stage release #####################################################################################################
FROM iconify-api-install AS release
#### Stage RELEASE #####################################################################################################
FROM iconify-api-install AS RELEASE
ARG BUILD_DATE
ARG BUILD_VERSION
ARG BUILD_REF

View File

@ -6,12 +6,6 @@ This repository contains Iconify API script. It is a HTTP server, written in Nod
- Generates SVG, which you can link to in HTML or stylesheet.
- Provides search engine for hosted icons, which can be used by icon pickers.
## NPM Package
This package is also available at NPM, allowing using API code in custom wrappers.
NPM package contains only compiled files, to build custom Docker image you need to use source files from Git repository, not NPM package.
## Docker
To build a Docker image, run `./docker.sh`.
@ -91,7 +85,7 @@ By default, API will use memory management functions. It stores only recently us
000
If your API gets a lot of traffic (above 1k requests per minute), it is better to not use memory management. With such high number of queries, disc read/write operations might cause degraded performance. API can easily handle 10 times more traffic on a basic VPS if everything is in memory and can be accessed instantly.
See [memory management in full API docs](https://iconify.design/docs/api/hosting-js/config.html).
See [memory management in full API docs](https://docs.iconify.design/api/hosting-js/config.html).
### Updating icons
@ -131,7 +125,7 @@ Previous version of API was also available as PHP script. This has been disconti
This file is basic.
Full documentation is available on [Iconify documentation website](https://iconify.design/docs/api/).
Full documentation is available on [Iconify documentation website](https://docs.iconify.design/api/).
## Sponsors
@ -149,4 +143,4 @@ Iconify API is licensed under MIT license.
This licence does not apply to icons hosted on API and files generated by API. You can host icons with any license, without any restrictions. Common decency applies, such as not hosting pirated versions of commercial icon sets (not sure why anyone would use commercial icon sets when so many excellent open source icon sets are available, but anyway...).
© 2022-PRESENT Vjacheslav Trushkin
© 2022 Vjacheslav Trushkin / Iconify OÜ

2
license.txt Normal file → Executable file
View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022-PRESENT Vjacheslav Trushkin
Copyright (c) 2022 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

6153
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,17 +3,17 @@
"description": "Iconify API",
"author": "Vjacheslav Trushkin",
"license": "MIT",
"version": "3.2.0",
"type": "module",
"private": true,
"version": "3.0.1",
"bugs": "https://github.com/iconify/api/issues",
"homepage": "https://github.com/iconify/api",
"repository": {
"type": "git",
"url": "https://github.com/iconify/api.git"
},
"packageManager": "npm@11.6.4",
"packageManager": "npm@9.6.6",
"engines": {
"node": ">=22.20.0"
"node": ">=16.15.0"
},
"scripts": {
"build": "tsc -b",
@ -26,17 +26,17 @@
"docker:publish": "docker push iconify/api"
},
"dependencies": {
"@fastify/formbody": "^8.0.2",
"@iconify/tools": "^5.0.0",
"@fastify/formbody": "^7.4.0",
"@iconify/tools": "^2.2.6",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^3.1.0",
"dotenv": "^17.2.3",
"fastify": "^5.6.2"
"@iconify/utils": "^2.1.5",
"dotenv": "^16.0.3",
"fastify": "^4.17.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^24.10.1",
"typescript": "^5.9.3",
"vitest": "^4.0.14"
"@types/jest": "^29.5.1",
"@types/node": "^18.16.12",
"typescript": "^5.0.4",
"vitest": "^0.31.1"
}
}

View File

@ -1,27 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"rangeStrategy": "bump",
"packageRules": [
{
"matchDepTypes": ["peerDependencies"],
"enabled": false
},
{
"matchPackageNames": ["fastify"],
"allowedVersions": "<4.0.0"
},
{
"matchPackageNames": ["@fastify/formbody"],
"allowedVersions": "<8.0.0"
},
{
"matchPackageNames": ["@iconify/utils"],
"allowedVersions": "<3.0.0"
},
{
"matchPackageNames": ["@iconify/tools"],
"allowedVersions": "<5.0.0"
}
]
}

View File

@ -1,13 +1,13 @@
import type { AppConfig } from '../types/config/app.js';
import type { SplitIconSetConfig } from '../types/config/split.js';
import type { MemoryStorageConfig } from '../types/storage.js';
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/docs/api/',
redirectIndex: 'https://docs.iconify.design/api/',
// Region to add to `/version` response
// Used to tell which server is responding when running multiple servers

View File

@ -1,10 +1,10 @@
import { DirectoryDownloader } from '../downloaders/directory.js';
import { createJSONDirectoryImporter } from '../importers/full/directory-json.js';
import { directoryExists } from '../misc/files.js';
import type { Importer } from '../types/importers.js';
import type { ImportedData } from '../types/importers/common.js';
import { fullPackageImporter } from './importers/full-package.js';
import { splitPackagesImporter } from './importers/split-packages.js';
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

View File

@ -1,40 +0,0 @@
import { createRequire } from 'node:module';
import { dirname } from 'node:path';
import { Importer } from '../../types/importers.js';
import { createIconSetsPackageImporter } from '../../importers/full/json.js';
import { ImportedData } from '../../types/importers/common.js';
import { DirectoryDownloader } from '../../downloaders/directory.js';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote.js';
import { RemoteDownloader } from '../../downloaders/remote.js';
/**
* Create importer for package
*/
export function createPackageIconSetImporter(
packageName = '@iconify/json',
useRemoteFallback = false,
autoUpdateRemotePackage = false
): Importer {
// Try to locate package
let dir: string | undefined;
try {
const req = createRequire(import.meta.url);
const filename = req.resolve(`${packageName}/package.json`);
dir = filename ? dirname(filename) : undefined;
} catch (err) {
//
}
if (dir) {
return createIconSetsPackageImporter(new DirectoryDownloader<ImportedData>(dir), {});
}
if (!useRemoteFallback) {
throw new Error(`Cannot find package "${packageName}"`);
}
// Try to download it, update if
const npm: RemoteDownloaderOptions = {
downloadType: 'npm',
package: packageName,
};
return createIconSetsPackageImporter(new RemoteDownloader<ImportedData>(npm, autoUpdateRemotePackage));
}

View File

@ -1,7 +1,7 @@
import { RemoteDownloader } from '../../downloaders/remote.js';
import { createIconSetsPackageImporter } from '../../importers/full/json.js';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote.js';
import type { ImportedData } from '../../types/importers/common.js';
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

View File

@ -1,7 +1,7 @@
import { RemoteDownloader } from '../../downloaders/remote.js';
import { createJSONCollectionsListImporter } from '../../importers/collections/collections.js';
import { createJSONPackageIconSetImporter } from '../../importers/icon-set/json-package.js';
import type { IconSetImportedData, ImportedData } from '../../types/importers/common.js';
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;

View File

@ -1,5 +1,5 @@
import type { IconifyJSON } from '@iconify/types';
import type { IconSetIconsListIcons, IconSetAPIv2IconsList } from '../../../types/icon-set/extra.js';
import type { IconSetIconsListIcons, IconSetAPIv2IconsList } from '../../../types/icon-set/extra';
/**
* Prepare data for icons list API v2 response

View File

@ -1,13 +1,13 @@
import type { IconifyAliases, IconifyJSON, IconifyOptional } from '@iconify/types';
import { defaultIconProps } from '@iconify/utils/lib/icon/defaults';
import { appConfig } from '../../../config/app.js';
import { appConfig } from '../../../config/app';
import type {
IconSetIconNames,
IconSetIconsListIcons,
IconSetIconsListTag,
IconStyle,
} from '../../../types/icon-set/extra.js';
import { getIconStyle } from './style.js';
} from '../../../types/icon-set/extra';
import { getIconStyle } from './style';
const customisableProps = Object.keys(defaultIconProps) as (keyof IconifyOptional)[];

View File

@ -1,4 +1,4 @@
import type { IconStyle } from '../../../types/icon-set/extra.js';
import type { IconStyle } from '../../../types/icon-set/extra';
function getValues(body: string, prop: string): string[] {
const chunks = body.split(prop + '="');

View File

@ -1,5 +1,5 @@
import type { IconifyJSON } from '@iconify/types';
import type { IconSetIconsListIcons } from '../../../types/icon-set/extra.js';
import type { IconSetIconsListIcons } from '../../../types/icon-set/extra';
/**
* Removes bad items

View File

@ -1,7 +1,7 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
import { defaultIconDimensions } from '@iconify/utils/lib/icon/defaults';
import type { SplitIconSetConfig } from '../../../types/config/split.js';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split.js';
import type { SplitIconSetConfig } from '../../../types/config/split';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split';
const iconDimensionProps = Object.keys(defaultIconDimensions) as (keyof typeof defaultIconDimensions)[];
@ -21,14 +21,7 @@ export function splitIconSetMainData(iconSet: IconifyJSON): SplitIconifyJSONMain
for (let i = 0; i < iconSetMainDataProps.length; i++) {
const prop = iconSetMainDataProps[i];
if (iconSet[prop]) {
const value = iconSet[prop as 'prefix'];
if (typeof value === 'object') {
// Make sure object has null as constructor
result[prop as 'prefix'] = Object.create(null);
Object.assign(result[prop as 'prefix'], iconSet[prop as 'prefix']);
} else {
result[prop as 'prefix'] = iconSet[prop as 'prefix'];
}
result[prop as 'prefix'] = iconSet[prop as 'prefix'];
} else if (prop === 'aliases') {
result[prop] = Object.create(null);
}

View File

@ -1,16 +1,17 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
import { appConfig, splitIconSetConfig, storageConfig } from '../../../config/app.js';
import type { SplitIconSetConfig } from '../../../types/config/split.js';
import type { StorageIconSetThemes, StoredIconSet, StoredIconSetDone } from '../../../types/icon-set/storage.js';
import type { SplitRecord } from '../../../types/split.js';
import type { MemoryStorage, MemoryStorageItem } from '../../../types/storage.js';
import { createSplitRecordsTree, splitRecords } from '../../storage/split.js';
import { createStorage, createStoredItem } from '../../storage/create.js';
import { getIconSetSplitChunksCount, splitIconSetMainData } from './split.js';
import { removeBadIconSetItems } from '../lists/validate.js';
import { prepareAPIv2IconsList } from '../lists/icons-v2.js';
import { generateIconSetIconsTree } from '../lists/icons.js';
import { themeKeys, findIconSetThemes } from './themes.js';
import { appConfig, splitIconSetConfig, storageConfig } from '../../../config/app';
import type { SplitIconSetConfig } from '../../../types/config/split';
import type { StorageIconSetThemes, 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';
import { removeBadIconSetItems } from '../lists/validate';
import { prepareAPIv2IconsList } from '../lists/icons-v2';
import { generateIconSetIconsTree } from '../lists/icons';
import { themeKeys } from './themes';
import { findIconSetThemes } from './themes';
/**
* Storage

View File

@ -1,5 +1,5 @@
import { IconifyJSON } from '@iconify/types';
import { StorageIconSetThemes } from '../../../types/icon-set/storage.js';
import { StorageIconSetThemes } from '../../../types/icon-set/storage';
/**
* Themes to copy

View File

@ -1,9 +1,9 @@
import type { ExtendedIconifyAlias, ExtendedIconifyIcon, IconifyIcons } from '@iconify/types';
import { mergeIconData } from '@iconify/utils/lib/icon/merge';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split.js';
import type { StoredIconSet } from '../../../types/icon-set/storage.js';
import { searchSplitRecordsTree } from '../../storage/split.js';
import { getStoredItem } from '../../storage/get.js';
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

View File

@ -1,7 +1,7 @@
import type { IconifyJSON, IconifyAliases, IconifyIcons } from '@iconify/types';
import type { StoredIconSet } from '../../../types/icon-set/storage.js';
import { searchSplitRecordsTreeForSet } from '../../storage/split.js';
import { getStoredItem } from '../../storage/get.js';
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

View File

@ -1,6 +1,6 @@
import type { StoredIconSet } from '../types/icon-set/storage.js';
import type { IconSetEntry, Importer } from '../types/importers.js';
import { updateSearchIndex } from './search.js';
import type { StoredIconSet } from '../types/icon-set/storage';
import type { IconSetEntry, Importer } from '../types/importers';
import { updateSearchIndex } from './search';
/**
* All importers
@ -125,9 +125,8 @@ export function updateIconSets(): number {
/**
* Trigger update
*/
export function triggerIconSetsUpdate(index?: number | null, done?: (success?: boolean) => void) {
export function triggerIconSetsUpdate() {
if (!importers) {
done?.();
return;
}
console.log('Checking for updates...');
@ -141,9 +140,6 @@ export function triggerIconSetsUpdate(index?: number | null, done?: (success?: b
// Check for updates
let updated = false;
for (let i = 0; i < importers?.length; i++) {
if (typeof index === 'number' && i !== index) {
continue;
}
updated = (await importers[i].checkForUpdate()) || updated;
}
return updated;
@ -151,10 +147,6 @@ export function triggerIconSetsUpdate(index?: number | null, done?: (success?: b
.then((updated) => {
console.log(updated ? 'Update complete' : 'Nothing to update');
updateIconSets();
done?.(true);
})
.catch((err) => {
console.error(err);
done?.(false);
});
.catch(console.error);
}

View File

@ -22,13 +22,6 @@ export function loaded() {
}
}
/**
* Get state
*/
export function isLoading() {
return loading;
}
/**
* Run when app is ready
*/

View File

@ -1,6 +1,6 @@
import { appConfig } from '../config/app.js';
import type { IconSetEntry } from '../types/importers.js';
import type { SearchIndexData } from '../types/search.js';
import { appConfig } from '../config/app';
import type { IconSetEntry } from '../types/importers';
import type { SearchIndexData } from '../types/search';
interface SearchIndex {
data?: SearchIndexData;

View File

@ -1,10 +1,10 @@
import { appConfig } from '../../config/app.js';
import type { IconSetIconNames } from '../../types/icon-set/extra.js';
import type { IconSetEntry } from '../../types/importers.js';
import type { SearchIndexData, SearchKeywordsEntry, SearchParams, SearchResultsData } from '../../types/search.js';
import { getPartialKeywords } from './partial.js';
import { filterSearchPrefixes, filterSearchPrefixesList } from './prefixes.js';
import { splitKeyword } from './split.js';
import { appConfig } from '../../config/app';
import type { IconSetIconNames } from '../../types/icon-set/extra';
import type { IconSetEntry } from '../../types/importers';
import type { SearchIndexData, SearchKeywordsEntry, SearchParams, SearchResultsData } from '../../types/search';
import { getPartialKeywords } from './partial';
import { filterSearchPrefixes, filterSearchPrefixesList } from './prefixes';
import { splitKeyword } from './split';
/**
* Run search
@ -41,6 +41,28 @@ export function search(
return;
}
// Check for partial
const partial = keywords.partial;
let partialKeywords: string[] | undefined;
let isFirstKeywordExact = true;
if (partial) {
// Get all partial keyword matches
const cache = getPartialKeywords(partial, true, data);
const exists = data.keywords[partial];
if (!cache || !cache.length) {
// No partial matches: check if keyword exists
if (!exists) {
return;
}
partialKeywords = [partial];
} else {
// Partial keywords exist
isFirstKeywordExact = !!exists;
partialKeywords = exists ? [partial].concat(cache) : cache.slice(0);
}
}
// Get prefixes
const basePrefixes = filterSearchPrefixes(data, iconSets, fullParams);
@ -69,184 +91,161 @@ export function search(
return newItem;
};
const limit = params.limit;
const softLimit = params.softLimit;
interface ExtendedSearchKeywordsEntry extends SearchKeywordsEntry {
// Add prefixes cache to avoid re-calculating it for every partial keyword
filteredPrefixes?: Readonly<string[]>;
}
const runSearch = (search: ExtendedSearchKeywordsEntry, isExact: boolean, partial?: string) => {
// Filter prefixes (or get it from cache)
let filteredPrefixes: Readonly<string[]>;
if (search.filteredPrefixes) {
filteredPrefixes = search.filteredPrefixes;
} else {
filteredPrefixes = search.prefixes ? filterSearchPrefixesList(basePrefixes, search.prefixes) : basePrefixes;
// Filter by required keywords
for (let i = 0; i < search.keywords.length; i++) {
filteredPrefixes = filteredPrefixes.filter((prefix) => data.keywords[search.keywords[i]]?.has(prefix));
// Run all searches
const check = (isExact: boolean, partial?: string) => {
for (let searchIndex = 0; searchIndex < keywords.searches.length; searchIndex++) {
// Add prefixes cache to avoid re-calculating it for every partial keyword
interface ExtendedSearchKeywordsEntry extends SearchKeywordsEntry {
filteredPrefixes?: Readonly<string[]>;
}
const search = keywords.searches[searchIndex] as ExtendedSearchKeywordsEntry;
search.filteredPrefixes = filteredPrefixes;
}
if (!filteredPrefixes.length) {
return;
}
// Filter prefixes (or get it from cache)
let filteredPrefixes: Readonly<string[]>;
if (search.filteredPrefixes) {
filteredPrefixes = search.filteredPrefixes;
} else {
filteredPrefixes = search.prefixes
? filterSearchPrefixesList(basePrefixes, search.prefixes)
: basePrefixes;
// Get keywords
const testKeywords = partial ? search.keywords.concat([partial]) : search.keywords;
const testMatches = search.test ? search.test.concat(testKeywords) : testKeywords;
// Filter by required keywords
for (let i = 0; i < search.keywords.length; i++) {
filteredPrefixes = filteredPrefixes.filter((prefix) =>
data.keywords[search.keywords[i]].has(prefix)
);
}
// Check for partial keyword if testing for exact match
if (partial) {
filteredPrefixes = filteredPrefixes.filter((prefix) => data.keywords[partial]?.has(prefix));
}
// Check icons
for (let prefixIndex = 0; prefixIndex < filteredPrefixes.length; prefixIndex++) {
const prefix = filteredPrefixes[prefixIndex];
const prefixAddedIcons = addedIcons[prefix] || (addedIcons[prefix] = new Set());
const iconSet = iconSets[prefix].item;
const iconSetIcons = iconSet.icons;
const iconSetKeywords = iconSetIcons.keywords;
if (!iconSetKeywords) {
// This should not happen!
search.filteredPrefixes = filteredPrefixes;
}
if (!filteredPrefixes.length) {
continue;
}
// Check icons in current prefix
let matches: IconSetIconNames[] | undefined;
let failed = false;
for (let keywordIndex = 0; keywordIndex < testKeywords.length && !failed; keywordIndex++) {
const keyword = testKeywords[keywordIndex];
const keywordMatches = iconSetKeywords[keyword];
if (!keywordMatches) {
failed = true;
break;
}
// Get keywords
const testKeywords = partial ? search.keywords.concat([partial]) : search.keywords;
const testMatches = search.test ? search.test.concat(testKeywords) : testKeywords;
if (!matches) {
// Copy all matches
matches = Array.from(keywordMatches);
} else {
// Match previous set
matches = matches.filter((item) => keywordMatches.has(item));
}
}
// Test matched icons
if (!failed && matches) {
for (let matchIndex = 0; matchIndex < matches.length; matchIndex++) {
const item = matches[matchIndex];
if (prefixAddedIcons.has(item)) {
// Already added
continue;
}
// Check style
if (
// Style is set
fullParams.style &&
// Enabled in config
appConfig.allowFilterIconsByStyle &&
// Icon set has mixed style (so it is assigned to icons) -> check icon
iconSetIcons.iconStyle === 'mixed' &&
item._is !== fullParams.style
) {
// Different icon style
continue;
}
// Find icon name that matches all keywords
let length: number | undefined;
const name = item.find((name, index) => {
for (let i = 0; i < testMatches.length; i++) {
if (name.indexOf(testMatches[i]) === -1) {
return false;
}
// Get length
if (!index) {
// First item sets `_l`, unless it didn't match any prefixes/suffixes
length = item._l || name.length;
} else if (iconSet.themeParts) {
// Alias: calculate length
const themeParts = iconSet.themeParts;
for (let partIndex = 0; partIndex < themeParts.length; partIndex++) {
const part = themeParts[partIndex];
if (name.startsWith(part + '-') || name.endsWith('-' + part)) {
length = name.length - part.length - 1;
break;
}
}
}
}
return true;
});
if (name) {
// Add icon
prefixAddedIcons.add(item);
const list = getMatchResult(length || name.length, !isExact);
list.names.push(prefix + ':' + name);
allMatchesLength++;
if (!isExact && allMatchesLength >= limit) {
// Return only if checking for partials and limit reached
return;
}
}
}
}
}
};
const runAllSearches = (isExact: boolean) => {
for (let searchIndex = 0; searchIndex < keywords.searches.length; searchIndex++) {
const search = keywords.searches[searchIndex];
const partial = search.partial;
// Check for partial keyword if testing for exact match
if (partial) {
// Has partial
if (isExact) {
if (data.keywords[partial]) {
runSearch(search, true, partial);
}
} else {
// Get all partial matches
const keywords = getPartialKeywords(partial, true, data);
if (keywords) {
for (let keywordIndex = 0; keywordIndex < keywords.length; keywordIndex++) {
runSearch(search, false, keywords[keywordIndex]);
}
}
}
} else {
// No partial for this search
if (!isExact) {
filteredPrefixes = filteredPrefixes.filter((prefix) => data.keywords[partial].has(prefix));
}
// Check icons
for (let prefixIndex = 0; prefixIndex < filteredPrefixes.length; prefixIndex++) {
const prefix = filteredPrefixes[prefixIndex];
const prefixAddedIcons = addedIcons[prefix] || (addedIcons[prefix] = new Set());
const iconSet = iconSets[prefix].item;
const iconSetIcons = iconSet.icons;
const iconSetKeywords = iconSetIcons.keywords;
if (!iconSetKeywords) {
// This should not happen!
continue;
}
// Exact match
runSearch(search, true);
}
// Check icons in current prefix
let matches: IconSetIconNames[] | undefined;
let failed = false;
for (let keywordIndex = 0; keywordIndex < testKeywords.length && !failed; keywordIndex++) {
const keyword = testKeywords[keywordIndex];
const keywordMatches = iconSetKeywords[keyword];
if (!keywordMatches) {
failed = true;
break;
}
// Check limit
if (!isExact && allMatchesLength >= limit) {
return;
if (!matches) {
// Copy all matches
matches = Array.from(keywordMatches);
} else {
// Match previous set
matches = matches.filter((item) => keywordMatches.has(item));
}
}
// Test matched icons
if (!failed && matches) {
for (let matchIndex = 0; matchIndex < matches.length; matchIndex++) {
const item = matches[matchIndex];
if (prefixAddedIcons.has(item)) {
// Already added
continue;
}
// Check style
if (
// Style is set
fullParams.style &&
// Enabled in config
appConfig.allowFilterIconsByStyle &&
// Icon set has mixed style (so it is assigned to icons) -> check icon
iconSetIcons.iconStyle === 'mixed' &&
item._is !== fullParams.style
) {
// Different icon style
continue;
}
// Find icon name that matches all keywords
let length: number | undefined;
const name = item.find((name, index) => {
for (let i = 0; i < testMatches.length; i++) {
if (name.indexOf(testMatches[i]) === -1) {
return false;
}
// Get length
if (!index) {
// First item sets `_l`, unless it didn't match any prefixes/suffixes
length = item._l || name.length;
} else if (iconSet.themeParts) {
// Alias: calculate length
const themeParts = iconSet.themeParts;
for (let partIndex = 0; partIndex < themeParts.length; partIndex++) {
const part = themeParts[partIndex];
if (name.startsWith(part + '-') || name.endsWith('-' + part)) {
length = name.length - part.length - 1;
break;
}
}
}
}
return true;
});
if (name) {
// Add icon
prefixAddedIcons.add(item);
const list = getMatchResult(length || name.length, !isExact);
list.names.push(prefix + ':' + name);
allMatchesLength++;
if (!isExact && allMatchesLength >= limit) {
// Return only if checking for partials and limit reached
return;
}
}
}
}
}
}
};
// Check all keywords
try {
runAllSearches(true);
if (allMatchesLength < limit) {
runAllSearches(false);
if (!partialKeywords) {
check(true);
} else {
let partial: string | undefined;
while ((partial = partialKeywords.shift())) {
check(isFirstKeywordExact, partial);
if (allMatchesLength >= limit) {
break;
}
// Next check will be for partial keyword
isFirstKeywordExact = false;
}
} catch (err) {
console.warn('Got exception when searching for:', params);
console.error(err);
}
// Generate results
@ -257,9 +256,9 @@ export function search(
// Extract results
const results: string[] = [];
const prefixes: Set<string> = new Set();
for (let i = 0; i < allMatches.length && (softLimit || results.length < limit); i++) {
for (let i = 0; i < allMatches.length && results.length < limit; i++) {
const { names } = allMatches[i];
for (let j = 0; j < names.length && (softLimit || results.length < limit); j++) {
for (let j = 0; j < names.length && results.length < limit; j++) {
const name = names[j];
results.push(name);
prefixes.add(name.split(':').shift() as string);

View File

@ -1,5 +1,5 @@
import type { PartialKeywords, SearchIndexData } from '../../types/search.js';
import { searchIndex } from '../search.js';
import type { PartialKeywords, SearchIndexData } from '../../types/search';
import { searchIndex } from '../search';
export const minPartialKeywordLength = 2;

View File

@ -1,6 +1,6 @@
import { appConfig } from '../../config/app.js';
import type { IconSetEntry } from '../../types/importers.js';
import type { SearchIndexData, SearchParams } from '../../types/search.js';
import { appConfig } from '../../config/app';
import type { IconSetEntry } from '../../types/importers';
import type { SearchIndexData, SearchParams } from '../../types/search';
/**
* Filter prefixes by keyword

View File

@ -1,8 +1,8 @@
import { matchIconName } from '@iconify/utils/lib/icon/name';
import { paramToBoolean } from '../../misc/bool.js';
import type { IconStyle } from '../../types/icon-set/extra.js';
import type { SearchKeywords, SearchKeywordsEntry } from '../../types/search.js';
import { minPartialKeywordLength } from './partial.js';
import { paramToBoolean } from '../../misc/bool';
import type { IconStyle } from '../../types/icon-set/extra';
import type { SearchKeywords, SearchKeywordsEntry } from '../../types/search';
import { minPartialKeywordLength } from './partial';
interface SplitOptions {
// Can include prefix
@ -21,15 +21,20 @@ interface SplitResultItem {
// Strings to test icon name
test?: string[];
}
// Partial keyword. It is last chunk of last keyword, which cannot be treated as prefix
interface SplitResult {
searches: SplitResultItem[];
// Partial keyword. It is last chunk of last keyword, which cannot be treated
// as prefix, so it is identical to all searches
partial?: string;
}
type SplitResult = SplitResultItem[];
export function splitKeywordEntries(values: string[], options: SplitOptions): SplitResult | undefined {
const results: SplitResult = [];
const results: SplitResult = {
searches: [],
};
let invalid = false;
// Split each entry into arrays
@ -78,14 +83,8 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
return (items[0].empty ? '-' : '') + items.map((item) => item.value).join('-');
}
interface ResultsSet {
keywords: Set<string>;
test: Set<string>;
partial?: string;
}
// Function to add item
function addToSet(items: Entry[], set: ResultsSet, allowPartial: boolean) {
function add(items: Entry[], keywords: Set<string>, test: Set<string>, checkPartial: boolean) {
let partial: string | undefined;
// Add keywords
@ -93,10 +92,10 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
for (let i = 0; i <= max; i++) {
const value = items[i];
if (!value.empty) {
if (i === max && allowPartial && value.value.length >= minPartialKeywordLength) {
if (i === max && checkPartial && value.value.length >= minPartialKeywordLength) {
partial = value.value;
} else {
set.keywords.add(value.value);
keywords.add(value.value);
}
}
}
@ -104,30 +103,20 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
// Get test value
const testValue = valuesToString(items);
if (testValue) {
set.test.add(testValue);
test.add(testValue);
}
// Add partial
if (allowPartial && partial) {
if (set.partial && set.partial !== partial) {
console.error('Different partial keywords. This should not be happening!');
// Validate partial
if (checkPartial) {
if (results.searches.length) {
if (results.partial !== partial) {
// Partial should be identical for all searches. Something went wrong !!!
console.error('Mismatches partials when splitting keywords:', values);
delete results.partial;
}
} else {
results.partial = partial;
}
set.partial = partial;
}
}
// Add results set to result
function addToResult(set: ResultsSet, prefix?: string) {
if (set.keywords.size || set.partial) {
const item: SplitResultItem = {
keywords: Array.from(set.keywords),
prefix,
partial: set.partial,
};
if (set.test.size) {
item.test = Array.from(set.test);
}
results.push(item);
}
}
@ -145,14 +134,22 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
const prefix = firstItem.length > 1 ? valuesToString(firstItem) : firstItem[0].value;
if (prefix) {
// Valid prefix
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
const keywords: Set<string> = new Set();
const test: Set<string> = new Set();
for (let i = 1; i <= lastIndex; i++) {
addToSet(splitValues[i], set, options.partial && i === lastIndex);
add(splitValues[i], keywords, test, options.partial && i === lastIndex);
}
if (keywords.size || results.partial) {
const item: SplitResultItem = {
keywords: Array.from(keywords),
prefix,
};
if (test.size) {
item.test = Array.from(test);
}
results.searches.push(item);
}
addToResult(set, prefix);
}
}
}
@ -162,26 +159,41 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
if (maxFirstItemIndex && !firstItem[0].empty && !firstItem[1].empty) {
const modifiedFirstItem = firstItem.slice(0);
const prefix = modifiedFirstItem.shift()!.value;
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
const keywords: Set<string> = new Set();
const test: Set<string> = new Set();
for (let i = 0; i <= lastIndex; i++) {
addToSet(i ? splitValues[i] : modifiedFirstItem, set, options.partial && i === lastIndex);
add(i ? splitValues[i] : modifiedFirstItem, keywords, test, options.partial && i === lastIndex);
}
if (keywords.size || results.partial) {
const item: SplitResultItem = {
keywords: Array.from(keywords),
prefix,
};
if (test.size) {
item.test = Array.from(test);
}
results.searches.push(item);
}
addToResult(set, prefix);
}
}
// Add as is
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
const keywords: Set<string> = new Set();
const test: Set<string> = new Set();
for (let i = 0; i <= lastIndex; i++) {
addToSet(splitValues[i], set, options.partial && i === lastIndex);
add(splitValues[i], keywords, test, options.partial && i === lastIndex);
}
if (keywords.size || results.partial) {
// Add item
const item: SplitResultItem = {
keywords: Array.from(keywords),
};
if (test.size) {
item.test = Array.from(test);
}
results.searches.push(item);
}
addToResult(set);
// Merge values
if (splitValues.length > 1) {
@ -221,14 +233,22 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
...splitValues.slice(endIndex + 1),
];
const newLastIndex = newSplitValues.length - 1;
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
const keywords: Set<string> = new Set();
const test: Set<string> = new Set();
for (let i = 0; i <= newLastIndex; i++) {
addToSet(newSplitValues[i], set, options.partial && i === newLastIndex);
add(newSplitValues[i], keywords, test, false);
}
if (keywords.size || results.partial) {
// Add item
const item: SplitResultItem = {
keywords: Array.from(keywords),
};
if (test.size) {
item.test = Array.from(test);
}
results.searches.push(item);
}
addToResult(set);
}
}
}
@ -464,7 +484,7 @@ export function splitKeyword(keyword: string, allowPartial = true): SearchKeywor
return;
}
const searches: SearchKeywordsEntry[] = entries.map((item) => {
const searches: SearchKeywordsEntry[] = entries.searches.map((item) => {
return {
...item,
prefixes: item.prefix
@ -485,5 +505,6 @@ export function splitKeyword(keyword: string, allowPartial = true): SearchKeywor
return {
searches,
params,
partial: entries.partial,
};
}

View File

@ -1,4 +1,4 @@
import type { MemoryStorageItem, MemoryStorageCallback } from '../../types/storage.js';
import type { MemoryStorageItem, MemoryStorageCallback } from '../../types/storage';
/**
* Run all callbacks from storage

View File

@ -1,5 +1,5 @@
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage.js';
import { runStorageCallbacks } from './callbacks.js';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage';
import { runStorageCallbacks } from './callbacks';
/**
* Stop timer

View File

@ -1,7 +1,7 @@
import { appConfig } from '../../config/app.js';
import type { MemoryStorage, MemoryStorageConfig, MemoryStorageItem } from '../../types/storage.js';
import { cleanupStoredItem } from './cleanup.js';
import { writeStoredItem } from './write.js';
import { appConfig } from '../../config/app';
import type { MemoryStorage, MemoryStorageConfig, MemoryStorageItem } from '../../types/storage';
import { cleanupStoredItem } from './cleanup';
import { writeStoredItem } from './write';
/**
* Create storage

View File

@ -1,5 +1,5 @@
import type { MemoryStorageItem, MemoryStorageCallback, MemoryStorage } from '../../types/storage.js';
import { loadStoredItem } from './load.js';
import type { MemoryStorageItem, MemoryStorageCallback, MemoryStorage } from '../../types/storage';
import { loadStoredItem } from './load';
/**
* Get storage data when ready

View File

@ -1,7 +1,7 @@
import { readFile, readFileSync } from 'node:fs';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage.js';
import { runStorageCallbacks } from './callbacks.js';
import { addStorageToCleanup } from './cleanup.js';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage';
import { runStorageCallbacks } from './callbacks';
import { addStorageToCleanup } from './cleanup';
/**
* Load data

View File

@ -1,4 +1,4 @@
import type { SplitDataTree, SplitRecord, SplitRecordCallback } from '../../types/split.js';
import type { SplitDataTree, SplitRecord, SplitRecordCallback } from '../../types/split';
/**
* Split records into `count` chunks

View File

@ -1,6 +1,6 @@
import { rm } from 'node:fs/promises';
import { appConfig } from '../../config/app.js';
import type { MemoryStorage } from '../../types/storage.js';
import { appConfig } from '../../config/app';
import type { MemoryStorage } from '../../types/storage';
/**
* Remove old cache

View File

@ -1,7 +1,7 @@
import { writeFile, mkdir } from 'node:fs';
import { dirname } from 'node:path';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage.js';
import { addStorageToCleanup } from './cleanup.js';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage';
import { addStorageToCleanup } from './cleanup';
const createdDirs: Set<string> = new Set();

View File

@ -1,4 +1,4 @@
import type { DownloaderStatus, DownloaderType } from '../types/downloaders/base.js';
import type { DownloaderStatus, DownloaderType } from '../types/downloaders/base';
/**
* loadDataFromDirectory()

View File

@ -1,4 +1,4 @@
import { BaseDownloader } from './base.js';
import { BaseDownloader } from './base';
/**
* Custom downloader

View File

@ -1,5 +1,5 @@
import { directoryExists, hashFiles, listFilesInDirectory } from '../misc/files.js';
import { BaseDownloader } from './base.js';
import { directoryExists, hashFiles, listFilesInDirectory } from '../misc/files';
import { BaseDownloader } from './base';
/**
* Directory downloader

View File

@ -1,9 +1,9 @@
import { directoryExists } from '../misc/files.js';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../types/downloaders/remote.js';
import { BaseDownloader } from './base.js';
import { downloadRemoteArchive } from './remote/download.js';
import { getRemoteDownloaderCacheKey } from './remote/key.js';
import { getDownloaderVersion, saveDownloaderVersion } from './remote/versions.js';
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

View File

@ -2,7 +2,7 @@ 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.js';
import { directoryExists } from '../../misc/files';
import type {
GitDownloaderOptions,
GitDownloaderVersion,
@ -12,7 +12,7 @@ import type {
GitLabDownloaderVersion,
NPMDownloaderOptions,
NPMDownloaderVersion,
} from '../../types/downloaders/remote.js';
} from '../../types/downloaders/remote';
/**
* Check git repo for update

View File

@ -2,15 +2,15 @@ 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.js';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../../types/downloaders/remote.js';
import { appConfig } from '../../config/app';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../../types/downloaders/remote';
import {
isGitHubUpdateAvailable,
isGitLabUpdateAvailable,
isGitUpdateAvailable,
isNPMUpdateAvailable,
} from './check-update.js';
import { getDownloadDirectory } from './target.js';
} from './check-update';
import { getDownloadDirectory } from './target';
/**
* Download files from remote archive

View File

@ -1,5 +1,5 @@
import { hashString } from '../../misc/hash.js';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote.js';
import { hashString } from '../../misc/hash';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote';
/**
* Get cache key

View File

@ -1,6 +1,6 @@
import { appConfig } from '../../config/app.js';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote.js';
import { getRemoteDownloaderCacheKey } from './key.js';
import { appConfig } from '../../config/app';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote';
import { getRemoteDownloaderCacheKey } from './key';
/**
* Get directory

View File

@ -1,11 +1,11 @@
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import { appConfig } from '../../config/app.js';
import { appConfig } from '../../config/app';
import type {
RemoteDownloaderType,
RemoteDownloaderVersion,
RemoteDownloaderVersionMixin,
} from '../../types/downloaders/remote.js';
} from '../../types/downloaders/remote';
// Storage
type StoredVersions = Record<string, RemoteDownloaderVersion>;

View File

@ -1,10 +0,0 @@
export function errorText(code: number) {
switch (code) {
case 404:
return 'Not found';
case 400:
return 'Bad request';
}
return 'Internal server error';
}

View File

@ -1,6 +0,0 @@
/**
* Basic cleanup for parameters
*/
export function cleanupQueryValue(value: string | undefined) {
return value ? value.replace(/['"<>&]/g, '') : undefined;
}

View File

@ -1,45 +0,0 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkJSONPQuery, sendJSONResponse } from './json.js';
import { createIconsDataResponse } from '../responses/icons.js';
type CallbackResult = object | number;
/**
* Handle icons data API response
*/
export function handleIconsDataResponse(
prefix: string,
wrapJS: boolean,
query: FastifyRequest['query'],
res: FastifyReply
) {
const q = (query || {}) as Record<string, string>;
// Check for JSONP
const wrap = checkJSONPQuery(q, wrapJS, 'SimpleSVG._loaderCallback');
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Function to send response
const respond = (result: CallbackResult) => {
if (typeof result === 'number') {
res.send(result);
} else {
sendJSONResponse(result, q, wrap, res);
}
};
// Get result
const result = createIconsDataResponse(prefix, q);
if (result instanceof Promise) {
result.then(respond).catch((err) => {
console.error(err);
respond(500);
});
} else {
respond(result);
}
}

View File

@ -1,44 +0,0 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkJSONPQuery, sendJSONResponse } from './json.js';
import { errorText } from './errors.js';
type CallbackResult = object | number;
/**
* Handle JSON API response generated by a callback
*/
export function handleJSONResponse(
req: FastifyRequest,
res: FastifyReply,
callback: (query: Record<string, string>) => CallbackResult | Promise<CallbackResult>
) {
const q = (req.query || {}) as Record<string, string>;
// Check for JSONP
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.code(400).send(errorText(400));
return;
}
// Function to send response
const respond = (result: CallbackResult) => {
if (typeof result === 'number') {
res.code(result).send(errorText(result));
} else {
sendJSONResponse(result, q, wrap, res);
}
};
// Get result
const result = callback(q);
if (result instanceof Promise) {
result.then(respond).catch((err) => {
console.error(err);
respond(500);
});
} else {
respond(result);
}
}

View File

@ -1,21 +1,19 @@
import fastify from 'fastify';
import fastifyFormBody from '@fastify/formbody';
import { appConfig, httpHeaders } from '../config/app.js';
import { runWhenLoaded } from '../data/loading.js';
import { iconNameRoutePartialRegEx, iconNameRouteRegEx, splitIconName } from '../misc/name.js';
import { createAPIv1IconsListResponse } from './responses/collection-v1.js';
import { createAPIv2CollectionResponse } from './responses/collection-v2.js';
import { createCollectionsListResponse } from './responses/collections.js';
import { handleIconsDataResponse } from './helpers/send-icons.js';
import { createKeywordsResponse } from './responses/keywords.js';
import { createLastModifiedResponse } from './responses/modified.js';
import { createAPIv2SearchResponse } from './responses/search.js';
import { generateSVGResponse } from './responses/svg.js';
import { generateUpdateResponse } from './responses/update.js';
import { initVersionResponse, versionResponse } from './responses/version.js';
import { generateIconsStyleResponse } from './responses/css.js';
import { handleJSONResponse } from './helpers/send.js';
import { errorText } from './helpers/errors.js';
import { appConfig, httpHeaders } from '../config/app';
import { runWhenLoaded } from '../data/loading';
import { iconNameRoutePartialRegEx, iconNameRouteRegEx, splitIconName } from '../misc/name';
import { generateAPIv1IconsListResponse } from './responses/collection-v1';
import { generateAPIv2CollectionResponse } from './responses/collection-v2';
import { generateCollectionsListResponse } from './responses/collections';
import { generateIconsDataResponse } from './responses/icons';
import { generateKeywordsResponse } from './responses/keywords';
import { generateLastModifiedResponse } from './responses/modified';
import { generateAPIv2SearchResponse } from './responses/search';
import { generateSVGResponse } from './responses/svg';
import { generateUpdateResponse } from './responses/update';
import { initVersionResponse, versionResponse } from './responses/version';
import { generateIconsStyleResponse } from './responses/css';
/**
* Start HTTP server
@ -23,9 +21,7 @@ import { errorText } from './helpers/errors.js';
export async function startHTTPServer() {
// Create HTP server
const server = fastify({
routerOptions: {
caseSensitive: true,
},
caseSensitive: true,
});
// Support `application/x-www-form-urlencoded`
@ -85,19 +81,19 @@ export async function startHTTPServer() {
generateSVGResponse(name.prefix, name.name, req.query, res);
});
} else {
res.code(404).send(errorText(404));
res.send(404);
}
});
// Icons data: /prefix/icons.json, /prefix.json
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.json', (req, res) => {
runWhenLoaded(() => {
handleIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
});
});
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').json', (req, res) => {
runWhenLoaded(() => {
handleIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
});
});
@ -111,19 +107,19 @@ export async function startHTTPServer() {
// Icons data: /prefix/icons.js, /prefix.js
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.js', (req, res) => {
runWhenLoaded(() => {
handleIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
});
});
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').js', (req, res) => {
runWhenLoaded(() => {
handleIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
});
});
// Last modification time
server.get('/last-modified', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, createLastModifiedResponse);
generateLastModifiedResponse(req.query, res);
});
});
@ -131,26 +127,26 @@ export async function startHTTPServer() {
// Icon sets list
server.get('/collections', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, createCollectionsListResponse);
generateCollectionsListResponse(req.query, res);
});
});
// Icons list, API v2
server.get('/collection', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, createAPIv2CollectionResponse);
generateAPIv2CollectionResponse(req.query, res);
});
});
// Icons list, API v1
server.get('/list-icons', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, (q) => createAPIv1IconsListResponse(q, false));
generateAPIv1IconsListResponse(req.query, res, false);
});
});
server.get('/list-icons-categorized', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, (q) => createAPIv1IconsListResponse(q, true));
generateAPIv1IconsListResponse(req.query, res, true);
});
});
@ -158,14 +154,14 @@ export async function startHTTPServer() {
// Search, currently version 2
server.get('/search', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, createAPIv2SearchResponse);
generateAPIv2SearchResponse(req.query, res);
});
});
// Keywords
server.get('/keywords', (req, res) => {
runWhenLoaded(() => {
handleJSONResponse(req, res, createKeywordsResponse);
generateKeywordsResponse(req.query, res);
});
});
}
@ -181,7 +177,7 @@ export async function startHTTPServer() {
// Options
server.options('/*', (req, res) => {
res.code(204).header('Content-Length', '0').send();
res.send(200);
});
// Robots
@ -201,21 +197,21 @@ export async function startHTTPServer() {
// Redirect
server.get('/', (req, res) => {
res.redirect(appConfig.redirectIndex, 301);
res.redirect(301, appConfig.redirectIndex);
});
// Error handling
server.setNotFoundHandler((req, res) => {
server.setDefaultRoute((req, res) => {
res.statusCode = 404;
console.log('404:', req.url);
// Need to set custom headers because hooks don't work here
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
res.header(header.key, header.value);
res.setHeader(header.key, header.value);
}
res.send();
res.end();
});
// Start it

View File

@ -1,18 +1,17 @@
import { getPrefixes, iconSets } from '../../data/icon-sets.js';
import type { IconSetAPIv2IconsList } from '../../types/icon-set/extra.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getPrefixes, iconSets } from '../../data/icon-sets';
import type { IconSetAPIv2IconsList } from '../../types/icon-set/extra';
import type { StoredIconSet } from '../../types/icon-set/storage';
import type {
APIv1ListIconsBaseResponse,
APIv1ListIconsCategorisedResponse,
APIv1ListIconsResponse,
} from '../../types/server/v1.js';
import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
// Response results, depends on `categorised` option
type PossibleResults = APIv1ListIconsResponse | APIv1ListIconsCategorisedResponse;
} from '../../types/server/v1';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
import { filterPrefixesByPrefix } from '../helpers/prefixes';
/**
* Create API v1 response
* Send API v2 response
*
* This response ignores the following parameters:
* - `aliases` -> always enabled
@ -20,15 +19,27 @@ type PossibleResults = APIv1ListIconsResponse | APIv1ListIconsCategorisedRespons
*
* Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled
*/
export function createAPIv1IconsListResponse(
query: Record<string, string>,
export function generateAPIv1IconsListResponse(
query: FastifyRequest['query'],
res: FastifyReply,
categorised: boolean
): PossibleResults | Record<string, PossibleResults> | number {
) {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
function parse(
prefix: string,
iconSet: StoredIconSet,
v2Cache: IconSetAPIv2IconsList
): APIv1ListIconsResponse | APIv1ListIconsCategorisedResponse {
const icons = iconSet.icons;
// Generate common data
const base: APIv1ListIconsBaseResponse = {
prefix,
@ -37,13 +48,13 @@ export function createAPIv1IconsListResponse(
if (v2Cache.title) {
base.title = v2Cache.title;
}
if (query.info && v2Cache.info) {
if (q.info && v2Cache.info) {
base.info = v2Cache.info;
}
if (query.aliases && v2Cache.aliases) {
if (q.aliases && v2Cache.aliases) {
base.aliases = v2Cache.aliases;
}
if (query.chars && v2Cache.chars) {
if (q.chars && v2Cache.chars) {
base.chars = v2Cache.chars;
}
@ -70,20 +81,22 @@ export function createAPIv1IconsListResponse(
return result;
}
if (query.prefix) {
const prefix = query.prefix;
if (q.prefix) {
const prefix = q.prefix;
const iconSet = iconSets[prefix]?.item;
if (!iconSet || !iconSet.apiV2IconsCache) {
return 404;
res.send(404);
return;
}
return parse(prefix, iconSet, iconSet.apiV2IconsCache);
sendJSONResponse(parse(prefix, iconSet, iconSet.apiV2IconsCache), q, wrap, res);
return;
}
if (query.prefixes) {
if (q.prefixes) {
const prefixes = filterPrefixesByPrefix(
getPrefixes(),
{
prefixes: query.prefixes,
prefixes: q.prefixes,
},
false
);
@ -112,18 +125,20 @@ export function createAPIv1IconsListResponse(
if (!items.length) {
// Empty list
return 404;
res.send(404);
return;
}
// Get all items
const result = Object.create(null) as Record<string, PossibleResults>;
const result = Object.create(null) as Record<string, ReturnType<typeof parse>>;
for (let i = 0; i < items.length; i++) {
const item = items[i];
result[item.prefix] = parse(item.prefix, item.iconSet, item.v2Cache);
}
return result;
sendJSONResponse(result, q, wrap, res);
return;
}
// Invalid
return 400;
res.send(400);
}

View File

@ -1,5 +1,7 @@
import { iconSets } from '../../data/icon-sets.js';
import type { APIv2CollectionResponse } from '../../types/server/v2.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { iconSets } from '../../data/icon-sets';
import type { APIv2CollectionResponse } from '../../types/server/v2';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
/**
* Send API v2 response
@ -10,18 +12,29 @@ import type { APIv2CollectionResponse } from '../../types/server/v2.js';
*
* Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled
*/
export function createAPIv2CollectionResponse(q: Record<string, string>): APIv2CollectionResponse | number {
export function generateAPIv2CollectionResponse(query: FastifyRequest['query'], res: FastifyReply) {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Get icon set
const prefix = q.prefix;
if (!prefix || !iconSets[prefix]) {
return 404;
res.send(404);
return;
}
const iconSet = iconSets[prefix].item;
const apiV2IconsCache = iconSet.apiV2IconsCache;
if (!apiV2IconsCache) {
// Disabled
return 404;
res.send(404);
return;
}
// Generate response
@ -39,5 +52,5 @@ export function createAPIv2CollectionResponse(q: Record<string, string>): APIv2C
delete response.chars;
}
return response;
sendJSONResponse(response, q, wrap, res);
}

View File

@ -1,6 +1,8 @@
import { getPrefixes, iconSets } from '../../data/icon-sets.js';
import type { APIv2CollectionsResponse } from '../../types/server/v2.js';
import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getPrefixes, iconSets } from '../../data/icon-sets';
import type { APIv2CollectionsResponse } from '../../types/server/v2';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
import { filterPrefixesByPrefix } from '../helpers/prefixes';
/**
* Send response
@ -10,7 +12,15 @@ import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
* Ignored parameters:
* - hidden (always enabled)
*/
export function createCollectionsListResponse(q: Record<string, string>): APIv2CollectionsResponse {
export function generateCollectionsListResponse(query: FastifyRequest['query'], res: FastifyReply) {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Filter prefixes
const prefixes = filterPrefixesByPrefix(getPrefixes('info'), q, false);
const response = Object.create(null) as APIv2CollectionsResponse;
@ -23,5 +33,5 @@ export function createCollectionsListResponse(q: Record<string, string>): APIv2C
}
}
return response;
sendJSONResponse(response, q, wrap, res);
}

View File

@ -1,12 +1,10 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { stringToColor } from '@iconify/utils/lib/colors';
import { getIconsCSS } from '@iconify/utils/lib/css/icons';
import { getStoredIconsData } from '../../data/icon-set/utils/get-icons';
import { iconSets } from '../../data/icon-sets';
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
import { getStoredIconsData } from '../../data/icon-set/utils/get-icons.js';
import { iconSets } from '../../data/icon-sets.js';
import { paramToBoolean } from '../../misc/bool.js';
import { errorText } from '../helpers/errors.js';
import { cleanupQueryValue } from '../helpers/query.js';
import { paramToBoolean } from '../../misc/bool';
/**
* Check selector for weird stuff
@ -28,7 +26,7 @@ export function generateIconsStyleResponse(prefix: string, query: FastifyRequest
if (!names || !names.length) {
// Missing or invalid icons parameter
res.code(404).send(errorText(404));
res.send(404);
return;
}
@ -36,7 +34,7 @@ export function generateIconsStyleResponse(prefix: string, query: FastifyRequest
const iconSet = iconSets[prefix];
if (!iconSet) {
// No such icon set
res.code(404).send(errorText(404));
res.send(404);
return;
}
@ -58,7 +56,7 @@ export function generateIconsStyleResponse(prefix: string, query: FastifyRequest
// 'color': string
// Sets color for monotone images
const color = cleanupQueryValue(qOptions.color);
const color = qOptions.color;
if (typeof color === 'string' && stringToColor(color)) {
options.color = color;
}
@ -99,7 +97,7 @@ export function generateIconsStyleResponse(prefix: string, query: FastifyRequest
// 'commonSelector': string
// Common selector for all requested icons
// Alias: 'common'
const commonSelector = cleanupQueryValue(qOptions.commonSelector || q.common);
const commonSelector = qOptions.commonSelector || q.common;
if (checkSelector(commonSelector)) {
options.commonSelector = commonSelector;
}
@ -107,7 +105,7 @@ export function generateIconsStyleResponse(prefix: string, query: FastifyRequest
// 'iconSelector': string
// Icon selector
// Alias: 'selector'
const iconSelector = cleanupQueryValue(qOptions.iconSelector || q.selector);
const iconSelector = qOptions.iconSelector || q.selector;
if (checkSelector(iconSelector)) {
options.iconSelector = iconSelector;
}
@ -115,7 +113,7 @@ export function generateIconsStyleResponse(prefix: string, query: FastifyRequest
// 'overrideSelector': string
// Selector for rules in icon that override common rules
// Alias: 'override'
const overrideSelector = cleanupQueryValue(qOptions.overrideSelector || q.override);
const overrideSelector = qOptions.overrideSelector || q.override;
if (checkSelector(overrideSelector)) {
options.overrideSelector = overrideSelector;
}

View File

@ -1,63 +1,45 @@
import { IconifyJSON } from '@iconify/types';
import { getStoredIconsData } from '../../data/icon-set/utils/get-icons.js';
import { iconSets } from '../../data/icon-sets.js';
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 createIconsDataResponse(
export function generateIconsDataResponse(
prefix: string,
q: Record<string, string | string[]>
): number | IconifyJSON | Promise<IconifyJSON | number> {
const iconNames = q.icons;
const names = typeof iconNames === 'string' ? iconNames.split(',') : iconNames;
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
return 404;
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
return 404;
res.send(404);
return;
}
// Get icons, possibly sync
let syncData: IconifyJSON | undefined;
let resolveData: undefined | ((data: IconifyJSON) => void);
// Get icons
getStoredIconsData(iconSet.item, names, (data) => {
// Send data
if (resolveData) {
resolveData(data);
} else {
syncData = data;
}
});
if (syncData) {
return syncData;
}
return new Promise((resolve) => {
resolveData = resolve;
});
}
/**
* Awaitable version of createIconsDataResponse()
*/
export function createIconsDataResponseAsync(
prefix: string,
q: Record<string, string | string[]>
): Promise<IconifyJSON | number> {
return new Promise((resolve, reject) => {
const result = createIconsDataResponse(prefix, q);
if (result instanceof Promise) {
result.then(resolve).catch(reject);
} else {
resolve(result);
}
sendJSONResponse(data, q, wrap, res);
});
}

View File

@ -1,16 +1,27 @@
import { matchIconName } from '@iconify/utils/lib/icon/name';
import { searchIndex } from '../../data/search.js';
import { getPartialKeywords } from '../../data/search/partial.js';
import type { APIv3KeywordsQuery, APIv3KeywordsResponse } from '../../types/server/keywords.js';
import { matchIconName } from '@iconify/utils';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { searchIndex } from '../../data/search';
import { getPartialKeywords } from '../../data/search/partial';
import type { APIv3KeywordsQuery, APIv3KeywordsResponse } from '../../types/server/keywords';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
/**
* Find full keywords for partial keyword
* Generate icons data
*/
export function createKeywordsResponse(q: Record<string, string>): number | APIv3KeywordsResponse {
export function generateKeywordsResponse(query: FastifyRequest['query'], res: FastifyReply) {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Check if search data is available
const searchIndexData = searchIndex.data;
if (!searchIndexData) {
return 404;
res.send(404);
return;
}
const keywords = searchIndexData.keywords;
@ -21,16 +32,15 @@ export function createKeywordsResponse(q: Record<string, string>): number | APIv
let failed = false;
if (typeof q.prefix === 'string') {
// Keywords should start with prefix
test = q.prefix;
suffixes = false;
} else if (typeof q.keyword === 'string') {
// All keywords that contain keyword
test = q.keyword;
suffixes = true;
} else {
// Invalid query
return 400;
res.send(400);
return;
}
test = test.toLowerCase().trim();
@ -61,5 +71,5 @@ export function createKeywordsResponse(q: Record<string, string>): number | APIv
matches: failed || invalid ? [] : getPartialKeywords(test, suffixes, searchIndexData)?.slice(0) || [],
};
return response;
sendJSONResponse(response, q, wrap, res);
}

View File

@ -1,11 +1,21 @@
import { getPrefixes, iconSets } from '../../data/icon-sets.js';
import type { APIv3LastModifiedResponse } from '../../types/server/modified.js';
import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getPrefixes, iconSets } from '../../data/icon-sets';
import type { APIv3LastModifiedResponse } from '../../types/server/modified';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
import { filterPrefixesByPrefix } from '../helpers/prefixes';
/**
* Get last modified time for all icon sets
* Generate icons data
*/
export function createLastModifiedResponse(q: Record<string, string>): number | APIv3LastModifiedResponse {
export function generateLastModifiedResponse(query: FastifyRequest['query'], res: FastifyReply) {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Filter prefixes
const prefixes = filterPrefixesByPrefix(getPrefixes(), q, false);
@ -26,5 +36,5 @@ export function createLastModifiedResponse(q: Record<string, string>): number |
}
}
return response;
sendJSONResponse(response, q, wrap, res);
}

View File

@ -1,9 +1,11 @@
import { iconSets } from '../../data/icon-sets.js';
import { searchIndex } from '../../data/search.js';
import { search } from '../../data/search/index.js';
import { paramToBoolean } from '../../misc/bool.js';
import type { SearchParams } from '../../types/search.js';
import type { APIv2SearchParams, APIv2SearchResponse } from '../../types/server/v2.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { iconSets } from '../../data/icon-sets';
import { searchIndex } from '../../data/search';
import { search } from '../../data/search/index';
import { paramToBoolean } from '../../misc/bool';
import type { SearchParams } from '../../types/search';
import type { APIv2SearchParams, APIv2SearchResponse } from '../../types/server/v2';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json';
const minSearchLimit = 32;
const maxSearchLimit = 999;
@ -12,17 +14,28 @@ const defaultSearchLimit = minSearchLimit * 2;
/**
* Send API v2 response
*/
export function createAPIv2SearchResponse(q: Record<string, string>): number | APIv2SearchResponse {
export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res: FastifyReply) {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Check if search data is available
const searchIndexData = searchIndex.data;
if (!searchIndexData) {
return 404;
res.send(404);
return;
}
// Get query
const keyword = q.query;
if (!keyword) {
return 400;
res.send(400);
return;
}
// Convert to params
@ -36,24 +49,18 @@ export function createAPIv2SearchResponse(q: Record<string, string>): number | A
if (v2Query.limit) {
const limit = parseInt(v2Query.limit);
if (!limit) {
return 400;
res.send(400);
return;
}
params.limit = Math.max(minSearchLimit, Math.min(limit, maxSearchLimit));
}
if (v2Query.min) {
const limit = parseInt(v2Query.min);
if (!limit) {
return 400;
}
params.limit = Math.max(minSearchLimit, Math.min(limit, maxSearchLimit));
params.softLimit = true;
}
let start = 0;
if (v2Query.start) {
start = parseInt(v2Query.start);
if (isNaN(start) || start < 0 || start >= params.limit) {
return 400;
res.send(400);
return;
}
}
@ -114,5 +121,5 @@ export function createAPIv2SearchResponse(q: Record<string, string>): number | A
};
}
return response;
sendJSONResponse(response, q, wrap, res);
}

View File

@ -1,14 +1,8 @@
import { iconToHTML } from '@iconify/utils/lib/svg/html';
import { iconToSVG } from '@iconify/utils/lib/svg/build';
import { flipFromString } from '@iconify/utils/lib/customisations/flip';
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
import { defaultIconDimensions } from '@iconify/utils/lib/icon/defaults';
import { defaultIconDimensions, flipFromString, iconToHTML, iconToSVG, rotateFromString } 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.js';
import { iconSets } from '../../data/icon-sets.js';
import { errorText } from '../helpers/errors.js';
import { cleanupQueryValue } from '../helpers/query.js';
import { getStoredIconData } from '../../data/icon-set/utils/get-icon';
import { iconSets } from '../../data/icon-sets';
/**
* Generate SVG
@ -18,7 +12,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
const iconSetItem = iconSets[prefix]?.item;
if (!iconSetItem) {
// No such icon set
res.code(404).send(errorText(404));
res.send(404);
return;
}
@ -26,7 +20,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
const icons = iconSetItem.icons;
if (!(icons.visible[name] || icons.hidden[name]) && !iconSetItem.icons.chars?.[name]) {
// No such icon
res.code(404).send(errorText(404));
res.send(404);
return;
}
@ -34,7 +28,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
getStoredIconData(iconSetItem, name, (data) => {
if (!data) {
// Invalid icon
res.code(404).send(errorText(404));
res.send(404);
return;
}
@ -44,8 +38,8 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
const customisations: IconifyIconCustomisations = {};
// Dimensions
customisations.width = cleanupQueryValue(q.width) || defaultIconCustomisations.width;
customisations.height = cleanupQueryValue(q.height) || defaultIconCustomisations.height;
customisations.width = q.width || defaultIconCustomisations.width;
customisations.height = q.height || defaultIconCustomisations.height;
// Rotation
customisations.rotate = q.rotate ? rotateFromString(q.rotate, 0) : 0;
@ -76,7 +70,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
let html = iconToHTML(body, svg.attributes);
// Change color
const color = cleanupQueryValue(q.color);
const color = q.color;
if (color && html.indexOf('currentColor') !== -1 && color.indexOf('"') === -1) {
html = html.split('currentColor').join(color);
}

View File

@ -1,7 +1,7 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { appConfig } from '../../config/app.js';
import { triggerIconSetsUpdate } from '../../data/icon-sets.js';
import { runWhenLoaded } from '../../data/loading.js';
import { appConfig } from '../../config/app';
import { triggerIconSetsUpdate } from '../../data/icon-sets';
import { runWhenLoaded } from '../../data/loading';
let pendingUpdate = false;
let lastError = 0;

View File

@ -1,6 +1,6 @@
import { readFile } from 'node:fs/promises';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { appConfig } from '../../config/app.js';
import { appConfig } from '../../config/app';
let version: string | undefined;

View File

@ -1,11 +1,11 @@
import type { BaseDownloader } from '../../downloaders/base.js';
import { maybeAwait } from '../../misc/async.js';
import type { BaseDownloader } from '../../downloaders/base';
import { maybeAwait } from '../../misc/async';
import type {
BaseCollectionsImporter,
CreateIconSetImporter,
CreateIconSetImporterResult,
} from '../../types/importers/collections.js';
import type { ImportedData } from '../../types/importers/common.js';
} from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
/**
* Base collections list importer

View File

@ -1,10 +1,10 @@
import { readFile } from 'node:fs/promises';
import type { IconifyInfo } from '@iconify/types';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base.js';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections.js';
import type { ImportedData } from '../../types/importers/common.js';
import { createBaseCollectionsListImporter } from './base.js';
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';
import type { IconifyInfo } from '@iconify/types';
interface JSONCollectionsListImporterOptions {
// File to load

View File

@ -1,7 +1,7 @@
import { CustomDownloader } from '../../downloaders/custom.js';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections.js';
import type { ImportedData } from '../../types/importers/common.js';
import { createBaseCollectionsListImporter } from './base.js';
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

View File

@ -1,8 +1,8 @@
import { readFile } from 'node:fs/promises';
import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import { prependSlash } from '../../misc/files.js';
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?

View File

@ -1,8 +1,8 @@
import { readFile } from 'node:fs/promises';
import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import { appConfig } from '../../config/app.js';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage';
import type { StoredIconSet } from '../../types/icon-set/storage';
import { appConfig } from '../../config/app';
export interface IconSetJSONPackageOptions {
// Ignore bad prefix?

View File

@ -1,12 +1,12 @@
import { readdir, stat } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base.js';
import { DirectoryDownloader } from '../../downloaders/directory.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections.js';
import type { ImportedData } from '../../types/importers/common.js';
import { createJSONIconSetImporter } from '../icon-set/json.js';
import { createBaseCollectionsListImporter } from '../collections/base.js';
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

View File

@ -1,12 +1,12 @@
import { readFile } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base.js';
import { DirectoryDownloader } from '../../downloaders/directory.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections.js';
import type { ImportedData } from '../../types/importers/common.js';
import { createJSONIconSetImporter } from '../icon-set/json.js';
import { createBaseCollectionsListImporter } from '../collections/base.js';
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

View File

@ -1,7 +1,7 @@
import type { BaseDownloader } from '../../downloaders/base.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import type { ImportedData } from '../../types/importers/common.js';
import type { BaseFullImporter } from '../../types/importers/full.js';
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

View File

@ -1,10 +1,10 @@
import { readdir, stat } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base.js';
import type { ImportedData } from '../../types/importers/common.js';
import type { BaseFullImporter } from '../../types/importers/full.js';
import { createBaseImporter } from './base.js';
import { type IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json.js';
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

View File

@ -1,11 +1,11 @@
import { readFile } from 'node:fs/promises';
import type { IconifyInfo } from '@iconify/types';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base.js';
import type { ImportedData } from '../../types/importers/common.js';
import type { BaseFullImporter } from '../../types/importers/full.js';
import { createBaseImporter } from './base.js';
import { IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json.js';
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';
import type { IconifyInfo } from '@iconify/types';
interface IconSetsPackageImporterOptions extends IconSetJSONOptions {
// Icon set filter
@ -23,17 +23,6 @@ export function createIconSetsPackageImporter<Downloader extends BaseDownloader<
// Load collections list
obj._loadCollectionsListFromDirectory = async (path: string) => {
// Log version
try {
const packageJSON = JSON.parse(await readFile(path + '/package.json', 'utf8'));
if (packageJSON.name && packageJSON.version) {
console.log(`Loading ${packageJSON.name} ${packageJSON.version}`);
}
} catch {
//
}
// Get prefixes
let prefixes: string[];
let data: Record<string, IconifyInfo>;
try {

View File

@ -1,7 +1,7 @@
import type { BaseDownloader } from '../../downloaders/base.js';
import type { BaseIconSetImporter } from '../../types/importers/icon-set.js';
import type { IconSetImportedData } from '../../types/importers/common.js';
import { IconSetJSONPackageOptions, importIconSetFromJSONPackage } from '../common/json-package.js';
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

View File

@ -1,7 +1,7 @@
import type { BaseDownloader } from '../../downloaders/base.js';
import type { BaseIconSetImporter } from '../../types/importers/icon-set.js';
import type { IconSetImportedData } from '../../types/importers/common.js';
import { IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json.js';
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

View File

@ -1,19 +1,30 @@
import { config } from 'dotenv';
import { loaded } from './data/loading.js';
import { startHTTPServer } from './http/index.js';
import { loadEnvConfig } from './misc/load-config.js';
import { initAPI } from './init.js';
import { getImporters } from './config/icon-sets';
import { iconSetsStorage } from './data/icon-set/store/storage';
import { setImporters, updateIconSets } from './data/icon-sets';
import { loaded } from './data/loading';
import { cleanupStorageCache } from './data/storage/startup';
import { startHTTPServer } from './http';
import { loadEnvConfig } from './misc/load-config';
(async () => {
// Configure environment
config();
loadEnvConfig();
// Reset old cache
await cleanupStorageCache(iconSetsStorage);
// Start HTTP server
startHTTPServer();
// Init API
await initAPI();
// 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();

View File

@ -1,36 +0,0 @@
import { getImporters } from './config/icon-sets.js';
import { iconSetsStorage } from './data/icon-set/store/storage.js';
import { setImporters, updateIconSets } from './data/icon-sets.js';
import { cleanupStorageCache } from './data/storage/startup.js';
import { Importer } from './types/importers.js';
interface InitOptions {
// Cleanup storage cache
cleanup?: boolean;
// Importers
importers?: Importer[];
}
/**
* Init API
*/
export async function initAPI(options: InitOptions = {}) {
// Reset old cache
if (options.cleanup !== false) {
await cleanupStorageCache(iconSetsStorage);
}
// Get all importers and load data
let importers = options.importers;
if (!importers) {
importers = await getImporters();
}
for (let i = 0; i < importers.length; i++) {
await importers[i].init();
}
// Update
setImporters(importers);
updateIconSets();
}

View File

@ -1,7 +1,7 @@
import { stat } from 'node:fs/promises';
import { scanDirectory } from '@iconify/tools/lib/misc/scan';
import type { FileEntry } from '../types/files.js';
import { hashString } from './hash.js';
import type { FileEntry } from '../types/files';
import { hashString } from './hash';
/**
* List all files in directory

View File

@ -1,5 +1,5 @@
import { appConfig, splitIconSetConfig, storageConfig } from '../config/app.js';
import { paramToBoolean } from './bool.js';
import { appConfig, splitIconSetConfig, storageConfig } from '../config/app';
import { paramToBoolean } from './bool';
interface ConfigurableItem {
config: unknown;

View File

@ -1,4 +1,4 @@
import type { StoredIconSet } from '../icon-set/storage.js';
import type { StoredIconSet } from '../icon-set/storage';
/**
* Generated data

View File

@ -1,8 +1,8 @@
import type { IconifyIcons, IconifyInfo, IconifyMetaData } from '@iconify/types';
import type { SplitDataTree } from '../split.js';
import type { MemoryStorage, MemoryStorageItem } from '../storage.js';
import type { IconSetIconsListIcons, IconSetAPIv2IconsList } from './extra.js';
import type { SplitIconifyJSONMainData } from './split.js';
import type { IconifyIcons, IconifyInfo, IconifyJSON, IconifyMetaData } from '@iconify/types';
import type { SplitDataTree } from '../split';
import type { MemoryStorage, MemoryStorageItem } from '../storage';
import type { IconSetIconsListIcons, IconSetAPIv2IconsList } from './extra';
import type { SplitIconifyJSONMainData } from './split';
/**
* Themes

View File

@ -1,6 +1,6 @@
import type { BaseDownloader } from '../downloaders/base.js';
import type { StoredIconSet } from './icon-set/storage.js';
import type { ImportedData } from './importers/common.js';
import type { BaseDownloader } from '../downloaders/base';
import type { StoredIconSet } from './icon-set/storage';
import type { ImportedData } from './importers/common';
/**
* Importer

View File

@ -1,7 +1,7 @@
import type { BaseDownloader } from '../../downloaders/base.js';
import type { MaybeAsync } from '../async.js';
import type { BaseMainImporter, IconSetImportedData } from './common.js';
import type { BaseIconSetImporter } from './icon-set.js';
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

View File

@ -1,5 +1,5 @@
import type { DownloaderType } from '../downloaders/base.js';
import type { StoredIconSet } from '../icon-set/storage.js';
import type { DownloaderType } from '../downloaders/base';
import type { StoredIconSet } from '../icon-set/storage';
/**
* Base icon set importer interface

View File

@ -1,4 +1,4 @@
import type { BaseMainImporter, IconSetImportedData } from './common.js';
import type { BaseMainImporter, IconSetImportedData } from './common';
/**
* Base full importer

View File

@ -1,4 +1,4 @@
import type { BaseImporter, IconSetImportedData } from './common.js';
import type { BaseImporter, IconSetImportedData } from './common';
/**
* Base icon set importer interface

View File

@ -1,4 +1,4 @@
import type { IconStyle } from './icon-set/extra.js';
import type { IconStyle } from './icon-set/extra';
/**
* List of keywords that can be used to autocomplete keyword
@ -50,7 +50,6 @@ export interface SearchParams {
// Search results limit
limit: number;
softLimit?: boolean; // True if limit can be exceeded
// Toggle partial matches
partial?: boolean;
@ -68,9 +67,6 @@ export interface SearchKeywordsEntry {
// Strings to test icon value
test?: string[];
// Partial keyword
partial?: string;
}
/**
@ -80,6 +76,9 @@ export interface SearchKeywords {
// List of searches
searches: SearchKeywordsEntry[];
// Partial keyword, used in all matches
partial?: string;
// Params extracted from keywords
params: Partial<SearchParams>;
}

View File

@ -103,9 +103,7 @@ export interface APIv2SearchParams extends APIv2CommonParams {
query: SearchQuery;
// Maximum number of items in response
// If `min` is set, `limit` is ignored
limit?: number; // Hard limit. Number of results will not exceed `limit`.
min?: number; // Soft limit. Number of results can exceed `limit` if function already retrieved more icons.
limit?: number;
// Start index for results
start?: number;

View File

@ -37,34 +37,40 @@ describe('Splitting keywords', () => {
prefix: false,
partial: false,
})
).toEqual([
{
keywords: ['home'],
},
]);
).toEqual({
searches: [
{
keywords: ['home'],
},
],
});
expect(
splitKeywordEntries(['home'], {
prefix: true,
partial: false,
})
).toEqual([
{
keywords: ['home'],
},
]);
).toEqual({
searches: [
{
keywords: ['home'],
},
],
});
expect(
splitKeywordEntries(['home'], {
prefix: true,
partial: true,
})
).toEqual([
{
keywords: [],
partial: 'home',
},
]);
).toEqual({
searches: [
{
keywords: [],
},
],
partial: 'home',
});
});
test('Multiple simple entries', () => {
@ -73,73 +79,79 @@ describe('Splitting keywords', () => {
prefix: false,
partial: false,
})
).toEqual([
{
keywords: ['mdi', 'home'],
},
{
keywords: ['mdihome'],
},
]);
).toEqual({
searches: [
{
keywords: ['mdi', 'home'],
},
{
keywords: ['mdihome'],
},
],
});
expect(
splitKeywordEntries(['mdi', 'home', 'outline'], {
prefix: false,
partial: false,
})
).toEqual([
{
keywords: ['mdi', 'home', 'outline'],
},
{
keywords: ['mdihome', 'outline'],
},
{
keywords: ['mdihomeoutline'],
},
{
keywords: ['mdi', 'homeoutline'],
},
]);
).toEqual({
searches: [
{
keywords: ['mdi', 'home', 'outline'],
},
{
keywords: ['mdihome', 'outline'],
},
{
keywords: ['mdihomeoutline'],
},
{
keywords: ['mdi', 'homeoutline'],
},
],
});
expect(
splitKeywordEntries(['mdi', 'home'], {
prefix: true,
partial: false,
})
).toEqual([
{
prefix: 'mdi',
keywords: ['home'],
},
{
keywords: ['mdi', 'home'],
},
{
keywords: ['mdihome'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi',
keywords: ['home'],
},
{
keywords: ['mdi', 'home'],
},
{
keywords: ['mdihome'],
},
],
});
expect(
splitKeywordEntries(['mdi', 'home'], {
prefix: true,
partial: true,
})
).toEqual([
{
prefix: 'mdi',
keywords: [],
partial: 'home',
},
{
keywords: ['mdi'],
partial: 'home',
},
{
keywords: [],
partial: 'mdihome',
},
]);
).toEqual({
searches: [
{
prefix: 'mdi',
keywords: [],
},
{
keywords: ['mdi'],
},
{
keywords: ['mdihome'],
},
],
partial: 'home',
});
});
test('Incomplete prefix', () => {
@ -148,46 +160,51 @@ describe('Splitting keywords', () => {
prefix: false,
partial: false,
})
).toEqual([
{
keywords: ['mdi', 'home'],
test: ['mdi-'],
},
]);
).toEqual({
searches: [
{
keywords: ['mdi', 'home'],
test: ['mdi-'],
},
],
});
expect(
splitKeywordEntries(['mdi-', 'home'], {
prefix: true,
partial: false,
})
).toEqual([
{
prefix: 'mdi-',
keywords: ['home'],
},
{
keywords: ['mdi', 'home'],
test: ['mdi-'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi-',
keywords: ['home'],
},
{
keywords: ['mdi', 'home'],
test: ['mdi-'],
},
],
});
expect(
splitKeywordEntries(['mdi-', 'home'], {
prefix: true,
partial: true,
})
).toEqual([
{
prefix: 'mdi-',
keywords: [],
partial: 'home',
},
{
keywords: ['mdi'],
partial: 'home',
test: ['mdi-'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi-',
keywords: [],
},
{
keywords: ['mdi'],
test: ['mdi-'],
},
],
partial: 'home',
});
});
test('Long entry', () => {
@ -196,48 +213,53 @@ describe('Splitting keywords', () => {
prefix: false,
partial: false,
})
).toEqual([
{
keywords: ['mdi', 'home', 'outline'],
test: ['mdi-home-outline'],
},
]);
).toEqual({
searches: [
{
keywords: ['mdi', 'home', 'outline'],
test: ['mdi-home-outline'],
},
],
});
expect(
splitKeywordEntries(['mdi-home-outline'], {
prefix: true,
partial: false,
})
).toEqual([
{
prefix: 'mdi',
keywords: ['home', 'outline'],
test: ['home-outline'],
},
{
keywords: ['mdi', 'home', 'outline'],
test: ['mdi-home-outline'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi',
keywords: ['home', 'outline'],
test: ['home-outline'],
},
{
keywords: ['mdi', 'home', 'outline'],
test: ['mdi-home-outline'],
},
],
});
expect(
splitKeywordEntries(['mdi-home-outline'], {
prefix: true,
partial: true,
})
).toEqual([
{
prefix: 'mdi',
keywords: ['home'],
partial: 'outline',
test: ['home-outline'],
},
{
keywords: ['mdi', 'home'],
partial: 'outline',
test: ['mdi-home-outline'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi',
keywords: ['home'],
test: ['home-outline'],
},
{
keywords: ['mdi', 'home'],
test: ['mdi-home-outline'],
},
],
partial: 'outline',
});
});
test('Complex entries', () => {
@ -246,71 +268,77 @@ describe('Splitting keywords', () => {
prefix: false,
partial: false,
})
).toEqual([
{
keywords: ['mdi', 'light', 'arrow', 'left'],
test: ['mdi-light', 'arrow-left'],
},
]);
).toEqual({
searches: [
{
keywords: ['mdi', 'light', 'arrow', 'left'],
test: ['mdi-light', 'arrow-left'],
},
],
});
expect(
splitKeywordEntries(['mdi-light', 'arrow-left'], {
prefix: true,
partial: false,
})
).toEqual([
{
prefix: 'mdi-light',
keywords: ['arrow', 'left'],
test: ['arrow-left'],
},
{
prefix: 'mdi',
keywords: ['light', 'arrow', 'left'],
test: ['arrow-left'],
},
{
keywords: ['mdi', 'light', 'arrow', 'left'],
test: ['mdi-light', 'arrow-left'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi-light',
keywords: ['arrow', 'left'],
test: ['arrow-left'],
},
{
prefix: 'mdi',
keywords: ['light', 'arrow', 'left'],
test: ['arrow-left'],
},
{
keywords: ['mdi', 'light', 'arrow', 'left'],
test: ['mdi-light', 'arrow-left'],
},
],
});
expect(
splitKeywordEntries(['mdi-light', 'arrow-left'], {
prefix: false,
partial: true,
})
).toEqual([
{
keywords: ['mdi', 'light', 'arrow'],
partial: 'left',
test: ['mdi-light', 'arrow-left'],
},
]);
).toEqual({
searches: [
{
keywords: ['mdi', 'light', 'arrow'],
test: ['mdi-light', 'arrow-left'],
},
],
partial: 'left',
});
expect(
splitKeywordEntries(['mdi-light', 'arrow-left'], {
prefix: true,
partial: true,
})
).toEqual([
{
prefix: 'mdi-light',
keywords: ['arrow'],
partial: 'left',
test: ['arrow-left'],
},
{
prefix: 'mdi',
keywords: ['light', 'arrow'],
partial: 'left',
test: ['arrow-left'],
},
{
keywords: ['mdi', 'light', 'arrow'],
partial: 'left',
test: ['mdi-light', 'arrow-left'],
},
]);
).toEqual({
searches: [
{
prefix: 'mdi-light',
keywords: ['arrow'],
test: ['arrow-left'],
},
{
prefix: 'mdi',
keywords: ['light', 'arrow'],
test: ['arrow-left'],
},
{
keywords: ['mdi', 'light', 'arrow'],
test: ['mdi-light', 'arrow-left'],
},
],
partial: 'left',
});
});
});

View File

@ -20,14 +20,13 @@ describe('Splitting keywords', () => {
prefixes: ['mdi'],
prefix: 'mdi', // leftover from internal function
keywords: [],
partial: 'home',
},
{
keywords: ['mdi'],
partial: 'home',
test: ['mdi-home'],
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('mdi-home', false)).toEqual({
@ -51,9 +50,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['mdi'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('mdi:home', false)).toEqual({
@ -72,9 +71,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['mdi'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('prefix:mdi home', false)).toEqual({
@ -93,9 +92,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['mdi'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('prefix=mdi home', false)).toEqual({
@ -114,9 +113,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['mdi'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('prefixes:mdi home', false)).toEqual({
@ -135,9 +134,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['fa6-', 'mdi-'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('prefixes:fa6-,mdi- home', false)).toEqual({
@ -156,9 +155,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['mdi', 'mdi-'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('prefixes=mdi* home', false)).toEqual({
@ -178,20 +177,18 @@ describe('Splitting keywords', () => {
prefixes: ['mdi-light'],
prefix: 'mdi-light',
keywords: [],
partial: 'home',
},
{
prefixes: ['mdi'],
prefix: 'mdi',
keywords: ['light'],
partial: 'home',
},
{
keywords: ['mdi', 'light'],
test: ['mdi-light'],
partial: 'home',
},
],
partial: 'home',
params: {},
});
expect(splitKeyword('mdi-light home', false)).toEqual({
@ -221,22 +218,20 @@ describe('Splitting keywords', () => {
prefixes: ['mdi-light'],
prefix: 'mdi-light',
keywords: ['home'],
partial: 'outline',
test: ['home-outline'],
},
{
prefixes: ['mdi'],
prefix: 'mdi',
keywords: ['light', 'home'],
partial: 'outline',
test: ['home-outline'],
},
{
keywords: ['mdi', 'light', 'home'],
partial: 'outline',
test: ['mdi-light', 'home-outline'],
},
],
partial: 'outline',
params: {},
});
expect(splitKeyword('mdi-light home-outline', false)).toEqual({
@ -267,9 +262,9 @@ describe('Splitting keywords', () => {
searches: [
{
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {
palette: true,
},
@ -279,9 +274,9 @@ describe('Splitting keywords', () => {
searches: [
{
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {
palette: false,
},
@ -292,9 +287,9 @@ describe('Splitting keywords', () => {
{
prefixes: ['mdi', 'mdi-', 'fa6-'],
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {},
});
@ -314,9 +309,9 @@ describe('Splitting keywords', () => {
searches: [
{
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {
style: 'fill',
},
@ -326,9 +321,9 @@ describe('Splitting keywords', () => {
searches: [
{
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {
style: 'stroke',
},
@ -338,9 +333,9 @@ describe('Splitting keywords', () => {
searches: [
{
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {
style: 'fill',
},
@ -350,9 +345,9 @@ describe('Splitting keywords', () => {
searches: [
{
keywords: [],
partial: 'home',
},
],
partial: 'home',
params: {
style: 'stroke',
},

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"module": "CommonJS",
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node",