720 lines
19 KiB
JavaScript
720 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const defaults = require('@iconify/utils/lib/icon/defaults');
|
|
const build = require('@iconify/utils/lib/svg/build');
|
|
const minify = require('@iconify/utils/lib/icon-set/minify');
|
|
const convertInfo = require('@iconify/utils/lib/icon-set/convert-info');
|
|
const iconSet_props = require('./props.cjs');
|
|
const svg_index = require('../svg/index.cjs');
|
|
const utils = require('@iconify/utils');
|
|
require('@iconify/utils/lib/misc/objects');
|
|
require('cheerio');
|
|
|
|
var __defProp = Object.defineProperty;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
var __publicField = (obj, key, value) => {
|
|
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
return value;
|
|
};
|
|
const themeKeys = ["prefixes", "suffixes"];
|
|
function sortThemeKeys(keys) {
|
|
return keys.sort(
|
|
(a, b) => a.length === b.length ? a.localeCompare(b) : b.length - a.length
|
|
);
|
|
}
|
|
class IconSet {
|
|
/**
|
|
* Load icon set
|
|
*/
|
|
constructor(data) {
|
|
/**
|
|
* Properties. You can write directly to almost any property, but avoid writing to
|
|
* 'entries' and 'categories' properties, there are functions for that.
|
|
*/
|
|
// Icon set prefix
|
|
__publicField(this, "prefix");
|
|
// Last modification time
|
|
__publicField(this, "lastModified");
|
|
// All icons
|
|
__publicField(this, "entries");
|
|
// Information block
|
|
__publicField(this, "info");
|
|
// Categories, using custom type. Use functions to update data
|
|
__publicField(this, "categories");
|
|
__publicField(this, "prefixes");
|
|
__publicField(this, "suffixes");
|
|
this.load(data);
|
|
}
|
|
/**
|
|
* Load icon set
|
|
*/
|
|
load(data) {
|
|
this.prefix = data.prefix;
|
|
const defaultProps = iconSet_props.filterProps(data, defaults.defaultIconDimensions, true);
|
|
this.entries = /* @__PURE__ */ Object.create(null);
|
|
const entries = this.entries;
|
|
for (const name in data.icons) {
|
|
const item = data.icons[name];
|
|
const entry = {
|
|
type: "icon",
|
|
body: item.body,
|
|
props: iconSet_props.filterProps(
|
|
{
|
|
...defaultProps,
|
|
...item
|
|
},
|
|
iconSet_props.defaultCommonProps,
|
|
true
|
|
),
|
|
chars: /* @__PURE__ */ new Set(),
|
|
categories: /* @__PURE__ */ new Set()
|
|
};
|
|
entries[name] = entry;
|
|
}
|
|
if (data.aliases) {
|
|
for (const name in data.aliases) {
|
|
if (entries[name]) {
|
|
continue;
|
|
}
|
|
const item = data.aliases[name];
|
|
const parent = item.parent;
|
|
const props = iconSet_props.filterProps(item, iconSet_props.defaultCommonProps, false);
|
|
const chars = /* @__PURE__ */ new Set();
|
|
if (Object.keys(props).length) {
|
|
const entry = {
|
|
type: "variation",
|
|
parent,
|
|
props,
|
|
chars
|
|
};
|
|
entries[name] = entry;
|
|
} else {
|
|
const entry = {
|
|
type: "alias",
|
|
parent,
|
|
chars
|
|
};
|
|
entries[name] = entry;
|
|
}
|
|
}
|
|
}
|
|
const info = data.info && convertInfo.convertIconSetInfo(data.info);
|
|
this.info = info || void 0;
|
|
if (data.chars) {
|
|
for (const char in data.chars) {
|
|
const name = data.chars[char];
|
|
const icon = entries[name];
|
|
if (icon) {
|
|
icon.chars.add(char);
|
|
}
|
|
}
|
|
}
|
|
this.categories = /* @__PURE__ */ new Set();
|
|
if (data.categories) {
|
|
for (const category in data.categories) {
|
|
const item = {
|
|
title: category,
|
|
count: 0
|
|
};
|
|
data.categories[category].forEach((iconName) => {
|
|
const icon = entries[iconName];
|
|
switch (icon?.type) {
|
|
case "icon":
|
|
icon.categories.add(item);
|
|
}
|
|
});
|
|
this.categories.add(item);
|
|
this.listCategory(item);
|
|
}
|
|
}
|
|
const prefixes = this.prefixes = /* @__PURE__ */ Object.create(null);
|
|
const suffixes = this.suffixes = /* @__PURE__ */ Object.create(null);
|
|
if (data.themes) {
|
|
for (const key in data.themes) {
|
|
const item = data.themes[key];
|
|
if (typeof item.prefix === "string") {
|
|
const prefix = item.prefix;
|
|
if (prefix.slice(-1) === "-") {
|
|
prefixes[prefix.slice(0, -1)] = item.title;
|
|
}
|
|
}
|
|
if (typeof item.suffix === "string") {
|
|
const suffix = item.suffix;
|
|
if (suffix.slice(0, 1) === "-") {
|
|
suffixes[suffix.slice(1)] = item.title;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
themeKeys.forEach((prop) => {
|
|
const items = data[prop];
|
|
if (items) {
|
|
this[prop] = /* @__PURE__ */ Object.create(null);
|
|
for (const key in items) {
|
|
this[prop][key] = items[key];
|
|
}
|
|
}
|
|
});
|
|
this.lastModified = data.lastModified || 0;
|
|
}
|
|
/**
|
|
* Update last modification time
|
|
*/
|
|
updateLastModified(value) {
|
|
this.lastModified = value || Math.floor(Date.now() / 1e3);
|
|
}
|
|
/**
|
|
* List icons
|
|
*/
|
|
list(types = ["icon", "variation"]) {
|
|
return Object.keys(this.entries).filter(
|
|
(name) => types.includes(this.entries[name].type)
|
|
);
|
|
}
|
|
/**
|
|
* forEach function to loop through all entries.
|
|
* Supports asynchronous callbacks.
|
|
*
|
|
* Callback should return false to stop loop.
|
|
*/
|
|
async forEach(callback, types = ["icon", "variation", "alias"]) {
|
|
const names = this.list(types);
|
|
for (let i = 0; i < names.length; i++) {
|
|
const name = names[i];
|
|
const item = this.entries[name];
|
|
if (item) {
|
|
let result = callback(name, item.type);
|
|
if (result instanceof Promise) {
|
|
result = await result;
|
|
}
|
|
if (result === false) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Synchronous version of forEach function to loop through all entries.
|
|
*
|
|
* Callback should return false to stop loop.
|
|
*/
|
|
forEachSync(callback, types = ["icon", "variation", "alias"]) {
|
|
const names = this.list(types);
|
|
for (let i = 0; i < names.length; i++) {
|
|
const name = names[i];
|
|
const item = this.entries[name];
|
|
if (item) {
|
|
const result = callback(name, item.type);
|
|
if (result === false) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get parent icons tree
|
|
*
|
|
* Returns parent icons list for each icon, null if failed to resolve.
|
|
* In parent icons list, first element is a direct parent, last is icon. Does not include item.
|
|
*
|
|
* Examples:
|
|
* 'alias3': ['alias2', 'alias1', 'icon']
|
|
* 'icon': []
|
|
* 'bad-icon': null
|
|
*/
|
|
getTree(names) {
|
|
const entries = this.entries;
|
|
const resolved = /* @__PURE__ */ Object.create(null);
|
|
function resolve(name) {
|
|
const item = entries[name];
|
|
if (!item) {
|
|
return resolved[name] = null;
|
|
}
|
|
if (item.type === "icon") {
|
|
return resolved[name] = [];
|
|
}
|
|
if (resolved[name] === void 0) {
|
|
resolved[name] = null;
|
|
const parent = item.parent;
|
|
const value = parent && resolve(parent);
|
|
if (value) {
|
|
resolved[name] = [parent].concat(value);
|
|
}
|
|
}
|
|
return resolved[name];
|
|
}
|
|
(names || Object.keys(entries)).forEach(resolve);
|
|
return resolved;
|
|
}
|
|
resolve(name, full = false) {
|
|
const entries = this.entries;
|
|
const item = entries[name];
|
|
const tree = item && (item.type === "icon" ? [] : this.getTree([name])[name]);
|
|
if (!tree) {
|
|
return null;
|
|
}
|
|
let result = {};
|
|
function parse(name2) {
|
|
const item2 = entries[name2];
|
|
if (item2.type === "alias") {
|
|
return;
|
|
}
|
|
result = utils.mergeIconData(item2.props, result);
|
|
if (item2.type === "icon") {
|
|
result.body = item2.body;
|
|
}
|
|
}
|
|
parse(name);
|
|
tree.forEach(parse);
|
|
return result && full ? { ...defaults.defaultIconProps, ...result } : result;
|
|
}
|
|
/**
|
|
* Generate HTML
|
|
*/
|
|
toString(name, customisations = {
|
|
width: "auto",
|
|
height: "auto"
|
|
}) {
|
|
const item = this.resolve(name);
|
|
if (!item) {
|
|
return null;
|
|
}
|
|
const result = build.iconToSVG(item, customisations);
|
|
const attributes = Object.keys(result.attributes).map(
|
|
(key) => ` ${key}="${result.attributes[key]}"`
|
|
).join("");
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"${attributes}>${result.body}</svg>`;
|
|
}
|
|
/**
|
|
* Get SVG instance for icon
|
|
*/
|
|
toSVG(name) {
|
|
const html = this.toString(name);
|
|
return html ? new svg_index.SVG(html) : null;
|
|
}
|
|
/**
|
|
* Export icon set
|
|
*/
|
|
export(validate = true) {
|
|
const icons = /* @__PURE__ */ Object.create(null);
|
|
const aliases = /* @__PURE__ */ Object.create(null);
|
|
const tree = validate ? this.getTree() : /* @__PURE__ */ Object.create(null);
|
|
const names = Object.keys(this.entries);
|
|
names.sort((a, b) => a.localeCompare(b));
|
|
names.forEach((name) => {
|
|
const item = this.entries[name];
|
|
switch (item.type) {
|
|
case "icon": {
|
|
const icon = {
|
|
body: item.body,
|
|
...item.props
|
|
};
|
|
icons[name] = icon;
|
|
break;
|
|
}
|
|
case "alias":
|
|
case "variation": {
|
|
if (validate && !tree[name]) {
|
|
break;
|
|
}
|
|
const props = item.type === "variation" ? item.props : {};
|
|
const alias = {
|
|
parent: item.parent,
|
|
...props
|
|
};
|
|
aliases[name] = alias;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
let info;
|
|
if (this.info) {
|
|
this.info.total = this.count();
|
|
info = JSON.parse(JSON.stringify(this.info));
|
|
}
|
|
const result = {
|
|
prefix: this.prefix
|
|
};
|
|
if (info) {
|
|
result.info = info;
|
|
}
|
|
if (this.lastModified) {
|
|
result.lastModified = this.lastModified;
|
|
}
|
|
result.icons = icons;
|
|
if (Object.keys(aliases).length) {
|
|
result.aliases = aliases;
|
|
}
|
|
const chars = this.chars(
|
|
Object.keys(icons).concat(Object.keys(aliases))
|
|
);
|
|
if (Object.keys(chars).length) {
|
|
result.chars = chars;
|
|
}
|
|
const categories = /* @__PURE__ */ Object.create(null);
|
|
Array.from(this.categories).sort((a, b) => a.title.localeCompare(b.title)).forEach((item) => {
|
|
const names2 = this.listCategory(item);
|
|
if (names2) {
|
|
names2.sort((a, b) => a.localeCompare(b));
|
|
categories[item.title] = names2;
|
|
}
|
|
});
|
|
if (Object.keys(categories).length) {
|
|
result.categories = categories;
|
|
}
|
|
themeKeys.forEach((prop) => {
|
|
const items = this[prop];
|
|
const keys = Object.keys(items);
|
|
if (keys.length) {
|
|
const sortedTheme = /* @__PURE__ */ Object.create(null);
|
|
const tested = this.checkTheme(prop === "prefixes");
|
|
keys.forEach((key) => {
|
|
if (tested.valid[key].length) {
|
|
sortedTheme[key] = items[key];
|
|
}
|
|
});
|
|
if (Object.keys(sortedTheme).length) {
|
|
result[prop] = sortedTheme;
|
|
}
|
|
}
|
|
});
|
|
minify.minifyIconSet(result);
|
|
return result;
|
|
}
|
|
/**
|
|
* Get characters map
|
|
*/
|
|
chars(names) {
|
|
const chars = /* @__PURE__ */ Object.create(null);
|
|
if (!names) {
|
|
names = Object.keys(this.entries);
|
|
}
|
|
for (let i = 0; i < names.length; i++) {
|
|
const name = names[i];
|
|
const item = this.entries[name];
|
|
item.chars.forEach((char) => {
|
|
chars[char] = name;
|
|
});
|
|
}
|
|
return chars;
|
|
}
|
|
/**
|
|
* Filter icons
|
|
*/
|
|
_filter(callback) {
|
|
const names = [];
|
|
for (const key in this.entries) {
|
|
const item = this.entries[key];
|
|
switch (item.type) {
|
|
case "icon": {
|
|
if (callback(key, item)) {
|
|
names.push(key);
|
|
}
|
|
break;
|
|
}
|
|
case "variation":
|
|
case "alias": {
|
|
const icon = this.resolve(key);
|
|
if (icon && callback(key, item, icon)) {
|
|
names.push(key);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
/**
|
|
* Count icons
|
|
*/
|
|
count() {
|
|
return this._filter((_key, item, icon) => {
|
|
if (item.type === "alias" || item.props.hidden || icon?.hidden) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}).length;
|
|
}
|
|
/**
|
|
* Find category by title
|
|
*/
|
|
findCategory(title, add) {
|
|
const categoryItem = Array.from(this.categories).find(
|
|
(item) => item.title === title
|
|
);
|
|
if (categoryItem) {
|
|
return categoryItem;
|
|
}
|
|
if (add) {
|
|
const newItem = {
|
|
title,
|
|
count: 0
|
|
};
|
|
this.categories.add(newItem);
|
|
return newItem;
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Count icons in category, remove category if empty
|
|
*
|
|
* Hidden icons and aliases do not count
|
|
*/
|
|
listCategory(category) {
|
|
const categoryItem = typeof category === "string" ? this.findCategory(category, false) : category;
|
|
if (!categoryItem) {
|
|
return null;
|
|
}
|
|
const icons = this._filter((_key, item) => {
|
|
if (item.type !== "icon" || item.props.hidden) {
|
|
return false;
|
|
}
|
|
return item.categories.has(categoryItem);
|
|
});
|
|
const count = icons.length;
|
|
categoryItem.count = count;
|
|
if (!count) {
|
|
this.categories.delete(categoryItem);
|
|
return null;
|
|
}
|
|
return icons;
|
|
}
|
|
/**
|
|
* Check if icon exists
|
|
*/
|
|
exists(name) {
|
|
return !!this.entries[name];
|
|
}
|
|
/**
|
|
* Remove icons. Returns number of removed icons
|
|
*
|
|
* If removeDependencies is a string, it represents new parent for all aliases of removed icon. New parent cannot be alias or variation.
|
|
*/
|
|
remove(name, removeDependencies = true) {
|
|
const entries = this.entries;
|
|
if (typeof removeDependencies === "string") {
|
|
const item2 = entries[removeDependencies];
|
|
if (name === removeDependencies || item2?.type !== "icon") {
|
|
return 0;
|
|
}
|
|
}
|
|
const item = entries[name];
|
|
if (!item) {
|
|
return 0;
|
|
}
|
|
this.updateLastModified();
|
|
if (typeof removeDependencies === "string") {
|
|
for (const key in entries) {
|
|
const item2 = entries[key];
|
|
if (item2.type !== "icon" && item2.parent === name) {
|
|
item2.parent = removeDependencies;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
delete entries[name];
|
|
let count = 1;
|
|
function remove(parent) {
|
|
const list = Object.keys(entries).filter((name2) => {
|
|
const item2 = entries[name2];
|
|
return item2.type !== "icon" && item2.parent === parent;
|
|
});
|
|
list.forEach((name2) => {
|
|
if (entries[name2]) {
|
|
delete entries[name2];
|
|
count++;
|
|
remove(name2);
|
|
}
|
|
});
|
|
}
|
|
if (removeDependencies === true) {
|
|
remove(name);
|
|
}
|
|
return count;
|
|
}
|
|
/**
|
|
* Rename icon
|
|
*/
|
|
rename(oldName, newName) {
|
|
if (oldName === newName) {
|
|
return false;
|
|
}
|
|
const entries = this.entries;
|
|
if (entries[newName]) {
|
|
if (!this.remove(newName)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!entries[oldName]) {
|
|
return false;
|
|
}
|
|
entries[newName] = entries[oldName];
|
|
delete entries[oldName];
|
|
for (const key in entries) {
|
|
const item = entries[key];
|
|
switch (item.type) {
|
|
case "icon":
|
|
break;
|
|
case "alias":
|
|
case "variation":
|
|
if (item.parent === oldName) {
|
|
item.parent = newName;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
this.updateLastModified();
|
|
return true;
|
|
}
|
|
/**
|
|
* Add/update item
|
|
*/
|
|
setItem(name, item) {
|
|
switch (item.type) {
|
|
case "alias":
|
|
case "variation": {
|
|
if (!this.entries[item.parent]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
this.entries[name] = item;
|
|
this.updateLastModified();
|
|
return true;
|
|
}
|
|
/**
|
|
* Add/update icon
|
|
*/
|
|
setIcon(name, icon) {
|
|
return this.setItem(name, {
|
|
type: "icon",
|
|
body: icon.body,
|
|
props: iconSet_props.filterProps(icon, iconSet_props.defaultCommonProps, true),
|
|
chars: /* @__PURE__ */ new Set(),
|
|
categories: /* @__PURE__ */ new Set()
|
|
});
|
|
}
|
|
/**
|
|
* Add/update alias without props
|
|
*/
|
|
setAlias(name, parent) {
|
|
return this.setItem(name, {
|
|
type: "alias",
|
|
parent,
|
|
chars: /* @__PURE__ */ new Set()
|
|
});
|
|
}
|
|
/**
|
|
* Add/update alias with props
|
|
*/
|
|
setVariation(name, parent, props) {
|
|
return this.setItem(name, {
|
|
type: "variation",
|
|
parent,
|
|
props,
|
|
chars: /* @__PURE__ */ new Set()
|
|
});
|
|
}
|
|
/**
|
|
* Icon from SVG. Updates old icon if it exists
|
|
*/
|
|
fromSVG(name, svg) {
|
|
const props = { ...svg.viewBox };
|
|
const body = svg.getBody();
|
|
const item = this.entries[name];
|
|
switch (item?.type) {
|
|
case "icon":
|
|
case "variation": {
|
|
return this.setItem(name, {
|
|
type: "icon",
|
|
body,
|
|
props,
|
|
chars: item.chars,
|
|
categories: item.type === "icon" ? item.categories : /* @__PURE__ */ new Set()
|
|
});
|
|
}
|
|
}
|
|
return this.setIcon(name, {
|
|
body,
|
|
...props
|
|
});
|
|
}
|
|
/**
|
|
* Add or remove character for icon
|
|
*/
|
|
toggleCharacter(iconName, char, add) {
|
|
const item = this.entries[iconName];
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
if (item.chars.has(char) !== add) {
|
|
item.chars[add ? "add" : "delete"](char);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Add or remove category for icon
|
|
*/
|
|
toggleCategory(iconName, category, add) {
|
|
const item = this.entries[iconName];
|
|
const categoryItem = this.findCategory(category, add);
|
|
if (!item || !categoryItem) {
|
|
return false;
|
|
}
|
|
switch (item.type) {
|
|
case "icon":
|
|
if (item.categories.has(categoryItem) !== add) {
|
|
categoryItem.count += add ? 1 : -1;
|
|
item.categories[add ? "add" : "delete"](categoryItem);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Find icons that belong to theme
|
|
*/
|
|
checkTheme(prefix) {
|
|
const themes = prefix ? this.prefixes : this.suffixes;
|
|
const keys = sortThemeKeys(Object.keys(themes));
|
|
const results = {
|
|
valid: /* @__PURE__ */ Object.create(null),
|
|
invalid: []
|
|
};
|
|
keys.forEach((key) => {
|
|
results.valid[key] = [];
|
|
});
|
|
results.invalid = this._filter((name, item, icon) => {
|
|
if (item.type === "alias" || item.props.hidden || icon?.hidden) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const search = keys[i];
|
|
if (search === "") {
|
|
results.valid[search].push(name);
|
|
return false;
|
|
}
|
|
const match = prefix ? search + "-" : "-" + search;
|
|
const length = match.length;
|
|
const test = prefix ? name.slice(0, length) : name.slice(0 - length);
|
|
if (test === match) {
|
|
results.valid[search].push(name);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
return results;
|
|
}
|
|
}
|
|
function blankIconSet(prefix) {
|
|
return new IconSet({
|
|
prefix,
|
|
icons: {}
|
|
});
|
|
}
|
|
|
|
exports.IconSet = IconSet;
|
|
exports.blankIconSet = blankIconSet;
|
|
exports.sortThemeKeys = sortThemeKeys;
|