Compare commits

...

71 Commits

Author SHA1 Message Date
Vjacheslav Trushkin a3d158f99c chore: make dockerfile work 2025-11-27 20:31:47 +02:00
Vjacheslav Trushkin 407792e2bc chore: publish new minor version 2025-11-27 19:14:13 +02:00
Vjacheslav Trushkin 8e35844326 chore: update all dependencies, update routes for latest fastify 2025-11-27 17:31:14 +02:00
Vjacheslav Trushkin f79638ef5f chore: update dependencies, clean up renovate config 2025-11-18 09:07:18 +02:00
Vjacheslav Trushkin 23b5dc510e
Merge pull request #61 from iconify/revert-51-renovate/fastify-formbody-8.x
Revert "fix(deps): update dependency @fastify/formbody to v8"
2025-11-18 08:30:40 +02:00
Vjacheslav Trushkin 238e12a016
Revert "fix(deps): update dependency @fastify/formbody to v8" 2025-11-18 08:30:29 +02:00
Vjacheslav Trushkin 26828c1152
Merge pull request #55 from iconify/renovate/node-22.x
chore(deps): update node.js to v22
2025-10-16 09:11:08 +03:00
renovate[bot] d24e229645
chore(deps): update node.js to v22 2025-10-14 05:30:30 +00:00
Vjacheslav Trushkin 62fa26b541
Merge pull request #51 from iconify/renovate/fastify-formbody-8.x
fix(deps): update dependency @fastify/formbody to v8
2025-10-14 08:28:09 +03:00
renovate[bot] 65af386d3c
fix(deps): update dependency @fastify/formbody to v8 2025-10-14 05:26:58 +00:00
Vjacheslav Trushkin ae0e4c929d
Merge pull request #42 from iconify/renovate/typescript-5.x
chore(deps): update dependency typescript to ^5.9.3
2025-10-14 08:25:25 +03:00
renovate[bot] 116d8e20df
chore(deps): update dependency typescript to ^5.9.3 2025-10-14 05:25:18 +00:00
Vjacheslav Trushkin a7e3d99d9e
Merge pull request #41 from iconify/renovate/node-20.x
chore(deps): update dependency @types/node to ^20.19.21
2025-10-14 08:24:36 +03:00
Vjacheslav Trushkin 2521142eca
Merge pull request #40 from iconify/renovate/fastify-4.x
fix(deps): update dependency fastify to ^4.29.1
2025-10-14 08:24:24 +03:00
Vjacheslav Trushkin 84590145c9
Merge pull request #39 from iconify/renovate/iconify-tools-4.x
fix(deps): update dependency @iconify/tools to ^4.1.4
2025-10-14 08:24:04 +03:00
renovate[bot] 88b88251aa
chore(deps): update dependency @types/node to ^20.19.21 2025-10-14 05:23:20 +00:00
Vjacheslav Trushkin 262783f704
Merge pull request #45 from iconify/renovate/npm-11.x
chore(deps): update npm to v11.6.2
2025-10-14 08:23:03 +03:00
Vjacheslav Trushkin 48b0da6d55
Merge pull request #50 from iconify/renovate/major-jest-monorepo
chore(deps): update dependency @types/jest to v30
2025-10-14 08:22:52 +03:00
Vjacheslav Trushkin 119538fbff
Merge pull request #47 from iconify/renovate/actions-checkout-5.x
chore(deps): update actions/checkout action to v5
2025-10-14 08:22:18 +03:00
renovate[bot] 3141131ab7
chore(deps): update dependency @types/jest to v30 2025-10-14 05:22:12 +00:00
renovate[bot] ba72402310
chore(deps): update actions/checkout action to v5 2025-10-14 05:21:57 +00:00
Vjacheslav Trushkin 6f33b7c4c5
Merge pull request #49 from iconify/renovate/actions-setup-node-6.x
chore(deps): update actions/setup-node action to v6
2025-10-14 08:20:38 +03:00
renovate[bot] 44884118f3
chore(deps): update actions/setup-node action to v6 2025-10-14 03:49:55 +00:00
renovate[bot] 9ec6b365a0
chore(deps): update npm to v11.6.2 2025-10-12 08:03:54 +00:00
renovate[bot] 5e1dbe8d4f
fix(deps): update dependency fastify to ^4.29.1 2025-10-08 19:57:57 +00:00
renovate[bot] 9b7b1a4279
fix(deps): update dependency @iconify/tools to ^4.1.4 2025-10-08 19:57:49 +00:00
Vjacheslav Trushkin 871028175b
chore: update renovate.json 2025-10-06 12:45:59 +03:00
Vjacheslav Trushkin a752e1ee7f
Merge pull request #35 from iconify/renovate/configure
chore: Configure Renovate
2025-10-06 12:45:06 +03:00
renovate[bot] 39e5bb4817
Add renovate.json 2025-10-06 08:02:52 +00:00
Vjacheslav Trushkin 2afff788df chore: update version number 2025-02-12 00:24:55 +02:00
Vjacheslav Trushkin 8300891cba chore: update dependencies 2025-02-12 00:20:29 +02:00
Vjacheslav Trushkin f62b8ba8e6 chore: clean up params in svg query 2025-02-12 00:16:48 +02:00
Vjacheslav Trushkin 65b0eca32e chore: update dependencies 2024-10-16 20:23:53 +03:00
Vjacheslav Trushkin a02dd19d51 chore: fix error codes 2024-10-16 20:20:08 +03:00
Vjacheslav Trushkin e44822a40d chore: allow updating specific importer 2024-05-14 16:33:31 +03:00
Vjacheslav Trushkin c5260541b8 chore: move github data to .github repo 2024-05-14 15:55:46 +03:00
Vjacheslav Trushkin 1d55e2dafd chore: publish as latest tag 2024-04-26 16:16:10 +03:00
Vjacheslav Trushkin 5d296cd782 chore: publish 3.1.0 2024-04-26 16:14:16 +03:00
Vjacheslav Trushkin 9c6a063676 chore: use null constructor when cloning icon set 2024-04-25 23:37:49 +03:00
Vjacheslav Trushkin c153237943 chore: revert mlly change 2024-02-14 11:05:50 +02:00
Vjacheslav Trushkin 283142a8d7 chore: update dependencies, log icon sets package version 2024-02-13 21:16:48 +02:00
Vjacheslav Trushkin c1ac0a5609 chore: update ci 2024-02-07 10:37:42 +02:00
Vjacheslav Trushkin 49a077bc7b chore: update ci 2024-02-07 10:36:29 +02:00
Vjacheslav Trushkin cb8baf9acf chore: update dependencies, use mlly to resolve packages 2024-02-07 10:33:40 +02:00
Vjacheslav Trushkin 0b9427baa4 chore: allow npm fallback if cannot resolve local package 2024-01-23 23:46:40 +02:00
Vjacheslav Trushkin 95423f118b chore: fix full local package importer 2024-01-18 21:50:58 +02:00
Vjacheslav Trushkin a391ce96cf chore: premade importer for npm package 2024-01-17 18:06:14 +02:00
Vjacheslav Trushkin 2963c7a666 chore: add options for init function, expose loading state 2024-01-17 14:29:31 +02:00
Vjacheslav Trushkin ceb3fc4394 chore: split api responses from server, allowing reuse, prepare npm package 2024-01-17 09:45:43 +02:00
Vjacheslav Trushkin 2181b8022c chore: change default redirect 2024-01-16 22:22:28 +02:00
Vjacheslav Trushkin 6471e051e9 chore: switch to es modules 2024-01-16 21:02:18 +02:00
Vjacheslav Trushkin 32582d8dae chore: update dependencies 2024-01-16 18:18:22 +02:00
Vjacheslav Trushkin ceaea7ceca chore: update dependencies 2023-05-31 23:16:00 +03:00
Vjacheslav Trushkin 5994184446 chore: update license years and doc links 2023-05-31 23:14:06 +03:00
Vjacheslav Trushkin c53a1e7a82 feat: add min parameter to search query 2023-05-22 08:57:19 +03:00
Vjacheslav Trushkin bcad340030 chore: publish version 3.0.2 2023-05-18 22:41:50 +03:00
Vjacheslav Trushkin 2e2ca0f3b2 chore: allow different partial keywords for multiple searches 2023-05-18 22:41:15 +03:00
Vjacheslav Trushkin bdd286b32d chore: publish 3.0.1 2023-05-18 18:59:50 +03:00
Vjacheslav Trushkin 5c31cfb4fd fix: prevent aliases from messing up search results 2023-05-18 18:55:56 +03:00
Vjacheslav Trushkin 21d5bdb462 chore: improved sorting for search results 2023-05-18 11:03:22 +03:00
Vjacheslav Trushkin 5cb4d91fb7 fix: search for combined keywords 2023-05-17 23:03:03 +03:00
Vjacheslav Trushkin 1c3a9af9c6 chore: update dependencies 2023-05-17 21:40:55 +03:00
Vjacheslav Trushkin a4fa8bfb4b chore: publish 3.0.0 2023-02-08 16:31:01 +02:00
Vjacheslav Trushkin 02cadfd2d9 chore: update dependencies 2023-02-08 16:28:41 +02:00
Vjacheslav Trushkin 2d8424a40e fix: allow changing docker repo 2023-01-08 23:17:51 +02:00
Vjacheslav Trushkin 20a8ad39ce chore: redirect to docs, do not copy .env in Docker 2023-01-08 14:23:04 +02:00
Vjacheslav Trushkin 8be5f675e5 chore: Docker commands 2023-01-08 13:46:52 +02:00
Vjacheslav Trushkin 624b63deb8 chore: rename Docker image to iconify/api, update Node version 2023-01-08 13:19:35 +02:00
Vjacheslav Trushkin e4e66a0a75 chore: update readme 2023-01-08 13:09:20 +02:00
Vjacheslav Trushkin 9bc5b3f3b8 fix: working Docker file 2023-01-08 12:58:35 +02:00
Vjacheslav Trushkin 6c613eeba6 feat: import icon sets from 'icons' directory, env variable to control Iconify icon sets import 2023-01-08 12:56:10 +02:00
96 changed files with 4568 additions and 4364 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

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

18
.npmignore Normal file
View File

@ -0,0 +1,18 @@
/.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=16
ARG NODE_VERSION=22
ARG OS=bullseye-slim
ARG ICONIFY_API_VERSION=3.0.0
ARG ICONIFY_API_VERSION=3.2.0
ARG SRC_PATH=./
#### Stage BASE ########################################################################################################
FROM ${ARCH}/node:${NODE_VERSION}-${OS} AS base
FROM --platform=${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
@ -28,10 +28,6 @@ RUN set -ex && \
nano \
git && \
mkdir -p /data/iconify-api && \
npm i selfupdate --location=global && \
deluser --remove-home node && \
useradd --home-dir /data/iconify-api --uid 1000 --shell /bin/bash iconify-api && \
chown -R iconify-api:root /data/iconify-api && chmod -R g+rwX /data/iconify-api && \
apt-get clean && \
rm -rf /tmp/* && \
# Restore the original sources.list
@ -46,15 +42,19 @@ WORKDIR /data/iconify-api
FROM base AS iconify-api-install
ARG SRC_PATH
# Make CERTAIN peer dependencies are installed, otherwise this will very likely fail
COPY ${SRC_PATH} /data/iconify-api/
COPY init.sh /init.sh
# Copy package files, install dependencies
COPY ${SRC_PATH}*.json ./
RUN npm ci
RUN cp -fR /data/iconify-api/src/config /data/config_default && \
npm install
# Copy src and icons
COPY ${SRC_PATH}src/ /data/iconify-api/src/
COPY ${SRC_PATH}icons/ /data/iconify-api/icons/
#### Stage RELEASE #####################################################################################################
FROM iconify-api-install AS RELEASE
# Build API
RUN npm run build
#### Stage release #####################################################################################################
FROM iconify-api-install AS release
ARG BUILD_DATE
ARG BUILD_VERSION
ARG BUILD_REF
@ -65,13 +65,13 @@ ARG TAG_SUFFIX=default
LABEL org.label-schema.build-date=${BUILD_DATE} \
org.label-schema.docker.dockerfile="Dockerfile" \
org.label-schema.license="MIT" \
org.label-schema.name="Iconify API.js" \
org.label-schema.name="Iconify API" \
org.label-schema.version=${BUILD_VERSION} \
org.label-schema.description="Node.js version of api.iconify.design" \
org.label-schema.url="https://github.com/iconify/api.js" \
org.label-schema.url="https://github.com/iconify/api" \
org.label-schema.vcs-ref=${BUILD_REF} \
org.label-schema.vcs-type="Git" \
org.label-schema.vcs-url="https://github.com/iconify/api.js" \
org.label-schema.vcs-url="https://github.com/iconify/api" \
org.label-schema.arch=${ARCH} \
authors="Vjacheslav Trushkin"
@ -86,4 +86,4 @@ EXPOSE 3000
# Add a healthcheck (default every 30 secs)
HEALTHCHECK CMD curl http://localhost:3000/ || exit 1
ENTRYPOINT ["/init.sh"]
CMD ["npm", "run", "start"]

View File

@ -6,6 +6,29 @@ 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`.
If you want to customise config, fork this repo, customise source code, then build Docker image and deploy API.
To run a Docker image, run `docker run -d -p 3000:3000 iconify/api` (change first 3000 to port you want to run API on).
NPM commands for working with Docker images:
- `npm run docker:build` - builds Docker image.
- `npm run docker:start` - starts Docker container on port 3000.
- `npm run docker:stop` - stops all Iconify API Docker containers.
- `npm run docker:cleanup` - removes all unused Iconify API Docker containers.
There is no command to remove unused images because of Docker limitations. You need to do it manually from Docker Desktop or command line.
## How to use it
First, you need to install NPM dependencies and run build script:
@ -24,6 +47,7 @@ npm run start
By default, server will:
- Automatically load latest icons from [`@iconify/json`](https://github.com/iconify/icon-sets).
- Load custom icon sets from `icons` directory.
- Serve data on port 3000.
You can customise API to:
@ -54,12 +78,21 @@ Options that can be changed with environment variables and their default values
- `HOST=0.0.0.0`: IP address or hostname HTTP server listens on.
- `PORT=3000`: port HTTP server listens on.
- `ICONIFY_SOURCE=full`: source for Iconify icon sets. Set to `full` to use `@iconify/json` package, `split` to use `@iconify-json/*` packages, `none` to use only custom icon sets.
- `REDIRECT_INDEX=https://iconify.design/`: redirect for `/` route. API does not serve any pages, so index page redirects to main website.
- `STATUS_REGION=`: custom text to add to `/version` route response. Iconify API is ran on network of servers, visitor is routed to closest server. It is used to tell which server user is connected to.
- `ENABLE_ICON_LISTS=true`: enables `/collections` route that lists icon sets and `/collection?prefix=whatever` route to get list of icons. Used by icon pickers. Disable it if you are using API only to serve icon data.
- `ENABLE_SEARCH_ENGINE=true`: enables `/search` route. Requires `ENABLE_ICON_LISTS` to be enabled.
- `ALLOW_FILTER_ICONS_BY_STYLE=true`: allows searching for icons based on fill or stroke, such as adding `style=fill` to search query. This feature uses a bit of memory, so it can be disabled. Requires `ENABLE_SEARCH_ENGINE` to be enabled.
### Memory management
By default, API will use memory management functions. It stores only recently used icons in memory, reducing memory usage.
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).
### Updating icons
Icons are automatically updated when server starts.
@ -98,7 +131,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://docs.iconify.design/api/).
Full documentation is available on [Iconify documentation website](https://iconify.design/docs/api/).
## Sponsors
@ -116,4 +149,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 Vjacheslav Trushkin / Iconify OÜ
© 2022-PRESENT Vjacheslav Trushkin

View File

@ -5,13 +5,13 @@
#./docker.sh arm64v8
# To test the docker image a command like this can be used:
#docker run --rm -p 3123:3000 --name iconify-api -v $(realpath "../iconify-cache"):/data/iconify-api/cache -v $(realpath "../iconify-config"):/data/iconify-api/src/config iconify/iconify-api:latest
#docker run --rm -p 3123:3000 --name iconify-api -v /absolute/path/iconify-cache:/data/iconify-api/cache -v /absolute/path/iconify-config:/data/iconify-api/src/config iconify/iconify-api:latest
#docker run --rm -p 3123:3000 --name iconify-api -v $(realpath "../iconify-cache"):/data/iconify-api/cache -v $(realpath "../iconify-config"):/data/iconify-api/src/config iconify/api:latest
#docker run --rm -p 3123:3000 --name iconify-api -v /absolute/path/iconify-cache:/data/iconify-api/cache -v /absolute/path/iconify-config:/data/iconify-api/src/config iconify/api:latest
DOCKER_REPO=iconify/api
ICONIFY_API_REPO=$(realpath "./")
BUILD_SOURCE=$(realpath "./")
SHARED_DIR=$BUILD_SOURCE/../shared
DOCKERFILE=$(realpath "./Dockerfile")
REPO_BRANCH="dev3"
SRC_PATH="./"
if [ -z "$1" ]; then
ARCH=amd64
@ -21,19 +21,10 @@ else
fi
echo "Starting to build for arch: $ARCH"
echo "Build BASE dir: $BUILD_SOURCE"
if [ ! -s "./package.json" ] && [ -s "./iconify-api.js/package.json" ]; then
# If the repo is not the same as where the Docker file is located,
# this will fix all paths
ICONIFY_API_REPO=$(realpath "./iconify-api.js/")
SRC_PATH="iconify-api.js/"
cd $ICONIFY_API_REPO
git checkout $REPO_BRANCH
cd $BUILD_SOURCE
fi
export ICONIFY_API_VERSION=$(grep -oE "\"version\": \"(\w*.\w*.\w*(-\w*)?)" $ICONIFY_API_REPO/package.json | cut -d\" -f4)
export ICONIFY_API_VERSION=$(grep -oE "\"version\": \"([0-9]+.[0-9]+.[a-z0-9.-]+)" $ICONIFY_API_REPO/package.json | cut -d\" -f4)
echo "iconify-api.js version: ${ICONIFY_API_VERSION}"
echo "Iconify API version: ${ICONIFY_API_VERSION}"
mkdir -p $BUILD_SOURCE/tmp
@ -62,6 +53,6 @@ time docker build --rm=false \
--build-arg TAG_SUFFIX=default \
--build-arg SRC_PATH="$SRC_PATH" \
--file $DOCKERFILE \
--tag iconify/iconify-api:latest --tag iconify/iconify-api:${ICONIFY_API_VERSION} $BUILD_SOURCE
--tag ${DOCKER_REPO}:latest --tag ${DOCKER_REPO}:${ICONIFY_API_VERSION} $BUILD_SOURCE
rm -fR $BUILD_SOURCE/tmp

26
icons/README.md Normal file
View File

@ -0,0 +1,26 @@
# Icons
This directory contains custom icon sets and icons.
## Icon sets
Icon sets are stored in IconifyJSON format.
You can use Iconify Tools to create icon sets. See [Iconify Tools documentation](https://docs.iconify.design/tools/tools2/).
Each icon set has prefix. For icon set to be imported, filename must match icon set prefix, for example, `line-md.json` for icon set with `line-md` prefix.
Icon sets that have `info` property and are not marked as hidden, appear in icon sets list and search results (unless those features are disabled).
If you want icon set to be hidden, all you have to do is not add `info` property when creating icon set or remove it.
## Icons
TODO
Currently not supported yet. Create icon sets instead.
## Conflicts
If API serves both custom and Iconify icon sets, it is possible to have conflicting names. If 2 icon sets with identical prefix exist in both sources, custom icon set will be used.
To disable Iconify icon sets, set env variable `ICONIFY_SOURCE` to `none`. You can use `.env` file in root directory.

14
init.sh
View File

@ -1,14 +0,0 @@
#!/bin/bash -e
# This file is included in the Docker image
exit_func() {
echo "SIGTERM detected"
exit 1
}
trap exit_func SIGTERM SIGINT
echo "Initializing Iconify API.js..."
cd /data/iconify-api
# Only copy files which don't exist in target
cp -rn /data/config_default/. /data/iconify-api/src/config/
npm run build
node --expose-gc lib/index.js

2
license.txt Executable file → Normal file
View File

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

6600
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,35 +3,40 @@
"description": "Iconify API",
"author": "Vjacheslav Trushkin",
"license": "MIT",
"private": true,
"version": "3.0.0-beta.3",
"version": "3.2.0",
"type": "module",
"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@8.19.2",
"packageManager": "npm@11.6.4",
"engines": {
"node": ">=16.15.0"
"node": ">=22.20.0"
},
"scripts": {
"build": "tsc -b",
"test": "vitest --config vitest.config.mjs",
"start": "node --expose-gc lib/index.js"
"start": "node --expose-gc lib/index.js",
"docker:build": "./docker.sh",
"docker:start": "docker run -d -p 3000:3000 iconify/api",
"docker:stop": "docker ps -q --filter ancestor=iconify/api | xargs -r docker stop",
"docker:cleanup": "docker ps -q -a --filter ancestor=iconify/api | xargs -r docker rm",
"docker:publish": "docker push iconify/api"
},
"dependencies": {
"@fastify/formbody": "^7.4.0",
"@iconify/tools": "^2.2.0",
"@fastify/formbody": "^8.0.2",
"@iconify/tools": "^5.0.0",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^2.0.11",
"dotenv": "^16.0.3",
"fastify": "^4.11.0"
"@iconify/utils": "^3.1.0",
"dotenv": "^17.2.3",
"fastify": "^5.6.2"
},
"devDependencies": {
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
"typescript": "^4.9.4",
"vitest": "^0.26.3"
"@types/jest": "^30.0.0",
"@types/node": "^24.10.1",
"typescript": "^5.9.3",
"vitest": "^4.0.14"
}
}

27
renovate.json Normal file
View File

@ -0,0 +1,27 @@
{
"$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';
import type { SplitIconSetConfig } from '../types/config/split';
import type { MemoryStorageConfig } from '../types/storage';
import type { AppConfig } from '../types/config/app.js';
import type { SplitIconSetConfig } from '../types/config/split.js';
import type { MemoryStorageConfig } from '../types/storage.js';
/**
* Main configuration
*/
export const appConfig: AppConfig = {
// Index page
redirectIndex: 'https://iconify.design/',
redirectIndex: 'https://iconify.design/docs/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';
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';
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';
/**
* Sources
@ -20,8 +20,8 @@ export async function getImporters(): Promise<Importer[]> {
*
* Uses pre-configured importers. See `importers` sub-directory
*/
type IconifyIconSetsOptions = 'full' | 'split' | false;
const iconifyIconSets = 'full' as IconifyIconSetsOptions;
type IconifyIconSetsOptions = 'full' | 'split' | 'none';
const iconifyIconSets = (process.env['ICONIFY_SOURCE'] || 'full') as IconifyIconSetsOptions;
switch (iconifyIconSets) {
case 'full':
@ -34,11 +34,14 @@ export async function getImporters(): Promise<Importer[]> {
}
/**
* Add custom icons from `json` directory
* Add custom icons from `icons` directory
*/
if (await directoryExists('json')) {
if (await directoryExists('icons')) {
importers.push(
createJSONDirectoryImporter(new DirectoryDownloader<ImportedData>('json'), {
createJSONDirectoryImporter(new DirectoryDownloader<ImportedData>('icons'), {
// Skip icon sets with mismatched prefix
ignoreInvalidPrefix: false,
// Filter icon sets. Returns true if icon set should be included, false if not.
filter: (prefix) => {
return true;

View File

@ -0,0 +1,40 @@
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';
import { createIconSetsPackageImporter } from '../../importers/full/json';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote';
import type { ImportedData } from '../../types/importers/common';
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';
/**
* Importer for all icon sets from `@iconify/json` package

View File

@ -1,7 +1,7 @@
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';
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';
// 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';
import type { IconSetIconsListIcons, IconSetAPIv2IconsList } from '../../../types/icon-set/extra.js';
/**
* Prepare data for icons list API v2 response

View File

@ -1,20 +1,20 @@
import type { IconifyAliases, IconifyJSON, IconifyOptional } from '@iconify/types';
import { defaultIconProps } from '@iconify/utils/lib/icon/defaults';
import { appConfig } from '../../../config/app';
import { appConfig } from '../../../config/app.js';
import type {
IconSetIconNames,
IconSetIconsListIcons,
IconSetIconsListTag,
IconStyle,
} from '../../../types/icon-set/extra';
import { getIconStyle } from './style';
} from '../../../types/icon-set/extra.js';
import { getIconStyle } from './style.js';
const customisableProps = Object.keys(defaultIconProps) as (keyof IconifyOptional)[];
/**
* Generate icons tree
*/
export function generateIconSetIconsTree(iconSet: IconifyJSON): IconSetIconsListIcons {
export function generateIconSetIconsTree(iconSet: IconifyJSON, commonChunks?: string[]): IconSetIconsListIcons {
const iconSetIcons = iconSet.icons;
const iconSetAliases = iconSet.aliases || (Object.create(null) as IconifyAliases);
@ -245,7 +245,10 @@ export function generateIconSetIconsTree(iconSet: IconifyJSON): IconSetIconsList
const iconKeywords: Set<string> = new Set();
for (let i = 0; i < icon.length; i++) {
icon[i].split('-').forEach((chunk) => {
const name = icon[i];
// Add keywords
name.split('-').forEach((chunk) => {
if (iconKeywords.has(chunk)) {
return;
}
@ -253,6 +256,17 @@ export function generateIconSetIconsTree(iconSet: IconifyJSON): IconSetIconsList
(keywords[chunk] || (keywords[chunk] = new Set())).add(icon);
});
}
// Check for length based on first name
if (commonChunks) {
for (let j = 0; j < commonChunks.length; j++) {
const chunk = commonChunks[j];
if (name.startsWith(chunk + '-') || name.endsWith('-' + chunk)) {
icon._l = name.length - chunk.length - 1;
break;
}
}
}
}
// Icon style

View File

@ -1,4 +1,4 @@
import type { IconStyle } from '../../../types/icon-set/extra';
import type { IconStyle } from '../../../types/icon-set/extra.js';
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';
import type { IconSetIconsListIcons } from '../../../types/icon-set/extra.js';
/**
* 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';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split';
import type { SplitIconSetConfig } from '../../../types/config/split.js';
import type { SplitIconifyJSONMainData } from '../../../types/icon-set/split.js';
const iconDimensionProps = Object.keys(defaultIconDimensions) as (keyof typeof defaultIconDimensions)[];
@ -21,7 +21,14 @@ export function splitIconSetMainData(iconSet: IconifyJSON): SplitIconifyJSONMain
for (let i = 0; i < iconSetMainDataProps.length; i++) {
const prop = iconSetMainDataProps[i];
if (iconSet[prop]) {
result[prop as 'prefix'] = iconSet[prop as 'prefix'];
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'];
}
} else if (prop === 'aliases') {
result[prop] = Object.create(null);
}

View File

@ -1,26 +1,22 @@
import type { IconifyIcons, IconifyJSON } from '@iconify/types';
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 { 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';
/**
* Storage
*/
export const iconSetsStorage = createStorage<IconifyIcons>(storageConfig);
/**
* Themes to copy
*/
const themeKeys: (keyof StorageIconSetThemes)[] = ['themes', 'prefixes', 'suffixes'];
/**
* Counter for prefixes
*/
@ -36,7 +32,33 @@ export function storeLoadedIconSet(
storage: MemoryStorage<IconifyIcons> = iconSetsStorage,
config: SplitIconSetConfig = splitIconSetConfig
) {
const icons = generateIconSetIconsTree(iconSet);
let themes: StorageIconSetThemes | undefined;
let themeParts: string[] | undefined;
if (appConfig.enableIconLists) {
// Get themes
if (appConfig.enableIconLists) {
const themesList: StorageIconSetThemes = {};
for (let i = 0; i < themeKeys.length; i++) {
const key = themeKeys[i];
if (iconSet[key]) {
themesList[key as 'prefixes'] = iconSet[key as 'prefixes'];
themes = themesList;
}
}
// Get common parts of icon names for optimised search
if (appConfig.enableSearchEngine) {
const data = findIconSetThemes(iconSet);
if (data.length) {
themeParts = data;
}
}
}
}
// Get icons
const icons = generateIconSetIconsTree(iconSet, themeParts);
removeBadIconSetItems(iconSet, icons);
// Fix icons counter
@ -47,17 +69,6 @@ export function storeLoadedIconSet(
// Get common items
const common = splitIconSetMainData(iconSet);
// Get themes
const themes: StorageIconSetThemes = {};
if (appConfig.enableIconLists) {
for (let i = 0; i < themeKeys.length; i++) {
const key = themeKeys[i];
if (iconSet[key]) {
themes[key as 'prefixes'] = iconSet[key as 'prefixes'];
}
}
}
// Get number of chunks
const chunksCount = getIconSetSplitChunksCount(iconSet.icons, config);
@ -94,16 +105,16 @@ export function storeLoadedIconSet(
items: storedItems,
tree,
icons,
themes,
};
if (iconSet.info) {
result.info = iconSet.info;
}
if (appConfig.enableIconLists) {
for (const key in themes) {
result.themes = themes;
break;
}
result.apiV2IconsCache = prepareAPIv2IconsList(iconSet, icons);
if (appConfig.enableSearchEngine && themeParts?.length) {
result.themeParts = themeParts;
}
}
done(result);
}

View File

@ -0,0 +1,69 @@
import { IconifyJSON } from '@iconify/types';
import { StorageIconSetThemes } from '../../../types/icon-set/storage.js';
/**
* Themes to copy
*/
export const themeKeys: (keyof StorageIconSetThemes)[] = ['prefixes', 'suffixes'];
/**
* Hardcoded list of themes
*
* Should contain only simple items, without '-'
*/
const hardcodedThemes: Set<string> = new Set([
'baseline',
'outline',
'round',
'sharp',
'twotone',
'thin',
'light',
'bold',
'fill',
'duotone',
'linear',
'line',
'solid',
'filled',
'outlined',
]);
/**
* Find icon
*/
export function findIconSetThemes(iconSet: IconifyJSON): string[] {
const results: Set<string> = new Set();
// Add prefixes / suffixes from themes
themeKeys.forEach((key) => {
const items = iconSet[key];
if (items) {
Object.keys(items).forEach((item) => {
if (item) {
results.add(item);
}
});
}
});
// Check all icons and aliases
const names = Object.keys(iconSet.icons).concat(Object.keys(iconSet.aliases || {}));
for (let i = 0; i < names.length; i++) {
const name = names[i];
const parts = name.split('-');
if (parts.length > 1) {
const firstChunk = parts.shift() as string;
const lastChunk = parts.pop() as string;
if (hardcodedThemes.has(firstChunk)) {
results.add(firstChunk);
}
if (hardcodedThemes.has(lastChunk)) {
results.add(lastChunk);
}
}
}
// Return as array, sorted by length
return Array.from(results).sort((a, b) => b.length - a.length);
}

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';
import type { StoredIconSet } from '../../../types/icon-set/storage';
import { searchSplitRecordsTree } from '../../storage/split';
import { getStoredItem } from '../../storage/get';
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';
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';
import { searchSplitRecordsTreeForSet } from '../../storage/split';
import { getStoredItem } from '../../storage/get';
import type { StoredIconSet } from '../../../types/icon-set/storage.js';
import { searchSplitRecordsTreeForSet } from '../../storage/split.js';
import { getStoredItem } from '../../storage/get.js';
/**
* Get list of icons that must be retrieved

View File

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

View File

@ -22,6 +22,13 @@ 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';
import type { IconSetEntry } from '../types/importers';
import type { SearchIndexData } from '../types/search';
import { appConfig } from '../config/app.js';
import type { IconSetEntry } from '../types/importers.js';
import type { SearchIndexData } from '../types/search.js';
interface SearchIndex {
data?: SearchIndexData;

View File

@ -1,10 +1,10 @@
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';
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';
/**
* Run search
@ -41,145 +41,160 @@ export function search(
return;
}
// Check for partial
const partial = keywords.partial;
let partialKeywords: string[] | undefined;
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
partialKeywords = exists ? [partial].concat(cache) : cache.slice(0);
}
}
// Get prefixes
const basePrefixes = filterSearchPrefixes(data, iconSets, fullParams);
// Prepare variables
const addedIcons = Object.create(null) as Record<string, Set<IconSetIconNames>>;
const results: string[] = [];
// Results, sorted
interface TemporaryResultItem {
length: number;
partial: boolean;
names: string[];
}
const allMatches: TemporaryResultItem[] = [];
let allMatchesLength = 0;
const getMatchResult = (length: number, partial: boolean): TemporaryResultItem => {
const result = allMatches.find((item) => item.length === length && item.partial === partial);
if (result) {
return result;
}
const newItem: TemporaryResultItem = {
length,
partial,
names: [],
};
allMatches.push(newItem);
return newItem;
};
const limit = params.limit;
const softLimit = params.softLimit;
// Run all searches
const check = (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[]>;
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));
}
const search = keywords.searches[searchIndex] as ExtendedSearchKeywordsEntry;
// 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;
search.filteredPrefixes = filteredPrefixes;
}
if (!filteredPrefixes.length) {
return;
}
// Filter by required keywords
for (let i = 0; i < search.keywords.length; i++) {
filteredPrefixes = filteredPrefixes.filter((prefix) =>
data.keywords[search.keywords[i]].has(prefix)
);
}
// Get keywords
const testKeywords = partial ? search.keywords.concat([partial]) : search.keywords;
const testMatches = search.test ? search.test.concat(testKeywords) : testKeywords;
search.filteredPrefixes = filteredPrefixes;
}
if (!filteredPrefixes.length) {
// 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!
continue;
}
// Get keywords
const testKeywords = partial ? search.keywords.concat([partial]) : search.keywords;
const testMatches = search.test ? search.test.concat(testKeywords) : testKeywords;
// 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 for partial keyword if testing for exact match
if (partial) {
filteredPrefixes = filteredPrefixes.filter((prefix) => data.keywords[partial].has(prefix));
if (!matches) {
// Copy all matches
matches = Array.from(keywordMatches);
} else {
// Match previous set
matches = matches.filter((item) => keywordMatches.has(item));
}
}
// 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;
}
// 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;
// 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;
}
if (!matches) {
// Copy all matches
matches = Array.from(keywordMatches);
} else {
// Match previous set
matches = matches.filter((item) => keywordMatches.has(item));
// 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;
}
}
// 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;
}
// 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;
}
// 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
const name = item.find((name) => {
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);
results.push(prefix + ':' + name);
if (results.length >= limit) {
return;
}
}
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;
}
}
}
@ -187,23 +202,72 @@ export function search(
}
};
// Check all keywords
if (!partialKeywords) {
check();
} else {
let partial: string | undefined;
while ((partial = partialKeywords.shift())) {
check(partial);
if (results.length >= limit) {
break;
const runAllSearches = (isExact: boolean) => {
for (let searchIndex = 0; searchIndex < keywords.searches.length; searchIndex++) {
const search = keywords.searches[searchIndex];
const partial = search.partial;
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) {
continue;
}
// Exact match
runSearch(search, true);
}
// Check limit
if (!isExact && allMatchesLength >= limit) {
return;
}
}
};
// Check all keywords
try {
runAllSearches(true);
if (allMatchesLength < limit) {
runAllSearches(false);
}
} catch (err) {
console.warn('Got exception when searching for:', params);
console.error(err);
}
// Generate results
if (results.length) {
if (allMatchesLength) {
// Sort matches
allMatches.sort((a, b) => (a.partial !== b.partial ? (a.partial ? 1 : -1) : a.length - b.length));
// Extract results
const results: string[] = [];
const prefixes: Set<string> = new Set();
for (let i = 0; i < allMatches.length && (softLimit || results.length < limit); i++) {
const { names } = allMatches[i];
for (let j = 0; j < names.length && (softLimit || results.length < limit); j++) {
const name = names[j];
results.push(name);
prefixes.add(name.split(':').shift() as string);
}
}
return {
prefixes: Object.keys(addedIcons).filter((prefix) => !!addedIcons[prefix]?.size),
prefixes: Array.from(prefixes),
names: results,
hasMore: results.length >= limit,
};

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { matchIconName } from '@iconify/utils/lib/icon/name';
import { paramToBoolean } from '../../misc/bool';
import type { IconStyle } from '../../types/icon-set/extra';
import type { SearchKeywords, SearchKeywordsEntry } from '../../types/search';
import { minPartialKeywordLength } from './partial';
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';
interface SplitOptions {
// Can include prefix
@ -21,23 +21,18 @@ interface SplitResultItem {
// Strings to test icon name
test?: string[];
}
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 keyword. It is last chunk of last keyword, which cannot be treated as prefix
partial?: string;
}
type SplitResult = SplitResultItem[];
export function splitKeywordEntries(values: string[], options: SplitOptions): SplitResult | undefined {
const results: SplitResult = {
searches: [],
};
const results: SplitResult = [];
let invalid = false;
// Split each entry
// Split each entry into arrays
interface Entry {
value: string;
empty: boolean;
@ -83,8 +78,14 @@ 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 add(items: Entry[], keywords: Set<string>, test: Set<string>, checkPartial: boolean) {
function addToSet(items: Entry[], set: ResultsSet, allowPartial: boolean) {
let partial: string | undefined;
// Add keywords
@ -92,10 +93,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 && checkPartial && value.value.length >= minPartialKeywordLength) {
if (i === max && allowPartial && value.value.length >= minPartialKeywordLength) {
partial = value.value;
} else {
keywords.add(value.value);
set.keywords.add(value.value);
}
}
}
@ -103,20 +104,30 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
// Get test value
const testValue = valuesToString(items);
if (testValue) {
test.add(testValue);
set.test.add(testValue);
}
// 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;
// Add partial
if (allowPartial && partial) {
if (set.partial && set.partial !== partial) {
console.error('Different partial keywords. This should not be happening!');
}
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);
}
}
@ -134,22 +145,14 @@ export function splitKeywordEntries(values: string[], options: SplitOptions): Sp
const prefix = firstItem.length > 1 ? valuesToString(firstItem) : firstItem[0].value;
if (prefix) {
// Valid prefix
const keywords: Set<string> = new Set();
const test: Set<string> = new Set();
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
for (let i = 1; i <= lastIndex; i++) {
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);
addToSet(splitValues[i], set, options.partial && i === lastIndex);
}
addToResult(set, prefix);
}
}
}
@ -159,40 +162,76 @@ 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 keywords: Set<string> = new Set();
const test: Set<string> = new Set();
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
for (let i = 0; i <= lastIndex; i++) {
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);
addToSet(i ? splitValues[i] : modifiedFirstItem, set, options.partial && i === lastIndex);
}
addToResult(set, prefix);
}
}
// Add as is
const keywords: Set<string> = new Set();
const test: Set<string> = new Set();
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
for (let i = 0; i <= lastIndex; i++) {
add(splitValues[i], keywords, test, options.partial && i === lastIndex);
addToSet(splitValues[i], set, options.partial && i === lastIndex);
}
addToResult(set);
if (keywords.size || results.partial) {
const item: SplitResultItem = {
keywords: Array.from(keywords),
};
if (test.size) {
item.test = Array.from(test);
// Merge values
if (splitValues.length > 1) {
// Check which items can be used for merge
// Merge only simple keywords
const validIndexes: Set<number> = new Set();
for (let i = 0; i <= lastIndex; i++) {
const item = splitValues[i];
if (item.length === 1 && !item[0].empty) {
validIndexes.add(i);
}
}
if (validIndexes.size > 1) {
for (let startIndex = 0; startIndex < lastIndex; startIndex++) {
if (!validIndexes.has(startIndex)) {
continue;
}
for (let endIndex = startIndex + 1; endIndex <= lastIndex; endIndex++) {
if (!validIndexes.has(endIndex)) {
// Break loop
break;
}
// Generate new values list
const newSplitValues: Entry[][] = [
...splitValues.slice(0, startIndex),
[
{
value: splitValues
.slice(startIndex, endIndex + 1)
.map((item) => item[0].value)
.join(''),
empty: false,
},
],
...splitValues.slice(endIndex + 1),
];
const newLastIndex = newSplitValues.length - 1;
const set: ResultsSet = {
keywords: new Set(),
test: new Set(),
};
for (let i = 0; i <= newLastIndex; i++) {
addToSet(newSplitValues[i], set, options.partial && i === newLastIndex);
}
addToResult(set);
}
}
}
results.searches.push(item);
}
return results;
@ -425,7 +464,7 @@ export function splitKeyword(keyword: string, allowPartial = true): SearchKeywor
return;
}
const searches: SearchKeywordsEntry[] = entries.searches.map((item) => {
const searches: SearchKeywordsEntry[] = entries.map((item) => {
return {
...item,
prefixes: item.prefix
@ -446,6 +485,5 @@ 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';
import type { MemoryStorageItem, MemoryStorageCallback } from '../../types/storage.js';
/**
* Run all callbacks from storage

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { rm } from 'node:fs/promises';
import { appConfig } from '../../config/app';
import type { MemoryStorage } from '../../types/storage';
import { appConfig } from '../../config/app.js';
import type { MemoryStorage } from '../../types/storage.js';
/**
* 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';
import { addStorageToCleanup } from './cleanup';
import type { MemoryStorage, MemoryStorageItem } from '../../types/storage.js';
import { addStorageToCleanup } from './cleanup.js';
const createdDirs: Set<string> = new Set();

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
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';
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';
/**
* 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';
import { directoryExists } from '../../misc/files.js';
import type {
GitDownloaderOptions,
GitDownloaderVersion,
@ -12,7 +12,7 @@ import type {
GitLabDownloaderVersion,
NPMDownloaderOptions,
NPMDownloaderVersion,
} from '../../types/downloaders/remote';
} from '../../types/downloaders/remote.js';
/**
* 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';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../../types/downloaders/remote';
import { appConfig } from '../../config/app.js';
import type { RemoteDownloaderOptions, RemoteDownloaderVersion } from '../../types/downloaders/remote.js';
import {
isGitHubUpdateAvailable,
isGitLabUpdateAvailable,
isGitUpdateAvailable,
isNPMUpdateAvailable,
} from './check-update';
import { getDownloadDirectory } from './target';
} from './check-update.js';
import { getDownloadDirectory } from './target.js';
/**
* Download files from remote archive

View File

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

View File

@ -1,6 +1,6 @@
import { appConfig } from '../../config/app';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote';
import { getRemoteDownloaderCacheKey } from './key';
import { appConfig } from '../../config/app.js';
import type { RemoteDownloaderOptions } from '../../types/downloaders/remote.js';
import { getRemoteDownloaderCacheKey } from './key.js';
/**
* 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';
import { appConfig } from '../../config/app.js';
import type {
RemoteDownloaderType,
RemoteDownloaderVersion,
RemoteDownloaderVersionMixin,
} from '../../types/downloaders/remote';
} from '../../types/downloaders/remote.js';
// Storage
type StoredVersions = Record<string, RemoteDownloaderVersion>;

View File

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

View File

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

View File

@ -0,0 +1,45 @@
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);
}
}

44
src/http/helpers/send.ts Normal file
View File

@ -0,0 +1,44 @@
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,19 +1,21 @@
import fastify from 'fastify';
import fastifyFormBody from '@fastify/formbody';
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';
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';
/**
* Start HTTP server
@ -21,7 +23,9 @@ import { generateIconsStyleResponse } from './responses/css';
export async function startHTTPServer() {
// Create HTP server
const server = fastify({
caseSensitive: true,
routerOptions: {
caseSensitive: true,
},
});
// Support `application/x-www-form-urlencoded`
@ -81,19 +85,19 @@ export async function startHTTPServer() {
generateSVGResponse(name.prefix, name.name, req.query, res);
});
} else {
res.send(404);
res.code(404).send(errorText(404));
}
});
// Icons data: /prefix/icons.json, /prefix.json
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.json', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
handleIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
});
});
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').json', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
handleIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
});
});
@ -107,19 +111,19 @@ export async function startHTTPServer() {
// Icons data: /prefix/icons.js, /prefix.js
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.js', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
handleIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
});
});
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').js', (req, res) => {
runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
handleIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
});
});
// Last modification time
server.get('/last-modified', (req, res) => {
runWhenLoaded(() => {
generateLastModifiedResponse(req.query, res);
handleJSONResponse(req, res, createLastModifiedResponse);
});
});
@ -127,26 +131,26 @@ export async function startHTTPServer() {
// Icon sets list
server.get('/collections', (req, res) => {
runWhenLoaded(() => {
generateCollectionsListResponse(req.query, res);
handleJSONResponse(req, res, createCollectionsListResponse);
});
});
// Icons list, API v2
server.get('/collection', (req, res) => {
runWhenLoaded(() => {
generateAPIv2CollectionResponse(req.query, res);
handleJSONResponse(req, res, createAPIv2CollectionResponse);
});
});
// Icons list, API v1
server.get('/list-icons', (req, res) => {
runWhenLoaded(() => {
generateAPIv1IconsListResponse(req.query, res, false);
handleJSONResponse(req, res, (q) => createAPIv1IconsListResponse(q, false));
});
});
server.get('/list-icons-categorized', (req, res) => {
runWhenLoaded(() => {
generateAPIv1IconsListResponse(req.query, res, true);
handleJSONResponse(req, res, (q) => createAPIv1IconsListResponse(q, true));
});
});
@ -154,14 +158,14 @@ export async function startHTTPServer() {
// Search, currently version 2
server.get('/search', (req, res) => {
runWhenLoaded(() => {
generateAPIv2SearchResponse(req.query, res);
handleJSONResponse(req, res, createAPIv2SearchResponse);
});
});
// Keywords
server.get('/keywords', (req, res) => {
runWhenLoaded(() => {
generateKeywordsResponse(req.query, res);
handleJSONResponse(req, res, createKeywordsResponse);
});
});
}
@ -177,7 +181,7 @@ export async function startHTTPServer() {
// Options
server.options('/*', (req, res) => {
res.send(200);
res.code(204).header('Content-Length', '0').send();
});
// Robots
@ -197,21 +201,21 @@ export async function startHTTPServer() {
// Redirect
server.get('/', (req, res) => {
res.redirect(301, appConfig.redirectIndex);
res.redirect(appConfig.redirectIndex, 301);
});
// Error handling
server.setDefaultRoute((req, res) => {
server.setNotFoundHandler((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.setHeader(header.key, header.value);
res.header(header.key, header.value);
}
res.end();
res.send();
});
// Start it

View File

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

View File

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

View File

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

View File

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

View File

@ -1,45 +1,63 @@
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';
import { IconifyJSON } from '@iconify/types';
import { getStoredIconsData } from '../../data/icon-set/utils/get-icons.js';
import { iconSets } from '../../data/icon-sets.js';
/**
* Generate icons data
*/
export function generateIconsDataResponse(
export function createIconsDataResponse(
prefix: string,
wrapJS: boolean,
query: FastifyRequest['query'],
res: FastifyReply
) {
const q = (query || {}) as Record<string, string>;
const names = q.icons?.split(',');
q: Record<string, string | string[]>
): number | IconifyJSON | Promise<IconifyJSON | number> {
const iconNames = q.icons;
const names = typeof iconNames === 'string' ? iconNames.split(',') : iconNames;
if (!names || !names.length) {
// Missing or invalid icons parameter
res.send(404);
return;
}
// Check for JSONP
const wrap = checkJSONPQuery(q, wrapJS, 'SimpleSVG._loaderCallback');
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
return 404;
}
// Get icon set
const iconSet = iconSets[prefix];
if (!iconSet) {
// No such icon set
res.send(404);
return;
return 404;
}
// Get icons
// Get icons, possibly sync
let syncData: IconifyJSON | undefined;
let resolveData: undefined | ((data: IconifyJSON) => void);
getStoredIconsData(iconSet.item, names, (data) => {
// Send data
sendJSONResponse(data, q, wrap, res);
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);
}
});
}

View File

@ -1,27 +1,16 @@
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';
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';
/**
* Generate icons data
* Find full keywords for partial keyword
*/
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;
}
export function createKeywordsResponse(q: Record<string, string>): number | APIv3KeywordsResponse {
// Check if search data is available
const searchIndexData = searchIndex.data;
if (!searchIndexData) {
res.send(404);
return;
return 404;
}
const keywords = searchIndexData.keywords;
@ -32,15 +21,16 @@ export function generateKeywordsResponse(query: FastifyRequest['query'], res: Fa
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
res.send(400);
return;
return 400;
}
test = test.toLowerCase().trim();
@ -71,5 +61,5 @@ export function generateKeywordsResponse(query: FastifyRequest['query'], res: Fa
matches: failed || invalid ? [] : getPartialKeywords(test, suffixes, searchIndexData)?.slice(0) || [],
};
sendJSONResponse(response, q, wrap, res);
return response;
}

View File

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

View File

@ -1,11 +1,9 @@
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';
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';
const minSearchLimit = 32;
const maxSearchLimit = 999;
@ -14,28 +12,17 @@ const defaultSearchLimit = minSearchLimit * 2;
/**
* Send API v2 response
*/
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;
}
export function createAPIv2SearchResponse(q: Record<string, string>): number | APIv2SearchResponse {
// Check if search data is available
const searchIndexData = searchIndex.data;
if (!searchIndexData) {
res.send(404);
return;
return 404;
}
// Get query
const keyword = q.query;
if (!keyword) {
res.send(400);
return;
return 400;
}
// Convert to params
@ -49,18 +36,24 @@ export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res:
if (v2Query.limit) {
const limit = parseInt(v2Query.limit);
if (!limit) {
res.send(400);
return;
return 400;
}
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) {
res.send(400);
return;
return 400;
}
}
@ -121,5 +114,5 @@ export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res:
};
}
sendJSONResponse(response, q, wrap, res);
return response;
}

View File

@ -1,8 +1,14 @@
import { defaultIconDimensions, flipFromString, iconToHTML, iconToSVG, rotateFromString } from '@iconify/utils';
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 { defaultIconCustomisations, IconifyIconCustomisations } from '@iconify/utils/lib/customisations/defaults';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getStoredIconData } from '../../data/icon-set/utils/get-icon';
import { iconSets } from '../../data/icon-sets';
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';
/**
* Generate SVG
@ -12,7 +18,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
const iconSetItem = iconSets[prefix]?.item;
if (!iconSetItem) {
// No such icon set
res.send(404);
res.code(404).send(errorText(404));
return;
}
@ -20,7 +26,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.send(404);
res.code(404).send(errorText(404));
return;
}
@ -28,7 +34,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
getStoredIconData(iconSetItem, name, (data) => {
if (!data) {
// Invalid icon
res.send(404);
res.code(404).send(errorText(404));
return;
}
@ -38,8 +44,8 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
const customisations: IconifyIconCustomisations = {};
// Dimensions
customisations.width = q.width || defaultIconCustomisations.width;
customisations.height = q.height || defaultIconCustomisations.height;
customisations.width = cleanupQueryValue(q.width) || defaultIconCustomisations.width;
customisations.height = cleanupQueryValue(q.height) || defaultIconCustomisations.height;
// Rotation
customisations.rotate = q.rotate ? rotateFromString(q.rotate, 0) : 0;
@ -70,7 +76,7 @@ export function generateSVGResponse(prefix: string, name: string, query: Fastify
let html = iconToHTML(body, svg.attributes);
// Change color
const color = q.color;
const color = cleanupQueryValue(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';
import { triggerIconSetsUpdate } from '../../data/icon-sets';
import { runWhenLoaded } from '../../data/loading';
import { appConfig } from '../../config/app.js';
import { triggerIconSetsUpdate } from '../../data/icon-sets.js';
import { runWhenLoaded } from '../../data/loading.js';
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';
import { appConfig } from '../../config/app.js';
let version: string | undefined;

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { CustomDownloader } from '../../downloaders/custom';
import type { BaseCollectionsImporter, CreateIconSetImporter } from '../../types/importers/collections';
import type { ImportedData } from '../../types/importers/common';
import { createBaseCollectionsListImporter } from './base';
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';
/**
* Create importer for hardcoded list of icon sets

View File

@ -1,11 +1,13 @@
import { readFile } from 'node:fs/promises';
import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage';
import type { StoredIconSet } from '../../types/icon-set/storage';
import { prependSlash } from '../../misc/files';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import { prependSlash } from '../../misc/files.js';
export interface IconSetJSONOptions {
// Ignore bad prefix?
// false -> skip icon sets with mismatched prefix
// true -> import icon set with mismatched prefix
ignoreInvalidPrefix?: boolean;
}
@ -26,9 +28,12 @@ export async function importIconSetFromJSON(
}
if (data.prefix !== prefix) {
if (!options.ignoreInvalidPrefix) {
console.error(
`Error loading "${prefix}" icon set: bad prefix (enable ignoreInvalidPrefix option in importer to skip this check)`
);
if (options.ignoreInvalidPrefix === void 0) {
// Show warning if option is not set
console.error(
`Error loading "${prefix}" icon set: bad prefix (enable ignoreInvalidPrefix option in importer to import icon set)`
);
}
return;
}
data.prefix = 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';
import type { StoredIconSet } from '../../types/icon-set/storage';
import { appConfig } from '../../config/app';
import { asyncStoreLoadedIconSet } from '../../data/icon-set/store/storage.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js';
import { appConfig } from '../../config/app.js';
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';
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';
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';
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';
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';
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';
interface IconSetsPackageImporterOptions {
// Icon set filter

View File

@ -1,7 +1,7 @@
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';
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';
/**
* 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';
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 { 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';
interface JSONDirectoryImporterOptions extends IconSetJSONOptions {
// Icon set filter

View File

@ -1,11 +1,11 @@
import { readFile } from 'node:fs/promises';
import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { BaseDownloader } from '../../downloaders/base';
import type { ImportedData } from '../../types/importers/common';
import type { BaseFullImporter } from '../../types/importers/full';
import { createBaseImporter } from './base';
import { IconSetJSONOptions, importIconSetFromJSON } from '../common/icon-set-json';
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';
interface IconSetsPackageImporterOptions extends IconSetJSONOptions {
// Icon set filter
@ -23,6 +23,17 @@ 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';
import type { BaseIconSetImporter } from '../../types/importers/icon-set';
import type { IconSetImportedData } from '../../types/importers/common';
import { IconSetJSONPackageOptions, importIconSetFromJSONPackage } from '../common/json-package';
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';
interface JSONPackageIconSetImporterOptions extends IconSetJSONPackageOptions {
// Icon set prefix

View File

@ -1,7 +1,7 @@
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';
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';
interface JSONIconSetImporterOptions extends IconSetJSONOptions {
// Icon set prefix

View File

@ -1,30 +1,19 @@
import { config } from 'dotenv';
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';
import { loaded } from './data/loading.js';
import { startHTTPServer } from './http/index.js';
import { loadEnvConfig } from './misc/load-config.js';
import { initAPI } from './init.js';
(async () => {
// Configure environment
config();
loadEnvConfig();
// Reset old cache
await cleanupStorageCache(iconSetsStorage);
// Start HTTP server
startHTTPServer();
// Get all importers and load data
const importers = await getImporters();
for (let i = 0; i < importers.length; i++) {
await importers[i].init();
}
setImporters(importers);
updateIconSets();
// Init API
await initAPI();
// Loaded
loaded();

36
src/init.ts Normal file
View File

@ -0,0 +1,36 @@
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';
import { hashString } from './hash';
import type { FileEntry } from '../types/files.js';
import { hashString } from './hash.js';
/**
* List all files in directory

View File

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

View File

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

View File

@ -9,7 +9,11 @@ export type IconStyle = 'fill' | 'stroke';
* Extra props added to icons
*/
export interface ExtraIconSetIconNamesProps {
// Icon style
_is?: IconStyle;
// Name length without prefix
_l?: number;
}
/**

View File

@ -1,17 +1,13 @@
import type { IconifyIcons, IconifyInfo, IconifyJSON } 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';
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';
/**
* Themes
*/
export interface StorageIconSetThemes {
themes?: IconifyJSON['themes'];
prefixes?: IconifyJSON['prefixes'];
suffixes?: IconifyJSON['suffixes'];
}
export type StorageIconSetThemes = Pick<IconifyMetaData, 'prefixes' | 'suffixes'>;
/**
* Generated data
@ -36,6 +32,7 @@ export interface StoredIconSet {
// Themes
themes?: StorageIconSetThemes;
themeParts?: string[];
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import type { IconStyle } from './icon-set/extra';
import type { IconStyle } from './icon-set/extra.js';
/**
* List of keywords that can be used to autocomplete keyword
@ -50,6 +50,7 @@ export interface SearchParams {
// Search results limit
limit: number;
softLimit?: boolean; // True if limit can be exceeded
// Toggle partial matches
partial?: boolean;
@ -67,6 +68,9 @@ export interface SearchKeywordsEntry {
// Strings to test icon value
test?: string[];
// Partial keyword
partial?: string;
}
/**
@ -76,9 +80,6 @@ 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,7 +103,9 @@ export interface APIv2SearchParams extends APIv2CommonParams {
query: SearchQuery;
// Maximum number of items in response
limit?: number;
// 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.
// Start index for results
start?: number;

View File

@ -79,21 +79,21 @@ describe('Searching icons', () => {
'mdi-test-prefix:hand-cycle',
'mdi-test-prefix:power-cycle',
'mdi-test-prefix:bicycle',
'mdi-test-prefix:bicycle-basket',
'mdi-test-prefix:bicycle-cargo',
'mdi-test-prefix:bicycle-electric',
'mdi-test-prefix:bicycle-penny-farthing',
'emojione-v1:bicycle',
'mdi-test-prefix:battery-recycle',
'mdi-test-prefix:battery-recycle-outline',
'mdi-test-prefix:recycle',
'mdi-test-prefix:recycle-variant',
'mdi-test-prefix:water-recycle',
'mdi-test-prefix:unicycle',
'mdi-test-prefix:motorcycle',
'mdi-test-prefix:motorcycle-electric',
'mdi-test-prefix:motorcycle-off',
'emojione-v1:motorcycle',
'mdi-test-prefix:bicycle-cargo',
'mdi-test-prefix:water-recycle',
'mdi-test-prefix:bicycle-basket',
'mdi-test-prefix:motorcycle-off',
'mdi-test-prefix:battery-recycle',
'mdi-test-prefix:battery-recycle-outline',
'mdi-test-prefix:recycle-variant',
'mdi-test-prefix:bicycle-electric',
'mdi-test-prefix:motorcycle-electric',
'mdi-test-prefix:bicycle-penny-farthing',
],
hasMore: false,
});
@ -116,19 +116,19 @@ describe('Searching icons', () => {
'mdi-test-prefix:hand-cycle',
'mdi-test-prefix:power-cycle',
'mdi-test-prefix:bicycle',
'mdi-test-prefix:bicycle-basket',
'mdi-test-prefix:bicycle-cargo',
'mdi-test-prefix:bicycle-electric',
'mdi-test-prefix:bicycle-penny-farthing',
'mdi-test-prefix:battery-recycle',
'mdi-test-prefix:battery-recycle-outline',
'mdi-test-prefix:recycle',
'mdi-test-prefix:recycle-variant',
'mdi-test-prefix:water-recycle',
'mdi-test-prefix:unicycle',
'mdi-test-prefix:motorcycle',
'mdi-test-prefix:motorcycle-electric',
'mdi-test-prefix:bicycle-cargo',
'mdi-test-prefix:water-recycle',
'mdi-test-prefix:bicycle-basket',
'mdi-test-prefix:motorcycle-off',
'mdi-test-prefix:battery-recycle',
'mdi-test-prefix:battery-recycle-outline',
'mdi-test-prefix:recycle-variant',
'mdi-test-prefix:bicycle-electric',
'mdi-test-prefix:motorcycle-electric',
'mdi-test-prefix:bicycle-penny-farthing',
],
hasMore: false,
});

View File

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

View File

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

View File

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