2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-28 04:59:07 +00:00

Move version 2 to a big monorepo

This commit is contained in:
Vjacheslav Trushkin 2020-04-28 12:47:35 +03:00
commit 58d4cf3d49
168 changed files with 70290 additions and 0 deletions

9
.editorconfig Normal file
View File

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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
node_modules

8
.prettierrc Normal file
View File

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

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"spellright.language": [
"en"
],
"spellright.documentTypes": [
"markdown",
"latex",
"plaintext"
]
}

69
README.md Normal file
View File

@ -0,0 +1,69 @@
### What is Iconify?
Iconify is the most versatile icon framework.
- Unified icon framework that can be used with any icon library.
- Out of the box includes 60+ icon sets with 50,000 icons.
- Embed icons in HTML with SVG framework or components for front-end frameworks.
- Embed icons in designs with plug-ins for Figma, Sketch and Adobe XD.
- Add icon search to your applications with Iconify Icon Finder.
For more information visit [https://iconify.design/](https://iconify.design/).
## This repository
This repository is a big monorepo that contains several implementations of Iconify icon framework.
There are two types of Iconify implementations:
- Implementations that rely on icon packages.
- Implementations that rely on Iconify API.
### Implementations: without API
These Iconify implementations require the developer to provide icon data and expect that icon data to be included in the bundle.
Examples: Iconify for React, Iconify for Vue.
They are easy to use and do not require an internet connection to work, similar to other React/Vue components.
### Implementations: with API
Iconify is designed to be easy to use. One of the main features is the Iconify API.
Iconify API provides data for over 50,000 icons! API is hosted on publicly available servers, spread out geographically to make sure visitors from all over the world have the fastest possible connection with redundancies in place to make sure it is always online.
#### Why is API needed?
When you use an icon font, each visitor loads an entire font, even if your page only uses a few icons. This is a major downside of using icon fonts. That limits developers to one or two fonts or icon sets.
Unlike icon fonts, Iconify implementations that use API do not load the entire icon set. Unlike fonts and SVG frameworks, Iconify only loads icons that are used on the current page instead of loading entire icon sets. Iconify API provides icon data to Iconify SVG framework and other implementations that rely on Iconify API.
## Available packages
There are several Iconify implementations included in this repository:
| Implementation | Usage | with API | without API |
| ------------------------------------ | ----- | :------: | :---------: |
| [SVG Framework](./packages/iconify/) | HTML | + | + |
| [React component](./packages/react/) | React | - | + |
| [Vue component](./packages/vue/) | Vue | - | + |
Other packages:
- [Iconify types](./packages/types/) - TypeScript types used by various implementations.
- [Iconify core](./packages/core/) - common files used by various implementations.
- [React demo](./packages/react-demo/) - demo for React component. Run `npm start` to start demo.
- [Vue demo](./packages/vue-demo/) - demo for Vue component. Run `npm serve` to start demo.
- [Browser tests](./packages/browser-tests/) - unit tests for SVG framework. Must be ran in browser.
## License
Iconify is dual-licensed under Apache 2.0 and GPL 2.0 license. You may select, at your option, one of the above-listed licenses.
`SPDX-License-Identifier: Apache-2.0 OR GPL-2.0`
This license does not apply to icons. Icons are released under different licenses, see each icon set for details.
Icons available by default are all licensed under some kind of open-source or free license.
© 2020 Vjacheslav Trushkin

7
lerna.json Normal file
View File

@ -0,0 +1,7 @@
{
"version": "independent",
"npmClient": "npm",
"packages": [
"packages/*"
]
}

6590
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "iconify",
"private": true,
"description": "The most versatile icon framework",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"license": "(Apache-2.0 OR GPL-2.0)",
"workspaces": [
"packages/*"
],
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
"repository": {
"type": "git",
"url": "git://github.com/iconify/iconify.git"
},
"scripts": {
"bootstrap": "lerna bootstrap --force-local",
"clean": "lerna clean",
"link": "lerna link --force-local",
"setup": "npm run clean && npm run bootstrap"
},
"devDependencies": {
"lerna": "^3.20.2"
}
}

0
packages/.gitignore vendored Normal file
View File

4
packages/browser-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
node_modules
lib
dist

View File

@ -0,0 +1,97 @@
const path = require('path');
const child_process = require('child_process');
// List of commands to run
const commands = [];
// Parse command line
const compile = {
core: false,
iconify: false,
lib: true,
dist: true,
};
process.argv.slice(2).forEach(cmd => {
if (cmd.slice(0, 2) !== '--') {
return;
}
const parts = cmd.slice(2).split('-');
if (parts.length === 2) {
// Parse 2 part commands like --with-lib
const key = parts.pop();
if (compile[key] === void 0) {
return;
}
switch (parts.shift()) {
case 'with':
// enable module
compile[key] = true;
break;
case 'without':
// disable module
compile[key] = false;
break;
case 'only':
// disable other modules
Object.keys(compile).forEach(key2 => {
compile[key2] = key2 === key;
});
break;
}
}
});
// Compile core before compiling this package
if (compile.core) {
commands.push({
cmd: 'npm',
args: ['run', 'build'],
cwd: path.dirname(__dirname) + '/core',
});
}
if (compile.iconify || compile.core) {
commands.push({
cmd: 'npm',
args: ['run', 'build'],
cwd: path.dirname(__dirname) + '/iconify',
});
}
// Compile other packages
Object.keys(compile).forEach(key => {
if (key !== 'core' && key !== 'iconify' && compile[key]) {
commands.push({
cmd: 'npm',
args: ['run', 'build:' + key],
});
}
});
/**
* Run next command
*/
const next = () => {
const item = commands.shift();
if (item === void 0) {
process.exit(0);
}
if (item.cwd === void 0) {
item.cwd = __dirname;
}
const result = child_process.spawnSync(item.cmd, item.args, {
cwd: item.cwd,
stdio: 'inherit',
});
if (result.status === 0) {
process.nextTick(next);
} else {
process.exit(result.status);
}
};
next();

1139
packages/browser-tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "@iconify/iconify-browser-tests",
"private": true,
"description": "Browser tests for @iconify/iconify package",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "2.0.0-dev",
"license": "(Apache-2.0 OR GPL-2.0)",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
"repository": {
"type": "git",
"url": "git://github.com/iconify/iconify.git"
},
"scripts": {
"build": "node build",
"build:lib": "tsc -b",
"build:dist": "rollup -c rollup.config.js"
},
"devDependencies": {
"@cyberalien/redundancy": "^1.0.0",
"@iconify/core": "^1.0.0-beta.0",
"@iconify/iconify": "^2.0.0-beta.0",
"@iconify/types": "^1.0.1",
"@rollup/plugin-buble": "^0.21.1",
"@rollup/plugin-commonjs": "^11.0.2",
"@rollup/plugin-node-resolve": "^7.1.1",
"@types/chai": "^4.2.8",
"@types/mocha": "^5.2.7",
"chai": "^4.2.0",
"mocha": "^6.2.2",
"rollup": "^1.32.0",
"typescript": "^3.8.3"
},
"dependencies": {}
}

View File

@ -0,0 +1,54 @@
import fs from 'fs';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import buble from '@rollup/plugin-buble';
const match = '-test.ts';
const files = fs
.readdirSync('tests')
.sort()
.filter(file => file.slice(0 - match.length) === match)
.map(file => file.slice(0, file.length - match.length));
// Get config files
const tests = [];
const config = files.map(file => {
tests.push(file + '.js');
return {
input: 'lib/' + file + match.replace('.ts', '.js'),
output: {
file: 'dist/' + file + '.js',
format: 'iife',
globals: {
mocha: 'mocha',
chai: 'chai',
},
},
external: ['mocha', 'chai'],
plugins: [
resolve({
browser: true,
extensions: ['.js'],
}),
commonjs(),
buble(),
],
};
});
// Write tests.html
let content = fs.readFileSync(__dirname + '/tests/tests.html', 'utf8');
content = content.replace(
'<!-- tests -->',
tests
.map(file => {
return '<script src="./' + file + '"></script>';
})
.join('')
);
try {
fs.mkdirSync(__dirname + '/dist', 0o755);
} catch (err) {}
fs.writeFileSync(__dirname + '/dist/tests.html', content, 'utf8');
export default config;

View File

@ -0,0 +1,199 @@
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 { setAPIConfig } from '@iconify/core/lib/api/config';
import { coreModules } from '@iconify/core/lib/modules';
const expect = chai.expect;
// Set API
setAPIModule({
prepare: prepareQuery,
send: sendQuery,
});
coreModules.api = API;
let prefixCounter = 0;
function nextPrefix(): string {
return 'fake-api-' + prefixCounter++;
}
describe('Testing fake API', () => {
it('Loading results', done => {
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1', 'icon2'],
data: {
prefix,
icons: {
icon1: {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
icon2: {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
setFakeData(prefix, data);
// Attempt to load icons
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
(loaded, missing, pending) => {
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
done();
}
);
});
it('Loading results with delay', done => {
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1', 'icon2'],
delay: 100,
data: {
prefix,
icons: {
icon1: {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
icon2: {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
setFakeData(prefix, data);
// Attempt to load icons
const start = Date.now();
API.loadIcons(
[
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
],
(loaded, missing, pending) => {
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
const end = Date.now();
expect(end - start).to.be.at.least(50);
expect(end - start).to.be.at.most(150);
done();
}
);
});
it('Loading partial results', done => {
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1'],
delay: 20,
data: {
prefix,
icons: {
icon1: {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 20,
timeout: 100,
limit: 1,
},
prefix
);
setFakeData(prefix, data);
// Attempt to load icons
let counter = 0;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
(loaded, missing, pending) => {
try {
counter++;
switch (counter) {
case 1:
// Loaded icon1
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
]);
expect(pending).to.be.eql([
{
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([]);
done();
break;
case 2:
done(
'Callback should not be called ' +
counter +
' times.'
);
}
} catch (err) {
done(err);
}
}
);
});
});

View File

@ -0,0 +1,344 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { finder } from '@iconify/iconify/lib/finders/iconify';
import { IconifyElement } from '@iconify/iconify/lib/element';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
const expect = chai.expect;
describe('Testing Iconify finder', () => {
it('Finding nodes and getting node name', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'<div><p>List of <span>icons</span></p><ul>' +
'<li>Valid icons: <span class="iconify" data-icon="mdi:home"></span><i class="iconify-inline" data-icon="mdi:account"></i></li>' +
'<li>Icon without name: <span class="iconify"></span></li>' +
'<li>Icon with extra classes: <i class="iconify iconify--mdi" data-icon="mdi:home"></i></li>' +
'<li>Icon within icon: <span class="iconify" data-icon="mdi:alert:invalid"><i class="iconify" data-icon="mdi:question">text</i></span></li>' +
'<li>Icon with wrong tag: <p class="iconify" data-icon="mdi:alert"></p></li>' +
'</ul></div>';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
// Test valid icons
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(finder.name(element as IconifyElement)).to.be.equal('mdi:home');
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:account');
expect(finder.name(element as IconifyElement)).to.be.equal(
'mdi:account'
);
// Icon without name
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal(null);
expect(finder.name(element as IconifyElement)).to.be.equal(null);
// Icon with extra classes
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(finder.name(element as IconifyElement)).to.be.equal('mdi:home');
// Icon within icon
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal(
'mdi:alert:invalid'
);
expect(finder.name(element as IconifyElement)).to.be.equal(
'mdi:alert:invalid' // Validation is done in finder.ts, not in finder instance
);
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:question');
expect(finder.name(element as IconifyElement)).to.be.equal(
'mdi:question'
);
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
it('Transformations and inline/block', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'Block icon:' +
' <span class="iconify-inline" data-icon="mdi:home" data-inline="false"></span>' +
'Inline rotated icons:' +
' <span class="iconify-inline" data-icon="mdi:account" data-rotate="90deg"></span>' +
' <span class="iconify iconify-inline" data-icon="mdi:account-circle" data-rotate="2"></span>' +
'Block rotated icons:' +
' <span class="iconify" data-icon="mdi:account-box" data-rotate="175%"></span>' +
// Invalid rotation
' <span class="iconify" data-icon="mdi:user" data-rotate="30%"></span>' +
'Flip:' +
' <span class="iconify" data-icon="ic:baseline-account" data-flip="horizontal,vertical"></span>' +
// Double 'horizontal' flip: second entry should not change anything
' <span class="iconify" data-icon="ic:twotone-account" data-flip="horizontal,vertical,horizontal"></span>' +
' <span class="iconify" data-icon="ic:round-account" data-hFlip="true"></span>' +
' <span class="iconify" data-icon="ic:sharp-account" data-vFlip="true"></span>' +
' <span class="iconify" data-icon="ic:box-account" data-vFlip="false"></span>' +
// Invalid value
' <span class="iconify" data-icon="ic:outline-account" data-hFlip="invalid"></span>' +
'';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
function testElement(
name: string,
expected: IconifyIconCustomisations
): void {
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.getAttribute('data-icon')).to.be.equal(name);
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
}
// Block icon
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
const expected: IconifyIconCustomisations = {
inline: false,
};
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
// Rotated icons
testElement('mdi:account', {
inline: true,
rotate: 1,
});
testElement('mdi:account-circle', {
inline: true,
rotate: 2,
});
testElement('mdi:account-box', {
inline: false,
rotate: 3,
});
testElement('mdi:user', {
inline: false,
// No rotation because 30% is not a valid value
});
// Flip
testElement('ic:baseline-account', {
inline: false,
hFlip: true,
vFlip: true,
});
testElement('ic:twotone-account', {
inline: false,
hFlip: true,
vFlip: true,
});
testElement('ic:round-account', {
inline: false,
hFlip: true,
});
testElement('ic:sharp-account', {
inline: false,
vFlip: true,
});
testElement('ic:box-account', {
inline: false,
vFlip: false,
});
testElement('ic:outline-account', {
inline: false,
});
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
it('Dimensions', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'Block icon:' +
' <span class="iconify iconify-inline" data-icon="mdi:home" data-inline="false"></span>' +
'Width and height:' +
' <span class="iconify" data-icon="mdi:account" data-width="24" data-height="24"></span>' +
' <span class="iconify" data-icon="mdi:account-box" data-width="100%" data-height="100%"></span>' +
'Width:' +
' <span class="iconify" data-icon="mdi:account-twotone" data-width="32" data-inline="true"></span>' +
' <span class="iconify" data-icon="mdi:account-outline" data-width="auto" data-height=""></span>' +
'Height:' +
' <span class="iconify-inline" data-icon="mdi:account-sharp" data-height="2em" data-inline="false"></span>' +
' <span class="iconify-inline" data-icon="mdi:account-square" data-height="auto" data-width=""></span>' +
'';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
function testElement(
name: string,
expected: IconifyIconCustomisations
): void {
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.getAttribute('data-icon')).to.be.equal(name);
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
}
// Block icon
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
const expected: IconifyIconCustomisations = {
inline: false,
};
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
// Width and height
testElement('mdi:account', {
inline: false,
width: '24',
height: '24',
});
testElement('mdi:account-box', {
inline: false,
width: '100%',
height: '100%',
});
// Width only
testElement('mdi:account-twotone', {
inline: true,
width: '32',
});
testElement('mdi:account-outline', {
inline: false,
width: 'auto',
});
// Height only
testElement('mdi:account-sharp', {
inline: false,
height: '2em',
});
testElement('mdi:account-square', {
inline: true,
height: 'auto',
});
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
it('Alignment', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'Inline icon:' +
' <i class="iconify" data-icon="mdi:home" data-inline="true"></i>' +
'Alignment:' +
' <i class="iconify" data-icon="mdi:account" data-align="left,top"></i>' +
' <i class="iconify" data-icon="mdi:account-box" data-align="right,slice"></i>' +
// 'bottom' overrides 'top', 'center' overrides 'right', extra comma
' <i class="iconify-inline" data-icon="mdi:account-outline" data-align="top,right,bottom,meet,center,"></i>' +
// spaces instead of comma, 'middle' overrides 'bottom'
' <i class="iconify iconify-inline" data-icon="mdi:account-twotone" data-align="bottom middle"></i>' +
'';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
function testElement(
name: string,
expected: IconifyIconCustomisations
): void {
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.getAttribute('data-icon')).to.be.equal(name);
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
}
// First icon
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
const expected: IconifyIconCustomisations = {
inline: true,
};
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
// Alignment
testElement('mdi:account', {
inline: false,
hAlign: 'left',
vAlign: 'top',
});
testElement('mdi:account-box', {
inline: false,
hAlign: 'right',
slice: true,
});
testElement('mdi:account-outline', {
inline: true,
hAlign: 'center',
vAlign: 'bottom',
slice: false,
});
testElement('mdi:account-twotone', {
inline: true,
vAlign: 'middle',
});
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
});

View File

@ -0,0 +1,74 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { addFinder, findPlaceholders } from '@iconify/iconify/lib/finder';
import { IconifyFinder } from '@iconify/iconify/lib/interfaces/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify-v1';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-v1-icon';
import { IconifyIconName } from '@iconify/core/lib/icon/name';
const expect = chai.expect;
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
describe('Testing legacy finder', () => {
it('Finding nodes', () => {
const node = getNode('finder');
node.innerHTML =
'<div><p>List of <span>icons</span></p><ul>' +
'<li>Valid icons:' +
' <span class="iconify" data-icon="mdi:home"></span>' +
' <i class="iconify" data-icon="mdi:account"></i>' +
'</li>' +
'<li>Icon without name:' +
' <span class="iconify"></span>' +
'</li>' +
'<li>Block icon:' +
' <iconify-icon data-icon="ic:baseline-account"></iconify-icon>' +
'</li>' +
'<li>Icon with wrong tag: <p class="iconify" data-icon="mdi:alert"></p></li>' +
'</ul></div>';
const items = findPlaceholders(node);
function testIcon(
name: IconifyIconName | null,
expectedFinder: IconifyFinder
): void {
const item = items.shift();
expect(item.name).to.be.eql(name);
expect(item.finder).to.be.equal(expectedFinder);
}
// Test all icons
testIcon(
{
prefix: 'mdi',
name: 'home',
},
iconifyFinder
);
testIcon(
{
prefix: 'mdi',
name: 'account',
},
iconifyFinder
);
testIcon(
{
prefix: 'ic',
name: 'baseline-account',
},
iconifyIconFinder
);
// End of list
expect(items.shift()).to.be.equal(void 0);
});
});

View File

@ -0,0 +1,74 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { addFinder, findPlaceholders } from '@iconify/iconify/lib/finder';
import { IconifyFinder } from '@iconify/iconify/lib/interfaces/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { IconifyIconName } from '@iconify/core/lib/icon/name';
const expect = chai.expect;
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
describe('Testing finder', () => {
it('Finding nodes', () => {
const node = getNode('finder');
node.innerHTML =
'<div><p>List of <span>icons</span></p><ul>' +
'<li>Valid icons:' +
' <span class="iconify" data-icon="mdi:home"></span>' +
' <i class="iconify" data-icon="mdi:account"></i>' +
'</li>' +
'<li>Icon without name:' +
' <span class="iconify"></span>' +
'</li>' +
'<li>Block icon:' +
' <iconify-icon data-icon="ic:baseline-account"></iconify-icon>' +
'</li>' +
'<li>Icon with wrong tag: <p class="iconify" data-icon="mdi:alert"></p></li>' +
'</ul></div>';
const items = findPlaceholders(node);
function testIcon(
name: IconifyIconName | null,
expectedFinder: IconifyFinder
): void {
const item = items.shift();
expect(item.name).to.be.eql(name);
expect(item.finder).to.be.equal(expectedFinder);
}
// Test all icons
testIcon(
{
prefix: 'mdi',
name: 'home',
},
iconifyFinder
);
testIcon(
{
prefix: 'mdi',
name: 'account',
},
iconifyFinder
);
testIcon(
{
prefix: 'ic',
name: 'baseline-account',
},
iconifyIconFinder
);
// End of list
expect(items.shift()).to.be.equal(void 0);
});
});

View File

@ -0,0 +1,219 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
const expect = chai.expect;
// Dummy svgAttributes variable
const svgAttributes: Record<string, unknown> = {};
/**
* Copy attributes from placeholder to SVG.
*
* This is similar to code used in render.ts
*
* @param placeholderElement
* @param svg
*/
function copyData(placeholderElement, svg) {
const svgStyle = svg.style;
// Copy attributes from placeholder
const placeholderAttributes = placeholderElement.attributes;
for (let i = 0; i < placeholderAttributes.length; i++) {
const item = placeholderAttributes.item(i);
if (item) {
const name = item.name;
if (
name !== 'class' &&
name !== 'style' &&
svgAttributes[name] === void 0
) {
try {
svg.setAttribute(name, item.value);
} catch (err) {}
}
}
}
// Copy styles from placeholder
const placeholderStyle = placeholderElement.style;
for (let i = 0; i < placeholderStyle.length; i++) {
const attr = placeholderStyle[i];
const value = placeholderStyle[attr];
if (value !== '') {
svgStyle[attr] = value;
}
}
}
describe('Testing copying node data', () => {
it('Inline attributes', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p title="Testing" data-foo="bar">Test</p>';
const source = node.querySelector('p');
const target = document.createElement('span');
copyData(source, target);
// Test title
expect(source.getAttribute('title')).to.be.equal(
'Testing',
'Source title should be set'
);
expect(target.getAttribute('title')).to.be.equal(
'Testing',
'Target title should be set'
);
// Test data-*
expect(source.getAttribute('data-foo')).to.be.equal(
'bar',
'Source data-foo should be set'
);
expect(target.getAttribute('data-foo')).to.be.equal(
'bar',
'Target data-foo should be set'
);
});
it('Inline style', () => {
const node = getNode('node-attributes');
node.innerHTML =
'<p style="color: red; border: 1px solid green; vertical-align: -.1em">Test</p>';
const source = node.querySelector('p');
const target = document.createElement('span');
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'red',
'Source color should be red'
);
expect(target.style.color).to.be.equal(
'red',
'Target color should be red'
);
// Test border width
expect(source.style.borderWidth).to.be.equal(
'1px',
'Source border width should be 1px'
);
expect(target.style.borderWidth).to.be.equal(
'1px',
'Target border width should be 1px'
);
// Test background color
expect(source.style.backgroundColor).to.be.equal(
'',
'Source background color should not be set'
);
expect(target.style.backgroundColor).to.be.equal(
'',
'Target background color should not be set'
);
});
it('DOM style', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p>Test</p>';
const source = node.querySelector('p');
source.style.color = 'green';
source.style.border = '2px solid blue';
const target = document.createElement('span');
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'green',
'Source color should be green'
);
expect(target.style.color).to.be.equal(
'green',
'Target color should be green'
);
// Test border width
expect(source.style.borderWidth).to.be.equal(
'2px',
'Source border width should be 2px'
);
expect(target.style.borderWidth).to.be.equal(
'2px',
'Target border width should be 2px'
);
// Test background color
expect(source.style.backgroundColor).to.be.equal(
'',
'Source background color should not be set'
);
expect(target.style.backgroundColor).to.be.equal(
'',
'Target background color should not be set'
);
});
it('Overwriting source style before copy', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p style="color: blue">Test</p>';
const source = node.querySelector('p');
// Overwrite inline style
source.style.color = 'purple';
const target = document.createElement('span');
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'purple',
'Source color should be purple'
);
expect(target.style.color).to.be.equal(
'purple',
'Target color should be purple'
);
});
it('Overwriting target style during copy', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p style="color: blue">Test</p>';
const source = node.querySelector('p');
const target = document.createElement('span');
// Set target style
target.style.color = 'purple';
target.style.verticalAlign = '-0.2em';
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'blue',
'Source color should be blue'
);
expect(target.style.color).to.be.equal(
'blue',
'Target color should be blue'
);
// Test vertical-align
expect(source.style.verticalAlign).to.be.equal(
'',
'Source vartical-align should not be set'
);
expect(target.style.verticalAlign).to.be.equal(
'-0.2em',
'Target vertical-align should be set'
);
});
});

View File

@ -0,0 +1,40 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { browserModules } from '@iconify/iconify/lib/modules';
import { observer } from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
describe('Testing observer creation', () => {
it('Creating observer and triggering event', done => {
const node = getNode('observer-creation');
browserModules.root = node;
let counter = 0;
node.innerHTML = '<div></div><ul><li>test</li><li>test2</li></ul>';
observer.init(root => {
expect(root).to.be.equal(node);
counter++;
// Should be called only once
expect(counter).to.be.equal(1);
expect(observer.isPaused()).to.be.equal(false);
// Pause observer
observer.pause();
expect(observer.isPaused()).to.be.equal(true);
done();
});
// Add few nodes to trigger observer
expect(observer.isPaused()).to.be.equal(false);
node.querySelector('div').innerHTML =
'<span class="test">Some text</span><i>!</i>';
});
});

View File

@ -0,0 +1,110 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { elementFinderProperty } from '@iconify/iconify/lib/element';
import { browserModules } from '@iconify/iconify/lib/modules';
import { observer } from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
describe('Testing observer with DOM manipulation', () => {
it('Series of events', (done) => {
const node = getNode('observer-manipulation');
browserModules.root = node;
let counter = 0;
let waitingCallback: string | boolean = true;
node.innerHTML =
'<div></div><ul><li>test</li><li>test2</li></ul><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg); vertical-align: -0.125em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi-home" data-inline="false" class="iconify"><path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"></path></svg>';
observer.init((root) => {
expect(root).to.be.equal(node);
expect(waitingCallback).to.be.equal(true);
counter++;
switch (counter) {
case 1:
// Added few nodes
// Remove few nodes. It should not trigger event listener
waitingCallback = 'removing nodes';
(() => {
const item = node.querySelector('ul > li:last-child');
const parent = item.parentNode;
parent.removeChild(item);
// Set timer for next step to make sure callback is not called
setTimeout(() => {
// Add node. This should trigger callback
const newItem = document.createElement('li');
parent.appendChild(newItem);
waitingCallback = true;
}, 50);
})();
break;
case 2:
// Added one list item
// Pause observer
waitingCallback = 'pause test';
(() => {
const item = node.querySelector('ul > li:last-child');
observer.pause();
item.innerHTML = '<string>Strong</strong> text!';
// Set timer for next step to make sure callback is not called
setTimeout(() => {
// Resume observer and wait a bit. Resuming observer should not trigger update
waitingCallback = 'resume test';
observer.resume();
setTimeout(() => {
// Change text of item: should remove <strong> and add new text node
waitingCallback = true;
item.innerHTML = 'Weak text!';
}, 50);
}, 50);
})();
break;
case 3:
waitingCallback = 'attributes on ul';
(() => {
const item = node.querySelector('ul');
item.setAttribute('data-foo', 'bar');
// Set timer for next step to make sure callback is not called
setTimeout(() => {
waitingCallback = true;
const item = node.querySelector('svg');
item[elementFinderProperty] = true; // Add fake finder property to trigger update
item.setAttribute('data-icon', 'mdi-account');
item.setAttribute('data-rotate', '2');
}, 50);
})();
break;
case 4:
(() => {
// Test removing attribute from SVG
const item = node.querySelector('svg');
item.removeAttribute('data-rotate');
})();
break;
case 5:
done();
break;
default:
done('Unexpected callback call!');
}
});
// Add few nodes to trigger observer
expect(observer.isPaused()).to.be.equal(false);
node.querySelector('div').innerHTML =
'<span class="test">Some text</span><i>!</i>';
});
});

View File

@ -0,0 +1,991 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { addFinder, findPlaceholders } from '@iconify/iconify/lib/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify-v1';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-v1-icon';
import { getStorage, addIconSet, getIcon } from '@iconify/core/lib/storage';
import { renderIcon } from '@iconify/iconify/lib/render';
import { stringToIcon } from '@iconify/core/lib/icon/name';
const expect = chai.expect;
// Add finders
addFinder(iconifyIconFinder);
addFinder(iconifyFinder);
describe('Testing legacy renderer', () => {
// Add mentioned icons to storage
const storage = getStorage('mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
it('Convert placeholders to SVG', () => {
const node = getNode('renderer');
node.innerHTML =
'<div><p>Testing renderer v1</p><ul>' +
'<li>Inline icons:<br />' +
' Red icon with red border: <span class="iconify" data-icon="mdi:home" style="color: red; border: 1px solid red;"></span><br />' +
' No vertical-align, green border: <i class="iconify test-icon iconify--mdi-account" data-icon="mdi:account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="mdi-account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <span class="iconify-icon" data-icon="mdi:account" data-flip="vertical" data-width="auto"></span>' +
'</li>' +
'<li>Changed by attribute:' +
' <iconify-icon data-icon="mdi:account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'<li>Mix of classes:' +
' <i class="iconify iconify-icon should-be-block" data-icon="mdi:home"></i>' +
'</li>' +
'</ul></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(6);
// Test finders to make sure icons are in correct order
expect(items[0].finder).to.be.equal(iconifyIconFinder);
expect(items[1].finder).to.be.equal(iconifyIconFinder);
expect(items[2].finder).to.be.equal(iconifyIconFinder);
expect(items[3].finder).to.be.equal(iconifyIconFinder);
expect(items[4].finder).to.be.equal(iconifyFinder);
expect(items[5].finder).to.be.equal(iconifyFinder);
/**
* Test third icon (first 2 should be last)
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account-cash',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi-account-cash'); // name should stay as is
expect(svg.getAttribute('class')).to.be.equal('iconify iconify--mdi');
expect(svg.getAttribute('title')).to.be.equal('<Cash>!'); // title, unescaped
expect(svg.style.verticalAlign).to.be.equal('');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi-account-cash');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test fourth item
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
vFlip: true,
width: 'auto',
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('24');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:account');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi iconify-icon'
);
// Block
expect(svg.style.verticalAlign).to.be.equal('');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:account');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test fifth icon
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account-box',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
rotate: 2,
width: '42',
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('42');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:account-box');
expect(svg.getAttribute('class')).to.be.equal('iconify iconify--mdi');
// IE rounds value
let verticalAlign = svg.style.verticalAlign;
expect(
verticalAlign === '-0.125em' || verticalAlign === '-0.12em'
).to.be.equal(true, 'Invalid vertical-align value: ' + verticalAlign);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:account-box');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Save SVG for rotation test below
const rotationSVG = svg;
/**
* Test sixth icon
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi iconify-icon should-be-block'
);
// Block
expect(svg.style.verticalAlign).to.be.equal('');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test first icon
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('width')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(svg.getAttribute('class')).to.be.equal('iconify iconify--mdi');
expect(svg.style.color).to.be.equal('red'); // color from inline style
expect(svg.style.borderWidth).to.be.equal('1px'); // border from inline style
// IE rounds value
verticalAlign = svg.style.verticalAlign;
expect(
verticalAlign === '-0.125em' || verticalAlign === '-0.12em'
).to.be.equal(true, 'Invalid vertical-align value: ' + verticalAlign);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test second item
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Set style
element.element.style.border = '1px solid green';
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:account');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi test-icon'
); // add 'test-icon' class, remove 'iconify--mdi-account'
expect(svg.style.verticalAlign).to.be.equal('0px'); // inline style overrides verticalAlign from data-align attribute
expect(svg.style.borderWidth).to.be.equal('1px'); // style set via DOM
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:account');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// All placeholder should have been replaced
expect(items).to.be.eql([]);
expect(findPlaceholders(node)).to.be.eql([]);
/**
* Test finding modified SVG
*/
// Remove rotation
rotationSVG.removeAttribute('data-rotate');
const items2 = findPlaceholders(node);
expect(items2.length).to.be.equal(1);
element = items2.shift();
expect(element.element).to.be.equal(rotationSVG);
expect(element.customisations).to.be.eql({
inline: true,
width: '42',
});
});
it('Change attributes', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing attributes v1: <span class="iconify" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('width')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(svg.getAttribute('class')).to.be.equal('iconify iconify--mdi');
expect(svg.style.color).to.be.equal('red'); // color from inline style
// IE rounds value
let verticalAlign = svg.style.verticalAlign;
expect(
verticalAlign === '-0.125em' || verticalAlign === '-0.12em'
).to.be.equal(true, 'Invalid vertical-align value: ' + verticalAlign);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Render SVG without changes
*/
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql(lastCustomisations); // customisations were not changed
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test attributes, compare them with last SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
[
'aria-hidden',
'focusable',
'role',
'width',
'height',
'viewBox',
'preserveAspectRatio',
'data-icon',
'class',
].forEach((attr) => {
expect(svg.getAttribute(attr)).to.be.equal(
lastSVG.getAttribute(attr),
'Different values for attribute ' + attr
);
});
['vertical-align', 'color'].forEach((attr) => {
expect(svg.style[attr]).to.be.equal(
lastSVG.style[attr],
'Different values for style ' + attr
);
});
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Copy variables for next test
lastElement = element;
lastCustomisations = customisations;
lastSVG = svg;
/**
* Rotate icon
*/
lastSVG.setAttribute('data-rotate', '1');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: true,
rotate: 1,
hFlip: true,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test changed attributes
expect(svg.getAttribute('data-rotate')).to.be.equal('1');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Copy variables for next test
lastElement = element;
lastCustomisations = customisations;
lastSVG = svg;
/**
* Change icon name and reset flip
*/
lastSVG.setAttribute('data-icon', 'mdi-account');
lastSVG.removeAttribute('data-flip');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
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({
prefix: 'mdi',
name: 'account',
});
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: true,
rotate: 1,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test changed attributes
expect(svg.getAttribute('data-rotate')).to.be.equal('1');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi-account');
expect(svg.getAttribute('data-flip')).to.be.equal(null);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi-account');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Invalid icon name', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing invalid icon name v1: <span class="iconify" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
/**
* Change icon name to invalid
*/
svg.setAttribute('data-icon', 'mdi');
const name = element.finder.name(svg);
expect(name).to.be.equal('mdi');
expect(stringToIcon(name as string)).to.be.equal(null);
});
it('Empty icon name', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing empty icon name v1: <span class="iconify" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
/**
* Change icon name to invalid
*/
svg.removeAttribute('data-icon');
expect(element.finder.name(svg)).to.be.equal(null);
});
it('Change icon name', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing icon name v1: <span class="iconify" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
expect(iconData.body.indexOf('M6 17c0-2')).to.be.equal(
-1,
'Wrong icon body: ' + iconData.body
);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Change name
*/
svg.setAttribute('data-icon', 'mdi:account-box');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' and 'name' properties
expect(element.name).to.not.be.eql(lastElement.name); // different 'name' property
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql(lastCustomisations); // customisations were not changed
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Test icon body to make sure icon was changed
expect(iconData.body.indexOf('M6 17c0-2')).to.not.be.equal(
-1,
'Wrong icon body: ' + iconData.body
);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:account-box');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Rotating icon', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing rotation v1: <span class="iconify-icon" data-icon="mdi:home"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
if (svg.innerHTML) {
const html = svg.innerHTML;
// Test icon body to make sure icon has no transformation
expect(html.indexOf('transform="')).to.be.equal(
-1,
'Found transform in icon: ' + html
);
}
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Rotate
*/
svg.setAttribute('data-rotate', '2');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: false,
rotate: 2,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
if (svg.innerHTML) {
const html = svg.innerHTML;
// Test icon body to make sure icon was changed
expect(html.indexOf('transform="')).to.not.be.equal(
-1,
'Missing transform in icon: ' + html
);
}
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Changing size', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing size v1: <span class="iconify" data-icon="mdi:home" style="box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Check dimensions
expect(svg.getAttribute('width')).to.be.equal('1em');
expect(svg.getAttribute('height')).to.be.equal('1em');
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Set height
*/
svg.setAttribute('data-height', '24');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: true,
height: '24',
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Check dimensions
expect(svg.getAttribute('width')).to.be.equal('24');
expect(svg.getAttribute('height')).to.be.equal('24');
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Changing alignment', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing alignment v1: <span class="iconify" data-icon="mdi:home" data-width="48" data-height="24" style="box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
width: '48',
height: '24',
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Check dimensions and alignment
expect(svg.getAttribute('width')).to.be.equal('48');
expect(svg.getAttribute('height')).to.be.equal('24');
expect(svg.getAttribute('preserveAspectRatio')).to.be.equal(
'xMidYMid meet'
);
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Set alignment
*/
svg.setAttribute('data-align', 'left, bottom');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: true,
width: '48',
height: '24',
hAlign: 'left',
vAlign: 'bottom',
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Check dimensions and alignment
expect(svg.getAttribute('width')).to.be.equal('48');
expect(svg.getAttribute('height')).to.be.equal('24');
expect(svg.getAttribute('preserveAspectRatio')).to.be.equal(
'xMinYMax meet'
);
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
});

View File

@ -0,0 +1,995 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { addFinder, findPlaceholders } from '@iconify/iconify/lib/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { getStorage, addIconSet, getIcon } from '@iconify/core/lib/storage';
import { renderIcon } from '@iconify/iconify/lib/render';
import { stringToIcon } from '@iconify/core/lib/icon/name';
const expect = chai.expect;
// Add finders
addFinder(iconifyIconFinder);
addFinder(iconifyFinder);
describe('Testing renderer', () => {
// Add mentioned icons to storage
const storage = getStorage('mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
it('Convert placeholders to SVG', () => {
const node = getNode('renderer');
node.innerHTML =
'<div><p>Testing renderer v2</p><ul>' +
'<li>Inline icons:<br />' +
' Red icon with red border: <span class="iconify-inline" data-icon="mdi:home" style="color: red; border: 1px solid red;"></span><br />' +
' No vertical-align, green border: <i class="iconify test-icon iconify-inline iconify--mdi-account" data-icon="mdi:account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="mdi-account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <span class="iconify-icon" data-icon="mdi:account" data-flip="vertical" data-width="auto"></span>' +
'</li>' +
'<li>Changed by attribute:' +
' <iconify-icon data-icon="mdi:account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'<li>Mix of classes:' +
' <i class="iconify iconify-icon should-be-block" data-icon="mdi:home"></i>' +
'</li>' +
'</ul></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(6);
// Test finders to make sure icons are in correct order
expect(items[0].finder).to.be.equal(iconifyIconFinder);
expect(items[1].finder).to.be.equal(iconifyIconFinder);
expect(items[2].finder).to.be.equal(iconifyIconFinder);
expect(items[3].finder).to.be.equal(iconifyIconFinder);
expect(items[4].finder).to.be.equal(iconifyFinder);
expect(items[5].finder).to.be.equal(iconifyFinder);
/**
* Test third icon (first 2 should be last)
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account-cash',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi-account-cash'); // name should stay as is
expect(svg.getAttribute('class')).to.be.equal('iconify iconify--mdi');
expect(svg.getAttribute('title')).to.be.equal('<Cash>!'); // title, unescaped
expect(svg.style.verticalAlign).to.be.equal('');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi-account-cash');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test fourth item
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
vFlip: true,
width: 'auto',
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('24');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:account');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi iconify-icon'
);
// Block
expect(svg.style.verticalAlign).to.be.equal('');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:account');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test fifth icon
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account-box',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
rotate: 2,
width: '42',
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('42');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:account-box');
expect(svg.getAttribute('class')).to.be.equal('iconify iconify--mdi');
// IE rounds value
let verticalAlign = svg.style.verticalAlign;
expect(
verticalAlign === '-0.125em' || verticalAlign === '-0.12em'
).to.be.equal(true, 'Invalid vertical-align value: ' + verticalAlign);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:account-box');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Save SVG for rotation test below
const rotationSVG = svg;
/**
* Test sixth icon
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi iconify-icon should-be-block'
);
// Block
expect(svg.style.verticalAlign).to.be.equal('');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test first icon
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
});
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('width')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi iconify-inline'
);
expect(svg.style.color).to.be.equal('red'); // color from inline style
expect(svg.style.borderWidth).to.be.equal('1px'); // border from inline style
// IE rounds value
verticalAlign = svg.style.verticalAlign;
expect(
verticalAlign === '-0.125em' || verticalAlign === '-0.12em'
).to.be.equal(true, 'Invalid vertical-align value: ' + verticalAlign);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
/**
* Test second item
*/
element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'account',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Set style
element.element.style.border = '1px solid green';
// Get icon data
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('height')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:account');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi test-icon iconify-inline'
); // add 'test-icon' class, remove 'iconify--mdi-account'
expect(svg.style.verticalAlign).to.be.equal('0px'); // inline style overrides verticalAlign from data-align attribute
expect(svg.style.borderWidth).to.be.equal('1px'); // style set via DOM
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:account');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// All placeholder should have been replaced
expect(items).to.be.eql([]);
expect(findPlaceholders(node)).to.be.eql([]);
/**
* Test finding modified SVG
*/
// Remove rotation
rotationSVG.removeAttribute('data-rotate');
const items2 = findPlaceholders(node);
expect(items2.length).to.be.equal(1);
element = items2.shift();
expect(element.element).to.be.equal(rotationSVG);
expect(element.customisations).to.be.eql({
inline: true,
width: '42',
});
});
it('Change attributes', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing attributes v2: <span class="iconify-inline" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Test SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
expect(svg.getAttribute('viewBox')).to.be.equal('0 0 24 24');
expect(svg.getAttribute('width')).to.be.equal('1em');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(svg.getAttribute('class')).to.be.equal(
'iconify iconify--mdi iconify-inline'
);
expect(svg.style.color).to.be.equal('red'); // color from inline style
// IE rounds value
let verticalAlign = svg.style.verticalAlign;
expect(
verticalAlign === '-0.125em' || verticalAlign === '-0.12em'
).to.be.equal(true, 'Invalid vertical-align value: ' + verticalAlign);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Render SVG without changes
*/
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql(lastCustomisations); // customisations were not changed
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test attributes, compare them with last SVG
expect(svg.tagName.toUpperCase()).to.be.equal('SVG');
[
'aria-hidden',
'focusable',
'role',
'width',
'height',
'viewBox',
'preserveAspectRatio',
'data-icon',
'class',
].forEach((attr) => {
expect(svg.getAttribute(attr)).to.be.equal(
lastSVG.getAttribute(attr),
'Different values for attribute ' + attr
);
});
['vertical-align', 'color'].forEach((attr) => {
expect(svg.style[attr]).to.be.equal(
lastSVG.style[attr],
'Different values for style ' + attr
);
});
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Copy variables for next test
lastElement = element;
lastCustomisations = customisations;
lastSVG = svg;
/**
* Rotate icon
*/
lastSVG.setAttribute('data-rotate', '1');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: true,
rotate: 1,
hFlip: true,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test changed attributes
expect(svg.getAttribute('data-rotate')).to.be.equal('1');
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
// Copy variables for next test
lastElement = element;
lastCustomisations = customisations;
lastSVG = svg;
/**
* Change icon name and reset flip
*/
lastSVG.setAttribute('data-icon', 'mdi-account');
lastSVG.removeAttribute('data-flip');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
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({
prefix: 'mdi',
name: 'account',
});
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: true,
rotate: 1,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test changed attributes
expect(svg.getAttribute('data-rotate')).to.be.equal('1');
expect(svg.getAttribute('data-icon')).to.be.equal('mdi-account');
expect(svg.getAttribute('data-flip')).to.be.equal(null);
// Test finder on SVG
expect(element.finder.name(svg)).to.be.equal('mdi-account');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Invalid icon name', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing invalid icon name v2: <span class="iconify-inline" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: true,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
/**
* Change icon name to invalid
*/
svg.setAttribute('data-icon', 'mdi');
const name = element.finder.name(svg);
expect(name).to.be.equal('mdi');
expect(stringToIcon(name as string)).to.be.equal(null);
});
it('Empty icon name', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing empty icon name v2: <span class="iconify" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
/**
* Change icon name to invalid
*/
svg.removeAttribute('data-icon');
expect(element.finder.name(svg)).to.be.equal(null);
});
it('Change icon name', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing icon name v2: <span class="iconify" data-icon="mdi:home" data-flip="horizontal" style="color: red; box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
hFlip: true,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
expect(iconData.body.indexOf('M6 17c0-2')).to.be.equal(
-1,
'Wrong icon body: ' + iconData.body
);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Change name
*/
svg.setAttribute('data-icon', 'mdi:account-box');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' and 'name' properties
expect(element.name).to.not.be.eql(lastElement.name); // different 'name' property
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql(lastCustomisations); // customisations were not changed
expect(customisations).to.be.eql({
inline: false,
hFlip: true,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Test icon body to make sure icon was changed
expect(iconData.body.indexOf('M6 17c0-2')).to.not.be.equal(
-1,
'Wrong icon body: ' + iconData.body
);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:account-box');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Rotating icon', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing rotation v2: <span class="iconify-icon" data-icon="mdi:home"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyIconFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
if (svg.innerHTML) {
const html = svg.innerHTML;
// Test icon body to make sure icon has no transformation
expect(html.indexOf('transform="')).to.be.equal(
-1,
'Found transform in icon: ' + html
);
}
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Rotate
*/
svg.setAttribute('data-rotate', '2');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: false,
rotate: 2,
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
if (svg.innerHTML) {
const html = svg.innerHTML;
// Test icon body to make sure icon was changed
expect(html.indexOf('transform="')).to.not.be.equal(
-1,
'Missing transform in icon: ' + html
);
}
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Changing size', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing size v2: <span class="iconify" data-icon="mdi:home" style="box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Check dimensions
expect(svg.getAttribute('width')).to.be.equal('1em');
expect(svg.getAttribute('height')).to.be.equal('1em');
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Set height
*/
svg.setAttribute('data-height', '24');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: false,
height: '24',
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Check dimensions
expect(svg.getAttribute('width')).to.be.equal('24');
expect(svg.getAttribute('height')).to.be.equal('24');
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
it('Changing alignment', () => {
const node = getNode('renderer');
node.innerHTML =
'<div>Testing alignment v2: <span class="iconify" data-icon="mdi:home" data-width="48" data-height="24" style="box-shadow: 0 0 2px black;"></span></div>';
// Get items
const items = findPlaceholders(node);
expect(items.length).to.be.equal(1);
/**
* Test icon
*/
let element = items.shift();
// Test element
expect(element.name).to.be.eql({
prefix: 'mdi',
name: 'home',
});
expect(element.finder).to.be.equal(iconifyFinder);
// Get and test customisations
let customisations = element.finder.customisations(element.element);
expect(customisations).to.be.eql({
inline: false,
width: '48',
height: '24',
});
// Get icon data
let iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
// Render icon
let svg = renderIcon(element, customisations, iconData);
// Check dimensions and alignment
expect(svg.getAttribute('width')).to.be.equal('48');
expect(svg.getAttribute('height')).to.be.equal('24');
expect(svg.getAttribute('preserveAspectRatio')).to.be.equal(
'xMidYMid meet'
);
// Copy variables for next test
let lastElement = element;
let lastCustomisations = customisations;
let lastSVG = svg;
/**
* Set alignment
*/
svg.setAttribute('data-align', 'left, bottom');
// Create new element
element = {
element: svg,
finder: element.finder,
name: stringToIcon(element.finder.name(svg) as string),
};
expect(element).to.not.be.eql(lastElement); // different 'element' property
expect(element.name).to.be.eql(lastElement.name);
// Get customisations
customisations = element.finder.customisations(element.element);
expect(customisations).to.not.be.eql(lastCustomisations); // customisations were changed
expect(customisations).to.be.eql({
inline: false,
width: '48',
height: '24',
hAlign: 'left',
vAlign: 'bottom',
});
// Get icon data and render SVG
iconData = getIcon(storage, element.name.name);
expect(iconData).to.not.be.equal(null);
svg = renderIcon(element, customisations, iconData);
expect(svg).to.not.be.eql(lastSVG);
// Check dimensions and alignment
expect(svg.getAttribute('width')).to.be.equal('48');
expect(svg.getAttribute('height')).to.be.equal('24');
expect(svg.getAttribute('preserveAspectRatio')).to.be.equal(
'xMinYMax meet'
);
// Test finder
expect(element.finder.name(svg)).to.be.equal('mdi:home');
expect(element.finder.customisations(svg)).to.be.eql(customisations);
});
});

View File

@ -0,0 +1,66 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { addFinder } from '@iconify/iconify/lib/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { getStorage, addIconSet } from '@iconify/core/lib/storage';
import { browserModules } from '@iconify/iconify/lib/modules';
import { scanDOM } from '@iconify/iconify/lib/scan';
const expect = chai.expect;
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
describe('Scanning DOM', () => {
// Add mentioned icons to storage
const storage = getStorage('mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
it('Scan DOM with preloaded icons', () => {
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM</p><ul>' +
'<li>Valid icons:' +
' <span class="iconify" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify test-icon iconify--mdi-account" data-icon="mdi:account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icon:' +
' <iconify-icon data-icon="mdi-account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon data-icon="mdi:account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'</ul></div>';
browserModules.root = node;
scanDOM();
// Find elements
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(4);
});
});

View File

@ -0,0 +1,373 @@
import mocha from 'mocha';
import chai from 'chai';
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 { setAPIConfig } from '@iconify/core/lib/api/config';
import { coreModules } from '@iconify/core/lib/modules';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { browserModules } from '@iconify/iconify/lib/modules';
import { scanDOM } from '@iconify/iconify/lib/scan';
const expect = chai.expect;
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
// Set API
setAPIModule({
prepare: prepareQuery,
send: sendQuery,
});
coreModules.api = API;
let prefixCounter = 0;
function nextPrefix(): string {
return 'scan-dom-api-' + prefixCounter++;
}
describe('Scanning DOM with API', () => {
it('Scan DOM with API', (done) => {
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
[prefix1, prefix2]
);
// Set icons, load them with various delay
const data1: FakeData = {
icons: ['home', 'account-cash'],
delay: 100,
data: {
prefix: prefix1,
icons: {
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
delay: 500,
data: {
prefix: prefix2,
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(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="' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <i class="iconify-icon" data-icon="' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></i>' +
'</li>' +
'</ul></div>';
browserModules.root = node;
scanDOM();
// First API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
2,
'Expected to find 2 rendered SVG elements'
);
}, 200);
// Second API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
4,
'Expected to find 4 rendered SVG elements'
);
done();
}, 700);
});
it('Changing icon name before it loaded', (done) => {
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
[prefix1, prefix2]
);
// Set icons, load them with various delay
const data1: FakeData = {
icons: ['home', 'account-cash'],
delay: 100,
data: {
prefix: prefix1,
icons: {
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
delay: 500,
data: {
prefix: prefix2,
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(prefix2, data2);
const data1b: FakeData = {
icons: ['account', 'account-box'],
delay: 800, // +100ms for first query
data: {
prefix: prefix1,
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(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="' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline second-icon iconify--mdi-account" data-icon="' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>IconifyIcon finder:' +
' <iconify-icon class="third-icon" data-icon="' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon class="fourth-icon" data-icon="' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'</ul></div>';
browserModules.root = node;
scanDOM();
// Make sure no icons were rendered yet
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
0,
'Expected to find 0 rendered SVG elements'
);
// Change icon name
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');
// First API response should have loaded, but only 1 icon should have been rendered
setTimeout(() => {
// Loaded for prefix1: account-cash, home
// Loaded for prefix2: -
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1, '200ms delay error');
}, 200);
// Second API response should have loaded
setTimeout(() => {
// Loaded for prefix1: account-cash, home
// Loaded for prefix2: account, account-box
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(3, '700ms delay error');
}, 700);
// Renamed icon from first API response
setTimeout(() => {
// Loaded for prefix1: account-cash, home, account-box, account
// Loaded for prefix2: account, account-box
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(4, '1000ms delay error');
done();
}, 1100);
});
it('Changing icon name before it loaded to invalid name', (done) => {
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
[prefix1, prefix2]
);
// Set icons, load them with various delay
const data1: FakeData = {
icons: ['home', 'account-cash'],
delay: 100,
data: {
prefix: prefix1,
icons: {
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
delay: 500,
data: {
prefix: prefix2,
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(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="' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify test-icon iconify--mdi-account" data-icon="' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon data-icon="' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'</ul></div>';
browserModules.root = node;
scanDOM();
// Change icon name
const icon = node.querySelector('iconify-icon[title]');
expect(icon).to.not.be.equal(null);
icon.setAttribute('data-icon', 'foo');
// First API response should have loaded, but only 1 icon
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
}, 200);
// Second API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(3);
done();
}, 700);
});
});

View File

@ -0,0 +1,36 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import Iconify from '@iconify/iconify/lib/iconify';
const expect = chai.expect;
const selector =
'span.iconify, i.iconify, span.iconify-inline, i.iconify-inline';
const node = getNode('iconify-api');
Iconify.setRoot(node);
node.innerHTML =
'<div><p>Testing Iconify with API</p><ul>' +
'<li>Inline icons:' +
' <span class="iconify-inline" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify iconify-inline test-icon iconify--mdi-account" data-icon="mdi:account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <i class="iconify" data-icon="mdi:account-cash" title="&lt;Cash&gt;!"></i>' +
' <span class="iconify" data-icon="mdi:account-box" data-inline="true" data-rotate="2" data-width="42"></span>' +
'</li>' +
'</ul></div>';
describe('Testing Iconify object', () => {
it('Rendering icons with API', () => {
// Icons should have been replaced by now
let list = node.querySelectorAll(selector);
expect(list.length).to.be.equal(0);
list = node.querySelectorAll('svg.iconify');
expect(list.length).to.be.equal(4);
});
});

View File

@ -0,0 +1,104 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import Iconify from '@iconify/iconify/lib/iconify';
const expect = chai.expect;
const selector =
'span.iconify, i.iconify, span.iconify-inline, i.iconify-inline';
const node1 = getNode('iconify-basic');
const node2 = getNode('iconify-basic');
// Set root node
Iconify.setRoot(node1);
describe('Testing Iconify object', () => {
const prefix = 'invalid-' + Date.now();
// Add mentioned icons to storage
Iconify.addCollection({
prefix,
icons: {
'account-box': {
body:
'<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body:
'<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body:
'<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body:
'<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
// Add one icon separately
Iconify.addIcon(prefix + ':id-test', {
body:
'<defs><path id="ssvg-id-1st-place-medala" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medald" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalf" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalh" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalj" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalm" d="M.93.01h120.55v58.36H.93z"/><path d="M52.849 78.373v-3.908c3.681-.359 6.25-.958 7.703-1.798c1.454-.84 2.54-2.828 3.257-5.962h4.021v40.385h-5.437V78.373h-9.544z" id="ssvg-id-1st-place-medalp"/><linearGradient x1="49.998%" y1="-13.249%" x2="49.998%" y2="90.002%" id="ssvg-id-1st-place-medalb"><stop stop-color="#1E88E5" offset="13.55%"/><stop stop-color="#1565C0" offset="93.8%"/></linearGradient><linearGradient x1="26.648%" y1="2.735%" x2="77.654%" y2="105.978%" id="ssvg-id-1st-place-medalk"><stop stop-color="#64B5F6" offset="13.55%"/><stop stop-color="#2196F3" offset="94.62%"/></linearGradient><radialGradient cx="22.368%" cy="12.5%" fx="22.368%" fy="12.5%" r="95.496%" id="ssvg-id-1st-place-medalo"><stop stop-color="#FFEB3B" offset="29.72%"/><stop stop-color="#FBC02D" offset="95.44%"/></radialGradient></defs><g fill="none" fill-rule="evenodd"><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medalc" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medala"/></mask><path fill="url(#ssvg-id-1st-place-medalb)" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medalc)" d="M45.44 42.18h31.43l30-48.43H75.44z"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medale" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medald"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medale)" fill="#424242" fill-rule="nonzero"><path d="M101.23-3L75.2 39H50.85L77.11-3h24.12zm5.64-3H75.44l-30 48h31.42l30.01-48z"/></g></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medalg" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalf"/></mask><path d="M79 30H43c-4.42 0-8 3.58-8 8v16.04c0 2.17 1.8 3.95 4.02 3.96h.01c2.23-.01 4.97-1.75 4.97-3.96V44c0-1.1.9-2 2-2h30c1.1 0 2 .9 2 2v9.93c0 1.98 2.35 3.68 4.22 4.04c.26.05.52.08.78.08c2.21 0 4-1.79 4-4V38c0-4.42-3.58-8-8-8z" fill="#FDD835" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medalg)"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medali" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalh"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medali)" fill="#424242" fill-rule="nonzero"><path d="M79 32c3.31 0 6 2.69 6 6v16.04A2.006 2.006 0 0 1 82.59 56c-1.18-.23-2.59-1.35-2.59-2.07V44c0-2.21-1.79-4-4-4H46c-2.21 0-4 1.79-4 4v10.04c0 .88-1.64 1.96-2.97 1.96c-1.12-.01-2.03-.89-2.03-1.96V38c0-3.31 2.69-6 6-6h36zm0-2H43c-4.42 0-8 3.58-8 8v16.04c0 2.17 1.8 3.95 4.02 3.96h.01c2.23-.01 4.97-1.75 4.97-3.96V44c0-1.1.9-2 2-2h30c1.1 0 2 .9 2 2v9.93c0 1.98 2.35 3.68 4.22 4.04c.26.05.52.08.78.08c2.21 0 4-1.79 4-4V38c0-4.42-3.58-8-8-8z"/></g></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medall" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalj"/></mask><path fill="url(#ssvg-id-1st-place-medalk)" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medall)" d="M76.87 42.18H45.44l-30-48.43h31.43z"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medaln" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalm"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medaln)" fill="#424242" fill-rule="nonzero"><path d="M45.1-3l26.35 42H47.1L20.86-3H45.1zm1.77-3H15.44l30 48h31.42L46.87-6z"/></g></g><circle fill="url(#ssvg-id-1st-place-medalo)" fill-rule="nonzero" cx="64" cy="86" r="38"/><path d="M64 51c19.3 0 35 15.7 35 35s-15.7 35-35 35s-35-15.7-35-35s15.7-35 35-35zm0-3c-20.99 0-38 17.01-38 38s17.01 38 38 38s38-17.01 38-38s-17.01-38-38-38z" opacity=".2" fill="#424242" fill-rule="nonzero"/><path d="M47.3 63.59h33.4v44.4H47.3z"/><use fill="#000" xlink:href="#ssvg-id-1st-place-medalp"/><use fill="#FFA000" xlink:href="#ssvg-id-1st-place-medalp"/></g>',
width: 128,
height: 128,
});
it('Rendering icons without API', (done) => {
node1.innerHTML =
'<div><p>Testing Iconify without API</p>' +
' <span class="iconify-inline" data-icon="' +
prefix +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="' +
prefix +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
' <i class="iconify" data-icon="' +
prefix +
':account-cash" title="&lt;Cash&gt;!"></i>' +
' <span class="iconify" data-icon="' +
prefix +
':account-box" data-inline="true" data-rotate="2" data-width="42"></span>' +
' <span class="iconify" data-icon="' +
prefix +
':id-test"></span>' +
'</div>';
node2.innerHTML =
'<div><p>This node should not be replaced</p>' +
'<span class="iconify" data-icon="' +
prefix +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>';
// Icons should not have been replaced yet
let list = node1.querySelectorAll(selector);
expect(list.length).to.be.equal(5);
list = node2.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
// Check in ticks
setTimeout(() => {
setTimeout(() => {
list = node1.querySelectorAll(selector);
expect(list.length).to.be.equal(0);
list = node2.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
// Test SVG with ID
const idTest = node1.querySelector('#ssvg-id-1st-place-medala');
expect(idTest).to.be.equal(null, 'Expecting ID to be replaced');
done();
});
});
});
});

View File

@ -0,0 +1,100 @@
import { RedundancyPendingItem } from '@cyberalien/redundancy';
import {
APIQueryParams,
IconifyAPIPrepareQuery,
IconifyAPISendQuery,
} from '@iconify/core/lib/api/modules';
import { IconifyJSON } from '@iconify/types';
/**
* Fake data entry
*/
export interface FakeData {
icons: string[];
host?: string; // host to respond to
delay?: number; // 0 = instant reply
data: IconifyJSON; // data to send
}
/**
* Fake data storage
*/
const fakeData: Record<string, FakeData[]> = Object.create(null);
export function setFakeData(prefix: string, item: FakeData): void {
if (fakeData[prefix] === void 0) {
fakeData[prefix] = [];
}
fakeData[prefix].push(item);
}
interface FakeAPIQueryParams extends APIQueryParams {
data: FakeData;
}
/**
* Prepare params
*/
export const prepareQuery: IconifyAPIPrepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
// Find items that have query
const items: APIQueryParams[] = [];
let missing = icons.slice(0);
if (fakeData[prefix] !== void 0) {
fakeData[prefix].forEach(item => {
const matches = item.icons.filter(
icon => missing.indexOf(icon) !== -1
);
if (!matches.length) {
// No match
return;
}
// Contains at least one matching icon
missing = missing.filter(icon => matches.indexOf(icon) === -1);
const query: FakeAPIQueryParams = {
prefix,
icons: matches,
data: item,
};
items.push(query);
});
}
return items;
};
/**
* Load icons
*/
export const sendQuery: IconifyAPISendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
const prefix = params.prefix;
const icons = params.icons;
const data = (params as FakeAPIQueryParams).data;
if (!data) {
throw new Error('Fake data is missing in query params');
}
if (typeof data.host === 'string' && data.host !== host) {
// Host mismatch - do nothing
return;
}
const sendResponse = () => {
console.log('Sending data for prefix "' + prefix + '", icons:', icons);
status.done(data.data);
};
if (!data.delay) {
sendResponse();
} else {
setTimeout(sendResponse, data.delay);
}
};

View File

@ -0,0 +1,14 @@
let counter = 0;
/**
* Create node for test
*/
export function getNode(prefix = 'test') {
const id = prefix + '-' + Date.now() + '-' + counter++;
const node = document.createElement('div');
node.setAttribute('id', id);
document.getElementById('debug').appendChild(node);
return node;
}

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
<meta name="viewport" content="width=1024" />
<title>Tests</title>
<link href="../node_modules/mocha/mocha.css" rel="stylesheet" />
<style>
#debug {
display: none;
}
#debug > div {
margin: 8px 0;
border: 1px solid #ccc;
padding: 8px;
}
</style>
</head>
<body>
<div id="mocha"></div>
<div id="debug"></div>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/chai/chai.js"></script>
<script>
mocha.setup('bdd');
</script>
<!-- tests -->
<script class="mocha-exec">
mocha.run();
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"rootDir": "./tests",
"outDir": "./lib",
"target": "ES2019",
"module": "ESNext",
"declaration": false,
"sourceMap": false,
"strict": false,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@ -0,0 +1,2 @@
lib
tests-compiled

View File

@ -0,0 +1,30 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
mocha: true
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
project: './tsconfig-base.json'
},
plugins: ['@typescript-eslint'],
rules: {
'no-mixed-spaces-and-tabs': ['off'],
'no-unused-vars': ['off'],
'@typescript-eslint/no-unused-vars-experimental': ['error']
},
overrides: [
{
files: ['src/**/*.ts', 'tests/**/*.ts']
}
]
};

5
packages/core/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
node_modules
tests-compiled
lib
dist

1989
packages/core/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "@iconify/core",
"private": true,
"description": "Reusable files used by multiple Iconify packages",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "1.0.0-beta.0",
"license": "(Apache-2.0 OR GPL-2.0)",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
"repository": {
"type": "git",
"url": "git://github.com/iconify/iconify.git"
},
"scripts": {
"clean": "rm -rf lib compiled-tests",
"lint": "npx eslint {src,tests}/**/*.ts",
"prebuild": "npm run lint",
"build": "npx tsc -b",
"prewatch": "npm run lint",
"watch": "npx tsc -b -w",
"test": "npx mocha tests-compiled/*/*-test.js",
"pretest": "npm run build"
},
"devDependencies": {
"@types/chai": "^4.2.11",
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.34",
"@types/request": "^2.48.4",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"chai": "^4.2.0",
"eslint": "^6.8.0",
"mocha": "^6.2.3",
"typescript": "^3.8.3"
},
"dependencies": {
"@cyberalien/redundancy": "^1.0.0",
"@iconify/types": "^1.0.1"
},
"optionalDependencies": {
"axios": "^0.19.2"
}
}

View File

@ -0,0 +1,151 @@
import {
IconifyIconLoaderCallback,
IconifyIconLoaderAbort,
} from '../interfaces/loader';
import { getStorage } from '../storage';
import { SortedIcons } from '../icon/sort';
/**
* Storage for callbacks
*/
interface CallbackItem {
// id
id: number;
// Icons
icons: SortedIcons;
// Callback to call on any update
callback: IconifyIconLoaderCallback;
// Callback to call to remove item from queue
abort: IconifyIconLoaderAbort;
}
// 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);
/**
* Remove callback
*/
function removeCallback(prefixes: string[], id: number): void {
prefixes.forEach(prefix => {
const items = callbacks[prefix];
if (items) {
callbacks[prefix] = items.filter(row => row.id !== id);
}
});
}
/**
* Update all callbacks for prefix
*/
export function updateCallbacks(prefix: string): void {
if (!pendingUpdates[prefix]) {
pendingUpdates[prefix] = true;
setTimeout(() => {
pendingUpdates[prefix] = false;
if (callbacks[prefix] === void 0) {
return;
}
// Get all items
const items = callbacks[prefix].slice(0);
if (!items.length) {
return;
}
const storage = getStorage(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 => {
if (icon.prefix !== prefix) {
// Checking only current prefix
return true;
}
const name = icon.name;
if (storage.icons[name] !== void 0) {
// Loaded
icons.loaded.push({
prefix,
name,
});
} else if (storage.missing[name] !== void 0) {
// Missing
icons.missing.push({
prefix,
name,
});
} else {
// Pending
hasPending = true;
return true;
}
return false;
});
// Changes detected - call callback
if (icons.pending.length !== oldLength) {
if (!hasPending) {
// All icons have been loaded - remove callback from prefix
removeCallback([prefix], item.id);
}
item.callback(
icons.loaded.slice(0),
icons.missing.slice(0),
icons.pending.slice(0),
item.abort
);
}
});
});
}
}
/**
* Unique id counter for callbacks
*/
let idCounter = 0;
/**
* Add callback
*/
export function storeCallback(
callback: IconifyIconLoaderCallback,
icons: SortedIcons,
pendingPrefixes: string[]
): IconifyIconLoaderAbort {
// Create unique id and abort function
const id = idCounter++;
const abort = removeCallback.bind(null, pendingPrefixes, id);
if (!icons.pending.length) {
// Do not store item without pending icons and return function that does nothing
return abort;
}
// Create item and store it for all pending prefixes
const item: CallbackItem = {
id,
icons,
callback,
abort: abort,
};
pendingPrefixes.forEach(prefix => {
if (callbacks[prefix] === void 0) {
callbacks[prefix] = [];
}
callbacks[prefix].push(item);
});
return abort;
}

View File

@ -0,0 +1,127 @@
import { RedundancyConfig } from '@cyberalien/redundancy';
import { merge } from '../misc/merge';
/**
* API config
*/
export interface IconifyAPIConfig extends RedundancyConfig {
// Root path, after domain name and before prefix
path: string;
// URL length limit
maxURL: number;
}
/**
* Local storage interfaces
*/
interface ConfigStorage {
// API configuration for all prefixes
default: IconifyAPIConfig;
// Prefix specific API configuration
prefixes: Record<string, IconifyAPIConfig>;
}
/**
* Redundancy for API servers.
*
* API should have very high uptime because of implemented redundancy at server level, but
* sometimes bad things happen. On internet 100% uptime is not possible.
*
* There could be routing problems. Server might go down for whatever reason, but it takes
* few minutes to detect that downtime, so during those few minutes API might not be accessible.
*
* This script has some redundancy to mitigate possible network issues.
*
* If one host cannot be reached in 'rotate' (750 by default) ms, script will try to retrieve
* data from different host. Hosts have different configurations, pointing to different
* API servers hosted at different providers.
*/
const fallBackAPISources = [
'https://api.simplesvg.com',
'https://api.unisvg.com',
];
// Shuffle fallback API
const fallBackAPI: string[] = [];
while (fallBackAPISources.length > 0) {
if (fallBackAPISources.length === 1) {
fallBackAPI.push(fallBackAPISources.shift() as string);
} else {
// Get first or last item
if (Math.random() > 0.5) {
fallBackAPI.push(fallBackAPISources.shift() as string);
} else {
fallBackAPI.push(fallBackAPISources.pop() as string);
}
}
}
/**
* Default configuration
*/
const defaultConfig: IconifyAPIConfig = {
// API hosts
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,
};
/**
* 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.
*/
export function setAPIConfig(
customConfig: Partial<IconifyAPIConfig>,
prefix?: string | string[]
): void {
const mergedConfig = merge(
configStorage.default,
customConfig
) as IconifyAPIConfig;
if (prefix === void 0) {
configStorage.default = mergedConfig;
return;
}
(typeof prefix === 'string' ? [prefix] : prefix).forEach(prefix => {
configStorage.prefixes[prefix] = mergedConfig;
});
}
/**
* Get API configuration
*/
export function getAPIConfig(prefix: string): IconifyAPIConfig | null {
const value = configStorage.prefixes[prefix];
return value === void 0 ? configStorage.default : value;
}

View File

@ -0,0 +1,279 @@
import {
Redundancy,
initRedundancy,
RedundancyQueryCallback,
} from '@cyberalien/redundancy';
import { SortedIcons, sortIcons } from '../icon/sort';
import {
IconifyIconLoaderAbort,
IconifyIconLoaderCallback,
IconifyLoadIcons,
} from '../interfaces/loader';
import { IsPending, IconifyAPI } from '../interfaces/api';
import { storeCallback, updateCallbacks } from './callbacks';
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 { IconifyJSON } from '@iconify/types';
// Empty abort callback for loadIcons()
function emptyCallback(): void {
// Do nothing
}
/**
* List of icons that are being loaded.
*
* Icons are added to this list when they are being checked and
* removed from this list when they are added to storage as
* 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
*/
type PendingIcons = Record<string, number>;
const pendingIcons: Record<string, PendingIcons> = Object.create(null);
/**
* List of icons that are waiting to be loaded.
*
* List is passed to API module, then cleared.
*
* This list should not be used for any checks, use pendingIcons to check
* if icons is being loaded.
*
* [prefix] = array of icon names
*/
const iconsToLoad: 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);
// Redundancy instances cache
interface LocalCache {
config: IconifyAPIConfig | null;
redundancy: Redundancy | null;
}
const redundancyCache: Record<string, LocalCache> = Object.create(null);
/**
* Function called when new icons have been loaded
*/
function loadedNewIcons(prefix: string): void {
// Run only once per tick, possibly joining multiple API responses in one call
if (!loaderFlags[prefix]) {
loaderFlags[prefix] = true;
setTimeout(() => {
loaderFlags[prefix] = false;
updateCallbacks(prefix);
});
}
}
/**
* Load icons
*/
function loadNewIcons(prefix: string, icons: string[]): void {
function err(): void {
console.error(
'Unable to retrieve icons for prefix "' +
prefix +
'" because API is not configured properly.'
);
}
// Add icons to queue
if (iconsToLoad[prefix] === void 0) {
iconsToLoad[prefix] = icons;
} else {
iconsToLoad[prefix] = iconsToLoad[prefix].concat(icons).sort();
}
// Trigger update on next tick, mering multiple synchronous requests into one asynchronous request
if (!queueFlags[prefix]) {
queueFlags[prefix] = true;
setTimeout(() => {
queueFlags[prefix] = false;
// Get icons and delete queue
const icons = iconsToLoad[prefix];
delete iconsToLoad[prefix];
// Get API module
const api = getAPIModule(prefix);
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;
}
}
if (redundancyCache[prefix] === void 0) {
redundancyCache[prefix] = {
config,
redundancy: config ? initRedundancy(config) : null,
};
}
}
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);
params.forEach((item) => {
redundancy.query(
item,
api.send as RedundancyQueryCallback,
(data) => {
// Add icons to storage
const storage = getStorage(prefix);
try {
const added = addIconSet(
storage,
data as IconifyJSON,
'all'
);
if (typeof added === 'boolean') {
return;
}
// Remove added icons from pending list
const pending = pendingIcons[prefix];
added.forEach((name) => {
delete pending[name];
});
// Cache API response
if (coreModules.cache) {
coreModules.cache(data as IconifyJSON);
}
} catch (err) {
console.error(err);
}
// Trigger update on next tick
loadedNewIcons(prefix);
}
);
});
});
}
}
/**
* Check if icon is being loaded
*/
const isPending: IsPending = (prefix: string, icon: string): boolean => {
return (
pendingIcons[prefix] !== void 0 && pendingIcons[prefix][icon] !== void 0
);
};
/**
* Load icons
*/
const loadIcons: IconifyLoadIcons = (
icons: (IconifyIconName | string)[],
callback?: IconifyIconLoaderCallback
): IconifyIconLoaderAbort => {
// Clean up and copy icons list
const cleanedIcons = listToIcons(icons, true);
// Sort icons by missing/loaded/pending
// Pending means icon is either being requsted or is about to be requested
const sortedIcons: SortedIcons = sortIcons(cleanedIcons);
if (!sortedIcons.pending.length) {
// Nothing to load
let callCallback = true;
if (callback) {
setTimeout(() => {
if (callCallback) {
callback(
sortedIcons.loaded,
sortedIcons.missing,
sortedIcons.pending,
emptyCallback
);
}
});
}
return (): void => {
callCallback = false;
};
}
// Get all prefixes
const prefixes = getPrefixes(sortedIcons.pending);
// 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);
}
newIcons[prefix] = [];
});
// List of new icons
const time = Date.now();
// Filter pending icons list: find icons that are not being loaded yet
// 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 prefix = icon.prefix;
const name = icon.name;
const pendingQueue = pendingIcons[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);
}
});
// 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]);
}
});
// Store callback and return abort function
return callback
? storeCallback(callback, sortedIcons, prefixes)
: emptyCallback;
};
/**
* Export module
*/
export const API: IconifyAPI = {
isPending,
loadIcons,
};

View File

@ -0,0 +1,75 @@
import { RedundancyPendingItem } from '@cyberalien/redundancy';
/**
* Params for sendQuery()
*/
export interface APIQueryParams {
prefix: string;
icons: string[];
}
/**
* Functions to implement in module
*/
export type IconifyAPIPrepareQuery = (
prefix: string,
icons: string[]
) => APIQueryParams[];
export type IconifyAPISendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
) => void;
/**
* API modules
*/
export interface IconifyAPIModule {
prepare: IconifyAPIPrepareQuery;
send: IconifyAPISendQuery;
}
/**
* Local storate types and entries
*/
interface ModuleStorage {
default: IconifyAPIModule | null;
prefixes: Record<string, IconifyAPIModule>;
}
const storage: ModuleStorage = {
default: null,
prefixes: 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.
*/
export function setAPIModule(
item: IconifyAPIModule,
prefix?: string | string[]
): void {
if (prefix === void 0) {
storage.default = item;
return;
}
(typeof prefix === 'string' ? [prefix] : prefix).forEach(prefix => {
storage.prefixes[prefix] = item;
});
}
/**
* Get API module
*/
export function getAPIModule(prefix: string): IconifyAPIModule | null {
const value = storage.prefixes[prefix];
return value === void 0 ? storage.default : value;
}

View File

@ -0,0 +1,66 @@
/**
* Regular expressions for calculating dimensions
*/
const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g;
const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g;
/**
* Calculate second dimension when only 1 dimension is set
*
* @param {string|number} size One dimension (such as width)
* @param {number} ratio Width/height ratio.
* If size is width, ratio = height/width
* If size is height, ratio = width/height
* @param {number} [precision] Floating number precision in result to minimize output. Default = 2
* @return {string|number} Another dimension
*/
export function calcSize(
size: string | number,
ratio: number,
precision?: number
): string | number {
if (ratio === 1) {
return size;
}
precision = precision === void 0 ? 100 : precision;
if (typeof size === 'number') {
return Math.ceil(size * ratio * precision) / precision;
}
if (typeof size !== 'string') {
return size;
}
// Split code into sets of strings and numbers
const oldParts = size.split(unitsSplit);
if (oldParts === null || !oldParts.length) {
return size;
}
const newParts = [];
let code = oldParts.shift() as string;
let isNumber = unitsTest.test(code as string);
// eslint-disable-next-line no-constant-condition
while (true) {
if (isNumber) {
const num = parseFloat(code);
if (isNaN(num)) {
newParts.push(code);
} else {
newParts.push(Math.ceil(num * ratio * precision) / precision);
}
} else {
newParts.push(code);
}
// next
code = oldParts.shift() as string;
if (code === void 0) {
return newParts.join('');
}
isNumber = !isNumber;
}
}

View File

@ -0,0 +1,66 @@
/**
* Regular expression for finding ids
*/
const regex = /\sid="(\S+)"/g;
/**
* New random-ish prefix for ids
*/
const randomPrefix =
'IconifyId-' +
Date.now().toString(16) +
'-' +
((Math.random() * 0x1000000) | 0).toString(16) +
'-';
/**
* Counter for ids, increasing with every replacement
*/
let counter = 0;
/**
* Replace multiple occurance of same string
*/
function strReplace(search: string, replace: string, subject: string): string {
let pos = 0;
while ((pos = subject.indexOf(search, pos)) !== -1) {
subject =
subject.slice(0, pos) +
replace +
subject.slice(pos + search.length);
pos += replace.length;
}
return subject;
}
/**
* Replace IDs in SVG output with unique IDs
* Fast replacement without parsing XML, assuming commonly used patterns and clean XML (icon should have been cleaned up with Iconify Tools or SVGO).
*/
export function replaceIDs(
body: string,
prefix: string | (() => string) = randomPrefix
): string {
// Find all IDs
const ids: string[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(body))) {
ids.push(match[1]);
}
if (!ids.length) {
return body;
}
// Replace with unique ids
ids.forEach(id => {
const newID =
typeof prefix === 'function' ? prefix() : prefix + counter++;
body = strReplace('="' + id + '"', '="' + newID + '"', body);
body = strReplace('="#' + id + '"', '="#' + newID + '"', body);
body = strReplace('(#' + id + ')', '(#' + newID + ')', body);
});
return body;
}

View File

@ -0,0 +1,220 @@
import { FullIconifyIcon } from '../icon';
import { FullIconCustomisations } from '../customisations';
import { calcSize } from './calc-size';
/**
* Get preserveAspectRatio value
*/
function preserveAspectRatio(props: FullIconCustomisations): string {
let result = '';
switch (props.hAlign) {
case 'left':
result += 'xMin';
break;
case 'right':
result += 'xMax';
break;
default:
result += 'xMid';
}
switch (props.vAlign) {
case 'top':
result += 'YMin';
break;
case 'bottom':
result += 'YMax';
break;
default:
result += 'YMid';
}
result += props.slice ? ' slice' : ' meet';
return result;
}
/**
* Interface for getSVGData() result
*/
export interface IconifyIconBuildResult {
attributes: {
// Attributes for <svg>
width: string;
height: string;
preserveAspectRatio: string;
viewBox: string;
};
// Content
body: string;
// True if 'vertical-align: -0.125em' or equivalent should be added by implementation
inline?: boolean;
}
/**
* Interface for viewBox
*/
interface ViewBox {
left: number;
top: number;
width: number;
height: number;
}
/**
* Get SVG attributes and content from icon + customisations
*
* Does not generate style to make it compatible with frameworks that use objects for style, such as React.
* Instead, it generates verticalAlign value that should be added to style.
*
* Customisations should be normalised by platform specific parser.
* Result should be converted to <svg> by platform specific parser.
* Use replaceIDs to generate unique IDs for body.
*/
export function iconToSVG(
icon: FullIconifyIcon,
customisations: FullIconCustomisations
): IconifyIconBuildResult {
// viewBox
const box: ViewBox = {
left: icon.left,
top: icon.top,
width: icon.width,
height: icon.height,
};
// Apply transformations
const transformations: string[] = [];
let rotation = customisations.rotate;
if (customisations.hFlip) {
if (customisations.vFlip) {
rotation += 2;
} else {
// Horizontal flip
transformations.push(
'translate(' +
(box.width + box.left) +
' ' +
(0 - box.top) +
')'
);
transformations.push('scale(-1 1)');
box.top = box.left = 0;
}
} else if (customisations.vFlip) {
// Vertical flip
transformations.push(
'translate(' + (0 - box.left) + ' ' + (box.height + box.top) + ')'
);
transformations.push('scale(1 -1)');
box.top = box.left = 0;
}
let tempValue: number;
rotation = rotation % 4;
switch (rotation) {
case 1:
// 90deg
tempValue = box.height / 2 + box.top;
transformations.unshift(
'rotate(90 ' + tempValue + ' ' + tempValue + ')'
);
break;
case 2:
// 180deg
transformations.unshift(
'rotate(180 ' +
(box.width / 2 + box.left) +
' ' +
(box.height / 2 + box.top) +
')'
);
break;
case 3:
// 270deg
tempValue = box.width / 2 + box.left;
transformations.unshift(
'rotate(-90 ' + tempValue + ' ' + tempValue + ')'
);
break;
}
if (rotation % 2 === 1) {
// Swap width/height and x/y for 90deg or 270deg rotation
if (box.left !== 0 || box.top !== 0) {
tempValue = box.left;
box.left = box.top;
box.top = tempValue;
}
if (box.width !== box.height) {
tempValue = box.width;
box.width = box.height;
box.height = tempValue;
}
}
// Calculate dimensions
let width, height;
if (customisations.width === null && customisations.height === null) {
// Set height to '1em', calculate width
height = '1em';
width = calcSize(height, box.width / box.height);
} else if (
customisations.width !== null &&
customisations.height !== null
) {
// Values are set
width = customisations.width;
height = customisations.height;
} else if (customisations.height !== null) {
// Height is set
height = customisations.height;
width = calcSize(height, box.width / box.height);
} else {
// Width is set
width = customisations.width as number | string;
height = calcSize(width, box.height / box.width);
}
// Check for 'auto'
if (width === 'auto') {
width = box.width;
}
if (height === 'auto') {
height = box.height;
}
// Convert to string
width = typeof width === 'string' ? width : width + '';
height = typeof height === 'string' ? height : height + '';
// Generate body
let body = icon.body;
if (transformations.length) {
body =
'<g transform="' + transformations.join(' ') + '">' + body + '</g>';
}
// Result
const result: IconifyIconBuildResult = {
attributes: {
width,
height,
preserveAspectRatio: preserveAspectRatio(customisations),
viewBox:
box.left + ' ' + box.top + ' ' + box.width + ' ' + box.height,
},
body,
};
if (customisations.inline) {
result.inline = true;
}
return result;
}

308
packages/core/src/cache/storage.ts vendored Normal file
View File

@ -0,0 +1,308 @@
import { IconifyJSON } from '@iconify/types';
import { CacheIcons, LoadIconsCache } from '../interfaces/cache';
import { getStorage, addIconSet } from '../storage';
interface StorageType<T> {
local: T;
session: T;
}
type StorageConfig = StorageType<boolean>;
type StorageCount = StorageType<number>;
type StorageEmptyList = StorageType<number[]>;
export interface StoredItem {
cached: number;
data: IconifyJSON;
}
// After changing configuration change it in tests/*/fake_cache.ts
// Cache version. Bump when structure changes
const cacheVersion = 'iconify1';
// Cache keys
const cachePrefix = 'iconify';
const countKey = cachePrefix + '-count';
const versionKey = cachePrefix + '-version';
/**
* Cache expiration
*/
const hour = 3600000;
const cacheExpiration = 168; // In hours
/**
* Storage configuration
*/
export const config: StorageConfig = {
local: true,
session: true,
};
/**
* Flag to check if storage has been loaded
*/
let loaded = false;
/**
* Items counter
*/
export const count: StorageCount = {
local: 0,
session: 0,
};
/**
* List of empty items
*/
export const emptyList: StorageEmptyList = {
local: [],
session: [],
};
/**
* Fake window for unit testing
*/
type FakeWindow = Record<string, typeof localStorage>;
let _window: FakeWindow =
typeof window === 'undefined' ? {} : ((window as unknown) as FakeWindow);
export function mock(fakeWindow: FakeWindow): void {
loaded = false;
_window = fakeWindow;
}
/**
* Get global
*
* @param key
*/
function getGlobal(key: keyof StorageConfig): typeof localStorage | null {
const attr = key + 'Storage';
try {
if (
_window &&
_window[attr] &&
typeof _window[attr].length === 'number'
) {
return _window[attr];
}
} catch (err) {
//
}
// Failed - mark as disabled
config[key] = false;
return null;
}
/**
* Change current count for storage
*/
function setCount(
storage: typeof localStorage,
key: keyof StorageConfig,
value: number
): boolean {
try {
storage.setItem(countKey, value + '');
count[key] = value;
return true;
} catch (err) {
return false;
}
}
/**
* Get current count from storage
*
* @param storage
*/
function getCount(storage: typeof localStorage): number {
const count = storage.getItem(countKey);
if (count) {
const total = parseInt(count);
return total ? total : 0;
}
return 0;
}
/**
* Initialize storage
*
* @param storage
* @param key
*/
function initCache(
storage: typeof localStorage,
key: keyof StorageConfig
): void {
try {
storage.setItem(versionKey, cacheVersion);
} catch (err) {
//
}
setCount(storage, key, 0);
}
/**
* Destroy old cache
*
* @param storage
*/
function destroyCache(storage: typeof localStorage): void {
try {
const total = getCount(storage);
for (let i = 0; i < total; i++) {
storage.removeItem(cachePrefix + i);
}
} catch (err) {
//
}
}
/**
* Load icons from cache
*/
export const loadCache: LoadIconsCache = (): void => {
if (loaded) {
return;
}
loaded = true;
// Minimum time
const minTime = Math.floor(Date.now() / hour) - cacheExpiration;
// Load data from storage
function load(key: keyof StorageConfig): void {
const func = getGlobal(key);
if (!func) {
return;
}
// Get one item from storage
const getItem = (index: number): boolean => {
const name = cachePrefix + index;
const item = func.getItem(name);
if (typeof item !== 'string') {
// Does not exist
return false;
}
// Get item, validate it
let valid = true;
try {
// Parse, check time stamp
const data = JSON.parse(item as string) as StoredItem;
if (
typeof data !== 'object' ||
typeof data.cached !== 'number' ||
data.cached < minTime ||
typeof data.data !== 'object' ||
typeof data.data.prefix !== 'string'
) {
valid = false;
} else {
// Add icon set
const prefix = data.data.prefix;
const storage = getStorage(prefix);
valid = addIconSet(storage, data.data) as boolean;
}
} catch (err) {
valid = false;
}
if (!valid) {
func.removeItem(name);
}
return valid;
};
try {
// Get version
const version = func.getItem(versionKey);
if (version !== cacheVersion) {
if (version) {
// Version is set, but invalid - remove old entries
destroyCache(func);
}
// Empty data
initCache(func, key);
return;
}
// Get number of stored items
let total = getCount(func);
for (let i = total - 1; i >= 0; i--) {
if (!getItem(i)) {
// Remove item
if (i === total - 1) {
// Last item - reduce country
total--;
} else {
// Mark as empty
emptyList[key].push(i);
}
}
}
// Update total
setCount(func, key, total);
} catch (err) {
//
}
}
for (const key in config) {
load(key as keyof StorageConfig);
}
};
/**
* Function to cache icons
*/
export const storeCache: CacheIcons = (data: IconifyJSON): void => {
if (!loaded) {
loadCache();
}
function store(key: keyof StorageConfig): boolean {
if (!config[key]) {
return false;
}
const func = getGlobal(key);
if (!func) {
return false;
}
// Get item index
let index = emptyList[key].shift();
if (index === void 0) {
// Create new index
index = count[key];
if (!setCount(func, key, index + 1)) {
return false;
}
}
// Create and save item
try {
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data,
};
func.setItem(cachePrefix + index, JSON.stringify(item));
} catch (err) {
return false;
}
return true;
}
// Attempt to store at localStorage first, then at sessionStorage
if (!store('local')) {
store('session');
}
};

View File

@ -0,0 +1,30 @@
/**
* Get boolean customisation value from attribute
*/
export function toBoolean(
name: string,
value: unknown,
defaultValue: boolean
): boolean {
switch (typeof value) {
case 'boolean':
return value;
case 'number':
return !!value;
case 'string':
switch (value.toLowerCase()) {
case '1':
case 'true':
case name:
return true;
case '0':
case 'false':
case '':
return false;
}
}
return defaultValue;
}

View File

@ -0,0 +1,29 @@
import { FullIconCustomisations, defaults } from '../customisations';
// Get all keys
const allKeys: (keyof FullIconCustomisations)[] = Object.keys(
defaults
) as (keyof FullIconCustomisations)[];
// All keys without width/height
const filteredKeys = allKeys.filter(key => key !== 'width' && key !== 'height');
/**
* Compare sets of cusotmisations, return false if they are different, true if the same
*
* If dimensions are derived from props1 or props2, do not compare them.
*/
export function compare(
item1: FullIconCustomisations,
item2: FullIconCustomisations,
compareDimensions = true
): boolean {
const keys = compareDimensions ? allKeys : filteredKeys;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (item1[key] !== item2[key]) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,67 @@
import { merge } from '../misc/merge';
/**
* Icon alignment
*/
export type IconifyHorizontalIconAlignment = 'left' | 'center' | 'right';
export type IconifyVerticalIconAlignment = 'top' | 'middle' | 'bottom';
/**
* Icon size
*/
export type IconifyIconSize = null | string | number;
/**
* Icon customisations
*/
export interface IconifyIconCustomisations {
// Display mode
inline?: boolean;
// Dimensions
width?: IconifyIconSize;
height?: IconifyIconSize;
// Alignment
hAlign?: IconifyHorizontalIconAlignment;
vAlign?: IconifyVerticalIconAlignment;
slice?: boolean;
// Transformations
hFlip?: boolean;
vFlip?: boolean;
rotate?: number;
}
export type FullIconCustomisations = Required<IconifyIconCustomisations>;
/**
* Default icon customisations values
*/
export const defaults: FullIconCustomisations = Object.freeze({
// Display mode
inline: false,
// Dimensions
width: null,
height: null,
// Alignment
hAlign: 'center',
vAlign: 'middle',
slice: false,
// Transformations
hFlip: false,
vFlip: false,
rotate: 0,
});
/**
* Convert IconifyIconCustomisations to FullIconCustomisations
*/
export function fullCustomisations(
item: IconifyIconCustomisations
): FullIconCustomisations {
return merge(defaults, item) as FullIconCustomisations;
}

View File

@ -0,0 +1,40 @@
/**
* Get rotation value
*/
export function rotateFromString(value: string): number {
const units = value.replace(/^-?[0-9.]*/, '');
function cleanup(value: number): number {
while (value < 0) {
value += 4;
}
return value % 4;
}
if (units === '') {
const num = parseInt(value);
return isNaN(num) ? 0 : cleanup(num);
} else if (units !== value) {
let split = 0;
switch (units) {
case '%':
// 25% -> 1, 50% -> 2, ...
split = 25;
break;
case 'deg':
// 90deg -> 1, 180deg -> 2, ...
split = 90;
}
if (split) {
let num = parseFloat(value.slice(0, value.length - units.length));
if (isNaN(num)) {
return 0;
}
num = num / split;
return num % 1 === 0 ? cleanup(num) : 0;
}
}
return 0;
}

View File

@ -0,0 +1,67 @@
import { IconifyIconCustomisations } from '../customisations';
const separator = /[\s,]+/;
/**
* Additional shorthand customisations
*/
export interface ShorthandIconCustomisations {
// Sets both hFlip and vFlip
flip?: string;
// Sets hAlign, vAlign and slice
align?: string;
}
/**
* Apply "flip" string to icon customisations
*/
export function flipFromString(
custom: IconifyIconCustomisations,
flip: string
): void {
flip.split(separator).forEach(str => {
const value = str.trim();
switch (value) {
case 'horizontal':
custom.hFlip = true;
break;
case 'vertical':
custom.vFlip = true;
break;
}
});
}
/**
* Apply "align" string to icon customisations
*/
export function alignmentFromString(
custom: IconifyIconCustomisations,
align: string
): void {
align.split(separator).forEach(str => {
const value = str.trim();
switch (value) {
case 'left':
case 'center':
case 'right':
custom.hAlign = value;
break;
case 'top':
case 'middle':
case 'bottom':
custom.vAlign = value;
break;
case 'slice':
custom.slice = true;
break;
case 'meet':
custom.slice = false;
}
});
}

View File

@ -0,0 +1,26 @@
import { IconifyIcon } from '@iconify/types';
import { merge } from '../misc/merge';
export { IconifyIcon };
export type FullIconifyIcon = Required<IconifyIcon>;
/**
* Default values for IconifyIcon properties
*/
export const iconDefaults: FullIconifyIcon = Object.freeze({
body: '',
left: 0,
top: 0,
width: 16,
height: 16,
rotate: 0,
vFlip: false,
hFlip: false,
});
/**
* Create new icon with all properties
*/
export function fullIcon(icon: IconifyIcon): FullIconifyIcon {
return merge(iconDefaults, icon) as FullIconifyIcon;
}

View File

@ -0,0 +1,37 @@
import { IconifyIconName, stringToIcon, validateIcon } from './name';
/**
* Convert icons list from string/icon mix to icons and validate them
*/
export function listToIcons(
list: (string | IconifyIconName)[],
validate = true
): IconifyIconName[] {
const result: IconifyIconName[] = [];
list.forEach(item => {
const icon: IconifyIconName =
typeof item === 'string'
? (stringToIcon(item) as IconifyIconName)
: item;
if (!validate || validateIcon(icon)) {
result.push({
prefix: icon.prefix,
name: icon.name,
});
}
});
return result;
}
/**
* Get all prefixes
*/
export function getPrefixes(list: IconifyIconName[]): string[] {
const prefixes: Record<string, boolean> = Object.create(null);
list.forEach(icon => {
prefixes[icon.prefix] = true;
});
return Object.keys(prefixes);
}

View File

@ -0,0 +1,47 @@
import { IconifyIcon, iconDefaults } from './';
/**
* Icon keys
*/
const iconKeys = Object.keys(iconDefaults) as (keyof IconifyIcon)[];
/**
* Merge two icons
*
* icon2 overrides icon1
*/
export function mergeIcons(
icon1: IconifyIcon,
icon2: IconifyIcon
): IconifyIcon {
const icon = Object.create(null);
iconKeys.forEach(key => {
if (icon1[key] === void 0) {
if (icon2[key] !== void 0) {
icon[key] = icon2[key];
}
return;
}
if (icon2[key] === void 0) {
icon[key] = icon1[key];
return;
}
switch (key) {
case 'rotate':
icon[key] =
((icon1[key] as number) + (icon2[key] as number)) % 4;
return;
case 'hFlip':
case 'vFlip':
icon[key] = icon1[key] !== icon2[key];
return;
default:
icon[key] = icon2[key];
}
});
return icon;
}

View File

@ -0,0 +1,53 @@
/**
* Icon name
*/
export interface IconifyIconName {
readonly prefix: string;
readonly name: string;
}
/**
* Expression to test part of icon name.
*/
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"
const colonSeparated = value.split(':');
if (colonSeparated.length > 2) {
return null;
}
if (colonSeparated.length === 2) {
return {
prefix: colonSeparated[0],
name: colonSeparated[1],
};
}
// Attempt to split by dash: "prefix-name"
const dashSeparated = value.split('-');
if (dashSeparated.length > 1) {
return {
prefix: dashSeparated.shift() as string,
name: dashSeparated.join('-'),
};
}
return null;
};
/**
* Check if icon is valid.
*
* This function is not part of stringToIcon because validation is not needed for most code.
*/
export const validateIcon = (icon: IconifyIconName | null): boolean => {
if (!icon) {
return false;
}
return !!(icon.prefix.match(match) && icon.name.match(match));
};

View File

@ -0,0 +1,69 @@
import { getStorage, IconStorage } from '../storage';
import { IconifyIconName } from './name';
/**
* Sorted icons list
*/
export interface SortedIcons {
loaded: IconifyIconName[];
missing: IconifyIconName[];
pending: IconifyIconName[];
}
/**
* Check if icons have been loaded
*/
export function sortIcons(icons: IconifyIconName[]): SortedIcons {
const result: SortedIcons = {
loaded: [],
missing: [],
pending: [],
};
const storage: 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);
}
return a.prefix.localeCompare(b.prefix);
});
let lastIcon: IconifyIconName = {
prefix: '',
name: '',
};
icons.forEach(icon => {
if (lastIcon.prefix === icon.prefix && lastIcon.name === icon.name) {
return;
}
lastIcon = icon;
// Check icon
const prefix = icon.prefix;
const name = icon.name;
if (storage[prefix] === void 0) {
storage[prefix] = getStorage(prefix);
}
const localStorage = storage[prefix];
let list;
if (localStorage.icons[name] !== void 0) {
list = result.loaded;
} else if (localStorage.missing[name] !== void 0) {
list = result.missing;
} else {
list = result.pending;
}
const item: IconifyIconName = {
prefix,
name,
};
list.push(item);
});
return result;
}

View File

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

View File

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

View File

@ -0,0 +1,26 @@
import { IconifyIconName } from '../icon/name';
/**
* Function to abort loading (usually just removes callback because loading is already in progress)
*/
export type IconifyIconLoaderAbort = () => void;
/**
* Loader callback
*
* Provides list of icons that have been loaded
*/
export type IconifyIconLoaderCallback = (
loaded: IconifyIconName[],
missing: IconifyIconName[],
pending: IconifyIconName[],
unsubscribe: IconifyIconLoaderAbort
) => void;
/**
* Function to load icons
*/
export type IconifyLoadIcons = (
icons: (IconifyIconName | string)[],
callback?: IconifyIconLoaderCallback
) => IconifyIconLoaderAbort;

View File

@ -0,0 +1,22 @@
type MergeObject = Record<string, unknown>;
/**
* Merge two objects
*
* Replacement for Object.assign() that is not supported by IE, so it cannot be used in production yet.
*/
export function merge<T>(item1: T, item2?: T, item3?: T): T {
const result: MergeObject = Object.create(null);
const items = [item1, item2, item3];
for (let i = 0; i < 3; i++) {
const item = items[i];
if (typeof item === 'object' && item) {
for (const key in item) {
result[key] = (item as MergeObject)[key];
}
}
}
return result as T;
}

View File

@ -0,0 +1,18 @@
import { CacheIcons } from './interfaces/cache';
import { IconifyAPI } from './interfaces/api';
/**
* Dynamic modules.
*
* Used as storage for optional functions that may or may not exist.
* Each module must be set after including correct function for it, see build files as examples.
*/
interface Modules {
// API module
api?: IconifyAPI;
// Cache module (only function that stores cache. loading cache should be done when assigning module)
cache?: CacheIcons;
}
export const coreModules: Modules = {};

View File

@ -0,0 +1,219 @@
import {
IconifyJSON,
IconifyIcon,
IconifyOptional,
IconifyIcons,
IconifyAliases,
IconifyAlias,
} from '@iconify/types';
import { FullIconifyIcon, iconDefaults, fullIcon } from '../icon';
import { mergeIcons } from '../icon/merge';
import { merge } from '../misc/merge';
/**
* Get list of defaults keys
*/
const defaultsKeys = Object.keys(iconDefaults) as (keyof IconifyOptional)[];
/**
* List of icons
*/
type IconRecords = Record<string, FullIconifyIcon | null>;
/**
* Storage type
*/
export interface IconStorage {
prefix: string;
icons: IconRecords;
missing: Record<string, number>;
}
/**
* Storage by prefix
*/
const storage: Record<string, IconStorage> = Object.create(null);
/**
* Create new storage
*/
export function newStorage(prefix: string): IconStorage {
return {
prefix,
icons: Object.create(null),
missing: Object.create(null),
};
}
/**
* Get storage for prefix
*/
export function getStorage(prefix: string): IconStorage {
if (storage[prefix] === void 0) {
storage[prefix] = newStorage(prefix);
}
return storage[prefix];
}
/**
* Get all prefixes
*/
export function listStoredPrefixes(): string[] {
return Object.keys(storage);
}
/**
* Resolve alias
*/
function resolveAlias(
alias: IconifyAlias,
icons: IconifyIcons,
aliases: IconifyAliases,
level = 0
): IconifyIcon | null {
const parent = alias.parent;
if (icons[parent] !== void 0) {
return mergeIcons(icons[parent], (alias as unknown) as IconifyIcon);
}
if (aliases[parent] !== void 0) {
if (level > 2) {
// icon + alias + alias + alias = too much nesting, possibly infinite
throw new Error('Invalid alias');
}
const icon = resolveAlias(aliases[parent], icons, aliases, level + 1);
if (icon) {
return mergeIcons(icon, (alias as unknown) as IconifyIcon);
}
}
return null;
}
/**
* What to track when adding icon set:
*
* none - do not track anything, return true on success
* added - track added icons, return list of added icons on success
* all - track added and missing icons, return full list on success
*/
export type AddIconSetTracking = 'none' | 'added' | 'all';
/**
* Add icon set to storage
*
* Returns array of added icons if 'list' is true and icons were added successfully
*/
export function addIconSet(
storage: IconStorage,
data: IconifyJSON,
list: AddIconSetTracking = 'none'
): boolean | string[] {
const added: string[] = [];
try {
// Must be an object
if (typeof data !== 'object') {
return false;
}
// Check for missing icons list returned by API
if (data.not_found instanceof Array) {
const t = Date.now();
data.not_found.forEach(name => {
storage.missing[name] = t;
if (list === 'all') {
added.push(name);
}
});
}
// Must have 'icons' object
if (typeof data.icons !== 'object') {
return false;
}
// Get default values
const defaults = Object.create(null);
defaultsKeys.forEach(key => {
if (data[key] !== void 0 && typeof data[key] !== 'object') {
defaults[key] = data[key];
}
});
// Get icons
const icons = data.icons;
Object.keys(icons).forEach(name => {
const icon = icons[name];
if (typeof icon.body !== 'string') {
throw new Error('Invalid icon');
}
// Freeze icon to make sure it will not be modified
storage.icons[name] = Object.freeze(
merge(iconDefaults, defaults, icon)
);
if (list !== 'none') {
added.push(name);
}
});
// Get aliases
if (typeof data.aliases === 'object') {
const aliases = data.aliases;
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
storage.icons[name] = Object.freeze(
merge(iconDefaults, defaults, icon)
);
if (list !== 'none') {
added.push(name);
}
}
});
}
} catch (err) {
return false;
}
return list === 'none' ? true : added;
}
/**
* Add icon to storage
*/
export function addIcon(
storage: IconStorage,
name: string,
icon: IconifyIcon
): boolean {
try {
if (typeof icon.body === 'string') {
// Freeze icon to make sure it will not be modified
storage.icons[name] = Object.freeze(fullIcon(icon));
return true;
}
} catch (err) {
// Do nothing
}
return false;
}
/**
* Check if icon exists
*/
export function iconExists(storage: IconStorage, name: string): boolean {
return storage.icons[name] !== void 0;
}
/**
* Get icon data
*/
export function getIcon(
storage: IconStorage,
name: string
): Readonly<FullIconifyIcon> | null {
const value = storage.icons[name];
return value === void 0 ? null : value;
}

View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"outDir": "../lib",
"rootDir": "."
}
}

View File

@ -0,0 +1,60 @@
import 'mocha';
import { expect } from 'chai';
import { calcSize } from '../../lib/builder/calc-size';
describe('Testing calcSize', () => {
it('Simple size', () => {
const width = 36;
const height = 48;
// Get width from height and height from width
expect(calcSize('48', width / height)).to.be.equal('36');
expect(calcSize('36', height / width)).to.be.equal('48');
expect(calcSize(48, width / height)).to.be.equal(36);
expect(calcSize(36, height / width)).to.be.equal(48);
});
it('Numbers', () => {
const width = 36;
const height = 48;
// Simple numbers
expect(calcSize(24, width / height)).to.be.equal(18);
expect(calcSize(30, width / height)).to.be.equal(22.5);
expect(calcSize(99, width / height)).to.be.equal(74.25);
// Rounding numbers
expect(calcSize(100 / 3, height / width)).to.be.equal(44.45);
expect(calcSize(11.1111111, width / height)).to.be.equal(8.34);
expect(calcSize(11.1111111, width / height, 1000)).to.be.equal(8.334);
});
it('Strings', () => {
const width = 36;
const height = 48;
// Simple units
expect(calcSize('48px', width / height)).to.be.equal('36px');
expect(calcSize('24%', width / height)).to.be.equal('18%');
expect(calcSize('1em', width / height)).to.be.equal('0.75em');
// Add space
expect(calcSize('24 Pixels', width / height)).to.be.equal('18 Pixels');
// Multiple sets of numbers
expect(calcSize('48% + 5em', width / height)).to.be.equal(
'36% + 3.75em'
);
expect(calcSize('calc(1em + 8px)', height / width)).to.be.equal(
'calc(1.34em + 10.67px)'
);
expect(calcSize('-webkit-calc(1em + 8px)', width / height)).to.be.equal(
'-webkit-calc(0.75em + 6px)'
);
// Invalid strings
expect(calcSize('-.', width / height)).to.be.equal('-.');
expect(calcSize('@foo', width / height)).to.be.equal('@foo');
});
});

View File

@ -0,0 +1,79 @@
import 'mocha';
import { expect } from 'chai';
import {
stringToIcon,
validateIcon,
IconifyIconName,
} from '../../lib/icon/name';
describe('Testing icon name', () => {
it('Converting and validating', () => {
let icon;
// Simple prefix-name
icon = stringToIcon('fa-home') as IconifyIconName;
expect(icon).to.be.eql({
prefix: 'fa',
name: 'home',
});
expect(validateIcon(icon)).to.be.equal(true);
// Simple prefix:name
icon = stringToIcon('fa:arrow-left') as IconifyIconName;
expect(icon).to.be.eql({
prefix: 'fa',
name: 'arrow-left',
});
expect(validateIcon(icon)).to.be.equal(true);
// Longer prefix:name
icon = stringToIcon('mdi-light:home-outline') as IconifyIconName;
expect(icon).to.be.eql({
prefix: 'mdi-light',
name: 'home-outline',
});
expect(validateIcon(icon)).to.be.equal(true);
// Underscore is not an acceptable separator
icon = stringToIcon('fa_home');
expect(icon).to.be.eql(null);
expect(validateIcon(icon)).to.be.equal(false);
// Invalid character '_': fail validateIcon
icon = stringToIcon('fa:home_outline') as IconifyIconName;
expect(icon).to.be.eql({
prefix: 'fa',
name: 'home_outline',
});
expect(validateIcon(icon)).to.be.equal(false);
// 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') as IconifyIconName;
expect(icon).to.be.eql({
prefix: 'MD',
name: 'Home',
});
expect(validateIcon(icon)).to.be.equal(false);
// Numbers: pass
icon = stringToIcon('1:foo') as IconifyIconName;
expect(icon).to.be.eql({
prefix: '1',
name: 'foo',
});
expect(validateIcon(icon)).to.be.equal(true);
// Accented letters: fail validateIcon
icon = stringToIcon('md-fõö') as IconifyIconName;
expect(icon).to.be.eql({
prefix: 'md',
name: 'fõö',
});
expect(validateIcon(icon)).to.be.equal(false);
});
});

View File

@ -0,0 +1,403 @@
/* eslint-disable @typescript-eslint/ban-ts-ignore */
import 'mocha';
import { expect } from 'chai';
import {
newStorage,
addIcon,
iconExists,
getIcon,
addIconSet,
} from '../../lib/storage';
import { FullIconifyIcon, IconifyIcon } from '../../lib/icon';
import { IconifyJSON } from '@iconify/types';
describe('Testing storage', () => {
it('Adding icon', () => {
const storage = newStorage('foo');
// Add one icon
addIcon(storage, 'test', {
body: '<path d="" />',
width: 20,
height: 16,
});
// Add another icon with reserved keyword as name
addIcon(storage, 'constructor', {
body: '<g></g>',
width: 24,
height: 24,
rotate: 1,
});
// Add invalid icon
addIcon(storage, 'invalid', ({} as unknown) as IconifyIcon);
// Should not include 'invalid'
expect(Object.keys(storage.icons)).to.be.eql(['test', 'constructor']);
// Test iconExists
expect(iconExists(storage, 'test')).to.be.equal(true);
expect(iconExists(storage, 'constructor')).to.be.equal(true);
expect(iconExists(storage, 'invalid')).to.be.equal(false);
expect(iconExists(storage, 'missing')).to.be.equal(false);
// Test getIcon
let expected: FullIconifyIcon = {
body: '<path d="" />',
width: 20,
height: 16,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
const icon = getIcon(storage, 'test');
expect(icon).to.be.eql(expected);
expected = {
body: '<g></g>',
width: 24,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 1,
};
// Test icon mutation
let thrown = false;
try {
// @ts-ignore
icon.width = 12;
} catch (err) {
thrown = true;
}
expect(thrown).to.be.equal(true);
expect(getIcon(storage, 'constructor')).to.be.eql(expected);
expect(getIcon(storage, 'invalid')).to.be.equal(null);
expect(getIcon(storage, 'missing')).to.be.equal(null);
});
it('Adding simple icon set', () => {
const storage = newStorage('foo');
// Add two icons
expect(
addIconSet(storage, {
prefix: 'foo',
icons: {
icon1: {
body: '<path d="icon1" />',
width: 20,
},
icon2: {
body: '<path d="icon2" />',
width: 24,
},
},
height: 24,
})
).to.be.equal(true);
expect(Object.keys(storage.icons)).to.be.eql(['icon1', 'icon2']);
// Test iconExists
expect(iconExists(storage, 'icon1')).to.be.equal(true);
expect(iconExists(storage, 'icon2')).to.be.equal(true);
expect(iconExists(storage, 'invalid')).to.be.equal(false);
expect(iconExists(storage, 'missing')).to.be.equal(false);
// Test getIcon
let expected: FullIconifyIcon = {
body: '<path d="icon1" />',
width: 20,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
expect(getIcon(storage, 'icon1')).to.be.eql(expected);
expected = {
body: '<path d="icon2" />',
width: 24,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
expect(getIcon(storage, 'icon2')).to.be.eql(expected);
expect(getIcon(storage, 'invalid')).to.be.equal(null);
expect(getIcon(storage, 'missing')).to.be.equal(null);
});
it('Icon set with invalid default values', () => {
const storage = newStorage('foo');
// Missing prefix, invalid default values
expect(
addIconSet(storage, ({
icons: {
icon1: {
body: '<path d="icon1" />',
width: 20,
// Default should not override this
height: 20,
},
icon2: {
body: '<path d="icon2" />',
width: 24,
},
icon3: {
// Missing 'body'
width: 24,
},
},
height: 24,
// Objects should be ignored. Not testing other types because validation is done only for objects
rotate: {
foo: 1,
},
hFlip: null,
} as unknown) as IconifyJSON)
// Should return false because of exception, but still add icon1 and icon2 before failing on icon3
).to.be.equal(false);
expect(Object.keys(storage.icons)).to.be.eql(['icon1', 'icon2']);
// Test iconExists
expect(iconExists(storage, 'icon1')).to.be.equal(true);
expect(iconExists(storage, 'icon2')).to.be.equal(true);
expect(iconExists(storage, 'invalid')).to.be.equal(false);
expect(iconExists(storage, 'missing')).to.be.equal(false);
// Test getIcon
let expected: FullIconifyIcon = {
body: '<path d="icon1" />',
width: 20,
height: 20,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
expect(getIcon(storage, 'icon1')).to.be.eql(expected);
expected = {
body: '<path d="icon2" />',
width: 24,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
expect(getIcon(storage, 'icon2')).to.be.eql(expected);
expect(getIcon(storage, 'invalid')).to.be.equal(null);
expect(getIcon(storage, 'missing')).to.be.equal(null);
});
it('Icon set with simple aliases', () => {
const storage = newStorage('foo');
expect(
addIconSet(storage, {
prefix: 'foo',
icons: {
icon1: {
body: '<path d="icon1" />',
width: 20,
height: 20,
},
icon2: {
body: '<path d="icon2" />',
width: 24,
rotate: 1,
hFlip: true,
},
},
aliases: {
alias1: {
parent: 'icon1',
},
alias2: {
parent: 'icon2',
rotate: 1,
hFlip: true,
vFlip: true,
},
alias3: {
parent: 'icon3',
},
},
height: 24,
})
).to.be.equal(true);
expect(Object.keys(storage.icons)).to.be.eql([
'icon1',
'icon2',
'alias1',
'alias2',
]);
// Test getIcon
let expected: FullIconifyIcon = {
body: '<path d="icon1" />',
width: 20,
height: 20,
top: 0,
left: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
expect(getIcon(storage, 'alias1')).to.be.eql(expected);
expected = {
body: '<path d="icon2" />',
width: 24,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: true,
rotate: 2,
};
expect(getIcon(storage, 'alias2')).to.be.eql(expected);
expect(getIcon(storage, 'alias3')).to.be.equal(null);
});
it('Icon set with nested aliases', () => {
const storage = newStorage('foo');
expect(
addIconSet(storage, {
prefix: 'foo',
icons: {
icon1: {
body: '<path d="icon1" />',
width: 20,
height: 20,
},
icon2: {
body: '<path d="icon2" />',
width: 24,
rotate: 1,
hFlip: true,
},
},
aliases: {
alias2a: {
// Alias before parent
parent: 'alias2f',
width: 20,
height: 20,
},
alias2f: {
parent: 'icon2',
width: 22,
rotate: 1,
hFlip: true,
vFlip: true,
},
alias2z: {
// Alias after parent
parent: 'alias2f',
width: 21,
rotate: 3,
},
alias2z3: {
// 3 parents: alias2z, alias2f, icon2
parent: 'alias2z',
},
alias2z4: {
// 4 parents: alias2z3, alias2z, alias2f, icon2
parent: 'alias2z3',
},
},
height: 24,
})
// Should have thrown exception on 'alias2z4'
).to.be.equal(false);
expect(Object.keys(storage.icons)).to.be.eql([
'icon1',
'icon2',
'alias2a',
'alias2f',
'alias2z',
'alias2z3',
]);
// Test icon
let expected: FullIconifyIcon = {
body: '<path d="icon2" />',
width: 24,
height: 24,
top: 0,
left: 0,
hFlip: true,
vFlip: false,
rotate: 1,
};
expect(getIcon(storage, 'icon2')).to.be.eql(expected);
// Test simple alias
expected = {
body: '<path d="icon2" />',
width: 22,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: true,
rotate: 2,
};
expect(getIcon(storage, 'alias2f')).to.be.eql(expected);
// Test nested aliases
expected = {
body: '<path d="icon2" />',
width: 21,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: true,
rotate: 1, // 5
};
expect(getIcon(storage, 'alias2z')).to.be.eql(expected);
expected = {
body: '<path d="icon2" />',
width: 20,
height: 20,
top: 0,
left: 0,
hFlip: false,
vFlip: true,
rotate: 2,
};
expect(getIcon(storage, 'alias2a')).to.be.eql(expected);
// 3 levels
expected = {
body: '<path d="icon2" />',
width: 21,
height: 24,
top: 0,
left: 0,
hFlip: false,
vFlip: true,
rotate: 1, // 5
};
expect(getIcon(storage, 'alias2z3')).to.be.eql(expected);
});
});

View File

@ -0,0 +1,203 @@
import 'mocha';
import { expect } from 'chai';
import { iconToSVG, IconifyIconBuildResult } from '../../lib/builder';
import { FullIconifyIcon, iconDefaults, fullIcon } from '../../lib/icon';
import {
FullIconCustomisations,
defaults,
fullCustomisations,
} from '../../lib/customisations';
describe('Testing iconToSVG', () => {
it('Empty icon', () => {
const custom: FullIconCustomisations = defaults;
const icon: FullIconifyIcon = iconDefaults;
const expected: IconifyIconBuildResult = {
attributes: {
width: '1em',
height: '1em',
preserveAspectRatio: 'xMidYMid meet',
viewBox: '0 0 16 16',
},
body: '',
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Auto size, inline, body', () => {
const custom: FullIconCustomisations = fullCustomisations({
inline: true,
height: 'auto',
});
const icon: FullIconifyIcon = fullIcon({
body: '<path d="" />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '16',
height: '16',
preserveAspectRatio: 'xMidYMid meet',
viewBox: '0 0 16 16',
},
body: '<path d="" />',
inline: true,
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Auto size, inline, body', () => {
const custom: FullIconCustomisations = fullCustomisations({
inline: true,
height: 'auto',
});
const icon: FullIconifyIcon = fullIcon({
body: '<path d="" />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '16',
height: '16',
preserveAspectRatio: 'xMidYMid meet',
viewBox: '0 0 16 16',
},
body: '<path d="" />',
inline: true,
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Custom size, alignment', () => {
const custom: FullIconCustomisations = fullCustomisations({
height: 'auto',
hAlign: 'left',
slice: true,
});
const icon: FullIconifyIcon = fullIcon({
width: 20,
height: 16,
body: '<path d="..." />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '20',
height: '16',
preserveAspectRatio: 'xMinYMid slice',
viewBox: '0 0 20 16',
},
body: '<path d="..." />',
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Rotation, alignment', () => {
const custom: FullIconCustomisations = fullCustomisations({
height: '40px',
vAlign: 'bottom',
rotate: 1,
});
const icon: FullIconifyIcon = fullIcon({
width: 20,
height: 16,
body: '<path d="..." />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '32px',
height: '40px',
preserveAspectRatio: 'xMidYMax meet',
viewBox: '0 0 16 20',
},
body: '<g transform="rotate(90 8 8)"><path d="..." /></g>',
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Flip, alignment', () => {
const custom: FullIconCustomisations = fullCustomisations({
height: '32',
vAlign: 'top',
hAlign: 'right',
hFlip: true,
});
const icon: FullIconifyIcon = fullIcon({
width: 20,
height: 16,
body: '<path d="..." />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '40',
height: '32',
preserveAspectRatio: 'xMaxYMin meet',
viewBox: '0 0 20 16',
},
body:
'<g transform="translate(20 0) scale(-1 1)"><path d="..." /></g>',
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Flip, rotation', () => {
const custom: FullIconCustomisations = fullCustomisations({
vFlip: true,
rotate: 1,
});
const icon: FullIconifyIcon = fullIcon({
width: 20,
height: 16,
body: '<path d="..." />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '0.8em',
height: '1em',
preserveAspectRatio: 'xMidYMid meet',
viewBox: '0 0 16 20',
},
body:
'<g transform="rotate(90 8 8) translate(0 16) scale(1 -1)"><path d="..." /></g>',
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
it('Flip and rotation canceling eachother', () => {
const custom: FullIconCustomisations = fullCustomisations({
width: '1em',
height: 'auto',
hFlip: true,
vFlip: true,
rotate: 2,
});
const icon: FullIconifyIcon = fullIcon({
width: 20,
height: 16,
body: '<path d="..." />',
});
const expected: IconifyIconBuildResult = {
attributes: {
width: '1em',
height: '16',
preserveAspectRatio: 'xMidYMid meet',
viewBox: '0 0 20 16',
},
body: '<path d="..." />',
};
const result = iconToSVG(icon, custom);
expect(result).to.be.eql(expected);
});
});

View File

@ -0,0 +1,369 @@
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/camelcase */
import 'mocha';
import { expect } from 'chai';
import {
callbacks,
updateCallbacks,
storeCallback,
} from '../../lib/api/callbacks';
import { sortIcons } from '../../lib/icon/sort';
import { getStorage, addIconSet } from '../../lib/storage';
describe('Testing API callbacks', () => {
let prefixCounter = 0;
function nextPrefix(): string {
prefixCounter++;
return 'api-cb-test-' + (prefixCounter < 10 ? '0' : '') + prefixCounter;
}
it('Simple callback', done => {
const prefix = nextPrefix();
let counter = 0;
const storage = getStorage(prefix);
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([
{
prefix,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
prefix,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
prefix,
name: 'icon2',
},
]);
expect(callbacks[prefix].length).to.be.equal(1);
// Add icon2 and trigger update
addIconSet(storage, {
prefix: prefix,
icons: {
icon2: {
body: '<g></g>',
},
},
});
updateCallbacks(prefix);
return;
case 2:
// Second run - icon2 should be added, completing callback
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([
{
prefix,
name: 'icon3',
},
]);
expect(pending).to.be.eql([]);
expect(callbacks[prefix].length).to.be.equal(0);
done();
}
},
sortIcons([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
{
prefix,
name: 'icon3',
},
]),
[prefix]
);
// Test callbacks
expect(callbacks[prefix].length).to.be.equal(1);
// Test update - should do nothing
updateCallbacks(prefix);
// 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(storage, {
prefix: prefix,
icons: {
icon1: {
body: '<g></g>',
},
},
not_found: ['icon3'],
});
updateCallbacks(prefix);
});
});
it('Callback that should not be stored', () => {
const prefix = nextPrefix();
const storage = getStorage(prefix);
addIconSet(storage, {
prefix,
icons: {
icon1: {
body: '<path d="" />',
},
icon2: {
body: '<path d="" />',
},
},
not_found: ['icon3'],
});
storeCallback(
(loaded, missing, pending, unsubscribe) => {
throw new Error('This code should not be executed!');
},
sortIcons([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
{
prefix,
name: 'icon3',
},
]),
[prefix]
);
// callbacks should not have been initialised
expect(callbacks[prefix]).to.be.equal(void 0);
});
it('Cancel callback', done => {
const prefix = nextPrefix();
let counter = 0;
const storage = getStorage(prefix);
const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => {
expect(unsubscribe).to.be.equal(abort);
counter++;
expect(counter).to.be.equal(1);
// First run - icon1 should be loaded, icon3 should be missing
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
prefix,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
prefix,
name: 'icon2',
},
]);
expect(callbacks[prefix].length).to.be.equal(1);
// Add icon2 and trigger update
addIconSet(storage, {
prefix: prefix,
icons: {
icon2: {
body: '<g></g>',
},
},
});
updateCallbacks(prefix);
// Unsubscribe and set timer to call done()
unsubscribe();
expect(callbacks[prefix].length).to.be.equal(0);
setTimeout(done);
},
sortIcons([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
{
prefix,
name: 'icon3',
},
]),
[prefix]
);
// Test callbacks
expect(callbacks[prefix].length).to.be.equal(1);
// Test update - should do nothing
updateCallbacks(prefix);
// 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(storage, {
prefix: prefix,
icons: {
icon1: {
body: '<g></g>',
},
},
not_found: ['icon3'],
});
updateCallbacks(prefix);
});
});
it('Multiple prefixes', done => {
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
let counter = 0;
const storage1 = getStorage(prefix1);
const storage2 = getStorage(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([
{
prefix: prefix1,
name: 'icon1',
},
]);
expect(missing).to.be.eql([
{
prefix: prefix1,
name: 'icon3',
},
]);
expect(pending).to.be.eql([
{
prefix: prefix2,
name: 'icon2',
},
]);
expect(callbacks[prefix1].length).to.be.equal(0);
expect(callbacks[prefix2].length).to.be.equal(1);
// Add icon2 and trigger update
addIconSet(storage2, {
prefix: prefix2,
icons: {
icon2: {
body: '<g></g>',
},
},
});
updateCallbacks(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);
done();
break;
default:
done('Callback was called ' + counter + ' times.');
}
},
sortIcons([
{
prefix: prefix1,
name: 'icon1',
},
{
prefix: prefix2,
name: 'icon2',
},
{
prefix: prefix1,
name: 'icon3',
},
]),
[prefix1, prefix2]
);
// Test callbacks
expect(callbacks[prefix1].length).to.be.equal(1);
expect(callbacks[prefix2].length).to.be.equal(1);
// Test update - should do nothing
updateCallbacks(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(prefix1);
});
});
});

View File

@ -0,0 +1,85 @@
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/* eslint-disable @typescript-eslint/no-unused-vars */
import 'mocha';
import { expect } from 'chai';
import { RedundancyPendingItem } from '@cyberalien/redundancy';
import {
setAPIConfig,
getAPIConfig,
IconifyAPIConfig,
} from '../../lib/api/config';
import {
setAPIModule,
APIQueryParams,
getAPIModule,
IconifyAPIModule,
} from '../../lib/api/modules';
describe('Testing API modules', () => {
let prefixCounter = 0;
function nextPrefix(): string {
prefixCounter++;
return (
'api-mod-test-' + (prefixCounter < 10 ? '0' : '') + prefixCounter
);
}
const prepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
prefix,
icons,
};
return [item];
};
const sendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
throw new Error('Unexpected API call');
};
it('Empty module', () => {
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://localhost:3000'],
maxURL: 500,
},
prefix
);
// Set fake module
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
// Get config
const config = getAPIConfig(prefix) as IconifyAPIConfig;
expect(config).to.not.be.equal(null);
// 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);
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);
});
});

View File

@ -0,0 +1,637 @@
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/* eslint-disable @typescript-eslint/no-unused-vars */
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 { API } from '../../lib/api/';
describe('Testing API loadIcons', () => {
let prefixCounter = 0;
function nextPrefix(): string {
prefixCounter++;
return (
'api-load-test-' + (prefixCounter < 10 ? '0' : '') + prefixCounter
);
}
it('Loading few icons', done => {
const prefix = nextPrefix();
let asyncCounter = 0;
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
// Icon loader
const prepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
prefix,
icons,
};
// This callback should be called first
expect(asyncCounter).to.be.equal(1);
asyncCounter++;
// Test input and return as one item
const expected: APIQueryParams = {
prefix,
icons: ['icon1', 'icon2'],
};
expect(item).to.be.eql(expected);
return [item];
};
const sendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
// This callback should be called after prepareQuery
expect(asyncCounter).to.be.equal(2);
asyncCounter++;
// Test input
expect(host).to.be.equal('https://api1.local');
const expected: APIQueryParams = {
prefix,
icons: ['icon1', 'icon2'],
};
expect(params).to.be.eql(expected);
// Send data
status.done({
prefix,
icons: {
icon1: {
body: '<path d="" />',
},
icon2: {
body: '<path d="" />',
},
},
});
// Counter should not have increased after status.done() call becuse parsing result should be done on next tick
expect(asyncCounter).to.be.equal(3);
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
// Load icons
API.loadIcons(
[
// as icon
{
prefix,
name: 'icon1',
},
// as string
prefix + ':icon2',
],
(loaded, missing, pending, unsubscribe) => {
// This callback should be called last
expect(asyncCounter).to.be.equal(3);
asyncCounter++;
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
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);
done();
}
);
// Test isPending
expect(API.isPending(prefix, 'icon1')).to.be.equal(true);
expect(API.isPending(prefix, '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 => {
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
},
prefix
);
// Icon loader
const prepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
// Split all icons in multiple queries, one icon per query
const results: APIQueryParams[] = [];
icons.forEach(icon => {
const item: APIQueryParams = {
prefix,
icons: [icon],
};
results.push(item);
});
expect(results.length).to.be.equal(2);
return results;
};
let queryCounter = 0;
const sendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
// Test input
expect(host).to.be.equal('https://api1.local');
// Icon names should match queryCounter: 'icon1' on first run, 'icon2' on second run
queryCounter++;
const expected: APIQueryParams = {
prefix,
icons: ['icon' + queryCounter],
};
expect(params).to.be.eql(expected);
// Send only requested icons
const icons = Object.create(null);
params.icons.forEach(icon => {
icons[icon] = {
body: '<path d="" />',
};
});
status.done({
prefix,
icons,
});
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', 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);
callbackCalled = true;
// Test data
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
done();
}
);
});
it('Fail on default host', done => {
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
},
prefix
);
// Icon loader
const prepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
prefix,
icons,
};
return [item];
};
let queryCounter = 0;
const sendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
queryCounter++;
switch (queryCounter) {
case 1:
// First call on api1
expect(host).to.be.equal('https://api1.local');
// Do nothing - fake failed response
break;
case 2:
// First call on api2
expect(host).to.be.equal('https://api2.local');
// Return result
status.done({
prefix,
icons: {
icon1: {
body: '<path d="" />',
},
icon2: {
body: '<path d="" />',
},
},
});
break;
default:
done(
`Unexpected additional call to sendQuery for host ${host}.`
);
}
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
callbackCalled = true;
// Test data
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
done();
}
);
});
it('Fail on default host, multiple queries', done => {
const prefix = nextPrefix();
// Set config
setAPIConfig(
{
resources: ['https://api1.local', 'https://api2.local'],
rotate: 100, // 100ms to speed up test
},
prefix
);
// Icon loader
const prepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
prefix,
icons,
};
return [item];
};
let queryCounter = 0;
const sendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
queryCounter++;
switch (queryCounter) {
case 1:
// First call on api1
expect(params.icons).to.be.eql(['icon1', 'icon2']);
expect(host).to.be.equal('https://api1.local');
// Do nothing - fake failed response
break;
case 2:
// First call on api2
expect(params.icons).to.be.eql(['icon1', 'icon2']);
expect(host).to.be.equal('https://api2.local');
// Return result
status.done({
prefix,
icons: {
icon1: {
body: '<path d="" />',
},
icon2: {
body: '<path d="" />',
},
},
});
break;
case 3:
// Second call, should have api2 as default
expect(params.icons).to.be.eql(['icon3', 'icon4']);
expect(host).to.be.equal('https://api2.local');
// Return result
status.done({
prefix,
icons: {
icon3: {
body: '<path d="" />',
},
icon4: {
body: '<path d="" />',
},
},
});
break;
default:
done(
`Unexpected additional call to sendQuery for host ${host}.`
);
}
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
prefix
);
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
callbackCalled = true;
// Test data
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
// Send another query on next tick
setTimeout(() => {
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon3', prefix + ':icon4'],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
callbackCalled = true;
// Test data
expect(loaded).to.be.eql([
{
prefix,
name: 'icon3',
},
{
prefix,
name: 'icon4',
},
]);
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
done();
}
);
});
}
);
});
it('Fail on default host, multiple queries with different prefixes', done => {
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]
);
// Icon loader
const prepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
const item: APIQueryParams = {
prefix,
icons,
};
return [item];
};
let queryCounter = 0;
const sendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
queryCounter++;
switch (queryCounter) {
case 1:
// First call on api1
expect(params.prefix).to.be.equal(prefix);
expect(params.icons).to.be.eql(['icon1', 'icon2']);
expect(host).to.be.equal('https://api1.local');
// Do nothing - fake failed response
break;
case 2:
// First call on api2
expect(params.prefix).to.be.equal(prefix);
expect(params.icons).to.be.eql(['icon1', 'icon2']);
expect(host).to.be.equal('https://api2.local');
// Return result
status.done({
prefix: params.prefix,
icons: {
icon1: {
body: '<path d="" />',
},
icon2: {
body: '<path d="" />',
},
},
});
break;
case 3:
// Second call, should have api2 as default
expect(params.prefix).to.be.equal(prefix2);
expect(params.icons).to.be.eql(['icon2', 'icon4']);
expect(host).to.be.equal('https://api2.local');
// Return result
status.done({
prefix: params.prefix,
icons: {
icon2: {
body: '<path d="" />',
},
icon4: {
body: '<path d="" />',
},
},
});
break;
default:
done(
`Unexpected additional call to sendQuery for host ${host}.`
);
}
};
setAPIModule(
{
prepare: prepareQuery,
send: sendQuery,
},
[prefix, prefix2]
);
// Load icons
let callbackCalled = false;
API.loadIcons(
[prefix + ':icon1', prefix + ':icon2'],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
callbackCalled = true;
// Test data
expect(loaded).to.be.eql([
{
prefix,
name: 'icon1',
},
{
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
// Send another query on next tick for different prefix that shares configuration
setTimeout(() => {
let callbackCalled = false;
API.loadIcons(
[prefix2 + ':icon2', prefix2 + ':icon4'],
(loaded, missing, pending, unsubscribe) => {
// Callback should be called only once
expect(callbackCalled).to.be.equal(false);
callbackCalled = true;
// Test data
expect(loaded).to.be.eql([
{
prefix: prefix2,
name: 'icon2',
},
{
prefix: prefix2,
name: 'icon4',
},
]);
expect(missing).to.be.eql([]);
expect(pending).to.be.eql([]);
done();
}
);
});
}
);
});
});

View File

@ -0,0 +1,219 @@
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/* eslint-disable @typescript-eslint/no-unused-vars */
import 'mocha';
import { expect } from 'chai';
import { count, config, loadCache } from '../../lib/cache/storage';
import {
nextPrefix,
createCache,
reset,
cachePrefix,
cacheVersion,
versionKey,
countKey,
} from './fake_cache';
describe('Testing mocked localStorage', () => {
it('No usable cache', () => {
reset({});
// Config before tests
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
// Attempt to load
loadCache();
// Everything should be disabled
expect(config).to.be.eql({
local: false,
session: false,
});
// Nothing should have loaded
expect(count).to.be.eql({
local: 0,
session: 0,
});
});
it('Empty localStorage', () => {
reset({
localStorage: createCache(),
});
// Config before tests
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
// Attempt to load
loadCache();
// sessionStorage should be disabled
expect(config).to.be.eql({
local: true,
session: false,
});
// Nothing should have loaded
expect(count).to.be.eql({
local: 0,
session: 0,
});
});
it('Restricted localStorage', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add one item
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '1');
cache.setItem(
cachePrefix + '0',
JSON.stringify({
cached: Date.now(),
data: {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
},
})
);
// Prevent reading and writing
cache.canRead = false;
cache.canWrite = false;
// Set cache and test it
reset({
localStorage: cache,
sessionStorage: cache,
});
// Config before tests
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
// Attempt to load
loadCache();
// Everything should be disabled because read-only mock throws errors
expect(config).to.be.eql({
local: false,
session: false,
});
// Nothing should have loaded
expect(count).to.be.eql({
local: 0,
session: 0,
});
});
it('localStorage with one item', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add one icon set
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '1');
cache.setItem(
cachePrefix + '0',
JSON.stringify({
cached: Date.now(),
data: {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
},
})
);
// Set cache and test it
reset({
localStorage: cache,
});
// Config before tests
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
// Attempt to load
loadCache();
// sessionStorage should be disabled
expect(config).to.be.eql({
local: true,
session: false,
});
// One item should be in localStorage
expect(count).to.be.eql({
local: 1,
session: 0,
});
});
it('localStorage and sessionStorage', () => {
reset({
localStorage: createCache(),
sessionStorage: createCache(),
});
// Config before tests
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
// Attempt to load
loadCache();
// Everything should be working
expect(config).to.be.eql({
local: true,
session: true,
});
// Empty storage
expect(count).to.be.eql({
local: 0,
session: 0,
});
});
});

View File

@ -0,0 +1,435 @@
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/* eslint-disable @typescript-eslint/no-unused-vars */
import 'mocha';
import { expect } from 'chai';
import {
loadCache,
count,
config,
emptyList,
StoredItem,
} from '../../lib/cache/storage';
import { getStorage, iconExists, getIcon } from '../../lib/storage';
import {
nextPrefix,
createCache,
reset,
cachePrefix,
cacheVersion,
versionKey,
countKey,
hour,
cacheExpiration,
} from './fake_cache';
import { IconifyIcon, IconifyJSON } from '@iconify/types';
describe('Testing loading from localStorage', () => {
it('Valid icon set', () => {
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),
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(prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
loadCache();
// Icon should exist now
expect(iconExists(icons, 'foo')).to.be.equal(true);
// 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();
// Add one icon set
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '1');
const item: StoredItem = {
// Expiration date
cached: Math.floor(Date.now() / hour) - cacheExpiration - 1,
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(prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
loadCache();
// Icon should not have loaded
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Check data
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
});
it('Bad icon set', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add one icon set
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '1');
cache.setItem(
cachePrefix + '0',
JSON.stringify({
cached: Math.floor(Date.now() / hour),
data: {
prefix: prefix,
icons: {
foo: {
// Missing 'body' property
width: 20,
},
},
},
})
);
// Set cache
reset({
localStorage: cache,
});
// Check icon storage
const icons = getStorage(prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
loadCache();
// Icon should not have loaded
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Check data
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
});
it('Wrong counter', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add one icon set
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '0'); // Should be at least "1"
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
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(prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
loadCache();
// Icon should not have loaded
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Check data
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
});
it('Missing entries at the end', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add one icon set
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '5');
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
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(prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Load localStorage
loadCache();
// Icon should exist now
expect(iconExists(icons, 'foo')).to.be.equal(true);
// 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('Missing entries', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add two icon sets
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '5');
// Missing: 0, 2, 3
const item1: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: {
prefix: prefix,
icons: {
foo1: {
body: '<g></g>',
},
},
},
};
const item4: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: {
prefix: prefix,
icons: {
foo4: {
body: '<g></g>',
},
},
},
};
cache.setItem(cachePrefix + '1', JSON.stringify(item1));
cache.setItem(cachePrefix + '4', JSON.stringify(item4));
// Set cache
reset({
localStorage: cache,
});
// Check icon storage
const icons = getStorage(prefix);
expect(iconExists(icons, 'foo1')).to.be.equal(false);
expect(iconExists(icons, 'foo4')).to.be.equal(false);
// Load localStorage
loadCache();
// Icons should exist now
expect(iconExists(icons, 'foo1')).to.be.equal(true);
expect(iconExists(icons, 'foo4')).to.be.equal(true);
// Check data
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 5,
session: 0,
});
expect(emptyList).to.be.eql({
local: [3, 2, 0], // reserse order
session: [],
});
});
it('Using both storage options', () => {
const prefix = nextPrefix();
const cache1 = createCache();
const cache2 = createCache();
// Add few icon sets
cache1.setItem(versionKey, cacheVersion);
cache2.setItem(versionKey, cacheVersion);
cache1.setItem(countKey, '6');
cache2.setItem(countKey, '3');
// Create 5 items
const icons: IconifyJSON[] = [];
const items: StoredItem[] = [];
for (let i = 0; i < 6; i++) {
const icon: IconifyJSON = {
prefix: prefix,
icons: {
['foo' + i]: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
icons.push(icon);
items.push(item);
}
// Add items 1,3,5 to localStorage
[1, 3, 5].forEach((index) => {
cache1.setItem(cachePrefix + index, JSON.stringify(items[index]));
});
// Add items 0 and 2 to sessionStorage
[0, 2].forEach((index) => {
cache2.setItem(cachePrefix + index, JSON.stringify(items[index]));
});
// Set cache
reset({
localStorage: cache1,
sessionStorage: cache2,
});
// Check icon storage
const iconsStorage = getStorage(prefix);
for (let i = 0; i < 6; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
false,
`Icon ${i} should not exist yet`
);
}
// Load localStorage
loadCache();
// Icons should exist now, except for number 4
for (let i = 0; i < 6; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
i !== 4,
`Icon ${i} failed loading test`
);
}
// Check data
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 6,
session: 3,
});
expect(emptyList).to.be.eql({
local: [4, 2, 0],
session: [1],
});
});
});

View File

@ -0,0 +1,683 @@
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/* eslint-disable @typescript-eslint/no-unused-vars */
import 'mocha';
import { expect } from 'chai';
import {
loadCache,
storeCache,
count,
config,
emptyList,
StoredItem,
} from '../../lib/cache/storage';
import { getStorage, iconExists } from '../../lib/storage';
import {
nextPrefix,
createCache,
reset,
cachePrefix,
cacheVersion,
versionKey,
countKey,
hour,
cacheExpiration,
} from './fake_cache';
import { IconifyJSON } from '@iconify/types';
describe('Testing saving to localStorage', () => {
it('One icon set', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add one icon set
const icon: IconifyJSON = {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
// Set cache
reset({
localStorage: cache,
});
// Check icon storage
const icons = getStorage(prefix);
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Save item
storeCache(icon);
// Storing in cache should not add item to storage
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Check data that should have been updated because storeCache()
// should call load function before first execution
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: [],
});
// Check cache
expect(cache.getItem(cachePrefix + '0')).to.be.equal(
JSON.stringify(item)
);
expect(cache.getItem(countKey)).to.be.equal('1');
expect(cache.getItem(versionKey)).to.be.equal(cacheVersion);
});
it('Multiple icon sets', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add icon sets
const icon0: IconifyJSON = {
prefix: prefix,
icons: {
foo0: {
body: '<g></g>',
},
},
};
const item0: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon0,
};
const icon1: IconifyJSON = {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
};
const item1: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon1,
};
// Set cache
reset({
localStorage: cache,
});
// Save items
storeCache(icon0);
storeCache(icon1);
// Check data that should have been updated because storeCache()
// should call load function before first execution
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 2,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Check cache
expect(cache.getItem(cachePrefix + '0')).to.be.equal(
JSON.stringify(item0)
);
expect(cache.getItem(cachePrefix + '1')).to.be.equal(
JSON.stringify(item1)
);
expect(cache.getItem(countKey)).to.be.equal('2');
expect(cache.getItem(versionKey)).to.be.equal(cacheVersion);
});
it('Adding icon set on unused spot', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add icon sets
const icon0: IconifyJSON = {
prefix: prefix,
icons: {
foo0: {
body: '<g></g>',
},
},
};
const item0: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon0,
};
const icon1: IconifyJSON = {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
};
const item1: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon1,
};
// Add item
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '2');
cache.setItem(cachePrefix + '1', JSON.stringify(item1));
// Set cache
reset({
localStorage: cache,
});
// Load data
loadCache();
// Check data
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 2,
session: 0,
});
expect(emptyList).to.be.eql({
local: [0],
session: [],
});
// Save items
storeCache(icon0);
// Check data
expect(count).to.be.eql({
local: 2,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Check cache
expect(cache.getItem(cachePrefix + '0')).to.be.equal(
JSON.stringify(item0)
);
expect(cache.getItem(cachePrefix + '1')).to.be.equal(
JSON.stringify(item1)
);
expect(cache.getItem(countKey)).to.be.equal('2');
expect(cache.getItem(versionKey)).to.be.equal(cacheVersion);
});
it('Adding multiple icon sets to existing data', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add icon sets
const icons: IconifyJSON[] = [];
const items: StoredItem[] = [];
for (let i = 0; i < 12; i++) {
const icon: IconifyJSON = {
prefix: prefix,
icons: {
['foo' + i]: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
// Make items 2 and 4 expire
if (i === 2 || i === 4) {
item.cached -= cacheExpiration + 1;
}
// Change expiration for items 6 and 8 to almost expire
if (i === 6 || i === 8) {
item.cached -= cacheExpiration - 1;
}
icons.push(icon);
items.push(item);
// Skip items 1, 5, 9+
if (i !== 1 && i !== 5 && i < 9) {
cache.setItem(cachePrefix + i, JSON.stringify(item));
}
}
cache.setItem(versionKey, cacheVersion);
cache.setItem(countKey, '10');
// Set cache
reset({
sessionStorage: cache,
});
// Load data
loadCache();
// Check data
expect(config).to.be.eql({
local: false,
session: true,
});
expect(count).to.be.eql({
local: 0,
session: 9, // item 9 was missing
});
expect(emptyList).to.be.eql({
local: [],
// mix of expired and skipped items
// reverse order, 9 should not be there because it is last item
session: [5, 4, 2, 1],
});
expect(cache.getItem(countKey)).to.be.equal('9');
// Check cached items
[0, 3, 6, 7, 8].forEach((index) => {
expect(cache.getItem(cachePrefix + index)).to.be.equal(
JSON.stringify(items[index]),
`Checking item ${index}`
);
});
// Check expired items - should have been deleted
// Also check items that weren't supposed to be added
[2, 4, 1, 5, 9, 10, 11, 12, 13].forEach((index) => {
expect(cache.getItem(cachePrefix + index)).to.be.equal(
null,
`Checking item ${index}`
);
});
// Add item 5
storeCache(icons[5]);
expect(count).to.be.eql({
local: 0,
session: 9,
});
expect(emptyList).to.be.eql({
local: [],
session: [4, 2, 1],
});
expect(cache.getItem(countKey)).to.be.equal('9');
// Add items 4, 2, 1
const list = [4, 2, 1];
list.slice(0).forEach((index) => {
expect(list.shift()).to.be.equal(index);
storeCache(icons[index]);
expect(count).to.be.eql({
local: 0,
session: 9,
});
expect(emptyList).to.be.eql({
local: [],
session: list,
});
expect(cache.getItem(countKey)).to.be.equal('9');
});
// Add item 10
storeCache(icons[10]);
expect(count).to.be.eql({
local: 0,
session: 10,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
expect(cache.getItem(countKey)).to.be.equal('10');
// Add item 11
storeCache(icons[11]);
expect(count).to.be.eql({
local: 0,
session: 11,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
expect(cache.getItem(countKey)).to.be.equal('11');
});
it('Overwrite outdated data', () => {
const prefix = nextPrefix();
const cache = createCache();
// Add data in old format
cache.setItem(versionKey, '1.0.6');
cache.setItem(countKey, '3');
for (let i = 0; i < 3; i++) {
cache.setItem(
cachePrefix + i,
JSON.stringify({
prefix: prefix,
icons: {
['foo' + i]: {
body: '<g></g>',
},
},
})
);
}
// Set cache
reset({
localStorage: cache,
});
// Check icon storage
const icons = getStorage(prefix);
expect(iconExists(icons, 'foo1')).to.be.equal(false);
// Load cache
loadCache();
expect(config).to.be.eql({
local: true,
session: false,
});
expect(count).to.be.eql({
local: 0,
session: 0,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Add one icon set
const icon: IconifyJSON = {
prefix: prefix,
icons: {
foo: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
// Save item
storeCache(icon);
// Storing in cache should not add item to storage
expect(iconExists(icons, 'foo')).to.be.equal(false);
// Check data that should have been updated because storeCache()
// should call load function before first execution
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: [],
});
// Check cache
expect(cache.getItem(cachePrefix + '0')).to.be.equal(
JSON.stringify(item)
);
expect(cache.getItem(countKey)).to.be.equal('1');
expect(cache.getItem(versionKey)).to.be.equal(cacheVersion);
});
it('Using both storage options', () => {
const prefix = nextPrefix();
const cache1 = createCache();
const cache2 = createCache();
// Add icon sets to localStorage
cache1.setItem(versionKey, cacheVersion);
cache1.setItem(countKey, '3');
[0, 1, 2].forEach((index) => {
const icon: IconifyJSON = {
prefix: prefix,
icons: {
['foo' + index]: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
cache1.setItem(cachePrefix + index, JSON.stringify(item));
});
// Add icon sets to sessionStorage
cache2.setItem(versionKey, cacheVersion);
cache2.setItem(countKey, '4');
[0, 1, 2, 3].forEach((index) => {
const icon: IconifyJSON = {
prefix: prefix,
icons: {
['bar' + index]: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
cache2.setItem(cachePrefix + index, JSON.stringify(item));
});
// Set cache
reset({
localStorage: cache1,
sessionStorage: cache2,
});
// Load data
loadCache();
// Check data
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 3,
session: 4,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Check icon storage
const iconsStorage = getStorage(prefix);
for (let i = 0; i < count.local; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
true,
`Icon foo${i} should have loaded`
);
}
for (let i = 0; i < count.session; i++) {
expect(iconExists(iconsStorage, 'bar' + i)).to.be.equal(
true,
`Icon bar${i} should have loaded`
);
}
// Add new item to localStorage
const icon: IconifyJSON = {
prefix: prefix,
icons: {
'new-icon': {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
storeCache(icon);
// Check data
expect(count).to.be.eql({
local: 4, // +1
session: 4,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Check cache
expect(cache1.getItem(cachePrefix + '3')).to.be.equal(
JSON.stringify(item)
);
});
it('Using both storage options, but localStorage is read only', () => {
const prefix = nextPrefix();
const cache1 = createCache();
const cache2 = createCache();
// Add icon sets to localStorage
cache1.setItem(versionKey, cacheVersion);
cache1.setItem(countKey, '3');
[0, 1, 2].forEach((index) => {
const icon: IconifyJSON = {
prefix: prefix,
icons: {
['foo' + index]: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
cache1.setItem(cachePrefix + index, JSON.stringify(item));
});
// Add icon sets to sessionStorage
cache2.setItem(versionKey, cacheVersion);
cache2.setItem(countKey, '4');
[0, 1, 2, 3].forEach((index) => {
const icon: IconifyJSON = {
prefix: prefix,
icons: {
['bar' + index]: {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
cache2.setItem(cachePrefix + index, JSON.stringify(item));
});
// Set cache
reset({
localStorage: cache1,
sessionStorage: cache2,
});
// Load data
loadCache();
// Check data
expect(config).to.be.eql({
local: true,
session: true,
});
expect(count).to.be.eql({
local: 3,
session: 4,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Check icon storage
const iconsStorage = getStorage(prefix);
for (let i = 0; i < count.local; i++) {
expect(iconExists(iconsStorage, 'foo' + i)).to.be.equal(
true,
`Icon foo${i} should have loaded`
);
}
for (let i = 0; i < count.session; i++) {
expect(iconExists(iconsStorage, 'bar' + i)).to.be.equal(
true,
`Icon bar${i} should have loaded`
);
}
// Set localStorage to read-only
cache1.canWrite = false;
// Add new item to localStorage
const icon: IconifyJSON = {
prefix: prefix,
icons: {
'new-icon': {
body: '<g></g>',
},
},
};
const item: StoredItem = {
cached: Math.floor(Date.now() / hour),
data: icon,
};
storeCache(icon);
// Check data
expect(count).to.be.eql({
local: 3,
session: 5,
});
expect(emptyList).to.be.eql({
local: [],
session: [],
});
// Check cache
expect(cache2.getItem(cachePrefix + '4')).to.be.equal(
JSON.stringify(item)
);
});
});

View File

@ -0,0 +1,114 @@
import { mock, count, config, emptyList } from '../../lib/cache/storage';
/**
* Get next icon set prefix for testing
*/
let prefixCounter = 0;
export function nextPrefix(): string {
return 'fake-storage-' + prefixCounter++;
}
// Cache version. Bump when structure changes
export const cacheVersion = 'iconify1';
// Cache keys
export const cachePrefix = 'iconify';
export const countKey = cachePrefix + '-count';
export const versionKey = cachePrefix + '-version';
/**
* Cache expiration
*/
export const hour = 3600000;
export const cacheExpiration = 168; // In hours
/**
* Storage class
*/
export class Storage {
canRead = true;
canWrite = true;
items: Record<string, string> = Object.create(null);
/**
* Get number of items
*/
get length(): number {
if (!this.canRead) {
throw new Error('Restricted storage');
}
return Object.keys(this.items).length;
}
/**
* Get item
*
* @param name
*/
getItem(name: string): string | null {
if (!this.canRead) {
throw new Error('Restricted storage');
}
return this.items[name] === void 0 ? null : this.items[name];
}
/**
* Set item
*
* @param name
* @param value
*/
setItem(name: string, value: string): void {
if (!this.canWrite) {
throw new Error('Read-only storage');
}
this.items[name] = value;
}
/**
* Remove item
*
* @param name
*/
removeItem(name: string): void {
if (!this.canWrite) {
throw new Error('Read-only storage');
}
delete this.items[name];
}
/**
* Clear everything
*/
clear(): void {
if (!this.canWrite) {
throw new Error('Read-only storage');
}
this.items = Object.create(null);
}
}
/**
* Create fake storage, assign localStorage type
*/
export function createCache(): typeof localStorage {
return (new Storage() as unknown) as typeof localStorage;
}
/**
* Reset test
*
* @param fakeWindow
*/
export function reset(fakeWindow: Record<string, typeof localStorage>): void {
// Replace window
mock(fakeWindow);
// Reset all data
for (const key in config) {
const attr = (key as unknown) as keyof typeof config;
config[attr] = true;
count[attr] = 0;
emptyList[attr] = [];
}
}

View File

@ -0,0 +1,8 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"types": ["node", "mocha"],
"outDir": "../tests-compiled",
"rootDir": "."
}
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@ -0,0 +1,12 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./src/tsconfig.json"
},
{
"path": "./tests/tsconfig.json"
}
]
}

4
packages/iconify/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
node_modules
dist
lib

324
packages/iconify/README.md Normal file
View File

@ -0,0 +1,324 @@
# What is Iconify?
Iconify is the most versatile icon framework.
- Unified icon framework that can be used with any icon library.
- Out of the box includes 60+ icon sets with 50,000 icons.
- Embed icons in HTML with SVG framework or components for front-end frameworks.
- Embed icons in designs with plug-ins for Figma, Sketch and Adobe XD.
- Add icon search to your applications with Iconify Icon Finder.
For more information visit [https://iconify.design/](https://iconify.design/).
# Iconify SVG framework
There are many fonts and SVG sets available, but they all have one thing in common: using any font or SVG set limits you to icons that are included in that set and forces browsers to load entire font or icons set. That limits developers to one or two fonts or icon sets.
Iconify uses a new innovative approach to loading icons. Unlike fonts and SVG frameworks, Iconify only loads icons that are used on the page instead of loading entire fonts. How is it done? By serving icons dynamically from publicly available JSON API (you can make a copy of script and API if you prefer to keep everything on your servers).
Iconify SVG framework is designed to be as easy to use as possible.
Add this line to your page to load Iconify SVG framework (you can add it to `<head>` section of page or before `</body>`):
```html
<script src="https://code.iconify.design/2/2.0.0-dev/iconify.min.js"></script>
```
or, if you are building a project with something like WebPack or Rollup, you can include the script by installing `@iconify/iconify` as a dependency and importing it in your project:
```js
import Iconify from '@iconify/iconify';
```
To add any icon, write something like this:
```html
<span class="iconify" data-icon="eva:people-outline"></span>
```
&nbsp;&nbsp;&nbsp; ![Sample](https://iconify.design/assets/images/eva-people-outline.svg)
or this:
```html
<span class="iconify-inline" data-icon="fa-solid:home"></span>
<a href="#">Return home!</a>
```
&nbsp;&nbsp;&nbsp; ![Screenshot](https://iconify.design/assets/images/inline-sample.png)
That is it. Change `data-icon` value to the name of the icon you want to use. There are over 50,000 premade icons to choose from, including FontAwesome, Material Design Icons, Entypo+, Box Icons, Unicons and even several emoji sets.
Do you want to make your own icon sets? Tools for making custom icon sets are available on GitHub. See documentation.
## How does it work?
The syntax is similar to icon fonts. Instead of inserting `SVG` in the document, you write a placeholder element, such `SPAN` or `I`.
Iconify SVG framework finds those placeholders and uses the following logic to parse them:
1. Retrieves icon name from `data-icon` attribute.
2. Checks if icon exists. If not, it sends a request to Iconify API to retrieve icon data.
3. Replaces placeholder element with `SVG`.
This is done in a fraction of a second. Iconify SVG framework watches DOM for changes, so whenever you add new placeholders, it immediately replaces them with `SVG`, making it easy to use with dynamic content, such as AJAX forms.
### Inline mode
Code examples above use different class names: the first example uses "iconify", the second example uses "iconify-inline".
What is the difference?
- "iconify" renders icon as is, so it behaves like an image.
- "iconify-inline" renders adds vertical alignment to the icon, making it behave like text (inline mode).
Usually, icon fonts do not render like normal images, they render like text. Text is aligned slightly below the baseline.
Visual example to show the difference between inline and block modes:
&nbsp;&nbsp;&nbsp; ![Inline icon](https://iconify.design/assets/images/inline.png)
Why is the inline mode needed?
- To easily align icons within the text, such as emojis.
- To make the transition from outdated icon fonts to SVG easier.
Use "iconify" for decorations, use "iconify-inline" if you want the icon to behave like an icon font.
#### data-inline attribute
In addition to using "iconify-inline" class, you can toggle inline mode with the `data-inline` attribute.
Set value to "true" to force inline mode, set value to "false" to use block mode.
Different ways to use block mode:
```html
<span class="iconify" data-icon="eva:people-outline"></span>
<span class="iconify" data-icon="eva:people-outline" data-inline="false"></span>
```
Different ways to use inline mode:
```html
<span class="iconify-inline" data-icon="eva:people-outline"></span>
<span class="iconify" data-icon="eva:people-outline" data-inline="true"></span>
<span
class="iconify"
data-icon="eva:people-outline"
style="vertical-align: -0.125em"
></span>
```
## Iconify API
When you use an icon font, each visitor loads an entire font, even if your page only uses a few icons. This is a major downside of using icon fonts. That limits developers to one or two fonts or icon sets.
Unlike icon fonts, Iconify SVG framework does not load the entire icon set. Unlike fonts and SVG frameworks, Iconify only loads icons that are used on the current page instead of loading entire icon sets. How is it done? By serving icons dynamically from publicly available JSON API.
### Custom API
Relying on a third party service is often not an option. Many companies and developers prefer to keep everything on their own servers to have full control.
Iconify API and icon sets are all [available on GitHub](https://github.com/iconify), making it easy to host API on your own server.
For more details see [Iconify API documentation](https://iconify.design/docs/api-hosting/).
You can also create custom Iconify API to serve your own icons. For more details see [hosting custom icons in Iconify documentation](https://iconify.design/docs/api-custom-hosting/).
### Using Iconify offline
While the default method of retrieving icons is to retrieve them from API, there are other options. Iconify SVG framework is designed to be as flexible as possible.
Easiest option to serve icons without API is by creating icon bundles.
Icon bundles are small scripts that you can load after Iconify SVG framework or bundle it together in one file.
For more details see [icon bundles in Iconify documentation](https://iconify.design/docs/icon-bundles/).
Another option is to import icons and bundle them with Iconify, similar to React and Vue components. Example:
```js
// Installation: npm install --save-dev @iconify/iconify
import Iconify from '@iconify/iconify';
// Installation: npm install --save-dev @iconify/icons-dashicons
import adminUsers from '@iconify/icons-dashicons/admin-users';
// Unlike React and Vue components, in SVG framework each icon added with addIcon() name must have a
// prefix and a name. In this example prefix is "dashicons" and name is "admin-users".
Iconify.addIcon('dashicons:admin-users', adminUsers);
```
```html
<span class="iconify" data-icon="dashicons:admin-users"></span>
```
See [Iconify for React](http://github.com/iconify/iconify/packages/react) documentation for more details.
## Color
There are 2 types of icons: monotone and coloured.
- Monotone icons are icons that use only 1 colour and you can change that colour. Most icon sets fall into this category: FontAwesome, Unicons, Material Design Icons, etc.
- Coloured icons are icons that use the preset palette. Most emoji icons fall into this category: Noto Emoji, Emoji One, etc. You cannot change the palette for those icons.
Monotone icons use font colour, just like glyph fonts. To change colour, you can do this:
```html
<span class="iconify icon-bell" data-icon="vaadin-bell"></span>
```
and add this to CSS:
```css
.icon-bell {
color: #f80;
}
.icon-bell:hover {
color: #f00;
}
```
Sample:
&nbsp;&nbsp;&nbsp; ![Sample](https://iconify.design/samples/icon-color.png)
## Dimensions
By default all icons are scaled to 1em height. To control icon height use font-size:
```html
<span class="iconify icon-clipboard" data-icon="emojione-clipboard"></span>
```
and add this to css:
```css
.icon-clipboard {
font-size: 32px;
}
```
Sample:
&nbsp;&nbsp;&nbsp; ![Sample](https://iconify.design/samples/icon-size.png)
you might also need to set line-height:
```css
.icon-clipboard {
font-size: 32px;
line-height: 1em;
}
```
You can also set custom dimensions using `data-width` and `data-height` attributes:
```html
<span
data-icon="twemoji-ice-cream"
data-width="32"
data-height="32"
class="iconify"
></span>
```
Sample:
&nbsp;&nbsp;&nbsp; ![Sample](https://iconify.design/samples/icon-size2.png)
## Transformations
You can rotate and flip icon by adding `data-flip` and `data-rotate` attributes:
```html
<span
data-icon="twemoji-helicopter"
class="iconify"
data-flip="horizontal"
></span>
<span data-icon="twemoji-helicopter" class="iconify" data-rotate="90deg"></span>
```
Possible values for `data-flip`: horizontal, vertical.
Possible values for `data-rotate`: 90deg, 180deg, 270deg.
If you use both flip and rotation, the icon is flipped first, then rotated.
To use custom transformations use CSS transform rule. Add `!important` after rule to override the SVG inline style (inline style exists to fix a SVG rendering bug in Firefox browser).
```html
<span data-icon="twemoji-helicopter" class="iconify icon-helicopter"></span>
```
```css
.icon-helicopter {
transform: 45deg !important;
}
```
Samples:
&nbsp;&nbsp;&nbsp; ![Sample](https://iconify.design/samples/icon-transform.png)
## Available icons
There are over 50,000 icons to choose from.
General collections (monotone icons):
- [Material Design Icons](https://iconify.design/icon-sets/mdi/) (5000+ icons)
- [Unicons](https://iconify.design/icon-sets/uil/) (1000+ icons)
- [Jam Icons](https://iconify.design/icon-sets/jam/) (900 icons)
- [IonIcons](https://iconify.design/icon-sets/ion/) (1200+ icons)
- [FontAwesome 4](https://iconify.design/icon-sets/fa/) and [FontAwesome 5](https://iconify.design/icon-sets/fa-solid/) (2000+ icons)
- [Vaadin Icons](https://iconify.design/icon-sets/vaadin/) (600+ icons)
- [Bootstrap Icons](https://iconify.design/icon-sets/bi/) (500+ icons)
- [Feather Icons](https://iconify.design/icon-sets/feather/) and [Feather Icon](https://iconify.design/icon-sets/fe/) (500+ icons)
- [IcoMoon Free](https://iconify.design/icon-sets/icomoon-free/) (400+ icons)
- [Dashicons](https://iconify.design/icon-sets/dashicons/) (300 icons)
and many others.
Emoji collections (mostly colored icons):
- [Emoji One](https://iconify.design/icon-sets/emojione/) (1800+ colored version 2 icons, 1400+ monotone version 2 icons, 1200+ version 1 icons)
- [OpenMoji](https://iconify.design/icon-sets/openmoji/) (3500+ icons)
- [Noto Emoji](https://iconify.design/icon-sets/noto/) (2000+ icons for version 2, 2000+ icons for version 1)
- [Twitter Emoji](https://iconify.design/icon-sets/twemoji/) (2000+ icons)
- [Firefox OS Emoji](https://iconify.design/icon-sets/fxemoji/) (1000+ icons)
Also, there are several thematic collections, such as weather icons, map icons, etc.
You can use browse or search available icons on the Iconify website: https://iconify.design/icon-sets/
Click an icon to get HTML code.
## Iconify vs SVG vs glyph fonts
Why use Iconify instead of fonts or other frameworks?
There is a tutorial that explains all differences. See http://iconify.design/docs/iconify-svg-fonts/
## Browser support
Iconify supports all modern browsers.
Old browsers that are supported:
- IE 9+
- iOS Safari for iOS 8+
IE 9, 10 and iOS 8 Safari do not support some modern functions that Iconify relies on. Iconify will automatically
load polyfills for those browsers. All newer browsers do not require those polyfills.
## License
Iconify is dual-licensed under Apache 2.0 and GPL 2.0 license. You may select, at your option, one of the above-listed licenses.
`SPDX-License-Identifier: Apache-2.0 OR GPL-2.0`
This license does not apply to icons. Icons are released under different licenses, see each icon set for details.
Icons available by default are all licensed under some kind of open-source or free license.
© 2016 - 2020 Vjacheslav Trushkin

View File

@ -0,0 +1,43 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "lib/iconify.d.ts",
"bundledPackages": [
"@iconify/types",
"@iconify/core",
"@cyberalien/redundancy"
],
"compiler": {},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/iconify.d.ts"
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
},
"ae-missing-release-tag": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
}
}
}
}

89
packages/iconify/build.js Normal file
View File

@ -0,0 +1,89 @@
const path = require('path');
const child_process = require('child_process');
// List of commands to run
const commands = [];
// Parse command line
const compile = {
core: false,
lib: true,
dist: true,
api: true,
};
process.argv.slice(2).forEach(cmd => {
if (cmd.slice(0, 2) !== '--') {
return;
}
const parts = cmd.slice(2).split('-');
if (parts.length === 2) {
// Parse 2 part commands like --with-lib
const key = parts.pop();
if (compile[key] === void 0) {
return;
}
switch (parts.shift()) {
case 'with':
// enable module
compile[key] = true;
break;
case 'without':
// disable module
compile[key] = false;
break;
case 'only':
// disable other modules
Object.keys(compile).forEach(key2 => {
compile[key2] = key2 === key;
});
break;
}
}
});
// Compile core before compiling this package
if (compile.core) {
commands.push({
cmd: 'npm',
args: ['run', 'build'],
cwd: path.dirname(__dirname) + '/core',
});
}
// Compile other packages
Object.keys(compile).forEach(key => {
if (key !== 'core' && compile[key]) {
commands.push({
cmd: 'npm',
args: ['run', 'build:' + key],
});
}
});
/**
* Run next command
*/
const next = () => {
const item = commands.shift();
if (item === void 0) {
process.exit(0);
}
if (item.cwd === void 0) {
item.cwd = __dirname;
}
const result = child_process.spawnSync(item.cmd, item.args, {
cwd: item.cwd,
stdio: 'inherit',
});
if (result.status === 0) {
process.nextTick(next);
} else {
process.exit(result.status);
}
};
next();

698
packages/iconify/package-lock.json generated Normal file
View File

@ -0,0 +1,698 @@
{
"name": "@iconify/iconify",
"version": "2.0.0-beta.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
"integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
"dev": true,
"requires": {
"@babel/highlight": "^7.8.3"
}
},
"@babel/highlight": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
"integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
"dev": true,
"requires": {
"chalk": "^2.0.0",
"esutils": "^2.0.2",
"js-tokens": "^4.0.0"
}
},
"@cyberalien/redundancy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@cyberalien/redundancy/-/redundancy-1.0.0.tgz",
"integrity": "sha512-/tx5GpGSyMn5FrOSggDSm9yJDLcEXiK0+zdCBssST6w9QgdJjoQ9lRBSxql/3vgQoI+7XbubWsP86jjbuVnFcA==",
"dev": true
},
"@microsoft/api-extractor": {
"version": "7.7.12",
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.7.12.tgz",
"integrity": "sha512-RYMG/dIZs7VXWUgx8Cwk73Czlr9qMUMolxwStFKowy3yMluzHlAKB2srV6csoWlSms6J75tLq6z1c0LXZksWxg==",
"dev": true,
"requires": {
"@microsoft/api-extractor-model": "7.7.10",
"@microsoft/tsdoc": "0.12.19",
"@rushstack/node-core-library": "3.19.6",
"@rushstack/ts-command-line": "4.3.13",
"colors": "~1.2.1",
"lodash": "~4.17.15",
"resolve": "1.8.1",
"source-map": "~0.6.1",
"typescript": "~3.7.2"
},
"dependencies": {
"typescript": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
"integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==",
"dev": true
}
}
},
"@microsoft/api-extractor-model": {
"version": "7.7.10",
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.7.10.tgz",
"integrity": "sha512-gMFDXwUgoQYz9TgatyNPALDdZN4xBC3Un3fGwlzME+vM13PoJ26pGuqI7kv/OlK9+q2sgrEdxWns8D3UnLf2TA==",
"dev": true,
"requires": {
"@microsoft/tsdoc": "0.12.19",
"@rushstack/node-core-library": "3.19.6"
}
},
"@microsoft/tsdoc": {
"version": "0.12.19",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.19.tgz",
"integrity": "sha512-IpgPxHrNxZiMNUSXqR1l/gePKPkfAmIKoDRP9hp7OwjU29ZR8WCJsOJ8iBKgw0Qk+pFwR+8Y1cy8ImLY6e9m4A==",
"dev": true
},
"@rollup/plugin-buble": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-buble/-/plugin-buble-0.21.1.tgz",
"integrity": "sha512-Tmd4V95cVyGTwh7qc9ZNkg53E/isFY4q/sqZK7mSyGajYp9Wb0gbJyZWAzYlg9kZxEHmwCDlvcHDcn56SpOCCQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.4",
"@types/buble": "^0.19.2",
"buble": "^0.19.8"
}
},
"@rollup/plugin-commonjs": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz",
"integrity": "sha512-MPYGZr0qdbV5zZj8/2AuomVpnRVXRU5XKXb3HVniwRoRCreGlf5kOE081isNWeiLIi6IYkwTX9zE0/c7V8g81g==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.0",
"estree-walker": "^1.0.1",
"is-reference": "^1.1.2",
"magic-string": "^0.25.2",
"resolve": "^1.11.0"
},
"dependencies": {
"resolve": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
"integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"@rollup/plugin-node-resolve": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.1.tgz",
"integrity": "sha512-14ddhD7TnemeHE97a4rLOhobfYvUVcaYuqTnL8Ti7Jxi9V9Jr5LY7Gko4HZ5k4h4vqQM0gBQt6tsp9xXW94WPA==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.6",
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.14.2"
},
"dependencies": {
"resolve": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
"integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"@rollup/plugin-replace": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.1.tgz",
"integrity": "sha512-qDcXj2VOa5+j0iudjb+LiwZHvBRRgWbHPhRmo1qde2KItTjuxDVQO21rp9/jOlzKR5YO0EsgRQoyox7fnL7y/A==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.4",
"magic-string": "^0.25.5"
}
},
"@rollup/pluginutils": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.8.tgz",
"integrity": "sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==",
"dev": true,
"requires": {
"estree-walker": "^1.0.1"
}
},
"@rushstack/node-core-library": {
"version": "3.19.6",
"resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.19.6.tgz",
"integrity": "sha512-1+FoymIdr9W9k0D8fdZBBPwi5YcMwh/RyESuL5bY29rLICFxSLOPK+ImVZ1OcWj9GEMsvDx5pNpJ311mHQk+MA==",
"dev": true,
"requires": {
"@types/node": "10.17.13",
"colors": "~1.2.1",
"fs-extra": "~7.0.1",
"jju": "~1.4.0",
"semver": "~5.3.0",
"timsort": "~0.3.0",
"z-schema": "~3.18.3"
}
},
"@rushstack/ts-command-line": {
"version": "4.3.13",
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.3.13.tgz",
"integrity": "sha512-BUBbjYu67NJGQkpHu8aYm7kDoMFizL1qx78+72XE3mX/vDdXYJzw/FWS7TPcMJmY4kNlYs979v2B0Q0qa2wRiw==",
"dev": true,
"requires": {
"@types/argparse": "1.0.33",
"argparse": "~1.0.9",
"colors": "~1.2.1"
}
},
"@types/argparse": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.33.tgz",
"integrity": "sha512-VQgHxyPMTj3hIlq9SY1mctqx+Jj8kpQfoLvDlVSDNOyuYs8JYfkuY3OW/4+dO657yPmNhHpePRx0/Tje5ImNVQ==",
"dev": true
},
"@types/buble": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@types/buble/-/buble-0.19.2.tgz",
"integrity": "sha512-uUD8zIfXMKThmFkahTXDGI3CthFH1kMg2dOm3KLi4GlC5cbARA64bEcUMbbWdWdE73eoc/iBB9PiTMqH0dNS2Q==",
"dev": true,
"requires": {
"magic-string": "^0.25.0"
}
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"@types/node": {
"version": "10.17.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz",
"integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==",
"dev": true
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"acorn": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true
},
"acorn-dynamic-import": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz",
"integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==",
"dev": true
},
"acorn-jsx": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
"integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
},
"buble": {
"version": "0.19.8",
"resolved": "https://registry.npmjs.org/buble/-/buble-0.19.8.tgz",
"integrity": "sha512-IoGZzrUTY5fKXVkgGHw3QeXFMUNBFv+9l8a4QJKG1JhG3nCMHTdEX1DCOg8568E2Q9qvAQIiSokv6Jsgx8p2cA==",
"dev": true,
"requires": {
"acorn": "^6.1.1",
"acorn-dynamic-import": "^4.0.0",
"acorn-jsx": "^5.0.1",
"chalk": "^2.4.2",
"magic-string": "^0.25.3",
"minimist": "^1.2.0",
"os-homedir": "^2.0.0",
"regexpu-core": "^4.5.4"
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"colors": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz",
"integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-reference": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz",
"integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==",
"dev": true,
"requires": {
"@types/estree": "0.0.39"
}
},
"jest-worker": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz",
"integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==",
"dev": true,
"requires": {
"merge-stream": "^2.0.0",
"supports-color": "^6.1.0"
},
"dependencies": {
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"jju": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
"integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
"dev": true
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"os-homedir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-2.0.0.tgz",
"integrity": "sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q==",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
"integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
"dev": true
},
"regenerate-unicode-properties": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz",
"integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==",
"dev": true,
"requires": {
"regenerate": "^1.4.0"
}
},
"regexpu-core": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz",
"integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==",
"dev": true,
"requires": {
"regenerate": "^1.4.0",
"regenerate-unicode-properties": "^8.2.0",
"regjsgen": "^0.5.1",
"regjsparser": "^0.6.4",
"unicode-match-property-ecmascript": "^1.0.4",
"unicode-match-property-value-ecmascript": "^1.2.0"
}
},
"regjsgen": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz",
"integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==",
"dev": true
},
"regjsparser": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz",
"integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==",
"dev": true,
"requires": {
"jsesc": "~0.5.0"
}
},
"resolve": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
"integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
"dev": true,
"requires": {
"path-parse": "^1.0.5"
}
},
"rollup": {
"version": "1.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz",
"integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==",
"dev": true,
"requires": {
"@types/estree": "*",
"@types/node": "*",
"acorn": "^7.1.0"
},
"dependencies": {
"acorn": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"dev": true
}
}
},
"rollup-plugin-terser": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.0.tgz",
"integrity": "sha512-XGMJihTIO3eIBsVGq7jiNYOdDMb3pVxuzY0uhOE/FM4x/u9nQgr3+McsjzqBn3QfHIpNSZmFnpoKAwHBEcsT7g==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.5.5",
"jest-worker": "^24.9.0",
"rollup-pluginutils": "^2.8.2",
"serialize-javascript": "^2.1.2",
"terser": "^4.6.2"
}
},
"rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
},
"dependencies": {
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
}
}
},
"semver": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true
},
"serialize-javascript": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
"integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"terser": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.6.tgz",
"integrity": "sha512-4lYPyeNmstjIIESr/ysHg2vUPRGf2tzF9z2yYwnowXVuVzLEamPN1Gfrz7f8I9uEPuHcbFlW4PLIAsJoxXyJ1g==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
}
},
"timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
"dev": true
},
"unicode-match-property-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
"dev": true,
"requires": {
"unicode-canonical-property-names-ecmascript": "^1.0.4",
"unicode-property-aliases-ecmascript": "^1.0.4"
}
},
"unicode-match-property-value-ecmascript": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz",
"integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==",
"dev": true
},
"unicode-property-aliases-ecmascript": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz",
"integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
"dev": true
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"validator": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz",
"integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==",
"dev": true
},
"z-schema": {
"version": "3.18.4",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz",
"integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==",
"dev": true,
"requires": {
"commander": "^2.7.1",
"lodash.get": "^4.0.0",
"lodash.isequal": "^4.0.0",
"validator": "^8.0.0"
}
}
}
}

View File

@ -0,0 +1,32 @@
{
"name": "@iconify/iconify",
"description": "Iconify common modules, used in multiple packages",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "2.0.0-beta.0",
"license": "(Apache-2.0 OR GPL-2.0)",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
"repository": {
"type": "git",
"url": "git://github.com/iconify/iconify.git"
},
"scripts": {
"build": "node build",
"build:lib": "tsc -b",
"build:dist": "rollup -c rollup.config.js",
"build:api": "api-extractor run --local --verbose"
},
"devDependencies": {
"@cyberalien/redundancy": "^1.0.0",
"@iconify/core": "^1.0.0-beta.0",
"@iconify/types": "^1.0.1",
"@microsoft/api-extractor": "^7.7.12",
"@rollup/plugin-buble": "^0.21.1",
"@rollup/plugin-commonjs": "^11.0.2",
"@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-replace": "^2.3.1",
"rollup": "^1.32.0",
"rollup-plugin-terser": "^5.2.0",
"typescript": "^3.7.4"
}
}

View File

@ -0,0 +1,69 @@
import { readFileSync } from 'fs';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import buble from '@rollup/plugin-buble';
import { terser } from 'rollup-plugin-terser';
import replace from '@rollup/plugin-replace';
const name = 'iconify';
const global = 'Iconify';
// Wrapper to export module as global and as ES module
const header = `/**
* (c) Vjacheslav Trushkin <cyberalien@gmail.com>
*
* For the full copyright and license information, please view the license.txt or license.gpl.txt
* files at https://github.com/iconify/iconify
*
* Licensed under Apache 2.0 or GPL 2.0 at your option.
* If derivative product is not compatible with one of licenses, you can pick one of licenses.
*
* @license Apache 2.0
* @license GPL 2.0
*/`;
const footer = `
// Export to window or web worker
try {
if (self.Iconify === void 0) {
self.Iconify = Iconify;
}
} catch (err) {
}
// Export as ES module
if (typeof exports === 'object') {
try {
exports.__esModule = true;
exports.default = Iconify;
} catch (err) {
}
}`;
// Get replacements
const replacements = {};
const packageJSON = JSON.parse(readFileSync('package.json', 'utf8'));
replacements['__iconify_version__'] = packageJSON.version;
// Export configuration
const config = [];
[false, true].forEach(compress => {
const item = {
input: `lib/${name}.js`,
output: [
{
file: `dist/${name}${compress ? '.min' : ''}.js`,
format: 'iife',
name: global,
banner: header,
footer,
},
],
plugins: [resolve(), commonjs(), replace(replacements), buble()],
};
if (compress) {
item.plugins.push(terser());
}
config.push(item);
});
export default config;

View File

@ -0,0 +1,42 @@
import { IconifyIconName } from '@iconify/core/lib/icon/name';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
import { IconifyFinder } from './interfaces/finder';
/**
* Icon status
*/
type IconStatus = 'missing' | 'loading' | 'loaded';
/**
* Data added to element to keep track of attribute changes
*/
export interface IconifyElementData {
name: IconifyIconName;
status: IconStatus;
customisations: IconifyIconCustomisations;
}
/**
* Extend Element type to allow TypeScript understand added properties
*/
interface IconifyElementStoredFinder {
iconifyFinder: IconifyFinder;
}
interface IconifyElementStoredData {
iconifyData: IconifyElementData;
}
export interface IconifyElement
extends HTMLElement,
IconifyElementStoredData,
IconifyElementStoredFinder {}
/**
* Names of properties to add to nodes
*/
export const elementFinderProperty: keyof IconifyElementStoredFinder = ('iconifyFinder' +
Date.now()) as keyof IconifyElementStoredFinder;
export const elementDataProperty: keyof IconifyElementStoredData = ('iconifyData' +
Date.now()) as keyof IconifyElementStoredData;

View File

@ -0,0 +1,148 @@
import {
elementFinderProperty,
IconifyElement,
elementDataProperty,
} from './element';
import {
IconifyIconName,
stringToIcon,
validateIcon,
} from '@iconify/core/lib/icon/name';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
import { IconifyFinder } from './interfaces/finder';
/**
* List of modules
*/
const finders: IconifyFinder[] = [];
/**
* Add module
*/
export function addFinder(finder: IconifyFinder): void {
if (finders.indexOf(finder) === -1) {
finders.push(finder);
}
}
/**
* Interface for found elements list
*/
export interface PlaceholderElement {
element: IconifyElement;
finder: IconifyFinder;
name: IconifyIconName;
customisations?: IconifyIconCustomisations;
}
/**
* Clean icon name: convert from string if needed and validate
*/
export function cleanIconName(
name: IconifyIconName | string | null
): IconifyIconName | null {
if (typeof name === 'string') {
name = stringToIcon(name);
}
return name === null || !validateIcon(name) ? null : name;
}
/**
* Compare customisations. Returns true if identical
*/
function compareCustomisations(
list1: IconifyIconCustomisations,
list2: IconifyIconCustomisations
): boolean {
const keys1 = Object.keys(list1) as (keyof IconifyIconCustomisations)[];
const keys2 = Object.keys(list2) as (keyof IconifyIconCustomisations)[];
if (keys1.length !== keys2.length) {
return false;
}
for (let i = 0; i < keys1.length; i++) {
const key = keys1[i];
if (list2[key] !== list1[key]) {
return false;
}
}
return true;
}
/**
* Find all placeholders
*/
export function findPlaceholders(root: HTMLElement): PlaceholderElement[] {
const results: PlaceholderElement[] = [];
finders.forEach(finder => {
const elements = finder.find(root);
Array.prototype.forEach.call(elements, item => {
const element = item as IconifyElement;
if (
element[elementFinderProperty] !== void 0 &&
element[elementFinderProperty] !== finder
) {
// Element is assigned to a different finder
return;
}
// Get icon name
const name = cleanIconName(finder.name(element));
if (name === null) {
// Invalid name - do not assign this finder to element
return;
}
// Assign finder to element and add it to results
element[elementFinderProperty] = finder;
const placeholder: PlaceholderElement = {
element,
finder,
name,
};
results.push(placeholder);
});
});
// Find all modified SVG
const elements = root.querySelectorAll('svg.iconify');
Array.prototype.forEach.call(elements, item => {
const element = item as IconifyElement;
const finder = element[elementFinderProperty];
const data = element[elementDataProperty];
if (!finder || !data) {
return;
}
// Get icon name
const name = cleanIconName(finder.name(element));
if (name === null) {
// Invalid name
return;
}
let updated = false;
let customisations;
if (name.prefix !== data.name.prefix || name.name !== data.name.name) {
updated = true;
} else {
customisations = finder.customisations(element);
if (!compareCustomisations(data.customisations, customisations)) {
updated = true;
}
}
// Add item to results
if (updated) {
const placeholder: PlaceholderElement = {
element,
finder,
name,
customisations,
};
results.push(placeholder);
}
});
return results;
}

View File

@ -0,0 +1,39 @@
import { IconifyFinder } from '../interfaces/finder';
import { IconifyElement } from '../element';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
import { finder as iconifyFinder } from './iconify';
const selector = 'iconify-icon';
const selectors = selector + ', i.' + selector + ', span.' + selector;
/**
* Export finder for:
* <iconify-icon />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selectors),
/**
* Get icon name from element
*/
name: iconifyFinder.name,
/**
* Get customisations list from element
*/
customisations: (node: IconifyElement): IconifyIconCustomisations => {
return iconifyFinder.customisations(node, {
inline: false,
});
},
/**
* Filter classes
*/
classFilter: iconifyFinder.classFilter,
};
export { finder };

View File

@ -0,0 +1,39 @@
import { IconifyFinder } from '../interfaces/finder';
import { IconifyElement } from '../element';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
import { finder as iconifyFinder } from './iconify-v1';
const selector = 'iconify-icon';
const selectors = selector + ', i.' + selector + ', span.' + selector;
/**
* Export finder for:
* <iconify-icon />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selectors),
/**
* Get icon name from element
*/
name: iconifyFinder.name,
/**
* Get customisations list from element
*/
customisations: (node: IconifyElement): IconifyIconCustomisations => {
return iconifyFinder.customisations(node, {
inline: false,
});
},
/**
* Filter classes
*/
classFilter: iconifyFinder.classFilter,
};
export { finder };

View File

@ -0,0 +1,161 @@
import { IconifyFinder } from '../interfaces/finder';
import { IconifyElement } from '../element';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
import { rotateFromString } from '@iconify/core/lib/customisations/rotate';
import {
flipFromString,
alignmentFromString,
} from '@iconify/core/lib/customisations/shorthand';
/**
* Check if attribute exists
*/
function hasAttribute(element: IconifyElement, key: string) {
return element.hasAttribute(key);
}
/**
* Get attribute value
*/
function getAttribute(element: IconifyElement, key: string): string {
return element.getAttribute(key) as string;
}
/**
* Get attribute value
*/
function getBooleanAttribute(
element: IconifyElement,
key: string
): boolean | null {
const value = element.getAttribute(key) as string;
if (value === key || value === 'true') {
return true;
}
if (value === '' || value === 'false') {
return false;
}
return null;
}
/**
* Boolean attributes
*/
const booleanAttributes: (keyof IconifyIconCustomisations)[] = [
'inline',
'hFlip',
'vFlip',
];
/**
* String attributes
*/
const stringAttributes: (keyof IconifyIconCustomisations)[] = [
'width',
'height',
];
/**
* Class names
*/
const mainClass = 'iconify';
/**
* Selector combining class names and tags
*/
const selector = 'i.' + mainClass + ', span.' + mainClass;
/**
* Export finder for:
* <span class="iconify" />
* <i class="iconify" />
* <span class="iconify-inline" />
* <i class="iconify-inline" />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selector),
/**
* Get icon name from element
*/
name: (element: IconifyElement): string | null => {
if (hasAttribute(element, 'data-icon')) {
return getAttribute(element, 'data-icon');
}
return null;
},
/**
* Get customisations list from element
*/
customisations: (
element: IconifyElement,
defaultValues: IconifyIconCustomisations = {
inline: true,
}
): IconifyIconCustomisations => {
const result: IconifyIconCustomisations = defaultValues;
// Rotation
if (hasAttribute(element, 'data-rotate')) {
const value = rotateFromString(
getAttribute(element, 'data-rotate')
);
if (value) {
result.rotate = value;
}
}
// Shorthand attributes
if (hasAttribute(element, 'data-flip')) {
flipFromString(result, getAttribute(element, 'data-flip'));
}
if (hasAttribute(element, 'data-align')) {
alignmentFromString(result, getAttribute(element, 'data-align'));
}
// Boolean attributes
booleanAttributes.forEach(attr => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getBooleanAttribute(element, 'data-' + attr);
if (typeof value === 'boolean') {
(result as Record<string, boolean>)[attr] = value;
}
}
});
// String attributes
stringAttributes.forEach(attr => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getAttribute(element, 'data-' + attr);
if (value !== '') {
(result as Record<string, string>)[attr] = value;
}
}
});
return result;
},
/**
* Filter classes
*/
classFilter: (classList: string[]): string[] => {
let result: string[] = [];
classList.forEach(className => {
if (
className !== 'iconify' &&
className !== '' &&
className.slice(0, 9) !== 'iconify--'
) {
result.push(className);
}
});
return result;
},
};
export { finder };

View File

@ -0,0 +1,177 @@
import { IconifyFinder } from '../interfaces/finder';
import { IconifyElement } from '../element';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
import { rotateFromString } from '@iconify/core/lib/customisations/rotate';
import {
flipFromString,
alignmentFromString,
} from '@iconify/core/lib/customisations/shorthand';
/**
* Check if attribute exists
*/
function hasAttribute(element: IconifyElement, key: string) {
return element.hasAttribute(key);
}
/**
* Get attribute value
*/
function getAttribute(element: IconifyElement, key: string): string {
return element.getAttribute(key) as string;
}
/**
* Get attribute value
*/
function getBooleanAttribute(
element: IconifyElement,
key: string
): boolean | null {
const value = element.getAttribute(key) as string;
if (value === key || value === 'true') {
return true;
}
if (value === '' || value === 'false') {
return false;
}
return null;
}
/**
* Boolean attributes
*/
const booleanAttributes: (keyof IconifyIconCustomisations)[] = [
'inline',
'hFlip',
'vFlip',
];
/**
* String attributes
*/
const stringAttributes: (keyof IconifyIconCustomisations)[] = [
'width',
'height',
];
/**
* Class names
*/
const mainClass = 'iconify';
const inlineClass = 'iconify-inline';
/**
* Selector combining class names and tags
*/
const selector =
'i.' +
mainClass +
', span.' +
mainClass +
', i.' +
inlineClass +
', span.' +
inlineClass;
/**
* Export finder for:
* <span class="iconify" />
* <i class="iconify" />
* <span class="iconify-inline" />
* <i class="iconify-inline" />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selector),
/**
* Get icon name from element
*/
name: (element: IconifyElement): string | null => {
if (hasAttribute(element, 'data-icon')) {
return getAttribute(element, 'data-icon');
}
return null;
},
/**
* Get customisations list from element
*/
customisations: (
element: IconifyElement,
defaultValues: IconifyIconCustomisations = {
inline: false,
}
): IconifyIconCustomisations => {
const result: IconifyIconCustomisations = defaultValues;
// Check class list for inline class
const className = element.getAttribute('class');
const classList = className ? className.split(/\s+/) : [];
if (classList.indexOf(inlineClass) !== -1) {
result.inline = true;
}
// Rotation
if (hasAttribute(element, 'data-rotate')) {
const value = rotateFromString(
getAttribute(element, 'data-rotate')
);
if (value) {
result.rotate = value;
}
}
// Shorthand attributes
if (hasAttribute(element, 'data-flip')) {
flipFromString(result, getAttribute(element, 'data-flip'));
}
if (hasAttribute(element, 'data-align')) {
alignmentFromString(result, getAttribute(element, 'data-align'));
}
// Boolean attributes
booleanAttributes.forEach(attr => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getBooleanAttribute(element, 'data-' + attr);
if (typeof value === 'boolean') {
(result as Record<string, boolean>)[attr] = value;
}
}
});
// String attributes
stringAttributes.forEach(attr => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getAttribute(element, 'data-' + attr);
if (value !== '') {
(result as Record<string, string>)[attr] = value;
}
}
});
return result;
},
/**
* Filter classes
*/
classFilter: (classList: string[]): string[] => {
let result: string[] = [];
classList.forEach(className => {
if (
className !== 'iconify' &&
className !== '' &&
className.slice(0, 9) !== 'iconify--'
) {
result.push(className);
}
});
return result;
},
};
export { finder };

View File

@ -0,0 +1,352 @@
// Core
import { IconifyJSON } from '@iconify/types';
import { merge } from '@iconify/core/lib/misc/merge';
import {
stringToIcon,
validateIcon,
IconifyIconName,
} from '@iconify/core/lib/icon/name';
import { IconifyIcon, FullIconifyIcon } from '@iconify/core/lib/icon';
import {
IconifyIconCustomisations,
fullCustomisations,
IconifyIconSize,
IconifyHorizontalIconAlignment,
IconifyVerticalIconAlignment,
} from '@iconify/core/lib/customisations';
import {
getStorage,
getIcon,
addIcon,
addIconSet,
listStoredPrefixes,
} from '@iconify/core/lib/storage';
import { iconToSVG, IconifyIconBuildResult } from '@iconify/core/lib/builder';
import { replaceIDs } from '@iconify/core/lib/builder/ids';
import { calcSize } from '@iconify/core/lib/builder/calc-size';
// Modules
import { coreModules } from '@iconify/core/lib/modules';
import { browserModules } from './modules';
// Finders
import { addFinder } from './finder';
import { finder as iconifyFinder } from './finders/iconify';
// import { finder as iconifyIconFinder } from './finders/iconify-icon';
// Cache
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 { setAPIConfig, IconifyAPIConfig } from '@iconify/core/lib/api/config';
import { prepareQuery, sendQuery } from './modules/api-jsonp';
// Observer
import { observer } from './modules/observer';
// Scan
import { scanDOM } from './scan';
/**
* Export required types
*/
// JSON stuff
export { IconifyIcon, IconifyJSON };
// Customisations
export {
IconifyIconCustomisations,
IconifyIconSize,
IconifyHorizontalIconAlignment,
IconifyVerticalIconAlignment,
};
// Build
export { IconifyIconBuildResult };
// API
export { IconifyAPIConfig };
/**
* Cache types
*/
export type IconifyCacheType = 'local' | 'session' | 'all';
/**
* Iconify interface
*/
export interface IconifyGlobal {
/* General section */
/**
* Get version
*/
getVersion: () => string;
/* Getting icons */
/**
* Check if icon exists
*/
iconExists: (name: string) => boolean;
/**
* Get icon data with all properties
*/
getIcon: (name: string) => IconifyIcon | null;
/**
* List all available icons
*/
listIcons: (prefix?: string) => string[];
/* Rendering icons */
/**
* Get icon data
*/
renderIcon: (
name: string,
customisations: IconifyIconCustomisations
) => IconifyIconBuildResult | null;
/**
* Replace IDs in icon body, should be used when parsing renderIcon() result
*/
replaceIDs: (body: string) => string;
/**
* Calculate width knowing height and width/height ratio (or vice versa)
*/
calculateSize: (
size: IconifyIconSize,
ratio: number,
precision?: number
) => IconifyIconSize;
/* Add icons */
/**
* Add icon to storage
*/
addIcon: (name: string, data: IconifyIcon) => boolean;
/**
* Add icon set to storage
*/
addCollection: (data: IconifyJSON) => boolean;
/* API stuff */
/**
* Pause DOM observer
*/
pauseObserver: () => void;
/**
* Resume DOM observer
*/
resumeObserver: () => void;
/**
* Set API configuration
*/
setAPIConfig: (
customConfig: Partial<IconifyAPIConfig>,
prefix?: string | string[]
) => void;
/* Scan DOM */
/**
* Scan DOM
*/
scanDOM: (root?: HTMLElement) => void;
/**
* Set root node
*/
setRoot: (root: HTMLElement) => void;
/**
* Toggle local and session storage
*/
enableCache: (storage: IconifyCacheType, value: boolean) => void;
}
/**
* Get icon name
*/
function getIconName(name: string): IconifyIconName | null {
const icon = stringToIcon(name);
if (!validateIcon(icon)) {
return null;
}
return icon;
}
/**
* Get icon data
*/
function getIconData(name: string): FullIconifyIcon | null {
const icon = getIconName(name);
return icon ? getIcon(getStorage(icon.prefix), icon.name) : null;
}
/**
* Get SVG data
*/
function getSVG(
name: string,
customisations: IconifyIconCustomisations
): IconifyIconBuildResult | null {
// Get icon data
const iconData = getIconData(name);
if (!iconData) {
return null;
}
// Clean up customisations
const changes = fullCustomisations(customisations);
// Get data
return iconToSVG(iconData, changes);
}
/**
* Global variable
*/
const Iconify: IconifyGlobal = {
// Version
getVersion: () => '__iconify_version__',
// Check if icon exists
iconExists: (name) => getIconData(name) !== void 0,
// Get raw icon data
getIcon: (name) => {
const result = getIconData(name);
return result ? merge(result) : null;
},
// List icons
listIcons: (prefix?: string) => {
let icons = [];
let prefixes = listStoredPrefixes();
let addPrefix = true;
if (typeof prefix === 'string') {
prefixes = prefixes.indexOf(prefix) !== -1 ? [] : [prefix];
addPrefix = false;
}
prefixes.forEach((prefix) => {
const storage = getStorage(prefix);
let icons = Object.keys(storage.icons);
if (addPrefix) {
icons = icons.map((name) => prefix + ':' + name);
}
icons = icons.concat(icons);
});
return icons;
},
// Render icon
renderIcon: getSVG,
// Replace IDs in body
replaceIDs: replaceIDs,
// Calculate size
calculateSize: calcSize,
// Add icon
addIcon: (name, data) => {
const icon = getIconName(name);
if (!icon) {
return false;
}
const storage = getStorage(icon.prefix);
return addIcon(storage, icon.name, data);
},
// Add icon set
addCollection: (data) => {
if (
typeof data !== 'object' ||
typeof data.prefix !== 'string' ||
!validateIcon({
prefix: data.prefix,
name: 'a',
})
) {
return false;
}
const storage = getStorage(data.prefix);
return !!addIconSet(storage, data);
},
// Pause observer
pauseObserver: observer.pause,
// Resume observer
resumeObserver: observer.resume,
// API configuration
setAPIConfig: setAPIConfig,
// Scan DOM
scanDOM: scanDOM,
// Set root node
setRoot: (root: HTMLElement) => {
browserModules.root = root;
// Restart observer
observer.init(scanDOM);
// Scan DOM on next tick
setTimeout(scanDOM);
},
// Allow storage
enableCache: (storage: IconifyCacheType, value: boolean) => {
switch (storage) {
case 'local':
case 'session':
config[storage] = value;
break;
case 'all':
for (const key in config) {
config[key] = value;
}
break;
}
},
};
/**
* Initialise stuff
*/
// Add finder modules
// addFinder(iconifyIconFinder);
addFinder(iconifyFinder);
// Set cache and load existing cache
coreModules.cache = storeCache;
loadCache();
// Set API
setAPIModule({
send: sendQuery,
prepare: prepareQuery,
});
coreModules.api = API;
// Load observer
browserModules.observer = observer;
setTimeout(() => {
// Init on next tick when entire document has been parsed
observer.init(scanDOM);
});
export default Iconify;

View File

@ -0,0 +1,41 @@
import { IconifyElement } from '../element';
import { IconifyIconName } from '@iconify/core/lib/icon/name';
import { IconifyIconCustomisations } from '@iconify/core/lib/customisations';
/**
* find - find elements that match plugin within root element
*/
export type IconifyFinderFind = (root: HTMLElement) => NodeList;
/**
* name - get icon name from element
*/
export type IconifyFinderName = (
element: IconifyElement
) => IconifyIconName | string | null;
/**
* customisations - get icon customisations
*/
export type IconifyFinderCustomisations = (
element: IconifyElement,
defaultVaues?: IconifyIconCustomisations
) => IconifyIconCustomisations;
/**
* classes - filter class list
*/
export type IconifyFinderClassFilter = (
// Classes to filter
classList: string[]
) => string[];
/**
* Interface for finder module
*/
export interface IconifyFinder {
find: IconifyFinderFind;
name: IconifyFinderName;
customisations: IconifyFinderCustomisations;
classFilter: IconifyFinderClassFilter;
}

View File

@ -0,0 +1,22 @@
/**
* Observer callback function
*/
export type ObserverCallback = (root: HTMLElement) => void;
/**
* Observer functions
*/
type InitObserver = (callback: ObserverCallback) => void;
type PauseObserver = () => void;
type ResumeObserver = () => void;
type IsObserverPaused = () => boolean;
/**
* Observer functions
*/
export interface Observer {
init: InitObserver;
pause: PauseObserver;
resume: ResumeObserver;
isPaused: IsObserverPaused;
}

View File

@ -0,0 +1,25 @@
import { Observer } from './interfaces/observer';
/**
* Dynamic modules.
*
* Also see modules.ts in core package.
*/
interface Modules {
// Root element
root?: HTMLElement;
// Observer module
observer?: Observer;
}
export const browserModules: Modules = {};
/**
* Get root element
*/
export function getRoot(): HTMLElement {
return browserModules.root
? browserModules.root
: (document.querySelector('body') as HTMLElement);
}

View File

@ -0,0 +1,212 @@
import { RedundancyPendingItem } from '@cyberalien/redundancy';
import {
APIQueryParams,
IconifyAPIPrepareQuery,
IconifyAPISendQuery,
} from '@iconify/core/lib/api/modules';
import { getAPIConfig } from '@iconify/core/lib/api/config';
/**
* Global
*/
type Callback = (data: unknown) => void;
type JSONPRoot = Record<string, Callback>;
let global: JSONPRoot | null = null;
/**
* Endpoint
*/
let endPoint = '{prefix}.js?icons={icons}&callback={callback}';
/**
* Cache
*/
const maxLengthCache: Record<string, number> = Object.create(null);
const pathCache: Record<string, string> = Object.create(null);
/**
* Get hash for query
*
* Hash is used in JSONP callback name, so same queries end up with same JSONP callback,
* allowing response to be cached in browser.
*/
function hash(str: string): number {
let total = 0,
i;
for (i = str.length - 1; i >= 0; i--) {
total += str.charCodeAt(i);
}
return total % 999;
}
/**
* Get root object
*/
function getGlobal(): JSONPRoot {
// Create root
if (global === null) {
// window
const globalRoot = (self as unknown) as Record<string, unknown>;
// Test for window.Iconify. If missing, create 'IconifyJSONP'
let prefix = 'Iconify';
let extraPrefix = '.cb';
if (globalRoot[prefix] === void 0) {
// Use 'IconifyJSONP' global
prefix = 'IconifyJSONP';
extraPrefix = '';
if (globalRoot[prefix] === void 0) {
globalRoot[prefix] = Object.create(null);
}
global = globalRoot[prefix] as JSONPRoot;
} else {
// Use 'Iconify.cb'
const iconifyRoot = globalRoot[prefix] as Record<string, JSONPRoot>;
if (iconifyRoot.cb === void 0) {
iconifyRoot.cb = Object.create(null);
}
global = iconifyRoot.cb;
}
// Change end point
endPoint = endPoint.replace(
'{callback}',
prefix + extraPrefix + '.{cb}'
);
}
return global;
}
/**
* Calculate maximum icons list length for prefix
*/
function calculateMaxLength(prefix: string): number {
// Get config and store path
const config = getAPIConfig(prefix);
if (!config) {
return 0;
}
// Calculate
let result;
if (!config.maxURL) {
result = 0;
} else {
let maxHostLength = 0;
config.resources.forEach((host) => {
maxHostLength = Math.max(maxHostLength, host.length);
});
// Make sure global is set
getGlobal();
// Extra width: prefix (3) + counter (4) - '{cb}' (4)
const extraLength = 3;
// Get available length
result =
config.maxURL -
maxHostLength -
config.path.length -
endPoint.replace('{prefix}', prefix).replace('{icons}', '').length -
extraLength;
}
// Cache stuff and return result
pathCache[prefix] = config.path;
maxLengthCache[prefix] = result;
return result;
}
/**
* Prepare params
*/
export const prepareQuery: IconifyAPIPrepareQuery = (
prefix: string,
icons: string[]
): APIQueryParams[] => {
const results: APIQueryParams[] = [];
// Get maximum icons list length
let maxLength = maxLengthCache[prefix];
if (maxLength === void 0) {
maxLength = calculateMaxLength(prefix);
}
// Split icons
let item: APIQueryParams = {
prefix,
icons: [],
};
let length = 0;
icons.forEach((name, index) => {
length += name.length + 1;
if (length >= maxLength && index > 0) {
// Next set
results.push(item);
item = {
prefix,
icons: [],
};
length = name.length;
}
item.icons.push(name);
});
results.push(item);
return results;
};
/**
* Load icons
*/
export const sendQuery: IconifyAPISendQuery = (
host: string,
params: APIQueryParams,
status: RedundancyPendingItem
): void => {
const prefix = params.prefix;
const icons = params.icons;
const iconsList = icons.join(',');
// Create callback prefix
const cbPrefix = prefix.split('-').shift().slice(0, 3);
const global = getGlobal();
// Callback hash
let cbCounter = hash(host + ':' + prefix + ':' + iconsList);
while (global[cbPrefix + cbCounter] !== void 0) {
cbCounter++;
}
const callbackName = cbPrefix + cbCounter;
let path =
pathCache[prefix] +
endPoint
.replace('{prefix}', prefix)
.replace('{icons}', iconsList)
.replace('{cb}', callbackName);
global[callbackName] = (data: unknown): void => {
// Remove callback and complete query
delete global[callbackName];
status.done(data);
};
// Create URI
const uri = host + path;
// console.log('API query:', uri);
// Create script and append it to head
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = uri;
document.head.appendChild(script);
};

View File

@ -0,0 +1,177 @@
import { elementFinderProperty, IconifyElement } from '../element';
import { ObserverCallback, Observer } from '../interfaces/observer';
import { getRoot } from '../modules';
/**
* MutationObserver instance, null until DOM is ready
*/
let instance: MutationObserver | null = null;
/**
* Callback
*/
let callback: ObserverCallback | null = null;
/**
* Parameters for mutation observer
*/
const observerParams: MutationObserverInit = {
childList: true,
subtree: true,
attributes: true,
};
/**
* Pause. Number instead of boolean to allow multiple pause/resume calls. Observer is resumed only when pause reaches 0
*/
let paused = 0;
/**
* Scan is pending when observer is resumed
*/
let scanPending = false;
/**
* Scan is already queued
*/
let scanQueued = false;
/**
* Queue DOM scan
*/
function queueScan(): void {
if (!scanQueued) {
scanQueued = true;
setTimeout(() => {
scanQueued = false;
scanPending = false;
if (callback) {
callback(getRoot());
}
});
}
}
/**
* Check mutations for added nodes
*/
function checkMutations(mutations: MutationRecord[]): void {
if (!scanPending) {
for (let i = 0; i < mutations.length; i++) {
const item = mutations[i];
if (
// Check for added nodes
(item.addedNodes && item.addedNodes.length > 0) ||
// Check for icon or placeholder with modified attributes
(item.type === 'attributes' &&
(item.target as IconifyElement)[elementFinderProperty] !==
void 0)
) {
scanPending = true;
if (!paused) {
queueScan();
}
return;
}
}
}
}
/**
* Start/resume observer
*/
function observe(): void {
if (instance) {
instance.observe(getRoot(), observerParams);
}
}
/**
* Start mutation observer
*/
function startObserver(): void {
if (instance !== null) {
return;
}
scanPending = true;
instance = new MutationObserver(checkMutations);
observe();
if (!paused) {
queueScan();
}
}
// Fake interface to test old IE properties
interface OldIEElement extends HTMLElement {
doScroll?: boolean;
}
/**
* Export module
*/
export const observer: Observer = {
/**
* Start observer when DOM is ready
*/
init: (cb: ObserverCallback): void => {
callback = cb;
if (instance && !paused) {
// Restart observer
instance.disconnect();
observe();
return;
}
setTimeout(() => {
const doc = document;
if (
doc.readyState === 'complete' ||
(doc.readyState !== 'loading' &&
!(doc.documentElement as OldIEElement).doScroll)
) {
startObserver();
} else {
doc.addEventListener('DOMContentLoaded', startObserver);
window.addEventListener('load', startObserver);
}
});
},
/**
* Pause observer
*/
pause: (): void => {
paused++;
if (paused > 1 || instance === null) {
return;
}
// Check pending records, stop observer
checkMutations(instance.takeRecords());
instance.disconnect();
},
/**
* Resume observer
*/
resume: (): void => {
if (!paused) {
return;
}
paused--;
if (!paused && instance) {
observe();
if (scanPending) {
queueScan();
}
}
},
/**
* Check if observer is paused
*/
isPaused: (): boolean => paused > 0,
};

View File

@ -0,0 +1,109 @@
import { PlaceholderElement } from './finder';
import { FullIconifyIcon } from '@iconify/core/lib/icon';
import {
IconifyIconCustomisations,
fullCustomisations,
} from '@iconify/core/lib/customisations';
import { iconToSVG } from '@iconify/core/lib/builder';
import { replaceIDs } from '@iconify/core/lib/builder/ids';
import {
IconifyElement,
IconifyElementData,
elementDataProperty,
elementFinderProperty,
} from './element';
/**
* Replace element with SVG
*/
export function renderIcon(
placeholder: PlaceholderElement,
customisations: IconifyIconCustomisations,
iconData: FullIconifyIcon
): IconifyElement | null {
const data = iconToSVG(iconData, fullCustomisations(customisations));
// Get class name
const placeholderElement = placeholder.element;
const placeholderClassName = placeholderElement.getAttribute('class');
const filteredClassList = placeholder.finder.classFilter(
placeholderClassName ? placeholderClassName.split(/\s+/) : []
);
const className =
'iconify iconify--' +
placeholder.name.prefix +
(filteredClassList.length ? ' ' + filteredClassList.join(' ') : '');
// Generate SVG as string
const html =
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" class="' +
className +
'">' +
replaceIDs(data.body) +
'</svg>';
// Create placeholder. Why placeholder? IE11 doesn't support innerHTML method on SVG.
const span = document.createElement('span');
span.innerHTML = html;
// Get SVG element
const svg = span.childNodes[0] as IconifyElement;
const svgStyle = svg.style;
// Add attributes
const svgAttributes = data.attributes as Record<string, string>;
Object.keys(svgAttributes).forEach((attr) => {
svg.setAttribute(attr, svgAttributes[attr]);
});
// Add custom styles
svgStyle.transform = 'rotate(360deg)';
if (data.inline) {
svgStyle.verticalAlign = '-0.125em';
}
// Copy attributes from placeholder
const placeholderAttributes = placeholderElement.attributes;
for (let i = 0; i < placeholderAttributes.length; i++) {
const item = placeholderAttributes.item(i);
if (item) {
const name = item.name;
if (
name !== 'class' &&
name !== 'style' &&
svgAttributes[name] === void 0
) {
try {
svg.setAttribute(name, item.value);
} catch (err) {}
}
}
}
// Copy styles from placeholder
const placeholderStyle = placeholderElement.style;
for (let i = 0; i < placeholderStyle.length; i++) {
const attr = placeholderStyle[i];
svgStyle[attr] = placeholderStyle[attr];
}
// Store data
const elementData: IconifyElementData = {
name: placeholder.name,
status: 'loaded',
customisations: customisations,
};
svg[elementDataProperty] = elementData;
svg[elementFinderProperty] = placeholder.finder;
// Replace placeholder
if (placeholderElement.parentNode) {
placeholderElement.parentNode.replaceChild(svg, placeholderElement);
} else {
// Placeholder has no parent? Remove SVG parent as well
span.removeChild(svg);
}
// Return new node
return svg;
}

View File

@ -0,0 +1,165 @@
import { IconifyIconName } from '@iconify/core/lib/icon/name';
import { getStorage, getIcon } from '@iconify/core/lib/storage';
import { coreModules } from '@iconify/core/lib/modules';
import { FullIconifyIcon } from '@iconify/core/lib/icon';
import { findPlaceholders } from './finder';
import { browserModules, getRoot } from './modules';
import { IconifyElementData, elementDataProperty } from './element';
import { renderIcon } from './render';
/**
* Flag to avoid scanning DOM too often
*/
let scanQueued = false;
/**
* Icons have been loaded
*/
function checkPendingIcons(): void {
if (!scanQueued) {
scanQueued = true;
setTimeout(() => {
if (scanQueued) {
scanQueued = false;
scanDOM();
}
});
}
}
/**
* Compare Icon objects. Returns true if icons are identical.
*
* Note: null means icon is invalid, so null to null comparison = false.
*/
const compareIcons = (
icon1: IconifyIconName | null,
icon2: IconifyIconName | null
): boolean => {
return (
icon1 !== null &&
icon2 !== null &&
icon1.name === icon2.name &&
icon1.prefix === icon2.prefix
);
};
/**
* Scan DOM for placeholders
*/
export function scanDOM(root?: HTMLElement): void {
scanQueued = false;
// Observer
let paused = false;
// List of icons to load
const loadIcons: Record<string, Record<string, boolean>> = Object.create(
null
);
// Get root node and placeholders
if (!root) {
root = getRoot();
}
findPlaceholders(root).forEach(item => {
const element = item.element;
const iconName = item.name;
const prefix = iconName.prefix;
const name = iconName.name;
let data: IconifyElementData = element[elementDataProperty];
// Icon has not been updated since last scan
if (data !== void 0 && compareIcons(data.name, iconName)) {
// Icon name was not changed and data is set - quickly return if icon is missing or still loading
switch (data.status) {
case 'missing':
return;
case 'loading':
if (
coreModules.api &&
coreModules.api.isPending(prefix, name)
) {
// Pending
return;
}
}
}
// Check icon
const storage = getStorage(prefix);
if (storage.icons[name] !== void 0) {
// Icon exists - replace placeholder
if (browserModules.observer && !paused) {
browserModules.observer.pause();
paused = true;
}
// Get customisations
const customisations =
item.customisations !== void 0
? item.customisations
: item.finder.customisations(element);
// Render icon
renderIcon(
item,
customisations,
getIcon(storage, name) as FullIconifyIcon
);
return;
}
if (storage.missing[name]) {
// Mark as missing
data = {
name: iconName,
status: 'missing',
customisations: {},
};
element[elementDataProperty] = data;
return;
}
if (coreModules.api) {
if (!coreModules.api.isPending(prefix, name)) {
// Add icon to loading queue
if (loadIcons[prefix] === void 0) {
loadIcons[prefix] = Object.create(null);
}
loadIcons[prefix][name] = true;
}
}
// Mark as loading
data = {
name: iconName,
status: 'loading',
customisations: {},
};
element[elementDataProperty] = data;
});
// 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
);
});
}
if (browserModules.observer && paused) {
browserModules.observer.resume();
}
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"target": "ES2019",
"module": "ESNext",
"declaration": true,
"sourceMap": false,
"strict": false,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}

23
packages/react-demo/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

14882
packages/react-demo/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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