2
0
mirror of https://github.com/iconify/iconify.git synced 2025-01-06 07:20:40 +00:00

Implement icon providers (similar to namespaces)

This commit is contained in:
Vjacheslav Trushkin 2020-05-29 22:08:45 +03:00
parent 0956dc7f9a
commit 2aad697264
31 changed files with 1204 additions and 535 deletions

View File

@ -2,14 +2,14 @@ import mocha from 'mocha';
import chai from 'chai';
import { FakeData, setFakeData, prepareQuery, sendQuery } from './fake-api';
import { API } from '@iconify/core/lib/api/';
import { setAPIModule } from '@iconify/core/lib/api/modules';
import { setDefaultAPIModule } from '@iconify/core/lib/api/modules';
import { setAPIConfig } from '@iconify/core/lib/api/config';
import { coreModules } from '@iconify/core/lib/modules';
const expect = chai.expect;
// Set API
setAPIModule({
setDefaultAPIModule({
prepare: prepareQuery,
send: sendQuery,
});
@ -21,7 +21,8 @@ function nextPrefix(): string {
}
describe('Testing fake API', () => {
it('Loading results', done => {
it('Loading results', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1', 'icon2'],
@ -41,24 +42,26 @@ describe('Testing fake API', () => {
height: 24,
},
};
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
setFakeData(prefix, data);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
setFakeData(provider, prefix, data);
// Attempt to load icons
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending) => {
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -68,7 +71,8 @@ describe('Testing fake API', () => {
);
});
it('Loading results with delay', done => {
it('Loading results with delay', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1', 'icon2'],
@ -89,23 +93,22 @@ describe('Testing fake API', () => {
height: 24,
},
};
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
setFakeData(prefix, data);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
setFakeData(provider, prefix, data);
// Attempt to load icons
const start = Date.now();
API.loadIcons(
[
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -113,10 +116,12 @@ describe('Testing fake API', () => {
(loaded, missing, pending) => {
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -129,7 +134,8 @@ describe('Testing fake API', () => {
);
});
it('Loading partial results', done => {
it('Loading partial results', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1'],
@ -146,21 +152,21 @@ describe('Testing fake API', () => {
height: 24,
},
};
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 20,
timeout: 100,
limit: 1,
},
prefix
);
setFakeData(prefix, data);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
rotate: 20,
timeout: 100,
limit: 1,
});
setFakeData(provider, prefix, data);
// Attempt to load icons
let counter = 0;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending) => {
try {
counter++;
@ -169,12 +175,14 @@ describe('Testing fake API', () => {
// Loaded icon1
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
]);
expect(pending).to.be.eql([
{
provider,
prefix,
name: 'icon2',
},

View File

@ -46,6 +46,7 @@ describe('Testing legacy finder', () => {
// Test all icons
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'home',
},
@ -54,6 +55,7 @@ describe('Testing legacy finder', () => {
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'account',
},
@ -62,6 +64,7 @@ describe('Testing legacy finder', () => {
testIcon(
{
provider: '',
prefix: 'ic',
name: 'baseline-account',
},

View File

@ -46,6 +46,7 @@ describe('Testing finder', () => {
// Test all icons
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'home',
},
@ -54,6 +55,7 @@ describe('Testing finder', () => {
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'account',
},
@ -62,6 +64,7 @@ describe('Testing finder', () => {
testIcon(
{
provider: '',
prefix: 'ic',
name: 'baseline-account',
},

View File

@ -18,7 +18,7 @@ addFinder(iconifyFinder);
describe('Testing legacy renderer', () => {
// Add mentioned icons to storage
const storage = getStorage('mdi');
const storage = getStorage('', 'mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {
@ -82,6 +82,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account-cash',
});
@ -124,6 +125,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account',
});
@ -167,6 +169,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account-box',
});
@ -214,6 +217,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -255,6 +259,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -299,6 +304,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account',
});
@ -372,6 +378,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -537,6 +544,7 @@ describe('Testing legacy renderer', () => {
expect(element).to.not.be.eql(lastElement); // different 'element' and 'name' properties
expect(element.name).to.not.be.eql(lastElement.name);
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account',
});
@ -582,6 +590,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -630,6 +639,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -676,6 +686,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -764,6 +775,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -861,6 +873,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -946,6 +959,7 @@ describe('Testing legacy renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});

View File

@ -18,7 +18,7 @@ addFinder(iconifyFinder);
describe('Testing renderer', () => {
// Add mentioned icons to storage
const storage = getStorage('mdi');
const storage = getStorage('', 'mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {
@ -82,6 +82,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account-cash',
});
@ -124,6 +125,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account',
});
@ -167,6 +169,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account-box',
});
@ -214,6 +217,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -255,6 +259,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -301,6 +306,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account',
});
@ -374,6 +380,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -541,6 +548,7 @@ describe('Testing renderer', () => {
expect(element).to.not.be.eql(lastElement); // different 'element' and 'name' properties
expect(element.name).to.not.be.eql(lastElement.name);
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'account',
});
@ -586,6 +594,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -634,6 +643,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -680,6 +690,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -768,6 +779,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -865,6 +877,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});
@ -950,6 +963,7 @@ describe('Testing renderer', () => {
// Test element
expect(element.name).to.be.eql({
provider: '',
prefix: 'mdi',
name: 'home',
});

View File

@ -17,7 +17,7 @@ addFinder(iconifyIconFinder);
describe('Scanning DOM', () => {
// Add mentioned icons to storage
const storage = getStorage('mdi');
const storage = getStorage('', 'mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {

View File

@ -5,7 +5,7 @@ import { getNode } from './node';
import { addFinder } from '@iconify/iconify/lib/finder';
import { FakeData, setFakeData, prepareQuery, sendQuery } from './fake-api';
import { API } from '@iconify/core/lib/api/';
import { setAPIModule } from '@iconify/core/lib/api/modules';
import { setDefaultAPIModule } from '@iconify/core/lib/api/modules';
import { setAPIConfig } from '@iconify/core/lib/api/config';
import { coreModules } from '@iconify/core/lib/modules';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
@ -20,7 +20,7 @@ addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
// Set API
setAPIModule({
setDefaultAPIModule({
prepare: prepareQuery,
send: sendQuery,
});
@ -33,16 +33,14 @@ function nextPrefix(): string {
describe('Scanning DOM with API', () => {
it('Scan DOM with API', (done) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
[prefix1, prefix2]
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Set icons, load them with various delay
const data1: FakeData = {
@ -64,7 +62,7 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix1, data1);
setFakeData(provider, prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
@ -85,24 +83,32 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix2, data2);
setFakeData(provider, prefix2, data2);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API</p><ul>' +
'<li>Inline icons:' +
' <span class="iconify iconify-inline" data-icon="' +
' <span class="iconify iconify-inline" data-icon="@' +
provider +
':' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="@' +
provider +
':' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="' +
' <iconify-icon data-icon="@' +
provider +
':' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <i class="iconify-icon" data-icon="' +
' <i class="iconify-icon" data-icon="@' +
provider +
':' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></i>' +
'</li>' +
@ -133,16 +139,14 @@ describe('Scanning DOM with API', () => {
});
it('Changing icon name before it loaded', (done) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
[prefix1, prefix2]
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Set icons, load them with various delay
const data1: FakeData = {
@ -164,7 +168,7 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix1, data1);
setFakeData(provider, prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
@ -185,7 +189,7 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix2, data2);
setFakeData(provider, prefix2, data2);
const data1b: FakeData = {
icons: ['account', 'account-box'],
@ -206,24 +210,32 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix1, data1b);
setFakeData(provider, prefix1, data1b);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API: renamed icon</p><ul>' +
'<li>Default finder:' +
' <span class="iconify-inline first-icon" data-icon="' +
' <span class="iconify-inline first-icon" data-icon="@' +
provider +
':' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline second-icon iconify--mdi-account" data-icon="' +
' <i class="iconify-inline second-icon iconify--mdi-account" data-icon="@' +
provider +
':' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>IconifyIcon finder:' +
' <iconify-icon class="third-icon" data-icon="' +
' <iconify-icon class="third-icon" data-icon="@' +
provider +
':' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon class="fourth-icon" data-icon="' +
' <iconify-icon class="fourth-icon" data-icon="@' +
provider +
':' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
@ -244,7 +256,10 @@ describe('Scanning DOM with API', () => {
const icon = node.querySelector('iconify-icon[title]');
expect(icon).to.not.be.equal(null);
expect(icon.getAttribute('class')).to.be.equal('third-icon');
icon.setAttribute('data-icon', prefix1 + ':account');
icon.setAttribute(
'data-icon',
'@' + provider + ':' + prefix1 + ':account'
);
// First API response should have loaded, but only 1 icon should have been rendered
setTimeout(() => {
@ -273,16 +288,14 @@ describe('Scanning DOM with API', () => {
});
it('Changing icon name before it loaded to invalid name', (done) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
[prefix1, prefix2]
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Set icons, load them with various delay
const data1: FakeData = {
@ -304,7 +317,7 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix1, data1);
setFakeData(provider, prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
@ -325,24 +338,32 @@ describe('Scanning DOM with API', () => {
height: 24,
},
};
setFakeData(prefix2, data2);
setFakeData(provider, prefix2, data2);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API: invalid name</p><ul>' +
'<li>Inline icons:' +
' <span class="iconify" data-icon="' +
' <span class="iconify" data-icon="@' +
provider +
':' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify test-icon iconify--mdi-account" data-icon="' +
' <i class="iconify test-icon iconify--mdi-account" data-icon="@' +
provider +
':' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="' +
' <iconify-icon data-icon="@' +
provider +
':' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon data-icon="' +
' <iconify-icon data-icon="@' +
provider +
':' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
@ -355,7 +376,7 @@ describe('Scanning DOM with API', () => {
// Change icon name
const icon = node.querySelector('iconify-icon[title]');
expect(icon).to.not.be.equal(null);
icon.setAttribute('data-icon', 'foo');
icon.setAttribute('data-icon', '@' + provider + ':foo');
// First API response should have loaded, but only 1 icon
setTimeout(() => {

View File

@ -19,13 +19,23 @@ export interface FakeData {
/**
* Fake data storage
*/
const fakeData: Record<string, FakeData[]> = Object.create(null);
const fakeData: Record<string, Record<string, FakeData[]>> = Object.create(
null
);
export function setFakeData(prefix: string, item: FakeData): void {
if (fakeData[prefix] === void 0) {
fakeData[prefix] = [];
export function setFakeData(
provider: string,
prefix: string,
item: FakeData
): void {
if (fakeData[provider] === void 0) {
fakeData[provider] = Object.create(null);
}
fakeData[prefix].push(item);
const providerFakeData = fakeData[provider];
if (providerFakeData[prefix] === void 0) {
providerFakeData[prefix] = [];
}
providerFakeData[prefix].push(item);
}
interface FakeAPIQueryParams extends APIQueryParams {
@ -36,6 +46,7 @@ interface FakeAPIQueryParams extends APIQueryParams {
* Prepare params
*/
export const prepareQuery: IconifyAPIPrepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
@ -43,10 +54,15 @@ export const prepareQuery: IconifyAPIPrepareQuery = (
const items: APIQueryParams[] = [];
let missing = icons.slice(0);
if (fakeData[prefix] !== void 0) {
fakeData[prefix].forEach(item => {
if (fakeData[provider] === void 0) {
fakeData[provider] = Object.create(null);
}
const providerFakeData = fakeData[provider];
if (providerFakeData[prefix] !== void 0) {
providerFakeData[prefix].forEach((item) => {
const matches = item.icons.filter(
icon => missing.indexOf(icon) !== -1
(icon) => missing.indexOf(icon) !== -1
);
if (!matches.length) {
// No match
@ -54,8 +70,9 @@ export const prepareQuery: IconifyAPIPrepareQuery = (
}
// Contains at least one matching icon
missing = missing.filter(icon => matches.indexOf(icon) === -1);
missing = missing.filter((icon) => matches.indexOf(icon) === -1);
const query: FakeAPIQueryParams = {
provider,
prefix,
icons: matches,
data: item,
@ -75,6 +92,7 @@ export const sendQuery: IconifyAPISendQuery = (
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
const provider = params.provider;
const prefix = params.prefix;
const icons = params.icons;
@ -88,7 +106,13 @@ export const sendQuery: IconifyAPISendQuery = (
}
const sendResponse = () => {
console.log('Sending data for prefix "' + prefix + '", icons:', icons);
console.log(
'Sending data for prefix "' +
(provider === '' ? '' : '@' + provider + ':') +
prefix +
'", icons:',
icons
);
status.done(data.data);
};

View File

@ -4,6 +4,7 @@ import {
} from '../interfaces/loader';
import { getStorage } from '../storage';
import { SortedIcons } from '../icon/sort';
import { IconifyIconSource } from '../icon/name';
/**
* Storage for callbacks
@ -22,49 +23,70 @@ interface CallbackItem {
abort: IconifyIconLoaderAbort;
}
// Records sorted by provider and prefix
// This export is only for unit testing, should not be used
export const callbacks: Record<string, CallbackItem[]> = Object.create(null);
const pendingUpdates: Record<string, boolean> = Object.create(null);
export const callbacks: Record<
string,
Record<string, CallbackItem[]>
> = Object.create(null);
const pendingUpdates: Record<string, Record<string, boolean>> = Object.create(
null
);
/**
* Remove callback
*/
function removeCallback(prefixes: string[], id: number): void {
prefixes.forEach(prefix => {
const items = callbacks[prefix];
function removeCallback(sources: IconifyIconSource[], id: number): void {
sources.forEach((source) => {
const provider = source.provider;
if (callbacks[provider] === void 0) {
return;
}
const providerCallbacks = callbacks[provider];
const prefix = source.prefix;
const items = providerCallbacks[prefix];
if (items) {
callbacks[prefix] = items.filter(row => row.id !== id);
providerCallbacks[prefix] = items.filter((row) => row.id !== id);
}
});
}
/**
* Update all callbacks for prefix
* Update all callbacks for provider and prefix
*/
export function updateCallbacks(prefix: string): void {
if (!pendingUpdates[prefix]) {
pendingUpdates[prefix] = true;
setTimeout(() => {
pendingUpdates[prefix] = false;
export function updateCallbacks(provider: string, prefix: string): void {
if (pendingUpdates[provider] === void 0) {
pendingUpdates[provider] = Object.create(null);
}
const providerPendingUpdates = pendingUpdates[provider];
if (callbacks[prefix] === void 0) {
if (!providerPendingUpdates[prefix]) {
providerPendingUpdates[prefix] = true;
setTimeout(() => {
providerPendingUpdates[prefix] = false;
if (
callbacks[provider] === void 0 ||
callbacks[provider][prefix] === void 0
) {
return;
}
// Get all items
const items = callbacks[prefix].slice(0);
const items = callbacks[provider][prefix].slice(0);
if (!items.length) {
return;
}
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
// Check each item for changes
let hasPending = false;
items.forEach((item: CallbackItem) => {
const icons = item.icons;
const oldLength = icons.pending.length;
icons.pending = icons.pending.filter(icon => {
icons.pending = icons.pending.filter((icon) => {
if (icon.prefix !== prefix) {
// Checking only current prefix
return true;
@ -74,12 +96,14 @@ export function updateCallbacks(prefix: string): void {
if (storage.icons[name] !== void 0) {
// Loaded
icons.loaded.push({
provider,
prefix,
name,
});
} else if (storage.missing[name] !== void 0) {
// Missing
icons.missing.push({
provider,
prefix,
name,
});
@ -96,7 +120,15 @@ export function updateCallbacks(prefix: string): void {
if (icons.pending.length !== oldLength) {
if (!hasPending) {
// All icons have been loaded - remove callback from prefix
removeCallback([prefix], item.id);
removeCallback(
[
{
provider,
prefix,
},
],
item.id
);
}
item.callback(
icons.loaded.slice(0),
@ -121,11 +153,11 @@ let idCounter = 0;
export function storeCallback(
callback: IconifyIconLoaderCallback,
icons: SortedIcons,
pendingPrefixes: string[]
pendingSources: IconifyIconSource[]
): IconifyIconLoaderAbort {
// Create unique id and abort function
const id = idCounter++;
const abort = removeCallback.bind(null, pendingPrefixes, id);
const abort = removeCallback.bind(null, pendingSources, id);
if (!icons.pending.length) {
// Do not store item without pending icons and return function that does nothing
@ -140,11 +172,17 @@ export function storeCallback(
abort: abort,
};
pendingPrefixes.forEach(prefix => {
if (callbacks[prefix] === void 0) {
callbacks[prefix] = [];
pendingSources.forEach((source) => {
const provider = source.provider;
const prefix = source.prefix;
if (callbacks[provider] === void 0) {
callbacks[provider] = Object.create(null);
}
callbacks[prefix].push(item);
const providerCallbacks = callbacks[provider];
if (providerCallbacks[prefix] === void 0) {
providerCallbacks[prefix] = [];
}
providerCallbacks[prefix].push(item);
});
return abort;

View File

@ -1,5 +1,4 @@
import { RedundancyConfig } from '@cyberalien/redundancy';
import { merge } from '../misc/merge';
/**
* API config
@ -12,17 +11,52 @@ export interface IconifyAPIConfig extends RedundancyConfig {
maxURL: number;
}
/**
* Local storage interfaces
*/
interface ConfigStorage {
// API configuration for all prefixes
default: IconifyAPIConfig;
export type PartialIconifyAPIConfig = Partial<IconifyAPIConfig>;
// Prefix specific API configuration
prefixes: Record<string, IconifyAPIConfig>;
/**
* Create full API configuration from partial data
*/
function createConfig(
source: PartialIconifyAPIConfig
): IconifyAPIConfig | null {
if (!source.resources) {
return null;
}
const result: IconifyAPIConfig = {
// API hosts
resources: source.resources,
// Root path
path: source.path === void 0 ? '/' : source.path,
// URL length limit
maxURL: source.maxURL ? source.maxURL : 500,
// Timeout before next host is used.
rotate: source.rotate ? source.rotate : 750,
// Timeout to retry same host.
timeout: source.timeout ? source.timeout : 5000,
// Number of attempts for each host.
limit: source.limit ? source.limit : 2,
// Randomise default API end point.
random: source.random === true,
// Start index
index: source.index ? source.index : 0,
};
return result;
}
/**
* Local storage
*/
const configStorage: Record<string, IconifyAPIConfig> = Object.create(null);
/**
* Redundancy for API servers.
*
@ -58,70 +92,28 @@ while (fallBackAPISources.length > 0) {
}
}
/**
* Default configuration
*/
const defaultConfig: IconifyAPIConfig = {
// API hosts
// Add default API
configStorage[''] = createConfig({
resources: ['https://api.iconify.design'].concat(fallBackAPI),
// Root path
path: '/',
// URL length limit
maxURL: 500,
// Timeout before next host is used.
rotate: 750,
// Timeout to retry same host.
timeout: 5000,
// Number of attempts for each host.
limit: 2,
// Randomise default API end point.
random: false,
// Start index
index: 0,
};
}) as IconifyAPIConfig;
/**
* Local storage
*/
const configStorage: ConfigStorage = {
default: defaultConfig,
prefixes: Object.create(null),
};
/**
* Add custom config for prefix(es)
*
* This function should be used before any API queries.
* On first API query computed configuration will be cached, so changes to config will not take effect.
* Add custom config for provider
*/
export function setAPIConfig(
customConfig: Partial<IconifyAPIConfig>,
prefix?: string | string[]
provider: string,
customConfig: PartialIconifyAPIConfig
): void {
const mergedConfig = merge(
configStorage.default,
customConfig
) as IconifyAPIConfig;
if (prefix === void 0) {
configStorage.default = mergedConfig;
const config = createConfig(customConfig);
if (config === null) {
return;
}
(typeof prefix === 'string' ? [prefix] : prefix).forEach(prefix => {
configStorage.prefixes[prefix] = mergedConfig;
});
configStorage[provider] = config;
}
/**
* Get API configuration
*/
export function getAPIConfig(prefix: string): IconifyAPIConfig | null {
const value = configStorage.prefixes[prefix];
return value === void 0 ? configStorage.default : value;
export function getAPIConfig(provider: string): IconifyAPIConfig | undefined {
return configStorage[provider];
}

View File

@ -15,8 +15,8 @@ import { getAPIModule } from './modules';
import { getAPIConfig, IconifyAPIConfig } from './config';
import { getStorage, addIconSet } from '../storage';
import { coreModules } from '../modules';
import { IconifyIconName } from '../icon/name';
import { listToIcons, getPrefixes } from '../icon/list';
import { IconifyIconName, IconifyIconSource } from '../icon/name';
import { listToIcons } from '../icon/list';
import { IconifyJSON } from '@iconify/types';
// Empty abort callback for loadIcons()
@ -32,10 +32,13 @@ function emptyCallback(): void {
* either an icon or a missing icon. This way same icon should
* never be requested twice.
*
* [prefix][icon] = time when icon was added to queue
* [provider][prefix][icon] = time when icon was added to queue
*/
type PendingIcons = Record<string, number>;
const pendingIcons: Record<string, PendingIcons> = Object.create(null);
const pendingIcons: Record<
string,
Record<string, PendingIcons>
> = Object.create(null);
/**
* List of icons that are waiting to be loaded.
@ -45,31 +48,39 @@ const pendingIcons: Record<string, PendingIcons> = Object.create(null);
* This list should not be used for any checks, use pendingIcons to check
* if icons is being loaded.
*
* [prefix] = array of icon names
* [provider][prefix] = array of icon names
*/
const iconsToLoad: Record<string, string[]> = Object.create(null);
const iconsToLoad: Record<string, Record<string, string[]>> = Object.create(
null
);
// Flags to merge multiple synchronous icon requests in one asynchronous request
const loaderFlags: Record<string, boolean> = Object.create(null);
const queueFlags: Record<string, boolean> = Object.create(null);
const loaderFlags: Record<string, Record<string, boolean>> = Object.create(
null
);
const queueFlags: Record<string, Record<string, boolean>> = Object.create(null);
// Redundancy instances cache
// Redundancy instances cache, sorted by provider
interface LocalCache {
config: IconifyAPIConfig | null;
redundancy: Redundancy | null;
config: IconifyAPIConfig;
redundancy: Redundancy;
}
const redundancyCache: Record<string, LocalCache> = Object.create(null);
/**
* Function called when new icons have been loaded
*/
function loadedNewIcons(prefix: string): void {
function loadedNewIcons(provider: string, prefix: string): void {
// Run only once per tick, possibly joining multiple API responses in one call
if (!loaderFlags[prefix]) {
loaderFlags[prefix] = true;
if (loaderFlags[provider] === void 0) {
loaderFlags[provider] = Object.create(null);
}
const providerLoaderFlags = loaderFlags[provider];
if (!providerLoaderFlags[prefix]) {
providerLoaderFlags[prefix] = true;
setTimeout(() => {
loaderFlags[prefix] = false;
updateCallbacks(prefix);
providerLoaderFlags[prefix] = false;
updateCallbacks(provider, prefix);
});
}
}
@ -77,7 +88,7 @@ function loadedNewIcons(prefix: string): void {
/**
* Load icons
*/
function loadNewIcons(prefix: string, icons: string[]): void {
function loadNewIcons(provider: string, prefix: string, icons: string[]): void {
function err(): void {
console.error(
'Unable to retrieve icons for prefix "' +
@ -86,68 +97,82 @@ function loadNewIcons(prefix: string, icons: string[]): void {
);
}
// Create nested objects if needed
if (iconsToLoad[provider] === void 0) {
iconsToLoad[provider] = Object.create(null);
}
const providerIconsToLoad = iconsToLoad[provider];
if (queueFlags[provider] === void 0) {
queueFlags[provider] = Object.create(null);
}
const providerQueueFlags = queueFlags[provider];
if (pendingIcons[provider] === void 0) {
pendingIcons[provider] = Object.create(null);
}
const providerPendingIcons = pendingIcons[provider];
// Add icons to queue
if (iconsToLoad[prefix] === void 0) {
iconsToLoad[prefix] = icons;
if (providerIconsToLoad[prefix] === void 0) {
providerIconsToLoad[prefix] = icons;
} else {
iconsToLoad[prefix] = iconsToLoad[prefix].concat(icons).sort();
providerIconsToLoad[prefix] = providerIconsToLoad[prefix]
.concat(icons)
.sort();
}
// Redundancy item
let cachedReundancy: LocalCache;
// Trigger update on next tick, mering multiple synchronous requests into one asynchronous request
if (!queueFlags[prefix]) {
queueFlags[prefix] = true;
if (!providerQueueFlags[prefix]) {
providerQueueFlags[prefix] = true;
setTimeout(() => {
queueFlags[prefix] = false;
providerQueueFlags[prefix] = false;
// Get icons and delete queue
const icons = iconsToLoad[prefix];
delete iconsToLoad[prefix];
const icons = providerIconsToLoad[prefix];
delete providerIconsToLoad[prefix];
// Get API module
const api = getAPIModule(prefix);
const api = getAPIModule(provider);
if (!api) {
// No way to load icons!
err();
return;
}
// Get Redundancy instance
if (redundancyCache[prefix] === void 0) {
const config = getAPIConfig(prefix);
// Attempt to find matching instance from other prefixes
// Using same Redundancy instance allows keeping track of failed hosts for multiple prefixes
for (const prefix2 in redundancyCache) {
const item = redundancyCache[prefix2];
if (item.config === config) {
redundancyCache[prefix] = item;
break;
// Get API config and Redundancy instance
if (cachedReundancy === void 0) {
if (redundancyCache[provider] === void 0) {
const config = getAPIConfig(provider);
if (!config) {
// No way to load icons because configuration is not set!
err();
return;
}
}
if (redundancyCache[prefix] === void 0) {
redundancyCache[prefix] = {
const redundancy = initRedundancy(config);
cachedReundancy = {
config,
redundancy: config ? initRedundancy(config) : null,
redundancy,
};
redundancyCache[provider] = cachedReundancy;
} else {
cachedReundancy = redundancyCache[provider];
}
}
const redundancy = redundancyCache[prefix].redundancy;
if (!redundancy) {
// No way to load icons because configuration is not set!
err();
return;
}
// Prepare parameters and run queries
const params = api.prepare(prefix, icons);
const params = api.prepare(provider, prefix, icons);
params.forEach((item) => {
redundancy.query(
cachedReundancy.redundancy.query(
item,
api.send as RedundancyQueryCallback,
(data) => {
// Add icons to storage
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
try {
const added = addIconSet(
storage,
@ -159,21 +184,24 @@ function loadNewIcons(prefix: string, icons: string[]): void {
}
// Remove added icons from pending list
const pending = pendingIcons[prefix];
const pending = providerPendingIcons[prefix];
added.forEach((name) => {
delete pending[name];
});
// Cache API response
if (coreModules.cache) {
coreModules.cache(data as IconifyJSON);
coreModules.cache(
provider,
data as IconifyJSON
);
}
} catch (err) {
console.error(err);
}
// Trigger update on next tick
loadedNewIcons(prefix);
loadedNewIcons(provider, prefix);
}
);
});
@ -184,9 +212,11 @@ function loadNewIcons(prefix: string, icons: string[]): void {
/**
* Check if icon is being loaded
*/
const isPending: IsPending = (prefix: string, icon: string): boolean => {
const isPending: IsPending = (icon: IconifyIconName): boolean => {
return (
pendingIcons[prefix] !== void 0 && pendingIcons[prefix][icon] !== void 0
pendingIcons[icon.provider] !== void 0 &&
pendingIcons[icon.provider][icon.prefix] !== void 0 &&
pendingIcons[icon.provider][icon.prefix][icon.name] !== void 0
);
};
@ -225,16 +255,42 @@ const loadIcons: IconifyLoadIcons = (
};
}
// Get all prefixes
const prefixes = getPrefixes(sortedIcons.pending);
// Get all sources for pending icons
const newIcons: Record<string, Record<string, string[]>> = Object.create(
null
);
const sources: IconifyIconSource[] = [];
let lastProvider: string, lastPrefix: string;
// Get pending icons queue for prefix and create new icons list
const newIcons: Record<string, string[]> = Object.create(null);
prefixes.forEach((prefix) => {
if (pendingIcons[prefix] === void 0) {
pendingIcons[prefix] = Object.create(null);
sortedIcons.pending.forEach((icon) => {
const provider = icon.provider;
const prefix = icon.prefix;
if (prefix === lastPrefix && provider === lastProvider) {
return;
}
lastProvider = provider;
lastPrefix = prefix;
sources.push({
provider,
prefix,
});
if (pendingIcons[provider] === void 0) {
pendingIcons[provider] = Object.create(null);
}
const providerPendingIcons = pendingIcons[provider];
if (providerPendingIcons[prefix] === void 0) {
providerPendingIcons[prefix] = Object.create(null);
}
if (newIcons[provider] === void 0) {
newIcons[provider] = Object.create(null);
}
const providerNewIcons = newIcons[provider];
if (providerNewIcons[prefix] === void 0) {
providerNewIcons[prefix] = [];
}
newIcons[prefix] = [];
});
// List of new icons
@ -244,29 +300,32 @@ const loadIcons: IconifyLoadIcons = (
// If icon was called before, it must exist in pendingIcons or storage, but because this
// function is called right after sortIcons() that checks storage, icon is definitely not in storage.
sortedIcons.pending.forEach((icon) => {
const provider = icon.provider;
const prefix = icon.prefix;
const name = icon.name;
const pendingQueue = pendingIcons[prefix];
const pendingQueue = pendingIcons[provider][prefix];
if (pendingQueue[name] === void 0) {
// New icon - add to pending queue to mark it as being loaded
pendingQueue[name] = time;
// Add it to new icons list to pass it to API module for loading
newIcons[prefix].push(name);
newIcons[provider][prefix].push(name);
}
});
// Load icons on next tick to make sure result is not returned before callback is stored and
// to consolidate multiple synchronous loadIcons() calls into one asynchronous API call
prefixes.forEach((prefix) => {
if (newIcons[prefix].length) {
loadNewIcons(prefix, newIcons[prefix]);
sources.forEach((source) => {
const provider = source.provider;
const prefix = source.prefix;
if (newIcons[provider][prefix].length) {
loadNewIcons(provider, prefix, newIcons[provider][prefix]);
}
});
// Store callback and return abort function
return callback
? storeCallback(callback, sortedIcons, prefixes)
? storeCallback(callback, sortedIcons, sources)
: emptyCallback;
};

View File

@ -4,6 +4,7 @@ import { RedundancyPendingItem } from '@cyberalien/redundancy';
* Params for sendQuery()
*/
export interface APIQueryParams {
provider: string;
prefix: string;
icons: string[];
}
@ -12,6 +13,7 @@ export interface APIQueryParams {
* Functions to implement in module
*/
export type IconifyAPIPrepareQuery = (
provider: string,
prefix: string,
icons: string[]
) => APIQueryParams[];
@ -33,43 +35,36 @@ export interface IconifyAPIModule {
/**
* Local storate types and entries
*/
interface ModuleStorage {
default: IconifyAPIModule | null;
prefixes: Record<string, IconifyAPIModule>;
interface ModulesStorage {
default?: IconifyAPIModule;
providers: Record<string, IconifyAPIModule>;
}
const storage: ModuleStorage = {
default: null,
prefixes: Object.create(null),
const storage: ModulesStorage = {
providers: Object.create(null),
};
/**
* Set API module
*
* If prefix is not set, function sets default method.
* If prefix is a string or array of strings, function sets method only for those prefixes.
*
* This should be used before sending any API requests. If used after sending API request, method
* is already cached so changing callback will not have any effect.
* Set default API module
*/
export function setAPIModule(
item: IconifyAPIModule,
prefix?: string | string[]
): void {
if (prefix === void 0) {
storage.default = item;
return;
}
export function setDefaultAPIModule(item: IconifyAPIModule): void {
storage.default = item;
}
(typeof prefix === 'string' ? [prefix] : prefix).forEach(prefix => {
storage.prefixes[prefix] = item;
});
/**
* Set API module
*/
export function setProviderAPIModule(
provider: string,
item: IconifyAPIModule
): void {
storage.providers[provider] = item;
}
/**
* Get API module
*/
export function getAPIModule(prefix: string): IconifyAPIModule | null {
const value = storage.prefixes[prefix];
return value === void 0 ? storage.default : value;
export function getAPIModule(provider: string): IconifyAPIModule | undefined {
return storage.providers[provider] === void 0
? storage.default
: storage.providers[provider];
}

View File

@ -13,13 +13,14 @@ type StorageEmptyList = StorageType<number[]>;
export interface StoredItem {
cached: number;
provider: string;
data: IconifyJSON;
}
// After changing configuration change it in tests/*/fake_cache.ts
// Cache version. Bump when structure changes
const cacheVersion = 'iconify1';
const cacheVersion = 'iconify2';
// Cache keys
const cachePrefix = 'iconify';
@ -200,14 +201,16 @@ export const loadCache: LoadIconsCache = (): void => {
typeof data !== 'object' ||
typeof data.cached !== 'number' ||
data.cached < minTime ||
typeof data.provider !== 'string' ||
typeof data.data !== 'object' ||
typeof data.data.prefix !== 'string'
) {
valid = false;
} else {
// Add icon set
const provider = data.provider;
const prefix = data.data.prefix;
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
valid = addIconSet(storage, data.data) as boolean;
}
} catch (err) {
@ -263,7 +266,10 @@ export const loadCache: LoadIconsCache = (): void => {
/**
* Function to cache icons
*/
export const storeCache: CacheIcons = (data: IconifyJSON): void => {
export const storeCache: CacheIcons = (
provider: string,
data: IconifyJSON
): void => {
if (!loaded) {
loadCache();
}
@ -292,6 +298,7 @@ export const storeCache: CacheIcons = (data: IconifyJSON): void => {
try {
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data,
};
func.setItem(cachePrefix + index, JSON.stringify(item));

View File

@ -9,13 +9,14 @@ export function listToIcons(
): IconifyIconName[] {
const result: IconifyIconName[] = [];
list.forEach(item => {
list.forEach((item) => {
const icon: IconifyIconName =
typeof item === 'string'
? (stringToIcon(item) as IconifyIconName)
: item;
if (!validate || validateIcon(icon)) {
result.push({
provider: icon.provider,
prefix: icon.prefix,
name: icon.name,
});
@ -26,12 +27,12 @@ export function listToIcons(
}
/**
* Get all prefixes
* Get all providers
*/
export function getPrefixes(list: IconifyIconName[]): string[] {
const prefixes: Record<string, boolean> = Object.create(null);
list.forEach(icon => {
prefixes[icon.prefix] = true;
export function getProviders(list: IconifyIconName[]): string[] {
const providers: Record<string, boolean> = Object.create(null);
list.forEach((icon) => {
providers[icon.provider] = true;
});
return Object.keys(prefixes);
return Object.keys(providers);
}

View File

@ -2,10 +2,16 @@
* Icon name
*/
export interface IconifyIconName {
readonly provider: string;
readonly prefix: string;
readonly name: string;
}
/**
* Icon source: icon object without name
*/
export type IconifyIconSource = Omit<IconifyIconName, 'name'>;
/**
* Expression to test part of icon name.
*/
@ -15,22 +21,40 @@ const match = /^[a-z0-9]+(-[a-z0-9]+)*$/;
* Convert string to Icon object.
*/
export const stringToIcon = (value: string): IconifyIconName | null => {
// Attempt to split by colon: "prefix:name"
let provider = '';
const colonSeparated = value.split(':');
if (colonSeparated.length > 2) {
// Check for provider with correct '@' at start
if (value.slice(0, 1) === '@') {
// First part is provider
if (colonSeparated.length < 2 || colonSeparated.length > 3) {
// "@provider:prefix:name" or "@provider:prefix-name"
return null;
}
provider = (colonSeparated.shift() as string).slice(1);
}
// Check split by colon: "prefix:name", "provider:prefix:name"
if (colonSeparated.length > 3 || !colonSeparated.length) {
return null;
}
if (colonSeparated.length === 2) {
if (colonSeparated.length > 1) {
// "prefix:name"
const name = colonSeparated.pop() as string;
const prefix = colonSeparated.pop() as string;
return {
prefix: colonSeparated[0],
name: colonSeparated[1],
// Allow provider without '@': "provider:prefix:name"
provider: colonSeparated.length > 0 ? colonSeparated[0] : provider,
prefix,
name,
};
}
// Attempt to split by dash: "prefix-name"
const dashSeparated = value.split('-');
const dashSeparated = colonSeparated[0].split('-');
if (dashSeparated.length > 1) {
return {
provider: provider,
prefix: dashSeparated.shift() as string,
name: dashSeparated.join('-'),
};
@ -49,5 +73,9 @@ export const validateIcon = (icon: IconifyIconName | null): boolean => {
return false;
}
return !!(icon.prefix.match(match) && icon.name.match(match));
return !!(
(icon.provider === '' || icon.provider.match(match)) &&
icon.prefix.match(match) &&
icon.name.match(match)
);
};

View File

@ -19,35 +19,50 @@ export function sortIcons(icons: IconifyIconName[]): SortedIcons {
missing: [],
pending: [],
};
const storage: Record<string, IconStorage> = Object.create(null);
const storage: Record<string, Record<string, IconStorage>> = Object.create(
null
);
// Sort icons alphabetically to prevent duplicates and make sure they are sorted in API queries
icons.sort((a, b) => {
if (a.prefix === b.prefix) {
return a.name.localeCompare(b.name);
if (a.provider !== b.provider) {
return a.provider.localeCompare(b.provider);
}
return a.prefix.localeCompare(b.prefix);
if (a.prefix !== b.prefix) {
return a.prefix.localeCompare(b.prefix);
}
return a.name.localeCompare(b.name);
});
let lastIcon: IconifyIconName = {
provider: '',
prefix: '',
name: '',
};
icons.forEach(icon => {
if (lastIcon.prefix === icon.prefix && lastIcon.name === icon.name) {
icons.forEach((icon) => {
if (
lastIcon.name === icon.name &&
lastIcon.prefix === icon.prefix &&
lastIcon.provider === icon.provider
) {
return;
}
lastIcon = icon;
// Check icon
const provider = icon.provider;
const prefix = icon.prefix;
const name = icon.name;
if (storage[prefix] === void 0) {
storage[prefix] = getStorage(prefix);
if (storage[provider] === void 0) {
storage[provider] = Object.create(null);
}
const providerStorage = storage[provider];
const localStorage = storage[prefix];
if (providerStorage[prefix] === void 0) {
providerStorage[prefix] = getStorage(provider, prefix);
}
const localStorage = providerStorage[prefix];
let list;
if (localStorage.icons[name] !== void 0) {
@ -59,6 +74,7 @@ export function sortIcons(icons: IconifyIconName[]): SortedIcons {
}
const item: IconifyIconName = {
provider,
prefix,
name,
};

View File

@ -1,9 +1,10 @@
import { IconifyLoadIcons } from './loader';
import { IconifyIconName } from '../icon/name';
/**
* Function to check if icon is pending
*/
export type IsPending = (prefix: string, name: string) => boolean;
export type IsPending = (icon: IconifyIconName) => boolean;
/**
* API interface

View File

@ -3,7 +3,7 @@ import { IconifyJSON } from '@iconify/types';
/**
* Function to cache loaded icons set
*/
export type CacheIcons = (data: IconifyJSON) => void;
export type CacheIcons = (provider: string, data: IconifyJSON) => void;
/**
* Function to load icons from cache

View File

@ -24,21 +24,25 @@ type IconRecords = Record<string, FullIconifyIcon | null>;
* Storage type
*/
export interface IconStorage {
provider: string;
prefix: string;
icons: IconRecords;
missing: Record<string, number>;
}
/**
* Storage by prefix
* Storage by provider and prefix
*/
const storage: Record<string, IconStorage> = Object.create(null);
const storage: Record<string, Record<string, IconStorage>> = Object.create(
null
);
/**
* Create new storage
*/
export function newStorage(prefix: string): IconStorage {
export function newStorage(provider: string, prefix: string): IconStorage {
return {
provider,
prefix,
icons: Object.create(null),
missing: Object.create(null),
@ -46,20 +50,31 @@ export function newStorage(prefix: string): IconStorage {
}
/**
* Get storage for prefix
* Get storage for provider and prefix
*/
export function getStorage(prefix: string): IconStorage {
if (storage[prefix] === void 0) {
storage[prefix] = newStorage(prefix);
export function getStorage(provider: string, prefix: string): IconStorage {
if (storage[provider] === void 0) {
storage[provider] = Object.create(null);
}
return storage[prefix];
const providerStorage = storage[provider];
if (providerStorage[prefix] === void 0) {
providerStorage[prefix] = newStorage(provider, prefix);
}
return providerStorage[prefix];
}
/**
* Get all providers
*/
export function listStoredProviders(): string[] {
return Object.keys(storage);
}
/**
* Get all prefixes
*/
export function listStoredPrefixes(): string[] {
return Object.keys(storage);
export function listStoredPrefixes(provider: string): string[] {
return storage[provider] === void 0 ? [] : Object.keys(storage[provider]);
}
/**
@ -119,7 +134,7 @@ export function addIconSet(
// Check for missing icons list returned by API
if (data.not_found instanceof Array) {
const t = Date.now();
data.not_found.forEach(name => {
data.not_found.forEach((name) => {
storage.missing[name] = t;
if (list === 'all') {
added.push(name);
@ -134,7 +149,7 @@ export function addIconSet(
// Get default values
const defaults = Object.create(null);
defaultsKeys.forEach(key => {
defaultsKeys.forEach((key) => {
if (data[key] !== void 0 && typeof data[key] !== 'object') {
defaults[key] = data[key];
}
@ -142,7 +157,7 @@ export function addIconSet(
// Get icons
const icons = data.icons;
Object.keys(icons).forEach(name => {
Object.keys(icons).forEach((name) => {
const icon = icons[name];
if (typeof icon.body !== 'string') {
throw new Error('Invalid icon');
@ -160,7 +175,7 @@ export function addIconSet(
// Get aliases
if (typeof data.aliases === 'object') {
const aliases = data.aliases;
Object.keys(aliases).forEach(name => {
Object.keys(aliases).forEach((name) => {
const icon = resolveAlias(aliases[name], icons, aliases, 1);
if (icon) {
// Freeze icon to make sure it will not be modified

View File

@ -7,12 +7,13 @@ import {
} from '../../lib/icon/name';
describe('Testing icon name', () => {
it('Converting and validating', () => {
it('Simple icon names', () => {
let icon;
// Simple prefix-name
icon = stringToIcon('fa-home') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: 'fa',
name: 'home',
});
@ -21,6 +22,7 @@ describe('Testing icon name', () => {
// Simple prefix:name
icon = stringToIcon('fa:arrow-left') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: 'fa',
name: 'arrow-left',
});
@ -29,11 +31,17 @@ describe('Testing icon name', () => {
// Longer prefix:name
icon = stringToIcon('mdi-light:home-outline') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: 'mdi-light',
name: 'home-outline',
});
expect(validateIcon(icon)).to.be.equal(true);
// Missing icon name
icon = stringToIcon('@iconify-home-icon');
expect(icon).to.be.eql(null);
expect(validateIcon(icon)).to.be.equal(false);
// Underscore is not an acceptable separator
icon = stringToIcon('fa_home');
expect(icon).to.be.eql(null);
@ -42,19 +50,21 @@ describe('Testing icon name', () => {
// Invalid character '_': fail validateIcon
icon = stringToIcon('fa:home_outline') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: 'fa',
name: 'home_outline',
});
expect(validateIcon(icon)).to.be.equal(false);
// Too many colons: fail stringToIcon
icon = stringToIcon('mdi-light:home:outline');
icon = stringToIcon('mdi:light:home:outline');
expect(icon).to.be.eql(null);
expect(validateIcon(icon)).to.be.equal(false);
// Upper case: fail validateIcon
icon = stringToIcon('MD:Home') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: 'MD',
name: 'Home',
});
@ -63,6 +73,7 @@ describe('Testing icon name', () => {
// Numbers: pass
icon = stringToIcon('1:foo') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: '1',
name: 'foo',
});
@ -71,9 +82,68 @@ describe('Testing icon name', () => {
// Accented letters: fail validateIcon
icon = stringToIcon('md-fõö') as IconifyIconName;
expect(icon).to.be.eql({
provider: '',
prefix: 'md',
name: 'fõö',
});
expect(validateIcon(icon)).to.be.equal(false);
});
it('Providers', () => {
let icon;
// Simple @provider:prefix-name
icon = stringToIcon('@iconify:fa-home') as IconifyIconName;
expect(icon).to.be.eql({
provider: 'iconify',
prefix: 'fa',
name: 'home',
});
expect(validateIcon(icon)).to.be.equal(true);
// Simple @provider:prefix:name
icon = stringToIcon('@iconify:fa:arrow-left') as IconifyIconName;
expect(icon).to.be.eql({
provider: 'iconify',
prefix: 'fa',
name: 'arrow-left',
});
expect(validateIcon(icon)).to.be.equal(true);
// Longer @provider:prefix:name
icon = stringToIcon(
'@iconify-backup:mdi-light:home-outline'
) as IconifyIconName;
expect(icon).to.be.eql({
provider: 'iconify-backup',
prefix: 'mdi-light',
name: 'home-outline',
});
expect(validateIcon(icon)).to.be.equal(true);
// Missing @ for provider
icon = stringToIcon(
'iconify-backup:mdi-light:home-outline'
) as IconifyIconName;
expect(icon).to.be.eql({
provider: 'iconify-backup',
prefix: 'mdi-light',
name: 'home-outline',
});
expect(validateIcon(icon)).to.be.equal(true);
// Too many colons: fail stringToIcon
icon = stringToIcon('@mdi:light:home:outline');
expect(icon).to.be.eql(null);
expect(validateIcon(icon)).to.be.equal(false);
// Upper case: fail validateIcon
icon = stringToIcon('@MD:home-outline') as IconifyIconName;
expect(icon).to.be.eql({
provider: 'MD',
prefix: 'home',
name: 'outline',
});
expect(validateIcon(icon)).to.be.equal(false);
});
});

View File

@ -13,7 +13,7 @@ import { IconifyJSON } from '@iconify/types';
describe('Testing storage', () => {
it('Adding icon', () => {
const storage = newStorage('foo');
const storage = newStorage('', 'foo');
// Add one icon
addIcon(storage, 'test', {
@ -82,7 +82,7 @@ describe('Testing storage', () => {
});
it('Adding simple icon set', () => {
const storage = newStorage('foo');
const storage = newStorage('', 'foo');
// Add two icons
expect(
@ -138,7 +138,7 @@ describe('Testing storage', () => {
});
it('Icon set with invalid default values', () => {
const storage = newStorage('foo');
const storage = newStorage('', 'foo');
// Missing prefix, invalid default values
expect(
@ -205,7 +205,7 @@ describe('Testing storage', () => {
});
it('Icon set with simple aliases', () => {
const storage = newStorage('foo');
const storage = newStorage('', 'foo');
expect(
addIconSet(storage, {
@ -275,7 +275,7 @@ describe('Testing storage', () => {
});
it('Icon set with nested aliases', () => {
const storage = newStorage('foo');
const storage = newStorage('', 'foo');
expect(
addIconSet(storage, {
@ -402,7 +402,7 @@ describe('Testing storage', () => {
});
it('Icon set with aliases that use transformations', () => {
const storage = newStorage('arty-animated');
const storage = newStorage('iconify', 'arty-animated');
const iconBody =
'<g stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" fill="none" fill-rule="evenodd"><path d="M40 64l48-48" class="animation-delay-0 animation-duration-10 animate-stroke stroke-length-102"/><path d="M40 64l48 48" class="animation-delay-0 animation-duration-10 animate-stroke stroke-length-102"/></g>';

View File

@ -18,11 +18,12 @@ describe('Testing API callbacks', () => {
return 'api-cb-test-' + (prefixCounter < 10 ? '0' : '') + prefixCounter;
}
it('Simple callback', done => {
it('Simple callback', (done) => {
const provider = 'iconify';
const prefix = nextPrefix();
let counter = 0;
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => {
expect(unsubscribe).to.be.equal(abort);
@ -33,23 +34,28 @@ describe('Testing API callbacks', () => {
// First run - icon1 should be loaded, icon3 should be missing
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
provider,
prefix,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
provider,
prefix,
name: 'icon2',
},
]);
expect(callbacks[prefix].length).to.be.equal(1);
expect(callbacks[provider][prefix].length).to.be.equal(
1
);
// Add icon2 and trigger update
addIconSet(storage, {
@ -61,54 +67,67 @@ describe('Testing API callbacks', () => {
},
});
updateCallbacks(prefix);
updateCallbacks(provider, prefix);
return;
case 2:
// Second run - icon2 should be added, completing callback
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([
{
provider,
prefix,
name: 'icon3',
},
]);
expect(pending).to.be.eql([]);
expect(callbacks[prefix].length).to.be.equal(0);
expect(callbacks[provider][prefix].length).to.be.equal(
0
);
done();
}
},
sortIcons([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
{
provider,
prefix,
name: 'icon3',
},
]),
[prefix]
[
{
provider,
prefix,
},
]
);
// Test callbacks
expect(callbacks[prefix].length).to.be.equal(1);
expect(callbacks[provider][prefix].length).to.be.equal(1);
// Test update - should do nothing
updateCallbacks(prefix);
updateCallbacks(provider, prefix);
// Wait for tick because updateCallbacks will use one
setTimeout(() => {
@ -125,14 +144,15 @@ describe('Testing API callbacks', () => {
},
not_found: ['icon3'],
});
updateCallbacks(prefix);
updateCallbacks(provider, prefix);
});
});
it('Callback that should not be stored', () => {
const provider = '';
const prefix = nextPrefix();
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
addIconSet(storage, {
prefix,
icons: {
@ -152,30 +172,39 @@ describe('Testing API callbacks', () => {
},
sortIcons([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
{
provider,
prefix,
name: 'icon3',
},
]),
[prefix]
[
{
provider,
prefix,
},
]
);
// callbacks should not have been initialised
expect(callbacks[prefix]).to.be.equal(void 0);
});
it('Cancel callback', done => {
it('Cancel callback', (done) => {
const provider = 'foo';
const prefix = nextPrefix();
let counter = 0;
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => {
expect(unsubscribe).to.be.equal(abort);
@ -186,23 +215,26 @@ describe('Testing API callbacks', () => {
// First run - icon1 should be loaded, icon3 should be missing
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
provider,
prefix,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
provider,
prefix,
name: 'icon2',
},
]);
expect(callbacks[prefix].length).to.be.equal(1);
expect(callbacks[provider][prefix].length).to.be.equal(1);
// Add icon2 and trigger update
addIconSet(storage, {
@ -214,35 +246,43 @@ describe('Testing API callbacks', () => {
},
});
updateCallbacks(prefix);
updateCallbacks(provider, prefix);
// Unsubscribe and set timer to call done()
unsubscribe();
expect(callbacks[prefix].length).to.be.equal(0);
expect(callbacks[provider][prefix].length).to.be.equal(0);
setTimeout(done);
},
sortIcons([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
{
provider,
prefix,
name: 'icon3',
},
]),
[prefix]
[
{
provider,
prefix,
},
]
);
// Test callbacks
expect(callbacks[prefix].length).to.be.equal(1);
expect(callbacks[provider][prefix].length).to.be.equal(1);
// Test update - should do nothing
updateCallbacks(prefix);
updateCallbacks(provider, prefix);
// Wait for tick because updateCallbacks will use one
setTimeout(() => {
@ -259,17 +299,18 @@ describe('Testing API callbacks', () => {
},
not_found: ['icon3'],
});
updateCallbacks(prefix);
updateCallbacks(provider, prefix);
});
});
it('Multiple prefixes', done => {
it('Multiple prefixes', (done) => {
const provider = '';
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
let counter = 0;
const storage1 = getStorage(prefix1);
const storage2 = getStorage(prefix2);
const storage1 = getStorage(provider, prefix1);
const storage2 = getStorage(provider, prefix2);
const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => {
@ -281,24 +322,31 @@ describe('Testing API callbacks', () => {
// First run - icon1 should be loaded, icon3 should be missing
expect(loaded).to.be.eql([
{
provider,
prefix: prefix1,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
provider,
prefix: prefix1,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
provider,
prefix: prefix2,
name: 'icon2',
},
]);
expect(callbacks[prefix1].length).to.be.equal(0);
expect(callbacks[prefix2].length).to.be.equal(1);
expect(callbacks[provider][prefix1].length).to.be.equal(
0
);
expect(callbacks[provider][prefix2].length).to.be.equal(
1
);
// Add icon2 and trigger update
addIconSet(storage2, {
@ -310,13 +358,17 @@ describe('Testing API callbacks', () => {
},
});
updateCallbacks(prefix2);
updateCallbacks(provider, prefix2);
break;
case 2:
// Second run - icon2 should be loaded
expect(callbacks[prefix1].length).to.be.equal(0);
expect(callbacks[prefix2].length).to.be.equal(0);
expect(callbacks[provider][prefix1].length).to.be.equal(
0
);
expect(callbacks[provider][prefix2].length).to.be.equal(
0
);
done();
break;
@ -326,27 +378,33 @@ describe('Testing API callbacks', () => {
},
sortIcons([
{
provider,
prefix: prefix1,
name: 'icon1',
},
{
provider,
prefix: prefix2,
name: 'icon2',
},
{
provider,
prefix: prefix1,
name: 'icon3',
},
]),
[prefix1, prefix2]
[
{ provider, prefix: prefix1 },
{ provider, prefix: prefix2 },
]
);
// Test callbacks
expect(callbacks[prefix1].length).to.be.equal(1);
expect(callbacks[prefix2].length).to.be.equal(1);
expect(callbacks[provider][prefix1].length).to.be.equal(1);
expect(callbacks[provider][prefix2].length).to.be.equal(1);
// Test update - should do nothing
updateCallbacks(prefix1);
updateCallbacks(provider, prefix1);
// Wait for tick because updateCallbacks will use one
setTimeout(() => {
@ -363,7 +421,150 @@ describe('Testing API callbacks', () => {
},
not_found: ['icon3'],
});
updateCallbacks(prefix1);
updateCallbacks(provider, prefix1);
});
});
it('Multiple providers', (done) => {
const provider1 = nextPrefix();
const provider2 = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
let counter = 0;
const storage1 = getStorage(provider1, prefix1);
const storage2 = getStorage(provider2, prefix2);
const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => {
expect(unsubscribe).to.be.equal(abort);
counter++;
switch (counter) {
case 1:
// First run - icon1 should be loaded, icon3 should be missing
expect(loaded).to.be.eql([
{
provider: provider1,
prefix: prefix1,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
provider: provider1,
prefix: prefix1,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
provider: provider2,
prefix: prefix2,
name: 'icon2',
},
]);
expect(
callbacks[provider1][prefix1].length
).to.be.equal(0);
expect(
callbacks[provider2][prefix2].length
).to.be.equal(1);
// Make sure providers/prefixes aren't mixed
expect(callbacks[provider1][prefix2]).to.be.equal(
void 0
);
expect(callbacks[provider2][prefix1]).to.be.equal(
void 0
);
// Add icon2 and trigger update
addIconSet(storage2, {
prefix: prefix2,
icons: {
icon2: {
body: '<g></g>',
},
},
});
updateCallbacks(provider2, prefix2);
break;
case 2:
// Second run - icon2 should be loaded
expect(
callbacks[provider1][prefix1].length
).to.be.equal(0);
expect(
callbacks[provider2][prefix2].length
).to.be.equal(0);
// Make sure providers/prefixes aren't mixed
expect(callbacks[provider1][prefix2]).to.be.equal(
void 0
);
expect(callbacks[provider2][prefix1]).to.be.equal(
void 0
);
done();
break;
default:
done('Callback was called ' + counter + ' times.');
}
},
sortIcons([
{
provider: provider1,
prefix: prefix1,
name: 'icon1',
},
{
provider: provider2,
prefix: prefix2,
name: 'icon2',
},
{
provider: provider1,
prefix: prefix1,
name: 'icon3',
},
]),
[
{ provider: provider1, prefix: prefix1 },
{ provider: provider2, prefix: prefix2 },
]
);
// Test callbacks
expect(callbacks[provider1][prefix1].length).to.be.equal(1);
expect(callbacks[provider2][prefix2].length).to.be.equal(1);
expect(callbacks[provider1][prefix2]).to.be.equal(void 0);
expect(callbacks[provider2][prefix1]).to.be.equal(void 0);
// Test update - should do nothing
updateCallbacks(provider1, prefix1);
// Wait for tick because updateCallbacks will use one
setTimeout(() => {
// Callback should not have been called yet
expect(counter).to.be.equal(0);
// Add few icons and run updateCallbacks
addIconSet(storage1, {
prefix: prefix1,
icons: {
icon1: {
body: '<g></g>',
},
},
not_found: ['icon3'],
});
updateCallbacks(provider1, prefix1);
});
});
});

View File

@ -9,7 +9,7 @@ import {
IconifyAPIConfig,
} from '../../lib/api/config';
import {
setAPIModule,
setProviderAPIModule,
APIQueryParams,
getAPIModule,
IconifyAPIModule,
@ -25,10 +25,12 @@ describe('Testing API modules', () => {
}
const prepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
provider,
prefix,
icons,
};
@ -44,42 +46,36 @@ describe('Testing API modules', () => {
};
it('Empty module', () => {
const prefix = nextPrefix();
const provider = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://localhost:3000'],
maxURL: 500,
},
prefix
);
setAPIConfig(provider, {
resources: ['https://localhost:3000'],
maxURL: 500,
});
// Set fake module
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
setProviderAPIModule(provider, {
prepare: prepareQuery,
send: sendQuery,
});
// Get config
const config = getAPIConfig(prefix) as IconifyAPIConfig;
expect(config).to.not.be.equal(null);
const config = getAPIConfig(provider) as IconifyAPIConfig;
expect(config).to.not.be.equal(void 0);
// Check setAPIConfig
expect(config.resources).to.be.eql(['https://localhost:3000']);
// Check getAPIModule()
const item = getAPIModule(prefix) as IconifyAPIModule;
expect(item).to.not.be.equal(null);
const item = getAPIModule(provider) as IconifyAPIModule;
expect(item).to.not.be.equal(void 0);
expect(item.prepare).to.be.equal(prepareQuery);
expect(item.send).to.be.equal(sendQuery);
// Get module for different prefix to make sure it is empty
const prefix2 = nextPrefix();
const item2 = getAPIModule(prefix2);
expect(item2).to.be.equal(null);
// Get module for different provider to make sure it is empty
const provider2 = nextPrefix();
const item2 = getAPIModule(provider2);
expect(item2).to.be.equal(void 0);
});
});

View File

@ -3,8 +3,8 @@
import 'mocha';
import { expect } from 'chai';
import { RedundancyPendingItem } from '@cyberalien/redundancy';
import { setAPIConfig } from '../../lib/api/config';
import { setAPIModule, APIQueryParams } from '../../lib/api/modules';
import { setAPIConfig, IconifyAPIConfig } from '../../lib/api/config';
import { setProviderAPIModule, APIQueryParams } from '../../lib/api/modules';
import { API } from '../../lib/api/';
describe('Testing API loadIcons', () => {
@ -16,24 +16,24 @@ describe('Testing API loadIcons', () => {
);
}
it('Loading few icons', done => {
it('Loading few icons', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
let asyncCounter = 0;
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Icon loader
const prepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
provider,
prefix,
icons,
};
@ -44,6 +44,7 @@ describe('Testing API loadIcons', () => {
// Test input and return as one item
const expected: APIQueryParams = {
provider,
prefix,
icons: ['icon1', 'icon2'],
};
@ -64,6 +65,7 @@ describe('Testing API loadIcons', () => {
// Test input
expect(host).to.be.equal('https://api1.local');
const expected: APIQueryParams = {
provider,
prefix,
icons: ['icon1', 'icon2'],
};
@ -86,24 +88,22 @@ describe('Testing API loadIcons', () => {
expect(asyncCounter).to.be.equal(3);
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
setProviderAPIModule(provider, {
prepare: prepareQuery,
send: sendQuery,
});
// Load icons
API.loadIcons(
[
// as icon
{
provider,
prefix,
name: 'icon1',
},
// as string
prefix + ':icon2',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending, unsubscribe) => {
// This callback should be called last
@ -112,10 +112,12 @@ describe('Testing API loadIcons', () => {
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -123,42 +125,54 @@ describe('Testing API loadIcons', () => {
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
expect(API.isPending(prefix, 'icon1')).to.be.equal(false);
expect(API.isPending(prefix, 'icon3')).to.be.equal(false);
expect(
API.isPending({
provider,
prefix,
name: 'icon1',
})
).to.be.equal(false);
expect(
API.isPending({ provider, prefix, name: 'icon3' })
).to.be.equal(false);
done();
}
);
// Test isPending
expect(API.isPending(prefix, 'icon1')).to.be.equal(true);
expect(API.isPending(prefix, 'icon3')).to.be.equal(false);
expect(API.isPending({ provider, prefix, name: 'icon1' })).to.be.equal(
true
);
expect(API.isPending({ provider, prefix, name: 'icon3' })).to.be.equal(
false
);
// Make sure asyncCounter wasn't increased because loading shoud happen on next tick
expect(asyncCounter).to.be.equal(0);
asyncCounter++;
});
it('Split results', done => {
it('Split results', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Icon loader
const prepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
// Split all icons in multiple queries, one icon per query
const results: APIQueryParams[] = [];
icons.forEach(icon => {
icons.forEach((icon) => {
const item: APIQueryParams = {
provider,
prefix,
icons: [icon],
};
@ -182,6 +196,7 @@ describe('Testing API loadIcons', () => {
// Icon names should match queryCounter: 'icon1' on first run, 'icon2' on second run
queryCounter++;
const expected: APIQueryParams = {
provider,
prefix,
icons: ['icon' + queryCounter],
};
@ -189,7 +204,7 @@ describe('Testing API loadIcons', () => {
// Send only requested icons
const icons = Object.create(null);
params.icons.forEach(icon => {
params.icons.forEach((icon) => {
icons[icon] = {
body: '<path d="" />',
};
@ -200,18 +215,18 @@ describe('Testing API loadIcons', () => {
});
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
setProviderAPIModule(provider, {
prepare: prepareQuery,
send: sendQuery,
});
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once because results should be sent in same tick
expect(callbackCalled).to.be.equal(false);
@ -220,10 +235,12 @@ describe('Testing API loadIcons', () => {
// Test data
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -235,24 +252,24 @@ describe('Testing API loadIcons', () => {
);
});
it('Fail on default host', done => {
it('Fail on default host', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
},
prefix
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
});
// Icon loader
const prepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
provider,
prefix,
icons,
};
@ -299,18 +316,18 @@ describe('Testing API loadIcons', () => {
}
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
setProviderAPIModule(provider, {
prepare: prepareQuery,
send: sendQuery,
});
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
@ -319,10 +336,12 @@ describe('Testing API loadIcons', () => {
// Test data
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -335,24 +354,24 @@ describe('Testing API loadIcons', () => {
);
});
it('Fail on default host, multiple queries', done => {
it('Fail on default host, multiple queries', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
},
prefix
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
});
// Icon loader
const prepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
provider,
prefix,
icons,
};
@ -420,18 +439,18 @@ describe('Testing API loadIcons', () => {
}
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
setProviderAPIModule(provider, {
prepare: prepareQuery,
send: sendQuery,
});
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
@ -440,10 +459,12 @@ describe('Testing API loadIcons', () => {
// Test data
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -455,7 +476,10 @@ describe('Testing API loadIcons', () => {
setTimeout(() => {
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon3', prefix + ':icon4'],
[
provider + ':' + prefix + ':icon3',
provider + ':' + prefix + ':icon4',
],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
@ -464,10 +488,12 @@ describe('Testing API loadIcons', () => {
// Test data
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon3',
},
{
provider,
prefix,
name: 'icon4',
},
@ -483,25 +509,25 @@ describe('Testing API loadIcons', () => {
);
});
it('Fail on default host, multiple queries with different prefixes', done => {
it('Fail on default host, multiple queries with different prefixes', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const prefix2 = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
},
[prefix, prefix2]
);
setAPIConfig(provider, {
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
});
// Icon loader
const prepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
provider,
prefix,
icons,
};
@ -572,18 +598,18 @@ describe('Testing API loadIcons', () => {
}
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
[prefix, prefix2]
);
setProviderAPIModule(provider, {
prepare: prepareQuery,
send: sendQuery,
});
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
@ -592,10 +618,12 @@ describe('Testing API loadIcons', () => {
// Test data
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
@ -607,7 +635,10 @@ describe('Testing API loadIcons', () => {
setTimeout(() => {
let callbackCalled = false;
API.loadIcons(
[prefix2 + ':icon2', prefix2 + ':icon4'],
[
provider + ':' + prefix2 + ':icon2',
provider + ':' + prefix2 + ':icon4',
],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
@ -616,10 +647,12 @@ describe('Testing API loadIcons', () => {
// Test data
expect(loaded).to.be.eql([
{
provider,
prefix: prefix2,
name: 'icon2',
},
{
provider,
prefix: prefix2,
name: 'icon4',
},

View File

@ -14,6 +14,8 @@ import {
} from './fake_cache';
describe('Testing mocked localStorage', () => {
const provider = '';
it('No usable cache', () => {
reset({});
@ -85,6 +87,7 @@ describe('Testing mocked localStorage', () => {
cachePrefix + '0',
JSON.stringify({
cached: Date.now(),
provider,
data: {
prefix: prefix,
icons: {
@ -143,6 +146,7 @@ describe('Testing mocked localStorage', () => {
cachePrefix + '0',
JSON.stringify({
cached: Date.now(),
provider,
data: {
prefix: prefix,
icons: {

View File

@ -24,6 +24,8 @@ import {
import { IconifyIcon, IconifyJSON } from '@iconify/types';
describe('Testing loading from localStorage', () => {
const provider = '';
it('Valid icon set', () => {
const prefix = nextPrefix();
const cache = createCache();
@ -34,6 +36,7 @@ describe('Testing loading from localStorage', () => {
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
@ -51,7 +54,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
@ -75,6 +78,64 @@ describe('Testing loading from localStorage', () => {
});
});
it('Different provider', () => {
const provider = nextPrefix();
const prefix = nextPrefix();
const cache = createCache();
// Add one icon set
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '1');
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
},
};
cache.setItem(cachePrefix + '0', JSON.stringify(item));
// Set cache
reset({
localStorage: cache,
});
// Check icon storage
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Check default provider
const icons2 = getStorage('', prefix);
expect(iconExists(icons2, 'foo')).to.be.equal(false);
// Load localStorage
loadCache();
// Icon should exist now
expect(iconExists(icons, 'foo')).to.be.equal(true);
expect(iconExists(icons2, 'foo')).to.be.equal(false);
// Check data
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 1,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
});
it('Expired icon set', () => {
const prefix = nextPrefix();
const cache = createCache();
@ -86,6 +147,7 @@ describe('Testing loading from localStorage', () => {
const item: StoredItem = {
// Expiration date
cached: Math.floor(Date.now() / hour) - cacheExpiration - 1,
provider,
data: {
prefix: prefix,
icons: {
@ -103,7 +165,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
@ -138,6 +200,7 @@ describe('Testing loading from localStorage', () => {
cachePrefix + '0',
JSON.stringify({
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
@ -156,7 +219,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
@ -190,6 +253,7 @@ describe('Testing loading from localStorage', () => {
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
@ -207,7 +271,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
@ -241,6 +305,7 @@ describe('Testing loading from localStorage', () => {
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
@ -258,7 +323,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
@ -293,6 +358,7 @@ describe('Testing loading from localStorage', () => {
// Missing: 0, 2, 3
const item1: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
@ -304,6 +370,7 @@ describe('Testing loading from localStorage', () => {
};
const item4: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: {
prefix: prefix,
icons: {
@ -323,7 +390,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo1')).to.be.equal(false);
expect(iconExists(icons, 'foo4')).to.be.equal(false);
@ -376,6 +443,7 @@ describe('Testing loading from localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
icons.push(icon);
@ -399,7 +467,7 @@ describe('Testing loading from localStorage', () => {
});
// Check icon storage
const iconsStorage = getStorage(prefix);
const iconsStorage = getStorage(provider, prefix);
for (let i = 0; i < 6; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
false,

View File

@ -25,6 +25,8 @@ import {
import { IconifyJSON } from '@iconify/types';
describe('Testing saving to localStorage', () => {
const provider = '';
it('One icon set', () => {
const prefix = nextPrefix();
const cache = createCache();
@ -40,6 +42,7 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
@ -49,11 +52,11 @@ describe('Testing saving to localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Save item
storeCache(icon);
storeCache(provider, icon);
// Storing in cache should not add item to storage
expect(iconExists(icons, 'foo')).to.be.equal(false);
@ -96,6 +99,7 @@ describe('Testing saving to localStorage', () => {
};
const item0: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon0,
};
const icon1: IconifyJSON = {
@ -108,6 +112,7 @@ describe('Testing saving to localStorage', () => {
};
const item1: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon1,
};
@ -117,8 +122,8 @@ describe('Testing saving to localStorage', () => {
});
// Save items
storeCache(icon0);
storeCache(icon1);
storeCache(provider, icon0);
storeCache(provider, icon1);
// Check data that should have been updated because storeCache()
// should call load function before first execution
@ -161,6 +166,7 @@ describe('Testing saving to localStorage', () => {
};
const item0: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon0,
};
const icon1: IconifyJSON = {
@ -173,6 +179,7 @@ describe('Testing saving to localStorage', () => {
};
const item1: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon1,
};
@ -204,7 +211,7 @@ describe('Testing saving to localStorage', () => {
});
// Save items
storeCache(icon0);
storeCache(provider, icon0);
// Check data
expect(count).to.be.eql({
@ -245,6 +252,7 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
@ -313,7 +321,7 @@ describe('Testing saving to localStorage', () => {
});
// Add item 5
storeCache(icons[5]);
storeCache(provider, icons[5]);
expect(count).to.be.eql({
local: 0,
session: 9,
@ -328,7 +336,7 @@ describe('Testing saving to localStorage', () => {
const list = [4, 2, 1];
list.slice(0).forEach((index) => {
expect(list.shift()).to.be.equal(index);
storeCache(icons[index]);
storeCache(provider, icons[index]);
expect(count).to.be.eql({
local: 0,
session: 9,
@ -341,7 +349,7 @@ describe('Testing saving to localStorage', () => {
});
// Add item 10
storeCache(icons[10]);
storeCache(provider, icons[10]);
expect(count).to.be.eql({
local: 0,
session: 10,
@ -353,7 +361,7 @@ describe('Testing saving to localStorage', () => {
expect(cache.getItem(countKey)).to.be.equal('10');
// Add item 11
storeCache(icons[11]);
storeCache(provider, icons[11]);
expect(count).to.be.eql({
local: 0,
session: 11,
@ -392,7 +400,7 @@ describe('Testing saving to localStorage', () => {
});
// Check icon storage
const icons = getStorage(prefix);
const icons = getStorage(provider, prefix);
expect(iconExists(icons, 'foo1')).to.be.equal(false);
// Load cache
@ -422,11 +430,12 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
// Save item
storeCache(icon);
storeCache(provider, icon);
// Storing in cache should not add item to storage
expect(iconExists(icons, 'foo')).to.be.equal(false);
@ -473,6 +482,7 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
cache1.setItem(cachePrefix + index, JSON.stringify(item));
@ -492,6 +502,7 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
cache2.setItem(cachePrefix + index, JSON.stringify(item));
@ -521,7 +532,7 @@ describe('Testing saving to localStorage', () => {
});
// Check icon storage
const iconsStorage = getStorage(prefix);
const iconsStorage = getStorage(provider, prefix);
for (let i = 0; i < count.local; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
true,
@ -546,9 +557,10 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
storeCache(icon);
storeCache(provider, icon);
// Check data
expect(count).to.be.eql({
@ -585,6 +597,7 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
cache1.setItem(cachePrefix + index, JSON.stringify(item));
@ -604,6 +617,7 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
cache2.setItem(cachePrefix + index, JSON.stringify(item));
@ -633,7 +647,7 @@ describe('Testing saving to localStorage', () => {
});
// Check icon storage
const iconsStorage = getStorage(prefix);
const iconsStorage = getStorage(provider, prefix);
for (let i = 0; i < count.local; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
true,
@ -661,9 +675,10 @@ describe('Testing saving to localStorage', () => {
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
provider,
data: icon,
};
storeCache(icon);
storeCache(provider, icon);
// Check data
expect(count).to.be.eql({

View File

@ -9,7 +9,7 @@ export function nextPrefix(): string {
}
// Cache version. Bump when structure changes
export const cacheVersion = 'iconify1';
export const cacheVersion = 'iconify2';
// Cache keys
export const cachePrefix = 'iconify';

View File

@ -19,6 +19,7 @@ import {
getIcon,
addIcon,
addIconSet,
listStoredProviders,
listStoredPrefixes,
} from '@iconify/core/lib/storage';
import { iconToSVG, IconifyIconBuildResult } from '@iconify/core/lib/builder';
@ -39,7 +40,7 @@ import { storeCache, loadCache, config } from '@iconify/core/lib/cache/storage';
// API
import { API } from '@iconify/core/lib/api/';
import { setAPIModule } from '@iconify/core/lib/api/modules';
import { setDefaultAPIModule } from '@iconify/core/lib/api/modules';
import { setAPIConfig, IconifyAPIConfig } from '@iconify/core/lib/api/config';
import { prepareQuery, sendQuery } from './modules/api-jsonp';
import {
@ -105,7 +106,7 @@ export interface IconifyGlobal {
/**
* List all available icons
*/
listIcons: (prefix?: string) => string[];
listIcons: (provider?: string, prefix?: string) => string[];
/**
* Load icons
@ -157,7 +158,7 @@ export interface IconifyGlobal {
/**
* Add icon set to storage
*/
addCollection: (data: IconifyJSON) => boolean;
addCollection: (data: IconifyJSON, provider?: string) => boolean;
/* API stuff */
/**
@ -174,8 +175,8 @@ export interface IconifyGlobal {
* Set API configuration
*/
setAPIConfig: (
customConfig: Partial<IconifyAPIConfig>,
prefix?: string | string[]
provider: string,
customConfig: Partial<IconifyAPIConfig>
) => void;
/* Scan DOM */
@ -211,7 +212,9 @@ function getIconName(name: string): IconifyIconName | null {
*/
function getIconData(name: string): FullIconifyIcon | null {
const icon = getIconName(name);
return icon ? getIcon(getStorage(icon.prefix), icon.name) : null;
return icon
? getIcon(getStorage(icon.provider, icon.prefix), icon.name)
: null;
}
/**
@ -282,23 +285,38 @@ const Iconify: IconifyGlobal = {
},
// List icons
listIcons: (prefix?: string) => {
listIcons: (provider?: string, prefix?: string) => {
let icons = [];
let prefixes = listStoredPrefixes();
let addPrefix = true;
if (typeof prefix === 'string') {
prefixes = prefixes.indexOf(prefix) !== -1 ? [] : [prefix];
addPrefix = false;
// Get providers
let providers: string[];
if (typeof provider === 'string') {
providers = [provider];
} else {
providers = listStoredProviders();
}
prefixes.forEach((prefix) => {
const storage = getStorage(prefix);
let icons = Object.keys(storage.icons);
if (addPrefix) {
icons = icons.map((name) => prefix + ':' + name);
// Get all icons
providers.forEach((provider) => {
let prefixes: string[];
if (typeof prefix === 'string') {
prefixes = [prefix];
} else {
prefixes = listStoredPrefixes(provider);
}
icons = icons.concat(icons);
prefixes.forEach((prefix) => {
const storage = getStorage(provider, prefix);
let icons = Object.keys(storage.icons).map(
(name) =>
(provider !== '' ? '@' + provider + ':' : '') +
prefix +
':' +
name
);
icons = icons.concat(icons);
});
});
return icons;
@ -331,16 +349,21 @@ const Iconify: IconifyGlobal = {
if (!icon) {
return false;
}
const storage = getStorage(icon.prefix);
const storage = getStorage(icon.provider, icon.prefix);
return addIcon(storage, icon.name, data);
},
// Add icon set
addCollection: (data) => {
addCollection: (data, provider?: string) => {
if (typeof provider !== 'string') {
provider = '';
}
if (
typeof data !== 'object' ||
typeof data.prefix !== 'string' ||
!validateIcon({
provider,
prefix: data.prefix,
name: 'a',
})
@ -348,7 +371,7 @@ const Iconify: IconifyGlobal = {
return false;
}
const storage = getStorage(data.prefix);
const storage = getStorage(provider, data.prefix);
return !!addIconSet(storage, data);
},
@ -404,11 +427,11 @@ coreModules.cache = storeCache;
loadCache();
// Set API
setAPIModule({
coreModules.api = API;
setDefaultAPIModule({
send: sendQuery,
prepare: prepareQuery,
});
coreModules.api = API;
// Load observer
browserModules.observer = observer;

View File

@ -84,9 +84,9 @@ function getGlobal(): JSONPRoot {
/**
* Calculate maximum icons list length for prefix
*/
function calculateMaxLength(prefix: string): number {
function calculateMaxLength(provider: string, prefix: string): number {
// Get config and store path
const config = getAPIConfig(prefix);
const config = getAPIConfig(provider);
if (!config) {
return 0;
}
@ -112,7 +112,10 @@ function calculateMaxLength(prefix: string): number {
config.maxURL -
maxHostLength -
config.path.length -
endPoint.replace('{prefix}', prefix).replace('{icons}', '').length -
endPoint
.replace('{provider}', provider)
.replace('{prefix}', prefix)
.replace('{icons}', '').length -
extraLength;
}
@ -126,6 +129,7 @@ function calculateMaxLength(prefix: string): number {
* Prepare params
*/
export const prepareQuery: IconifyAPIPrepareQuery = (
provider: string,
prefix: string,
icons: string[]
): APIQueryParams[] => {
@ -134,11 +138,12 @@ export const prepareQuery: IconifyAPIPrepareQuery = (
// Get maximum icons list length
let maxLength = maxLengthCache[prefix];
if (maxLength === void 0) {
maxLength = calculateMaxLength(prefix);
maxLength = calculateMaxLength(provider, prefix);
}
// Split icons
let item: APIQueryParams = {
provider,
prefix,
icons: [],
};
@ -149,6 +154,7 @@ export const prepareQuery: IconifyAPIPrepareQuery = (
// Next set
results.push(item);
item = {
provider,
prefix,
icons: [],
};
@ -170,6 +176,7 @@ export const sendQuery: IconifyAPISendQuery = (
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
const provider = params.provider;
const prefix = params.prefix;
const icons = params.icons;
const iconsList = icons.join(',');
@ -180,7 +187,9 @@ export const sendQuery: IconifyAPISendQuery = (
const global = getGlobal();
// Callback hash
let cbCounter = hash(host + ':' + prefix + ':' + iconsList);
let cbCounter = hash(
provider + ':' + host + ':' + prefix + ':' + iconsList
);
while (global[cbPrefix + cbCounter] !== void 0) {
cbCounter++;
}
@ -189,6 +198,7 @@ export const sendQuery: IconifyAPISendQuery = (
let path =
pathCache[prefix] +
endPoint
.replace('{provider}', provider)
.replace('{prefix}', prefix)
.replace('{icons}', iconsList)
.replace('{cb}', callbackName);

View File

@ -53,18 +53,20 @@ export function scanDOM(root?: HTMLElement): void {
// Observer
let paused = false;
// List of icons to load
const loadIcons: Record<string, Record<string, boolean>> = Object.create(
null
);
// List of icons to load: [provider][prefix][name] = boolean
const loadIcons: Record<
string,
Record<string, Record<string, boolean>>
> = Object.create(null);
// Get root node and placeholders
if (!root) {
root = getRoot();
}
findPlaceholders(root).forEach(item => {
findPlaceholders(root).forEach((item) => {
const element = item.element;
const iconName = item.name;
const provider = iconName.provider;
const prefix = iconName.prefix;
const name = iconName.name;
let data: IconifyElementData = element[elementDataProperty];
@ -79,7 +81,7 @@ export function scanDOM(root?: HTMLElement): void {
case 'loading':
if (
coreModules.api &&
coreModules.api.isPending(prefix, name)
coreModules.api.isPending({ provider, prefix, name })
) {
// Pending
return;
@ -88,7 +90,7 @@ export function scanDOM(root?: HTMLElement): void {
}
// Check icon
const storage = getStorage(prefix);
const storage = getStorage(provider, prefix);
if (storage.icons[name] !== void 0) {
// Icon exists - replace placeholder
if (browserModules.observer && !paused) {
@ -124,12 +126,16 @@ export function scanDOM(root?: HTMLElement): void {
}
if (coreModules.api) {
if (!coreModules.api.isPending(prefix, name)) {
if (!coreModules.api.isPending({ provider, prefix, name })) {
// Add icon to loading queue
if (loadIcons[prefix] === void 0) {
loadIcons[prefix] = Object.create(null);
if (loadIcons[provider] === void 0) {
loadIcons[provider] = Object.create(null);
}
loadIcons[prefix][name] = true;
const providerLoadIcons = loadIcons[provider];
if (providerLoadIcons[prefix] === void 0) {
providerLoadIcons[prefix] = Object.create(null);
}
providerLoadIcons[prefix][name] = true;
}
}
@ -145,17 +151,21 @@ export function scanDOM(root?: HTMLElement): void {
// Load icons
if (coreModules.api) {
const api = coreModules.api;
Object.keys(loadIcons).forEach(prefix => {
api.loadIcons(
Object.keys(loadIcons[prefix]).map(name => {
const icon: IconifyIconName = {
prefix,
name,
};
return icon;
}),
checkPendingIcons
);
Object.keys(loadIcons).forEach((provider) => {
const providerLoadIcons = loadIcons[provider];
Object.keys(providerLoadIcons).forEach((prefix) => {
api.loadIcons(
Object.keys(providerLoadIcons[prefix]).map((name) => {
const icon: IconifyIconName = {
provider,
prefix,
name,
};
return icon;
}),
checkPendingIcons
);
});
});
}