Notes/.obsidian/plugins/obsidian-icon-folder/main.js

4862 lines
784 KiB
JavaScript
Raw Normal View History

2024-01-22 10:12:48 -04:00
/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
*/
'use strict';
var obsidian = require('obsidian');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
class MetaData {
}
// This library file does not include any other dependency and is a standalone file that
// only include utility functions for manipulating or extracting svg information.
/**
* Extracts an SVG string from a given input string and returns a cleaned up and
* formatted SVG string.
* @param svgString SVG string to extract from.
* @returns Cleaned up and formatted SVG string.
*/
const extract = (svgString) => {
var _a, _b;
// Removes unnecessary spaces and newlines.
svgString = svgString.replace(/(\r\n|\n|\r)/gm, '');
svgString = svgString.replace(/>\s+</gm, '><');
// Create a parser for better parsing of HTML.
const parser = new DOMParser();
const svg = parser.parseFromString(svgString, 'text/html').querySelector('svg');
// Removes `width` and `height` from the `style` attribute.
if (svg.hasAttribute('style')) {
svg.style.width = '';
svg.style.height = '';
}
// Add `viewbox`, if it is not already a attribute.
if (svg.viewBox.baseVal.width === 0 && svg.viewBox.baseVal.height === 0) {
const width = (_a = svg.width.baseVal.value) !== null && _a !== void 0 ? _a : 16;
const height = (_b = svg.height.baseVal.value) !== null && _b !== void 0 ? _b : 16;
svg.viewBox.baseVal.width = width;
svg.viewBox.baseVal.height = height;
}
if (!svg.hasAttribute('fill')) {
svg.setAttribute('fill', 'currentColor');
}
svg.setAttribute('width', '16px');
svg.setAttribute('height', '16px');
return svg.outerHTML;
};
/**
* Sets the font size of an SVG string by modifying its width and/or height attributes.
* The font size will be always set in pixels.
* @param svgString SVG string to modify.
* @param fontSize Font size in pixels to set.
* @returns Modified SVG string.
*/
const setFontSize = (svgString, fontSize) => {
const widthRe = new RegExp(/width="\d+(px)?"/);
const heightRe = new RegExp(/height="\d+(px)?"/);
if (svgString.match(widthRe)) {
svgString = svgString.replace(widthRe, `width="${fontSize}px"`);
}
if (svgString.match(heightRe)) {
svgString = svgString.replace(heightRe, `height="${fontSize}px"`);
}
return svgString;
};
/**
* Replaces the fill or stroke color of an SVG string with a given color.
* @param svgString SVG string to modify.
* @param color Color to set. Defaults to 'currentColor'.
* @returns The modified SVG string.
*/
const colorize = (svgString, color) => {
if (!color) {
color = 'currentColor';
}
const parser = new DOMParser();
// Tries to parse the string into a HTML node.
const parsedNode = parser.parseFromString(svgString, 'text/html');
const svg = parsedNode.querySelector('svg');
if (svg) {
if (svg.hasAttribute('fill') && svg.getAttribute('fill') !== 'none') {
svg.setAttribute('fill', color);
}
else if (svg.hasAttribute('stroke') && svg.getAttribute('stroke') !== 'none') {
svg.setAttribute('stroke', color);
}
return svg.outerHTML;
}
return svgString;
};
var svg = {
extract,
colorize,
setFontSize,
};
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn) {
var module = { exports: {} };
return fn(module, module.exports), module.exports;
}
function commonjsRequire (path) {
throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
}
/*!
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
<http://stuartk.com/jszip>
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
JSZip uses the library pako released under the MIT license :
https://github.com/nodeca/pako/blob/main/LICENSE
*/
var jszip_min = createCommonjsModule(function (module, exports) {
!function(e){module.exports=e();}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof commonjsRequire&&commonjsRequire;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h);}return o[r].exports}for(var l="function"==typeof commonjsRequire&&commonjsRequire,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l};},{"./support":30,"./utils":32}],2:[function(e,t,r){var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i;}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o;},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate");},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e;}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return -1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return -1^e}(0|t,e,e.length,0):0};},{"./utils":32}],5:[function(e,t,r){r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null;},{}],6:[function(e,t,r){var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n};},{lie:37}],7:[function(e,t,r){var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e
});
/**
* Download a zip file from a url and return the bytes of the file as an ArrayBuffer.
* @param url String url of the zip file to download.
* @returns ArrayBuffer of the zip file.
*/
const downloadZipFile = (url) => __awaiter(void 0, void 0, void 0, function* () {
const fetched = yield obsidian.requestUrl({ url });
const bytes = fetched.arrayBuffer;
return bytes;
});
/**
* Transforms a JSZip file into a File object.
* @param file JSZip file to transform.
* @returns File object of the JSZip file.
*/
const getFileFromJSZipFile = (file) => __awaiter(void 0, void 0, void 0, function* () {
const fileData = yield file.async('blob');
const filename = file.name.split('/').pop();
return new File([fileData], filename);
});
/**
* Read a zip file and return the files inside it.
* @param bytes ArrayBuffer of the zip file.
* @param extraPath String path to filter the files inside the zip file. This can be used
* to set an extra path (like a directory inside the zip file) to filter the files.
* @returns Array of loaded files inside the zip file.
*/
const readZipFile = (bytes, extraPath = '') => __awaiter(void 0, void 0, void 0, function* () {
const zipper = new jszip_min();
const unzippedFiles = yield zipper.loadAsync(bytes);
return Promise.resolve(unzippedFiles).then((unzipped) => {
if (!Object.keys(unzipped.files).length) {
return Promise.reject('No file was found');
}
const files = [];
// Regex for retrieving the files inside the zip file or inside the directory of a
// zip file.
const regex = new RegExp(extraPath + '(.+)\\.svg', 'g');
Object.entries(unzippedFiles.files).forEach(([_, v]) => {
const matched = v.name.match(regex);
if (!v.dir && matched && matched.length > 0) {
files.push(v);
}
});
return files;
});
});
let path;
const setPath = (newPath) => {
if (newPath === 'plugins/obsidian-icon-folder/icons') {
newPath = '.obsidian/plugins/obsidian-icon-folder/icons';
new obsidian.Notice(`[${MetaData.pluginName}] Due to a change in version v1.2.2, the icon pack folder changed. Please change it in the settings to not be directly in /plugins.`, 8000);
}
path = newPath;
};
const preloadedIcons = [];
let iconPacks$1 = [];
const moveIconPackDirectories = (plugin, from, to) => __awaiter(void 0, void 0, void 0, function* () {
// Tries to move all icon packs to the new folder.
for (let i = 0; i < iconPacks$1.length; i++) {
const iconPack = iconPacks$1[i];
// Tries to create a new directory in the new path.
const doesDirExist = yield createDirectory(plugin, iconPack.name);
if (doesDirExist) {
new obsidian.Notice(`Directory with name ${iconPack.name} already exists.`);
continue;
}
new obsidian.Notice(`Moving ${iconPack.name}...`);
// Move the zip file.
if (yield plugin.app.vault.adapter.exists(`${from}/${iconPack.name}.zip`)) {
yield plugin.app.vault.adapter.copy(`${from}/${iconPack.name}.zip`, `${to}/${iconPack.name}.zip`);
}
// Move all other files inside of the iconpack directory.
const filesInDirectory = yield getFilesInDirectory(plugin, `${from}/${iconPack.name}`);
for (const file of filesInDirectory) {
const fileName = file.split('/').pop();
yield plugin.app.vault.adapter.copy(`${from}/${iconPack.name}/${fileName}`, `${to}/${iconPack.name}/${fileName}`);
}
new obsidian.Notice(`...moved ${iconPack.name}`);
}
// Removes all the existing icon packs in the `from` directory.
for (let i = 0; i < iconPacks$1.length; i++) {
const iconPack = iconPacks$1[i];
if (yield plugin.app.vault.adapter.exists(`${from}/${iconPack.name}`)) {
yield plugin.app.vault.adapter.rmdir(`${from}/${iconPack.name}`, true);
}
}
// Remove root directory that contains all the icon packs.
if (!to.startsWith(from)) {
yield plugin.app.vault.adapter.rmdir(`${from}`, true);
}
});
const createCustomIconPackDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () {
yield createDirectory(plugin, dir);
const prefix = createIconPackPrefix(dir);
iconPacks$1.push({ name: dir, icons: [], prefix, custom: true });
});
const deleteIconPack = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () {
iconPacks$1 = iconPacks$1.filter((iconPack) => iconPack.name !== dir);
// Check for the icon pack directory and delete it.
if (yield plugin.app.vault.adapter.exists(`${path}/${dir}`)) {
yield plugin.app.vault.adapter.rmdir(`${path}/${dir}`, true);
}
// Check for the icon pack zip file and delete it.
if (yield plugin.app.vault.adapter.exists(`${path}/${dir}.zip`)) {
yield plugin.app.vault.adapter.rmdir(`${path}/${dir}.zip`, true);
}
});
const doesIconPackExist = (plugin, iconPackName) => {
return plugin.app.vault.adapter.exists(`${path}/${iconPackName}`);
};
const createDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () {
const doesDirExist = yield plugin.app.vault.adapter.exists(`${path}/${dir}`);
if (!doesDirExist) {
yield plugin.app.vault.adapter.mkdir(`${path}/${dir}`);
}
return doesDirExist;
});
const getNormalizedName = (s) => {
return s
.split(/[ -]|[ _]/g)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
};
// export const normalizeFileName = async (plugin: Plugin, oldPath: string) => {
// const fileName = oldPath.split('/').pop();
// const newPath = oldPath.substring(0, oldPath.indexOf(fileName)) + getNormalizedName(fileName);
// await plugin.app.vault.adapter.rename(oldPath, newPath);
// };
const createZipFile = (plugin, filename, buffer) => __awaiter(void 0, void 0, void 0, function* () {
yield plugin.app.vault.adapter.writeBinary(`${path}/${filename}`, buffer);
});
const createFile = (plugin, iconPackName, filename, content, absoluteFilename) => __awaiter(void 0, void 0, void 0, function* () {
const normalizedFilename = getNormalizedName(filename);
const exists = yield plugin.app.vault.adapter.exists(`${path}/${iconPackName}/${normalizedFilename}`);
if (exists) {
const folderSplit = absoluteFilename.split('/');
if (folderSplit.length >= 2) {
const folderName = folderSplit[folderSplit.length - 2];
const newFilename = folderName + normalizedFilename;
yield plugin.app.vault.adapter.write(`${path}/${iconPackName}/${newFilename}`, content);
console.info(`[${MetaData.pluginName}] Renamed old file ${normalizedFilename} to ${newFilename} because of duplication.`);
new obsidian.Notice(`[${MetaData.pluginName}] Renamed ${normalizedFilename} to ${newFilename} to avoid duplication.`, 8000);
}
else {
console.warn(`[${MetaData.pluginName}] Could not create icons with duplicated file names (${normalizedFilename}).`);
new obsidian.Notice(`[${MetaData.pluginName}] Could not create duplicated icon name (${normalizedFilename})`, 8000);
}
}
else {
yield plugin.app.vault.adapter.write(`${path}/${iconPackName}/${normalizedFilename}`, content);
}
});
const createDefaultDirectory = (plugin) => __awaiter(void 0, void 0, void 0, function* () {
yield createDirectory(plugin, '');
});
const getAllIconPacks = () => {
return iconPacks$1;
};
const getFilesInDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () {
return (yield plugin.app.vault.adapter.list(dir)).files;
});
const validIconName = /^[(A-Z)|(0-9)]/;
const svgViewboxRegex = /viewBox="([^"]*)"/g;
const svgContentRegex = /<svg.*>(.*?)<\/svg>/g;
const generateIcon = (iconPackName, iconName, content) => {
if (content.length === 0) {
return;
}
content = content.replace(/(\r\n|\n|\r)/gm, '');
content = content.replace(/>\s+</gm, '><');
const normalizedName = iconName.charAt(0).toUpperCase() + iconName.substring(1);
if (!validIconName.exec(normalizedName)) {
console.log(`skipping icon with invalid name: ${iconName}`);
return null;
}
const svgViewboxMatch = content.match(svgViewboxRegex);
let svgViewbox = '';
if (svgViewboxMatch && svgViewboxMatch.length !== 0) {
svgViewbox = svgViewboxMatch[0];
}
const svgContentMatch = content.match(svgContentRegex);
if (!svgContentMatch) {
console.log(`skipping icon with invalid svg content: ${content}`);
return null;
}
const svgContent = svgContentMatch.map((val) => val.replace(/<\/?svg>/g, '').replace(/<svg.+?>/g, ''))[0];
const iconPackPrefix = createIconPackPrefix(iconPackName);
const icon = {
name: normalizedName.split('.svg')[0],
prefix: iconPackPrefix,
iconPackName,
filename: iconName,
svgContent,
svgViewbox,
svgElement: svg.extract(content),
};
return icon;
};
const createIconPackPrefix = (iconPackName) => {
if (iconPackName.includes('-')) {
const splitted = iconPackName.split('-');
let result = splitted[0].charAt(0).toUpperCase();
for (let i = 1; i < splitted.length; i++) {
result += splitted[i].charAt(0).toLowerCase();
}
return result;
}
return iconPackName.charAt(0).toUpperCase() + iconPackName.charAt(1).toLowerCase();
};
const loadUsedIcons = (plugin, icons) => __awaiter(void 0, void 0, void 0, function* () {
const iconPacks = (yield listPath(plugin)).folders.map((iconPack) => iconPack.split('/').pop());
for (let i = 0; i < icons.length; i++) {
const entry = icons[i];
if (!entry) {
continue;
}
yield loadIcon(plugin, iconPacks, entry);
}
});
const listPath = (plugin, listPath) => {
return plugin.app.vault.adapter.list(listPath !== null && listPath !== void 0 ? listPath : path);
};
const getIconPackNameByPrefix = (prefix) => {
var _a;
return (_a = iconPacks$1.find((iconPack) => iconPack.prefix === prefix)) === null || _a === void 0 ? void 0 : _a.name;
};
const nextIdentifier = (iconName) => {
return iconName.substring(1).search(/[(A-Z)|(0-9)]/) + 1;
};
const loadIcon = (plugin, iconPacks, iconName) => __awaiter(void 0, void 0, void 0, function* () {
const nextLetter = nextIdentifier(iconName);
const prefix = iconName.substring(0, nextLetter);
const name = iconName.substring(nextLetter);
const iconPack = iconPacks.find((folder) => {
const folderPrefix = createIconPackPrefix(folder);
return prefix === folderPrefix;
});
if (!iconPack) {
new obsidian.Notice(`Seems like you do not have an icon pack installed. (${iconName})`, 5000);
return;
}
const fullPath = path + '/' + iconPack + '/' + name + '.svg';
if (!(yield plugin.app.vault.adapter.exists(fullPath))) {
console.warn(`[obsidian-icon-folder] icon with name "${name}" was not found (full path: ${fullPath}).`);
return;
}
const content = yield plugin.app.vault.adapter.read(fullPath);
const icon = generateIcon(iconPack, name, content);
preloadedIcons.push(icon);
});
const initIconPacks = (plugin) => __awaiter(void 0, void 0, void 0, function* () {
// Remove the beginning slash because paths which start with `/` are the same as without
// a slash.
if (path.startsWith('/')) {
path = path.slice(1);
}
const loadedIconPacks = yield plugin.app.vault.adapter.list(path);
// Extract all zip files which will be downloaded icon packs.
const zipFiles = {};
for (let i = 0; i < loadedIconPacks.files.length; i++) {
const fileName = loadedIconPacks.files[i];
if (fileName.endsWith('.zip')) {
const arrayBuffer = yield plugin.app.vault.adapter.readBinary(fileName);
const files = yield readZipFile(arrayBuffer);
const iconPackName = fileName.split('/').pop().split('.zip')[0];
zipFiles[iconPackName] = files;
}
}
// Check for custom-made icon packs.
for (let i = 0; i < loadedIconPacks.folders.length; i++) {
const folderName = loadedIconPacks.folders[i].split('/').pop();
// Continue if the icon pack does have a zip file.
if (zipFiles[folderName]) {
continue;
}
const files = yield getFilesInDirectory(plugin, `${path}/${folderName}`);
const loadedIcons = [];
// Convert files into loaded svgs.
for (let j = 0; j < files.length; j++) {
const iconNameRegex = files[j].match(new RegExp(path + '/' + folderName + '/(.*)'));
const iconName = iconNameRegex[1];
const iconContent = yield plugin.app.vault.adapter.read(files[j]);
const icon = generateIcon(folderName, iconName, iconContent);
if (icon) {
loadedIcons.push(icon);
}
}
const prefix = createIconPackPrefix(folderName);
iconPacks$1.push({ name: folderName, icons: loadedIcons, prefix, custom: true });
console.log(`loaded icon pack ${folderName} (${loadedIcons.length})`);
}
// Extract all files from the zip files.
for (const zipFile in zipFiles) {
const files = zipFiles[zipFile];
const loadedIcons = yield getLoadedIconsFromZipFile(zipFile, files);
const prefix = createIconPackPrefix(zipFile);
iconPacks$1.push({ name: zipFile, icons: loadedIcons, prefix, custom: false });
console.log(`loaded icon pack ${zipFile} (${loadedIcons.length})`);
}
});
const getLoadedIconsFromZipFile = (iconPackName, files) => __awaiter(void 0, void 0, void 0, function* () {
const loadedIcons = [];
for (let j = 0; j < files.length; j++) {
const file = yield getFileFromJSZipFile(files[j]);
const iconContent = yield file.text();
const iconName = file.name;
const icon = generateIcon(iconPackName, iconName, iconContent);
if (icon) {
loadedIcons.push(icon);
}
}
return loadedIcons;
});
const addIconToIconPack = (iconPackName, iconName, iconContent) => {
// Normalize the icon name to remove `-` or `_` in the name.
iconName = getNormalizedName(iconName);
const icon = generateIcon(iconPackName, iconName, iconContent);
if (!icon) {
console.warn(`[obsidian-icon-folder] icon could not be generated (icon: ${iconName}, content: ${iconContent}).`);
return undefined;
}
const iconPack = iconPacks$1.find((iconPack) => iconPack.name === iconPackName);
if (!iconPack) {
console.warn(`[obsidian-icon-folder] iconpack with name "${iconPackName}" was not found.`);
return undefined;
}
iconPack.icons.push(icon);
return icon;
};
const removeIconFromIconPackDirectory = (plugin, iconPackName, iconName) => {
const iconPack = iconPacks$1.find((iconPack) => iconPack.name === iconPackName);
// Checks if icon pack is custom-made.
if (!iconPack.custom) {
return plugin.app.vault.adapter.rmdir(`${path}/${iconPackName}/${iconName}.svg`, true);
}
};
const extractIconToIconPack = (plugin, icon, iconContent) => __awaiter(void 0, void 0, void 0, function* () {
const doesIconPackDirExist = yield plugin.app.vault.adapter.exists(`${path}/${icon.iconPackName}`);
if (!doesIconPackDirExist) {
yield plugin.app.vault.adapter.mkdir(`${path}/${icon.iconPackName}`);
}
const doesIconFileExists = yield plugin.app.vault.adapter.exists(`${path}/${icon.iconPackName}/${icon.name}.svg`);
if (!doesIconFileExists) {
yield createFile(plugin, icon.iconPackName, `${icon.name}.svg`, iconContent);
}
});
const getAllLoadedIconNames = () => {
return iconPacks$1.reduce((total, iconPack) => {
total.push(...iconPack.icons);
return total;
}, []);
};
const registerIconPack = (name, arrayBuffer) => __awaiter(void 0, void 0, void 0, function* () {
const files = yield readZipFile(arrayBuffer);
const loadedIcons = yield getLoadedIconsFromZipFile(name, files);
const prefix = createIconPackPrefix(name);
iconPacks$1.push({ name, icons: loadedIcons, prefix, custom: false });
console.log(`loaded icon pack ${name} (${loadedIcons.length})`);
});
const doesIconExists = (iconName) => {
const icons = getAllLoadedIconNames();
return icons.find((icon) => icon.name === iconName || icon.prefix + icon.name === iconName) !== undefined;
};
const getSvgFromLoadedIcon = (iconPrefix, iconName) => {
let icon = '';
let foundIcon = preloadedIcons.find((icon) => icon.prefix.toLowerCase() === iconPrefix.toLowerCase() && icon.name.toLowerCase() === iconName.toLowerCase());
if (!foundIcon) {
iconPacks$1.forEach((iconPack) => {
const icon = iconPack.icons.find((icon) => icon.prefix.toLowerCase() === iconPrefix.toLowerCase() && icon.name.toLowerCase() === iconName.toLowerCase());
if (icon) {
foundIcon = icon;
}
});
}
if (foundIcon) {
icon = foundIcon.svgElement;
}
return icon;
};
/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */
var twemoji=function(){var twemoji={base:"https://twemoji.maxcdn.com/v/14.0.2/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt,false),this);}},parse:parse,replace:replace,test:test},escaper={"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"},re=/(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\
const shortNames = {
'😀': 'grinning face',
'😃': 'grinning face with big eyes',
'😄': 'grinning face with smiling eyes',
'😁': 'beaming face with smiling eyes',
'😆': 'grinning squinting face',
'😅': 'grinning face with sweat',
'🤣': 'rolling on the floor laughing',
'😂': 'face with tears of joy',
'🙂': 'slightly smiling face',
'🙃': 'upside-down face',
'🫠': '⊛ melting face',
'😉': 'winking face',
'😊': 'smiling face with smiling eyes',
'😇': 'smiling face with halo',
'🥰': 'smiling face with hearts',
'😍': 'smiling face with heart-eyes',
'🤩': 'star-struck',
'😘': 'face blowing a kiss',
'😗': 'kissing face',
'☺': 'smiling face',
'😚': 'kissing face with closed eyes',
'😙': 'kissing face with smiling eyes',
'🥲': 'smiling face with tear',
'😋': 'face savoring food',
'😛': 'face with tongue',
'😜': 'winking face with tongue',
'🤪': 'zany face',
'😝': 'squinting face with tongue',
'🤑': 'money-mouth face',
'🤗': 'smiling face with open hands',
'🤭': 'face with hand over mouth',
'🫢': '⊛ face with open eyes and hand over mouth',
'🫣': '⊛ face with peeking eye',
'🤫': 'shushing face',
'🤔': 'thinking face',
'🫡': '⊛ saluting face',
'🤐': 'zipper-mouth face',
'🤨': 'face with raised eyebrow',
'😐': 'neutral face',
'😑': 'expressionless face',
'😶': 'face without mouth',
'🫥': '⊛ dotted line face',
'😶‍🌫️': 'face in clouds',
'😏': 'smirking face',
'😒': 'unamused face',
'🙄': 'face with rolling eyes',
'😬': 'grimacing face',
'😮‍💨': 'face exhaling',
'🤥': 'lying face',
'😌': 'relieved face',
'😔': 'pensive face',
'😪': 'sleepy face',
'🤤': 'drooling face',
'😴': 'sleeping face',
'😷': 'face with medical mask',
'🤒': 'face with thermometer',
'🤕': 'face with head-bandage',
'🤢': 'nauseated face',
'🤮': 'face vomiting',
'🤧': 'sneezing face',
'🥵': 'hot face',
'🥶': 'cold face',
'🥴': 'woozy face',
'😵': 'face with crossed-out eyes',
'😵‍💫': 'face with spiral eyes',
'🤯': 'exploding head',
'🤠': 'cowboy hat face',
'🥳': 'partying face',
'🥸': 'disguised face',
'😎': 'smiling face with sunglasses',
'🤓': 'nerd face',
'🧐': 'face with monocle',
'😕': 'confused face',
'🫤': '⊛ face with diagonal mouth',
'😟': 'worried face',
'🙁': 'slightly frowning face',
'☹': 'frowning face',
'😮': 'face with open mouth',
'😯': 'hushed face',
'😲': 'astonished face',
'😳': 'flushed face',
'🥺': 'pleading face',
'🥹': '⊛ face holding back tears',
'😦': 'frowning face with open mouth',
'😧': 'anguished face',
'😨': 'fearful face',
'😰': 'anxious face with sweat',
'😥': 'sad but relieved face',
'😢': 'crying face',
'😭': 'loudly crying face',
'😱': 'face screaming in fear',
'😖': 'confounded face',
'😣': 'persevering face',
'😞': 'disappointed face',
'😓': 'downcast face with sweat',
'😩': 'weary face',
'😫': 'tired face',
'🥱': 'yawning face',
'😤': 'face with steam from nose',
'😡': 'pouting face',
'😠': 'angry face',
'🤬': 'face with symbols on mouth',
'😈': 'smiling face with horns',
'👿': 'angry face with horns',
'💀': 'skull',
'☠': 'skull and crossbones',
'💩': 'pile of poo',
'🤡': 'clown face',
'👹': 'ogre',
'👺': 'goblin',
'👻': 'ghost',
'👽': 'alien',
'👾': 'alien monster',
'🤖': 'robot',
'😺': 'grinning cat',
'😸': 'grinning cat with smiling eyes',
'😹': 'cat with tears of joy',
'😻': 'smiling cat with heart-eyes',
'😼': 'cat with wry smile',
'😽': 'kissing cat',
'🙀': 'weary cat',
'😿': 'crying cat',
'😾': 'pouting cat',
'🙈': 'see-no-evil monkey',
'🙉': 'hear-no-evil monkey',
'🙊': 'speak-no-evil monkey',
'💋': 'kiss mark',
'💌': 'love letter',
'💘': 'heart with arrow',
'💝': 'heart with ribbon',
'💖': 'sparkling heart',
'💗': 'growing heart',
'💓': 'beating heart',
'💞': 'revolving hearts',
'💕': 'two hearts',
'💟': 'heart decoration',
'❣': 'heart exclamation',
'💔': 'broken heart',
'❤️‍🔥': 'heart on fire',
'❤️‍🩹': 'mending heart',
'❤': 'red heart',
'🧡': 'orange heart',
'💛': 'yellow heart',
'💚': 'green heart',
'💙': 'blue heart',
'💜': 'purple heart',
'🤎': 'brown heart',
'🖤': 'black heart',
'🤍': 'white heart',
'💯': 'hundred points',
'💢': 'anger symbol',
'💥': 'collision',
'💫': 'dizzy',
'💦': 'sweat droplets',
'💨': 'dashing away',
'🕳': 'hole',
'💣': 'bomb',
'💬': 'speech balloon',
'👁️‍🗨️': 'eye in speech bubble',
'🗨': 'left speech bubble',
'🗯': 'right anger bubble',
'💭': 'thought balloon',
'💤': 'zzz',
'👋': 'waving hand',
'🤚': 'raised back of hand',
'🖐': 'hand with fingers splayed',
'✋': 'raised hand',
'🖖': 'vulcan salute',
'🫱': '⊛ rightwards hand',
'🫲': '⊛ leftwards hand',
'🫳': '⊛ palm down hand',
'🫴': '⊛ palm up hand',
'👌': 'OK hand',
'🤌': 'pinched fingers',
'🤏': 'pinching hand',
'✌': 'victory hand',
'🤞': 'crossed fingers',
'🫰': '⊛ hand with index finger and thumb crossed',
'🤟': 'love-you gesture',
'🤘': 'sign of the horns',
'🤙': 'call me hand',
'👈': 'backhand index pointing left',
'👉': 'backhand index pointing right',
'👆': 'backhand index pointing up',
'🖕': 'middle finger',
'👇': 'backhand index pointing down',
'☝': 'index pointing up',
'🫵': '⊛ index pointing at the viewer',
'👍': 'thumbs up',
'👎': 'thumbs down',
'✊': 'raised fist',
'👊': 'oncoming fist',
'🤛': 'left-facing fist',
'🤜': 'right-facing fist',
'👏': 'clapping hands',
'🙌': 'raising hands',
'🫶': '⊛ heart hands',
'👐': 'open hands',
'🤲': 'palms up together',
'🤝': 'handshake',
'🙏': 'folded hands',
'✍': 'writing hand',
'💅': 'nail polish',
'🤳': 'selfie',
'💪': 'flexed biceps',
'🦾': 'mechanical arm',
'🦿': 'mechanical leg',
'🦵': 'leg',
'🦶': 'foot',
'👂': 'ear',
'🦻': 'ear with hearing aid',
'👃': 'nose',
'🧠': 'brain',
'🫀': 'anatomical heart',
'🫁': 'lungs',
'🦷': 'tooth',
'🦴': 'bone',
'👀': 'eyes',
'👁': 'eye',
'👅': 'tongue',
'👄': 'mouth',
'🫦': '⊛ biting lip',
'👶': 'baby',
'🧒': 'child',
'👦': 'boy',
'👧': 'girl',
'🧑': 'person',
'👱': 'person: blond hair',
'👨': 'man',
'🧔': 'person: beard',
'🧔‍♂️': 'man: beard',
'🧔‍♀️': 'woman: beard',
'👨‍🦰': 'man: red hair',
'👨‍🦱': 'man: curly hair',
'👨‍🦳': 'man: white hair',
'👨‍🦲': 'man: bald',
'👩': 'woman',
'👩‍🦰': 'woman: red hair',
'🧑‍🦰': 'person: red hair',
'👩‍🦱': 'woman: curly hair',
'🧑‍🦱': 'person: curly hair',
'👩‍🦳': 'woman: white hair',
'🧑‍🦳': 'person: white hair',
'👩‍🦲': 'woman: bald',
'🧑‍🦲': 'person: bald',
'👱‍♀️': 'woman: blond hair',
'👱‍♂️': 'man: blond hair',
'🧓': 'older person',
'👴': 'old man',
'👵': 'old woman',
'🙍': 'person frowning',
'🙍‍♂️': 'man frowning',
'🙍‍♀️': 'woman frowning',
'🙎': 'person pouting',
'🙎‍♂️': 'man pouting',
'🙎‍♀️': 'woman pouting',
'🙅': 'person gesturing NO',
'🙅‍♂️': 'man gesturing NO',
'🙅‍♀️': 'woman gesturing NO',
'🙆': 'person gesturing OK',
'🙆‍♂️': 'man gesturing OK',
'🙆‍♀️': 'woman gesturing OK',
'💁': 'person tipping hand',
'💁‍♂️': 'man tipping hand',
'💁‍♀️': 'woman tipping hand',
'🙋': 'person raising hand',
'🙋‍♂️': 'man raising hand',
'🙋‍♀️': 'woman raising hand',
'🧏': 'deaf person',
'🧏‍♂️': 'deaf man',
'🧏‍♀️': 'deaf woman',
'🙇': 'person bowing',
'🙇‍♂️': 'man bowing',
'🙇‍♀️': 'woman bowing',
'🤦': 'person facepalming',
'🤦‍♂️': 'man facepalming',
'🤦‍♀️': 'woman facepalming',
'🤷': 'person shrugging',
'🤷‍♂️': 'man shrugging',
'🤷‍♀️': 'woman shrugging',
'🧑‍⚕️': 'health worker',
'👨‍⚕️': 'man health worker',
'👩‍⚕️': 'woman health worker',
'🧑‍🎓': 'student',
'👨‍🎓': 'man student',
'👩‍🎓': 'woman student',
'🧑‍🏫': 'teacher',
'👨‍🏫': 'man teacher',
'👩‍🏫': 'woman teacher',
'🧑‍⚖️': 'judge',
'👨‍⚖️': 'man judge',
'👩‍⚖️': 'woman judge',
'🧑‍🌾': 'farmer',
'👨‍🌾': 'man farmer',
'👩‍🌾': 'woman farmer',
'🧑‍🍳': 'cook',
'👨‍🍳': 'man cook',
'👩‍🍳': 'woman cook',
'🧑‍🔧': 'mechanic',
'👨‍🔧': 'man mechanic',
'👩‍🔧': 'woman mechanic',
'🧑‍🏭': 'factory worker',
'👨‍🏭': 'man factory worker',
'👩‍🏭': 'woman factory worker',
'🧑‍💼': 'office worker',
'👨‍💼': 'man office worker',
'👩‍💼': 'woman office worker',
'🧑‍🔬': 'scientist',
'👨‍🔬': 'man scientist',
'👩‍🔬': 'woman scientist',
'🧑‍💻': 'technologist',
'👨‍💻': 'man technologist',
'👩‍💻': 'woman technologist',
'🧑‍🎤': 'singer',
'👨‍🎤': 'man singer',
'👩‍🎤': 'woman singer',
'🧑‍🎨': 'artist',
'👨‍🎨': 'man artist',
'👩‍🎨': 'woman artist',
'🧑‍✈️': 'pilot',
'👨‍✈️': 'man pilot',
'👩‍✈️': 'woman pilot',
'🧑‍🚀': 'astronaut',
'👨‍🚀': 'man astronaut',
'👩‍🚀': 'woman astronaut',
'🧑‍🚒': 'firefighter',
'👨‍🚒': 'man firefighter',
'👩‍🚒': 'woman firefighter',
'👮': 'police officer',
'👮‍♂️': 'man police officer',
'👮‍♀️': 'woman police officer',
'🕵': 'detective',
'🕵️‍♂️': 'man detective',
'🕵️‍♀️': 'woman detective',
'💂': 'guard',
'💂‍♂️': 'man guard',
'💂‍♀️': 'woman guard',
'🥷': 'ninja',
'👷': 'construction worker',
'👷‍♂️': 'man construction worker',
'👷‍♀️': 'woman construction worker',
'🫅': '⊛ person with crown',
'🤴': 'prince',
'👸': 'princess',
'👳': 'person wearing turban',
'👳‍♂️': 'man wearing turban',
'👳‍♀️': 'woman wearing turban',
'👲': 'person with skullcap',
'🧕': 'woman with headscarf',
'🤵': 'person in tuxedo',
'🤵‍♂️': 'man in tuxedo',
'🤵‍♀️': 'woman in tuxedo',
'👰': 'person with veil',
'👰‍♂️': 'man with veil',
'👰‍♀️': 'woman with veil',
'🤰': 'pregnant woman',
'🫃': '⊛ pregnant man',
'🫄': '⊛ pregnant person',
'🤱': 'breast-feeding',
'👩‍🍼': 'woman feeding baby',
'👨‍🍼': 'man feeding baby',
'🧑‍🍼': 'person feeding baby',
'👼': 'baby angel',
'🎅': 'Santa Claus',
'🤶': 'Mrs. Claus',
'🧑‍🎄': 'mx claus',
'🦸': 'superhero',
'🦸‍♂️': 'man superhero',
'🦸‍♀️': 'woman superhero',
'🦹': 'supervillain',
'🦹‍♂️': 'man supervillain',
'🦹‍♀️': 'woman supervillain',
'🧙': 'mage',
'🧙‍♂️': 'man mage',
'🧙‍♀️': 'woman mage',
'🧚': 'fairy',
'🧚‍♂️': 'man fairy',
'🧚‍♀️': 'woman fairy',
'🧛': 'vampire',
'🧛‍♂️': 'man vampire',
'🧛‍♀️': 'woman vampire',
'🧜': 'merperson',
'🧜‍♂️': 'merman',
'🧜‍♀️': 'mermaid',
'🧝': 'elf',
'🧝‍♂️': 'man elf',
'🧝‍♀️': 'woman elf',
'🧞': 'genie',
'🧞‍♂️': 'man genie',
'🧞‍♀️': 'woman genie',
'🧟': 'zombie',
'🧟‍♂️': 'man zombie',
'🧟‍♀️': 'woman zombie',
'🧌': '⊛ troll',
'💆': 'person getting massage',
'💆‍♂️': 'man getting massage',
'💆‍♀️': 'woman getting massage',
'💇': 'person getting haircut',
'💇‍♂️': 'man getting haircut',
'💇‍♀️': 'woman getting haircut',
'🚶': 'person walking',
'🚶‍♂️': 'man walking',
'🚶‍♀️': 'woman walking',
'🧍': 'person standing',
'🧍‍♂️': 'man standing',
'🧍‍♀️': 'woman standing',
'🧎': 'person kneeling',
'🧎‍♂️': 'man kneeling',
'🧎‍♀️': 'woman kneeling',
'🧑‍🦯': 'person with white cane',
'👨‍🦯': 'man with white cane',
'👩‍🦯': 'woman with white cane',
'🧑‍🦼': 'person in motorized wheelchair',
'👨‍🦼': 'man in motorized wheelchair',
'👩‍🦼': 'woman in motorized wheelchair',
'🧑‍🦽': 'person in manual wheelchair',
'👨‍🦽': 'man in manual wheelchair',
'👩‍🦽': 'woman in manual wheelchair',
'🏃': 'person running',
'🏃‍♂️': 'man running',
'🏃‍♀️': 'woman running',
'💃': 'woman dancing',
'🕺': 'man dancing',
'🕴': 'person in suit levitating',
'👯': 'people with bunny ears',
'👯‍♂️': 'men with bunny ears',
'👯‍♀️': 'women with bunny ears',
'🧖': 'person in steamy room',
'🧖‍♂️': 'man in steamy room',
'🧖‍♀️': 'woman in steamy room',
'🧗': 'person climbing',
'🧗‍♂️': 'man climbing',
'🧗‍♀️': 'woman climbing',
'🤺': 'person fencing',
'🏇': 'horse racing',
'⛷': 'skier',
'🏂': 'snowboarder',
'🏌': 'person golfing',
'🏌️‍♂️': 'man golfing',
'🏌️‍♀️': 'woman golfing',
'🏄': 'person surfing',
'🏄‍♂️': 'man surfing',
'🏄‍♀️': 'woman surfing',
'🚣': 'person rowing boat',
'🚣‍♂️': 'man rowing boat',
'🚣‍♀️': 'woman rowing boat',
'🏊': 'person swimming',
'🏊‍♂️': 'man swimming',
'🏊‍♀️': 'woman swimming',
'⛹': 'person bouncing ball',
'⛹️‍♂️': 'man bouncing ball',
'⛹️‍♀️': 'woman bouncing ball',
'🏋': 'person lifting weights',
'🏋️‍♂️': 'man lifting weights',
'🏋️‍♀️': 'woman lifting weights',
'🚴': 'person biking',
'🚴‍♂️': 'man biking',
'🚴‍♀️': 'woman biking',
'🚵': 'person mountain biking',
'🚵‍♂️': 'man mountain biking',
'🚵‍♀️': 'woman mountain biking',
'🤸': 'person cartwheeling',
'🤸‍♂️': 'man cartwheeling',
'🤸‍♀️': 'woman cartwheeling',
'🤼': 'people wrestling',
'🤼‍♂️': 'men wrestling',
'🤼‍♀️': 'women wrestling',
'🤽': 'person playing water polo',
'🤽‍♂️': 'man playing water polo',
'🤽‍♀️': 'woman playing water polo',
'🤾': 'person playing handball',
'🤾‍♂️': 'man playing handball',
'🤾‍♀️': 'woman playing handball',
'🤹': 'person juggling',
'🤹‍♂️': 'man juggling',
'🤹‍♀️': 'woman juggling',
'🧘': 'person in lotus position',
'🧘‍♂️': 'man in lotus position',
'🧘‍♀️': 'woman in lotus position',
'🛀': 'person taking bath',
'🛌': 'person in bed',
'🧑‍🤝‍🧑': 'people holding hands',
'👭': 'women holding hands',
'👫': 'woman and man holding hands',
'👬': 'men holding hands',
'💏': 'kiss',
'👩‍❤️‍💋‍👨': 'kiss: woman, man',
'👨‍❤️‍💋‍👨': 'kiss: man, man',
'👩‍❤️‍💋‍👩': 'kiss: woman, woman',
'💑': 'couple with heart',
'👩‍❤️‍👨': 'couple with heart: woman, man',
'👨‍❤️‍👨': 'couple with heart: man, man',
'👩‍❤️‍👩': 'couple with heart: woman, woman',
'👪': 'family',
'👨‍👩‍👦': 'family: man, woman, boy',
'👨‍👩‍👧': 'family: man, woman, girl',
'👨‍👩‍👧‍👦': 'family: man, woman, girl, boy',
'👨‍👩‍👦‍👦': 'family: man, woman, boy, boy',
'👨‍👩‍👧‍👧': 'family: man, woman, girl, girl',
'👨‍👨‍👦': 'family: man, man, boy',
'👨‍👨‍👧': 'family: man, man, girl',
'👨‍👨‍👧‍👦': 'family: man, man, girl, boy',
'👨‍👨‍👦‍👦': 'family: man, man, boy, boy',
'👨‍👨‍👧‍👧': 'family: man, man, girl, girl',
'👩‍👩‍👦': 'family: woman, woman, boy',
'👩‍👩‍👧': 'family: woman, woman, girl',
'👩‍👩‍👧‍👦': 'family: woman, woman, girl, boy',
'👩‍👩‍👦‍👦': 'family: woman, woman, boy, boy',
'👩‍👩‍👧‍👧': 'family: woman, woman, girl, girl',
'👨‍👦': 'family: man, boy',
'👨‍👦‍👦': 'family: man, boy, boy',
'👨‍👧': 'family: man, girl',
'👨‍👧‍👦': 'family: man, girl, boy',
'👨‍👧‍👧': 'family: man, girl, girl',
'👩‍👦': 'family: woman, boy',
'👩‍👦‍👦': 'family: woman, boy, boy',
'👩‍👧': 'family: woman, girl',
'👩‍👧‍👦': 'family: woman, girl, boy',
'👩‍👧‍👧': 'family: woman, girl, girl',
'🗣': 'speaking head',
'👤': 'bust in silhouette',
'👥': 'busts in silhouette',
'🫂': 'people hugging',
'👣': 'footprints',
'🦰': 'red hair',
'🦱': 'curly hair',
'🦳': 'white hair',
'🦲': 'bald',
'🐵': 'monkey face',
'🐒': 'monkey',
'🦍': 'gorilla',
'🦧': 'orangutan',
'🐶': 'dog face',
'🐕': 'dog',
'🦮': 'guide dog',
'🐕‍🦺': 'service dog',
'🐩': 'poodle',
'🐺': 'wolf',
'🦊': 'fox',
'🦝': 'raccoon',
'🐱': 'cat face',
'🐈': 'cat',
'🐈‍⬛': 'black cat',
'🦁': 'lion',
'🐯': 'tiger face',
'🐅': 'tiger',
'🐆': 'leopard',
'🐴': 'horse face',
'🐎': 'horse',
'🦄': 'unicorn',
'🦓': 'zebra',
'🦌': 'deer',
'🦬': 'bison',
'🐮': 'cow face',
'🐂': 'ox',
'🐃': 'water buffalo',
'🐄': 'cow',
'🐷': 'pig face',
'🐖': 'pig',
'🐗': 'boar',
'🐽': 'pig nose',
'🐏': 'ram',
'🐑': 'ewe',
'🐐': 'goat',
'🐪': 'camel',
'🐫': 'two-hump camel',
'🦙': 'llama',
'🦒': 'giraffe',
'🐘': 'elephant',
'🦣': 'mammoth',
'🦏': 'rhinoceros',
'🦛': 'hippopotamus',
'🐭': 'mouse face',
'🐁': 'mouse',
'🐀': 'rat',
'🐹': 'hamster',
'🐰': 'rabbit face',
'🐇': 'rabbit',
'🐿': 'chipmunk',
'🦫': 'beaver',
'🦔': 'hedgehog',
'🦇': 'bat',
'🐻': 'bear',
'🐻‍❄️': 'polar bear',
'🐨': 'koala',
'🐼': 'panda',
'🦥': 'sloth',
'🦦': 'otter',
'🦨': 'skunk',
'🦘': 'kangaroo',
'🦡': 'badger',
'🐾': 'paw prints',
'🦃': 'turkey',
'🐔': 'chicken',
'🐓': 'rooster',
'🐣': 'hatching chick',
'🐤': 'baby chick',
'🐥': 'front-facing baby chick',
'🐦': 'bird',
'🐧': 'penguin',
'🕊': 'dove',
'🦅': 'eagle',
'🦆': 'duck',
'🦢': 'swan',
'🦉': 'owl',
'🦤': 'dodo',
'🪶': 'feather',
'🦩': 'flamingo',
'🦚': 'peacock',
'🦜': 'parrot',
'🐸': 'frog',
'🐊': 'crocodile',
'🐢': 'turtle',
'🦎': 'lizard',
'🐍': 'snake',
'🐲': 'dragon face',
'🐉': 'dragon',
'🦕': 'sauropod',
'🦖': 'T-Rex',
'🐳': 'spouting whale',
'🐋': 'whale',
'🐬': 'dolphin',
'🦭': 'seal',
'🐟': 'fish',
'🐠': 'tropical fish',
'🐡': 'blowfish',
'🦈': 'shark',
'🐙': 'octopus',
'🐚': 'spiral shell',
'🪸': '⊛ coral',
'🐌': 'snail',
'🦋': 'butterfly',
'🐛': 'bug',
'🐜': 'ant',
'🐝': 'honeybee',
'🪲': 'beetle',
'🐞': 'lady beetle',
'🦗': 'cricket',
'🪳': 'cockroach',
'🕷': 'spider',
'🕸': 'spider web',
'🦂': 'scorpion',
'🦟': 'mosquito',
'🪰': 'fly',
'🪱': 'worm',
'🦠': 'microbe',
'💐': 'bouquet',
'🌸': 'cherry blossom',
'💮': 'white flower',
'🪷': '⊛ lotus',
'🏵': 'rosette',
'🌹': 'rose',
'🥀': 'wilted flower',
'🌺': 'hibiscus',
'🌻': 'sunflower',
'🌼': 'blossom',
'🌷': 'tulip',
'🌱': 'seedling',
'🪴': 'potted plant',
'🌲': 'evergreen tree',
'🌳': 'deciduous tree',
'🌴': 'palm tree',
'🌵': 'cactus',
'🌾': 'sheaf of rice',
'🌿': 'herb',
'☘': 'shamrock',
'🍀': 'four leaf clover',
'🍁': 'maple leaf',
'🍂': 'fallen leaf',
'🍃': 'leaf fluttering in wind',
'🪹': '⊛ empty nest',
'🪺': '⊛ nest with eggs',
'🍇': 'grapes',
'🍈': 'melon',
'🍉': 'watermelon',
'🍊': 'tangerine',
'🍋': 'lemon',
'🍌': 'banana',
'🍍': 'pineapple',
'🥭': 'mango',
'🍎': 'red apple',
'🍏': 'green apple',
'🍐': 'pear',
'🍑': 'peach',
'🍒': 'cherries',
'🍓': 'strawberry',
'🫐': 'blueberries',
'🥝': 'kiwi fruit',
'🍅': 'tomato',
'🫒': 'olive',
'🥥': 'coconut',
'🥑': 'avocado',
'🍆': 'eggplant',
'🥔': 'potato',
'🥕': 'carrot',
'🌽': 'ear of corn',
'🌶': 'hot pepper',
'🫑': 'bell pepper',
'🥒': 'cucumber',
'🥬': 'leafy green',
'🥦': 'broccoli',
'🧄': 'garlic',
'🧅': 'onion',
'🍄': 'mushroom',
'🥜': 'peanuts',
'🫘': '⊛ beans',
'🌰': 'chestnut',
'🍞': 'bread',
'🥐': 'croissant',
'🥖': 'baguette bread',
'🫓': 'flatbread',
'🥨': 'pretzel',
'🥯': 'bagel',
'🥞': 'pancakes',
'🧇': 'waffle',
'🧀': 'cheese wedge',
'🍖': 'meat on bone',
'🍗': 'poultry leg',
'🥩': 'cut of meat',
'🥓': 'bacon',
'🍔': 'hamburger',
'🍟': 'french fries',
'🍕': 'pizza',
'🌭': 'hot dog',
'🥪': 'sandwich',
'🌮': 'taco',
'🌯': 'burrito',
'🫔': 'tamale',
'🥙': 'stuffed flatbread',
'🧆': 'falafel',
'🥚': 'egg',
'🍳': 'cooking',
'🥘': 'shallow pan of food',
'🍲': 'pot of food',
'🫕': 'fondue',
'🥣': 'bowl with spoon',
'🥗': 'green salad',
'🍿': 'popcorn',
'🧈': 'butter',
'🧂': 'salt',
'🥫': 'canned food',
'🍱': 'bento box',
'🍘': 'rice cracker',
'🍙': 'rice ball',
'🍚': 'cooked rice',
'🍛': 'curry rice',
'🍜': 'steaming bowl',
'🍝': 'spaghetti',
'🍠': 'roasted sweet potato',
'🍢': 'oden',
'🍣': 'sushi',
'🍤': 'fried shrimp',
'🍥': 'fish cake with swirl',
'🥮': 'moon cake',
'🍡': 'dango',
'🥟': 'dumpling',
'🥠': 'fortune cookie',
'🥡': 'takeout box',
'🦀': 'crab',
'🦞': 'lobster',
'🦐': 'shrimp',
'🦑': 'squid',
'🦪': 'oyster',
'🍦': 'soft ice cream',
'🍧': 'shaved ice',
'🍨': 'ice cream',
'🍩': 'doughnut',
'🍪': 'cookie',
'🎂': 'birthday cake',
'🍰': 'shortcake',
'🧁': 'cupcake',
'🥧': 'pie',
'🍫': 'chocolate bar',
'🍬': 'candy',
'🍭': 'lollipop',
'🍮': 'custard',
'🍯': 'honey pot',
'🍼': 'baby bottle',
'🥛': 'glass of milk',
'☕': 'hot beverage',
'🫖': 'teapot',
'🍵': 'teacup without handle',
'🍶': 'sake',
'🍾': 'bottle with popping cork',
'🍷': 'wine glass',
'🍸': 'cocktail glass',
'🍹': 'tropical drink',
'🍺': 'beer mug',
'🍻': 'clinking beer mugs',
'🥂': 'clinking glasses',
'🥃': 'tumbler glass',
'🫗': '⊛ pouring liquid',
'🥤': 'cup with straw',
'🧋': 'bubble tea',
'🧃': 'beverage box',
'🧉': 'mate',
'🧊': 'ice',
'🥢': 'chopsticks',
'🍽': 'fork and knife with plate',
'🍴': 'fork and knife',
'🥄': 'spoon',
'🔪': 'kitchen knife',
'🫙': '⊛ jar',
'🏺': 'amphora',
'🌍': 'globe showing Europe-Africa',
'🌎': 'globe showing Americas',
'🌏': 'globe showing Asia-Australia',
'🌐': 'globe with meridians',
'🗺': 'world map',
'🗾': 'map of Japan',
'🧭': 'compass',
'🏔': 'snow-capped mountain',
'⛰': 'mountain',
'🌋': 'volcano',
'🗻': 'mount fuji',
'🏕': 'camping',
'🏖': 'beach with umbrella',
'🏜': 'desert',
'🏝': 'desert island',
'🏞': 'national park',
'🏟': 'stadium',
'🏛': 'classical building',
'🏗': 'building construction',
'🧱': 'brick',
'🪨': 'rock',
'🪵': 'wood',
'🛖': 'hut',
'🏘': 'houses',
'🏚': 'derelict house',
'🏠': 'house',
'🏡': 'house with garden',
'🏢': 'office building',
'🏣': 'Japanese post office',
'🏤': 'post office',
'🏥': 'hospital',
'🏦': 'bank',
'🏨': 'hotel',
'🏩': 'love hotel',
'🏪': 'convenience store',
'🏫': 'school',
'🏬': 'department store',
'🏭': 'factory',
'🏯': 'Japanese castle',
'🏰': 'castle',
'💒': 'wedding',
'🗼': 'Tokyo tower',
'🗽': 'Statue of Liberty',
'⛪': 'church',
'🕌': 'mosque',
'🛕': 'hindu temple',
'🕍': 'synagogue',
'⛩': 'shinto shrine',
'🕋': 'kaaba',
'⛲': 'fountain',
'⛺': 'tent',
'🌁': 'foggy',
'🌃': 'night with stars',
'🏙': 'cityscape',
'🌄': 'sunrise over mountains',
'🌅': 'sunrise',
'🌆': 'cityscape at dusk',
'🌇': 'sunset',
'🌉': 'bridge at night',
'♨': 'hot springs',
'🎠': 'carousel horse',
'🛝': '⊛ playground slide',
'🎡': 'ferris wheel',
'🎢': 'roller coaster',
'💈': 'barber pole',
'🎪': 'circus tent',
'🚂': 'locomotive',
'🚃': 'railway car',
'🚄': 'high-speed train',
'🚅': 'bullet train',
'🚆': 'train',
'🚇': 'metro',
'🚈': 'light rail',
'🚉': 'station',
'🚊': 'tram',
'🚝': 'monorail',
'🚞': 'mountain railway',
'🚋': 'tram car',
'🚌': 'bus',
'🚍': 'oncoming bus',
'🚎': 'trolleybus',
'🚐': 'minibus',
'🚑': 'ambulance',
'🚒': 'fire engine',
'🚓': 'police car',
'🚔': 'oncoming police car',
'🚕': 'taxi',
'🚖': 'oncoming taxi',
'🚗': 'automobile',
'🚘': 'oncoming automobile',
'🚙': 'sport utility vehicle',
'🛻': 'pickup truck',
'🚚': 'delivery truck',
'🚛': 'articulated lorry',
'🚜': 'tractor',
'🏎': 'racing car',
'🏍': 'motorcycle',
'🛵': 'motor scooter',
'🦽': 'manual wheelchair',
'🦼': 'motorized wheelchair',
'🛺': 'auto rickshaw',
'🚲': 'bicycle',
'🛴': 'kick scooter',
'🛹': 'skateboard',
'🛼': 'roller skate',
'🚏': 'bus stop',
'🛣': 'motorway',
'🛤': 'railway track',
'🛢': 'oil drum',
'⛽': 'fuel pump',
'🛞': '⊛ wheel',
'🚨': 'police car light',
'🚥': 'horizontal traffic light',
'🚦': 'vertical traffic light',
'🛑': 'stop sign',
'🚧': 'construction',
'⚓': 'anchor',
'🛟': '⊛ ring buoy',
'⛵': 'sailboat',
'🛶': 'canoe',
'🚤': 'speedboat',
'🛳': 'passenger ship',
'⛴': 'ferry',
'🛥': 'motor boat',
'🚢': 'ship',
'✈': 'airplane',
'🛩': 'small airplane',
'🛫': 'airplane departure',
'🛬': 'airplane arrival',
'🪂': 'parachute',
'💺': 'seat',
'🚁': 'helicopter',
'🚟': 'suspension railway',
'🚠': 'mountain cableway',
'🚡': 'aerial tramway',
'🛰': 'satellite',
'🚀': 'rocket',
'🛸': 'flying saucer',
'🛎': 'bellhop bell',
'🧳': 'luggage',
'⌛': 'hourglass done',
'⏳': 'hourglass not done',
'⌚': 'watch',
'⏰': 'alarm clock',
'⏱': 'stopwatch',
'⏲': 'timer clock',
'🕰': 'mantelpiece clock',
'🕛': 'twelve oclock',
'🕧': 'twelve-thirty',
'🕐': 'one oclock',
'🕜': 'one-thirty',
'🕑': 'two oclock',
'🕝': 'two-thirty',
'🕒': 'three oclock',
'🕞': 'three-thirty',
'🕓': 'four oclock',
'🕟': 'four-thirty',
'🕔': 'five oclock',
'🕠': 'five-thirty',
'🕕': 'six oclock',
'🕡': 'six-thirty',
'🕖': 'seven oclock',
'🕢': 'seven-thirty',
'🕗': 'eight oclock',
'🕣': 'eight-thirty',
'🕘': 'nine oclock',
'🕤': 'nine-thirty',
'🕙': 'ten oclock',
'🕥': 'ten-thirty',
'🕚': 'eleven oclock',
'🕦': 'eleven-thirty',
'🌑': 'new moon',
'🌒': 'waxing crescent moon',
'🌓': 'first quarter moon',
'🌔': 'waxing gibbous moon',
'🌕': 'full moon',
'🌖': 'waning gibbous moon',
'🌗': 'last quarter moon',
'🌘': 'waning crescent moon',
'🌙': 'crescent moon',
'🌚': 'new moon face',
'🌛': 'first quarter moon face',
'🌜': 'last quarter moon face',
'🌡': 'thermometer',
'☀': 'sun',
'🌝': 'full moon face',
'🌞': 'sun with face',
'🪐': 'ringed planet',
'⭐': 'star',
'🌟': 'glowing star',
'🌠': 'shooting star',
'🌌': 'milky way',
'☁': 'cloud',
'⛅': 'sun behind cloud',
'⛈': 'cloud with lightning and rain',
'🌤': 'sun behind small cloud',
'🌥': 'sun behind large cloud',
'🌦': 'sun behind rain cloud',
'🌧': 'cloud with rain',
'🌨': 'cloud with snow',
'🌩': 'cloud with lightning',
'🌪': 'tornado',
'🌫': 'fog',
'🌬': 'wind face',
'🌀': 'cyclone',
'🌈': 'rainbow',
'🌂': 'closed umbrella',
'☂': 'umbrella',
'☔': 'umbrella with rain drops',
'⛱': 'umbrella on ground',
'⚡': 'high voltage',
'❄': 'snowflake',
'☃': 'snowman',
'⛄': 'snowman without snow',
'☄': 'comet',
'🔥': 'fire',
'💧': 'droplet',
'🌊': 'water wave',
'🎃': 'jack-o-lantern',
'🎄': 'Christmas tree',
'🎆': 'fireworks',
'🎇': 'sparkler',
'🧨': 'firecracker',
'✨': 'sparkles',
'🎈': 'balloon',
'🎉': 'party popper',
'🎊': 'confetti ball',
'🎋': 'tanabata tree',
'🎍': 'pine decoration',
'🎎': 'Japanese dolls',
'🎏': 'carp streamer',
'🎐': 'wind chime',
'🎑': 'moon viewing ceremony',
'🧧': 'red envelope',
'🎀': 'ribbon',
'🎁': 'wrapped gift',
'🎗': 'reminder ribbon',
'🎟': 'admission tickets',
'🎫': 'ticket',
'🎖': 'military medal',
'🏆': 'trophy',
'🏅': 'sports medal',
'🥇': '1st place medal',
'🥈': '2nd place medal',
'🥉': '3rd place medal',
'⚽': 'soccer ball',
'⚾': 'baseball',
'🥎': 'softball',
'🏀': 'basketball',
'🏐': 'volleyball',
'🏈': 'american football',
'🏉': 'rugby football',
'🎾': 'tennis',
'🥏': 'flying disc',
'🎳': 'bowling',
'🏏': 'cricket game',
'🏑': 'field hockey',
'🏒': 'ice hockey',
'🥍': 'lacrosse',
'🏓': 'ping pong',
'🏸': 'badminton',
'🥊': 'boxing glove',
'🥋': 'martial arts uniform',
'🥅': 'goal net',
'⛳': 'flag in hole',
'⛸': 'ice skate',
'🎣': 'fishing pole',
'🤿': 'diving mask',
'🎽': 'running shirt',
'🎿': 'skis',
'🛷': 'sled',
'🥌': 'curling stone',
'🎯': 'bullseye',
'🪀': 'yo-yo',
'🪁': 'kite',
'🎱': 'pool 8 ball',
'🔮': 'crystal ball',
'🪄': 'magic wand',
'🧿': 'nazar amulet',
'🪬': '⊛ hamsa',
'🎮': 'video game',
'🕹': 'joystick',
'🎰': 'slot machine',
'🎲': 'game die',
'🧩': 'puzzle piece',
'🧸': 'teddy bear',
'🪅': 'piñata',
'🪩': '⊛ mirror ball',
'🪆': 'nesting dolls',
'♠': 'spade suit',
'♥': 'heart suit',
'♦': 'diamond suit',
'♣': 'club suit',
'♟': 'chess pawn',
'🃏': 'joker',
'🀄': 'mahjong red dragon',
'🎴': 'flower playing cards',
'🎭': 'performing arts',
'🖼': 'framed picture',
'🎨': 'artist palette',
'🧵': 'thread',
'🪡': 'sewing needle',
'🧶': 'yarn',
'🪢': 'knot',
'👓': 'glasses',
'🕶': 'sunglasses',
'🥽': 'goggles',
'🥼': 'lab coat',
'🦺': 'safety vest',
'👔': 'necktie',
'👕': 't-shirt',
'👖': 'jeans',
'🧣': 'scarf',
'🧤': 'gloves',
'🧥': 'coat',
'🧦': 'socks',
'👗': 'dress',
'👘': 'kimono',
'🥻': 'sari',
'🩱': 'one-piece swimsuit',
'🩲': 'briefs',
'🩳': 'shorts',
'👙': 'bikini',
'👚': 'womans clothes',
'👛': 'purse',
'👜': 'handbag',
'👝': 'clutch bag',
'🛍': 'shopping bags',
'🎒': 'backpack',
'🩴': 'thong sandal',
'👞': 'mans shoe',
'👟': 'running shoe',
'🥾': 'hiking boot',
'🥿': 'flat shoe',
'👠': 'high-heeled shoe',
'👡': 'womans sandal',
'🩰': 'ballet shoes',
'👢': 'womans boot',
'👑': 'crown',
'👒': 'womans hat',
'🎩': 'top hat',
'🎓': 'graduation cap',
'🧢': 'billed cap',
'🪖': 'military helmet',
'⛑': 'rescue workers helmet',
'📿': 'prayer beads',
'💄': 'lipstick',
'💍': 'ring',
'💎': 'gem stone',
'🔇': 'muted speaker',
'🔈': 'speaker low volume',
'🔉': 'speaker medium volume',
'🔊': 'speaker high volume',
'📢': 'loudspeaker',
'📣': 'megaphone',
'📯': 'postal horn',
'🔔': 'bell',
'🔕': 'bell with slash',
'🎼': 'musical score',
'🎵': 'musical note',
'🎶': 'musical notes',
'🎙': 'studio microphone',
'🎚': 'level slider',
'🎛': 'control knobs',
'🎤': 'microphone',
'🎧': 'headphone',
'📻': 'radio',
'🎷': 'saxophone',
'🪗': 'accordion',
'🎸': 'guitar',
'🎹': 'musical keyboard',
'🎺': 'trumpet',
'🎻': 'violin',
'🪕': 'banjo',
'🥁': 'drum',
'🪘': 'long drum',
'📱': 'mobile phone',
'📲': 'mobile phone with arrow',
'☎': 'telephone',
'📞': 'telephone receiver',
'📟': 'pager',
'📠': 'fax machine',
'🔋': 'battery',
'🪫': '⊛ low battery',
'🔌': 'electric plug',
'💻': 'laptop',
'🖥': 'desktop computer',
'🖨': 'printer',
'⌨': 'keyboard',
'🖱': 'computer mouse',
'🖲': 'trackball',
'💽': 'computer disk',
'💾': 'floppy disk',
'💿': 'optical disk',
'📀': 'dvd',
'🧮': 'abacus',
'🎥': 'movie camera',
'🎞': 'film frames',
'📽': 'film projector',
'🎬': 'clapper board',
'📺': 'television',
'📷': 'camera',
'📸': 'camera with flash',
'📹': 'video camera',
'📼': 'videocassette',
'🔍': 'magnifying glass tilted left',
'🔎': 'magnifying glass tilted right',
'🕯': 'candle',
'💡': 'light bulb',
'🔦': 'flashlight',
'🏮': 'red paper lantern',
'🪔': 'diya lamp',
'📔': 'notebook with decorative cover',
'📕': 'closed book',
'📖': 'open book',
'📗': 'green book',
'📘': 'blue book',
'📙': 'orange book',
'📚': 'books',
'📓': 'notebook',
'📒': 'ledger',
'📃': 'page with curl',
'📜': 'scroll',
'📄': 'page facing up',
'📰': 'newspaper',
'🗞': 'rolled-up newspaper',
'📑': 'bookmark tabs',
'🔖': 'bookmark',
'🏷': 'label',
'💰': 'money bag',
'🪙': 'coin',
'💴': 'yen banknote',
'💵': 'dollar banknote',
'💶': 'euro banknote',
'💷': 'pound banknote',
'💸': 'money with wings',
'💳': 'credit card',
'🧾': 'receipt',
'💹': 'chart increasing with yen',
'✉': 'envelope',
'📧': 'e-mail',
'📨': 'incoming envelope',
'📩': 'envelope with arrow',
'📤': 'outbox tray',
'📥': 'inbox tray',
'📦': 'package',
'📫': 'closed mailbox with raised flag',
'📪': 'closed mailbox with lowered flag',
'📬': 'open mailbox with raised flag',
'📭': 'open mailbox with lowered flag',
'📮': 'postbox',
'🗳': 'ballot box with ballot',
'✏': 'pencil',
'✒': 'black nib',
'🖋': 'fountain pen',
'🖊': 'pen',
'🖌': 'paintbrush',
'🖍': 'crayon',
'📝': 'memo',
'💼': 'briefcase',
'📁': 'file folder',
'📂': 'open file folder',
'🗂': 'card index dividers',
'📅': 'calendar',
'📆': 'tear-off calendar',
'🗒': 'spiral notepad',
'🗓': 'spiral calendar',
'📇': 'card index',
'📈': 'chart increasing',
'📉': 'chart decreasing',
'📊': 'bar chart',
'📋': 'clipboard',
'📌': 'pushpin',
'📍': 'round pushpin',
'📎': 'paperclip',
'🖇': 'linked paperclips',
'📏': 'straight ruler',
'📐': 'triangular ruler',
'✂': 'scissors',
'🗃': 'card file box',
'🗄': 'file cabinet',
'🗑': 'wastebasket',
'🔒': 'locked',
'🔓': 'unlocked',
'🔏': 'locked with pen',
'🔐': 'locked with key',
'🔑': 'key',
'🗝': 'old key',
'🔨': 'hammer',
'🪓': 'axe',
'⛏': 'pick',
'⚒': 'hammer and pick',
'🛠': 'hammer and wrench',
'🗡': 'dagger',
'⚔': 'crossed swords',
'🔫': 'water pistol',
'🪃': 'boomerang',
'🏹': 'bow and arrow',
'🛡': 'shield',
'🪚': 'carpentry saw',
'🔧': 'wrench',
'🪛': 'screwdriver',
'🔩': 'nut and bolt',
'⚙': 'gear',
'🗜': 'clamp',
'⚖': 'balance scale',
'🦯': 'white cane',
'🔗': 'link',
'⛓': 'chains',
'🪝': 'hook',
'🧰': 'toolbox',
'🧲': 'magnet',
'🪜': 'ladder',
'⚗': 'alembic',
'🧪': 'test tube',
'🧫': 'petri dish',
'🧬': 'dna',
'🔬': 'microscope',
'🔭': 'telescope',
'📡': 'satellite antenna',
'💉': 'syringe',
'🩸': 'drop of blood',
'💊': 'pill',
'🩹': 'adhesive bandage',
'🩼': '⊛ crutch',
'🩺': 'stethoscope',
'🩻': '⊛ x-ray',
'🚪': 'door',
'🛗': 'elevator',
'🪞': 'mirror',
'🪟': 'window',
'🛏': 'bed',
'🛋': 'couch and lamp',
'🪑': 'chair',
'🚽': 'toilet',
'🪠': 'plunger',
'🚿': 'shower',
'🛁': 'bathtub',
'🪤': 'mouse trap',
'🪒': 'razor',
'🧴': 'lotion bottle',
'🧷': 'safety pin',
'🧹': 'broom',
'🧺': 'basket',
'🧻': 'roll of paper',
'🪣': 'bucket',
'🧼': 'soap',
'🫧': '⊛ bubbles',
'🪥': 'toothbrush',
'🧽': 'sponge',
'🧯': 'fire extinguisher',
'🛒': 'shopping cart',
'🚬': 'cigarette',
'⚰': 'coffin',
'🪦': 'headstone',
'⚱': 'funeral urn',
'🗿': 'moai',
'🪧': 'placard',
'🪪': '⊛ identification card',
'🏧': 'ATM sign',
'🚮': 'litter in bin sign',
'🚰': 'potable water',
'♿': 'wheelchair symbol',
'🚹': 'mens room',
'🚺': 'womens room',
'🚻': 'restroom',
'🚼': 'baby symbol',
'🚾': 'water closet',
'🛂': 'passport control',
'🛃': 'customs',
'🛄': 'baggage claim',
'🛅': 'left luggage',
'⚠': 'warning',
'🚸': 'children crossing',
'⛔': 'no entry',
'🚫': 'prohibited',
'🚳': 'no bicycles',
'🚭': 'no smoking',
'🚯': 'no littering',
'🚱': 'non-potable water',
'🚷': 'no pedestrians',
'📵': 'no mobile phones',
'🔞': 'no one under eighteen',
'☢': 'radioactive',
'☣': 'biohazard',
'⬆': 'up arrow',
'↗': 'up-right arrow',
'➡': 'right arrow',
'↘': 'down-right arrow',
'⬇': 'down arrow',
'↙': 'down-left arrow',
'⬅': 'left arrow',
'↖': 'up-left arrow',
'↕': 'up-down arrow',
'↔': 'left-right arrow',
'↩': 'right arrow curving left',
'↪': 'left arrow curving right',
'⤴': 'right arrow curving up',
'⤵': 'right arrow curving down',
'🔃': 'clockwise vertical arrows',
'🔄': 'counterclockwise arrows button',
'🔙': 'BACK arrow',
'🔚': 'END arrow',
'🔛': 'ON! arrow',
'🔜': 'SOON arrow',
'🔝': 'TOP arrow',
'🛐': 'place of worship',
'⚛': 'atom symbol',
'🕉': 'om',
'✡': 'star of David',
'☸': 'wheel of dharma',
'☯': 'yin yang',
'✝': 'latin cross',
'☦': 'orthodox cross',
'☪': 'star and crescent',
'☮': 'peace symbol',
'🕎': 'menorah',
'🔯': 'dotted six-pointed star',
'♈': 'Aries',
'♉': 'Taurus',
'♊': 'Gemini',
'♋': 'Cancer',
'♌': 'Leo',
'♍': 'Virgo',
'♎': 'Libra',
'♏': 'Scorpio',
'♐': 'Sagittarius',
'♑': 'Capricorn',
'♒': 'Aquarius',
'♓': 'Pisces',
'⛎': 'Ophiuchus',
'🔀': 'shuffle tracks button',
'🔁': 'repeat button',
'🔂': 'repeat single button',
'▶': 'play button',
'⏩': 'fast-forward button',
'⏭': 'next track button',
'⏯': 'play or pause button',
'◀': 'reverse button',
'⏪': 'fast reverse button',
'⏮': 'last track button',
'🔼': 'upwards button',
'⏫': 'fast up button',
'🔽': 'downwards button',
'⏬': 'fast down button',
'⏸': 'pause button',
'⏹': 'stop button',
'⏺': 'record button',
'⏏': 'eject button',
'🎦': 'cinema',
'🔅': 'dim button',
'🔆': 'bright button',
'📶': 'antenna bars',
'📳': 'vibration mode',
'📴': 'mobile phone off',
'♀': 'female sign',
'♂': 'male sign',
'⚧': 'transgender symbol',
'✖': 'multiply',
'': 'plus',
'': 'minus',
'➗': 'divide',
'🟰': '⊛ heavy equals sign',
'♾': 'infinity',
'‼': 'double exclamation mark',
'⁉': 'exclamation question mark',
'❓': 'red question mark',
'❔': 'white question mark',
'❕': 'white exclamation mark',
'❗': 'red exclamation mark',
'〰': 'wavy dash',
'💱': 'currency exchange',
'💲': 'heavy dollar sign',
'⚕': 'medical symbol',
'♻': 'recycling symbol',
'⚜': 'fleur-de-lis',
'🔱': 'trident emblem',
'📛': 'name badge',
'🔰': 'Japanese symbol for beginner',
'⭕': 'hollow red circle',
'✅': 'check mark button',
'☑': 'check box with check',
'✔': 'check mark',
'❌': 'cross mark',
'❎': 'cross mark button',
'➰': 'curly loop',
'➿': 'double curly loop',
'〽': 'part alternation mark',
'✳': 'eight-spoked asterisk',
'✴': 'eight-pointed star',
'❇': 'sparkle',
'©': 'copyright',
'®': 'registered',
'™': 'trade mark',
'#️⃣': 'keycap: #',
'*️⃣': 'keycap: *',
'0⃣': 'keycap: 0',
'1⃣': 'keycap: 1',
'2⃣': 'keycap: 2',
'3⃣': 'keycap: 3',
'4⃣': 'keycap: 4',
'5⃣': 'keycap: 5',
'6⃣': 'keycap: 6',
'7⃣': 'keycap: 7',
'8⃣': 'keycap: 8',
'9⃣': 'keycap: 9',
'🔟': 'keycap: 10',
'🔠': 'input latin uppercase',
'🔡': 'input latin lowercase',
'🔢': 'input numbers',
'🔣': 'input symbols',
'🔤': 'input latin letters',
'🅰': 'A button (blood type)',
'🆎': 'AB button (blood type)',
'🅱': 'B button (blood type)',
'🆑': 'CL button',
'🆒': 'COOL button',
'🆓': 'FREE button',
: 'information',
'🆔': 'ID button',
'Ⓜ': 'circled M',
'🆕': 'NEW button',
'🆖': 'NG button',
'🅾': 'O button (blood type)',
'🆗': 'OK button',
'🅿': 'P button',
'🆘': 'SOS button',
'🆙': 'UP! button',
'🆚': 'VS button',
'🈁': 'Japanese “here” button',
'🈂': 'Japanese “service charge” button',
'🈷': 'Japanese “monthly amount” button',
'🈶': 'Japanese “not free of charge” button',
'🈯': 'Japanese “reserved” button',
'🉐': 'Japanese “bargain” button',
'🈹': 'Japanese “discount” button',
'🈚': 'Japanese “free of charge” button',
'🈲': 'Japanese “prohibited” button',
'🉑': 'Japanese “acceptable” button',
'🈸': 'Japanese “application” button',
'🈴': 'Japanese “passing grade” button',
'🈳': 'Japanese “vacancy” button',
'㊗': 'Japanese “congratulations” button',
'㊙': 'Japanese “secret” button',
'🈺': 'Japanese “open for business” button',
'🈵': 'Japanese “no vacancy” button',
'🔴': 'red circle',
'🟠': 'orange circle',
'🟡': 'yellow circle',
'🟢': 'green circle',
'🔵': 'blue circle',
'🟣': 'purple circle',
'🟤': 'brown circle',
'⚫': 'black circle',
'⚪': 'white circle',
'🟥': 'red square',
'🟧': 'orange square',
'🟨': 'yellow square',
'🟩': 'green square',
'🟦': 'blue square',
'🟪': 'purple square',
'🟫': 'brown square',
'⬛': 'black large square',
'⬜': 'white large square',
'◼': 'black medium square',
'◻': 'white medium square',
'◾': 'black medium-small square',
'◽': 'white medium-small square',
'▪': 'black small square',
'▫': 'white small square',
'🔶': 'large orange diamond',
'🔷': 'large blue diamond',
'🔸': 'small orange diamond',
'🔹': 'small blue diamond',
'🔺': 'red triangle pointed up',
'🔻': 'red triangle pointed down',
'💠': 'diamond with a dot',
'🔘': 'radio button',
'🔳': 'white square button',
'🔲': 'black square button',
'🏁': 'chequered flag',
'🚩': 'triangular flag',
'🎌': 'crossed flags',
'🏴': 'black flag',
'🏳': 'white flag',
'🏳️‍🌈': 'rainbow flag',
'🏳️‍⚧️': 'transgender flag',
'🏴‍☠️': 'pirate flag',
'🇦🇨': 'flag: Ascension Island',
'🇦🇩': 'flag: Andorra',
'🇦🇪': 'flag: United Arab Emirates',
'🇦🇫': 'flag: Afghanistan',
'🇦🇬': 'flag: Antigua & Barbuda',
'🇦🇮': 'flag: Anguilla',
'🇦🇱': 'flag: Albania',
'🇦🇲': 'flag: Armenia',
'🇦🇴': 'flag: Angola',
'🇦🇶': 'flag: Antarctica',
'🇦🇷': 'flag: Argentina',
'🇦🇸': 'flag: American Samoa',
'🇦🇹': 'flag: Austria',
'🇦🇺': 'flag: Australia',
'🇦🇼': 'flag: Aruba',
'🇦🇽': 'flag: Åland Islands',
'🇦🇿': 'flag: Azerbaijan',
'🇧🇦': 'flag: Bosnia & Herzegovina',
'🇧🇧': 'flag: Barbados',
'🇧🇩': 'flag: Bangladesh',
'🇧🇪': 'flag: Belgium',
'🇧🇫': 'flag: Burkina Faso',
'🇧🇬': 'flag: Bulgaria',
'🇧🇭': 'flag: Bahrain',
'🇧🇮': 'flag: Burundi',
'🇧🇯': 'flag: Benin',
'🇧🇱': 'flag: St. Barthélemy',
'🇧🇲': 'flag: Bermuda',
'🇧🇳': 'flag: Brunei',
'🇧🇴': 'flag: Bolivia',
'🇧🇶': 'flag: Caribbean Netherlands',
'🇧🇷': 'flag: Brazil',
'🇧🇸': 'flag: Bahamas',
'🇧🇹': 'flag: Bhutan',
'🇧🇻': 'flag: Bouvet Island',
'🇧🇼': 'flag: Botswana',
'🇧🇾': 'flag: Belarus',
'🇧🇿': 'flag: Belize',
'🇨🇦': 'flag: Canada',
'🇨🇨': 'flag: Cocos (Keeling) Islands',
'🇨🇩': 'flag: Congo - Kinshasa',
'🇨🇫': 'flag: Central African Republic',
'🇨🇬': 'flag: Congo - Brazzaville',
'🇨🇭': 'flag: Switzerland',
'🇨🇮': 'flag: Côte dIvoire',
'🇨🇰': 'flag: Cook Islands',
'🇨🇱': 'flag: Chile',
'🇨🇲': 'flag: Cameroon',
'🇨🇳': 'flag: China',
'🇨🇴': 'flag: Colombia',
'🇨🇵': 'flag: Clipperton Island',
'🇨🇷': 'flag: Costa Rica',
'🇨🇺': 'flag: Cuba',
'🇨🇻': 'flag: Cape Verde',
'🇨🇼': 'flag: Curaçao',
'🇨🇽': 'flag: Christmas Island',
'🇨🇾': 'flag: Cyprus',
'🇨🇿': 'flag: Czechia',
'🇩🇪': 'flag: Germany',
'🇩🇬': 'flag: Diego Garcia',
'🇩🇯': 'flag: Djibouti',
'🇩🇰': 'flag: Denmark',
'🇩🇲': 'flag: Dominica',
'🇩🇴': 'flag: Dominican Republic',
'🇩🇿': 'flag: Algeria',
'🇪🇦': 'flag: Ceuta & Melilla',
'🇪🇨': 'flag: Ecuador',
'🇪🇪': 'flag: Estonia',
'🇪🇬': 'flag: Egypt',
'🇪🇭': 'flag: Western Sahara',
'🇪🇷': 'flag: Eritrea',
'🇪🇸': 'flag: Spain',
'🇪🇹': 'flag: Ethiopia',
'🇪🇺': 'flag: European Union',
'🇫🇮': 'flag: Finland',
'🇫🇯': 'flag: Fiji',
'🇫🇰': 'flag: Falkland Islands',
'🇫🇲': 'flag: Micronesia',
'🇫🇴': 'flag: Faroe Islands',
'🇫🇷': 'flag: France',
'🇬🇦': 'flag: Gabon',
'🇬🇧': 'flag: United Kingdom',
'🇬🇩': 'flag: Grenada',
'🇬🇪': 'flag: Georgia',
'🇬🇫': 'flag: French Guiana',
'🇬🇬': 'flag: Guernsey',
'🇬🇭': 'flag: Ghana',
'🇬🇮': 'flag: Gibraltar',
'🇬🇱': 'flag: Greenland',
'🇬🇲': 'flag: Gambia',
'🇬🇳': 'flag: Guinea',
'🇬🇵': 'flag: Guadeloupe',
'🇬🇶': 'flag: Equatorial Guinea',
'🇬🇷': 'flag: Greece',
'🇬🇸': 'flag: South Georgia & South Sandwich Islands',
'🇬🇹': 'flag: Guatemala',
'🇬🇺': 'flag: Guam',
'🇬🇼': 'flag: Guinea-Bissau',
'🇬🇾': 'flag: Guyana',
'🇭🇰': 'flag: Hong Kong SAR China',
'🇭🇲': 'flag: Heard & McDonald Islands',
'🇭🇳': 'flag: Honduras',
'🇭🇷': 'flag: Croatia',
'🇭🇹': 'flag: Haiti',
'🇭🇺': 'flag: Hungary',
'🇮🇨': 'flag: Canary Islands',
'🇮🇩': 'flag: Indonesia',
'🇮🇪': 'flag: Ireland',
'🇮🇱': 'flag: Israel',
'🇮🇲': 'flag: Isle of Man',
'🇮🇳': 'flag: India',
'🇮🇴': 'flag: British Indian Ocean Territory',
'🇮🇶': 'flag: Iraq',
'🇮🇷': 'flag: Iran',
'🇮🇸': 'flag: Iceland',
'🇮🇹': 'flag: Italy',
'🇯🇪': 'flag: Jersey',
'🇯🇲': 'flag: Jamaica',
'🇯🇴': 'flag: Jordan',
'🇯🇵': 'flag: Japan',
'🇰🇪': 'flag: Kenya',
'🇰🇬': 'flag: Kyrgyzstan',
'🇰🇭': 'flag: Cambodia',
'🇰🇮': 'flag: Kiribati',
'🇰🇲': 'flag: Comoros',
'🇰🇳': 'flag: St. Kitts & Nevis',
'🇰🇵': 'flag: North Korea',
'🇰🇷': 'flag: South Korea',
'🇰🇼': 'flag: Kuwait',
'🇰🇾': 'flag: Cayman Islands',
'🇰🇿': 'flag: Kazakhstan',
'🇱🇦': 'flag: Laos',
'🇱🇧': 'flag: Lebanon',
'🇱🇨': 'flag: St. Lucia',
'🇱🇮': 'flag: Liechtenstein',
'🇱🇰': 'flag: Sri Lanka',
'🇱🇷': 'flag: Liberia',
'🇱🇸': 'flag: Lesotho',
'🇱🇹': 'flag: Lithuania',
'🇱🇺': 'flag: Luxembourg',
'🇱🇻': 'flag: Latvia',
'🇱🇾': 'flag: Libya',
'🇲🇦': 'flag: Morocco',
'🇲🇨': 'flag: Monaco',
'🇲🇩': 'flag: Moldova',
'🇲🇪': 'flag: Montenegro',
'🇲🇫': 'flag: St. Martin',
'🇲🇬': 'flag: Madagascar',
'🇲🇭': 'flag: Marshall Islands',
'🇲🇰': 'flag: North Macedonia',
'🇲🇱': 'flag: Mali',
'🇲🇲': 'flag: Myanmar (Burma)',
'🇲🇳': 'flag: Mongolia',
'🇲🇴': 'flag: Macao SAR China',
'🇲🇵': 'flag: Northern Mariana Islands',
'🇲🇶': 'flag: Martinique',
'🇲🇷': 'flag: Mauritania',
'🇲🇸': 'flag: Montserrat',
'🇲🇹': 'flag: Malta',
'🇲🇺': 'flag: Mauritius',
'🇲🇻': 'flag: Maldives',
'🇲🇼': 'flag: Malawi',
'🇲🇽': 'flag: Mexico',
'🇲🇾': 'flag: Malaysia',
'🇲🇿': 'flag: Mozambique',
'🇳🇦': 'flag: Namibia',
'🇳🇨': 'flag: New Caledonia',
'🇳🇪': 'flag: Niger',
'🇳🇫': 'flag: Norfolk Island',
'🇳🇬': 'flag: Nigeria',
'🇳🇮': 'flag: Nicaragua',
'🇳🇱': 'flag: Netherlands',
'🇳🇴': 'flag: Norway',
'🇳🇵': 'flag: Nepal',
'🇳🇷': 'flag: Nauru',
'🇳🇺': 'flag: Niue',
'🇳🇿': 'flag: New Zealand',
'🇴🇲': 'flag: Oman',
'🇵🇦': 'flag: Panama',
'🇵🇪': 'flag: Peru',
'🇵🇫': 'flag: French Polynesia',
'🇵🇬': 'flag: Papua New Guinea',
'🇵🇭': 'flag: Philippines',
'🇵🇰': 'flag: Pakistan',
'🇵🇱': 'flag: Poland',
'🇵🇲': 'flag: St. Pierre & Miquelon',
'🇵🇳': 'flag: Pitcairn Islands',
'🇵🇷': 'flag: Puerto Rico',
'🇵🇸': 'flag: Palestinian Territories',
'🇵🇹': 'flag: Portugal',
'🇵🇼': 'flag: Palau',
'🇵🇾': 'flag: Paraguay',
'🇶🇦': 'flag: Qatar',
'🇷🇪': 'flag: Réunion',
'🇷🇴': 'flag: Romania',
'🇷🇸': 'flag: Serbia',
'🇷🇺': 'flag: Russia',
'🇷🇼': 'flag: Rwanda',
'🇸🇦': 'flag: Saudi Arabia',
'🇸🇧': 'flag: Solomon Islands',
'🇸🇨': 'flag: Seychelles',
'🇸🇩': 'flag: Sudan',
'🇸🇪': 'flag: Sweden',
'🇸🇬': 'flag: Singapore',
'🇸🇭': 'flag: St. Helena',
'🇸🇮': 'flag: Slovenia',
'🇸🇯': 'flag: Svalbard & Jan Mayen',
'🇸🇰': 'flag: Slovakia',
'🇸🇱': 'flag: Sierra Leone',
'🇸🇲': 'flag: San Marino',
'🇸🇳': 'flag: Senegal',
'🇸🇴': 'flag: Somalia',
'🇸🇷': 'flag: Suriname',
'🇸🇸': 'flag: South Sudan',
'🇸🇹': 'flag: São Tomé & Príncipe',
'🇸🇻': 'flag: El Salvador',
'🇸🇽': 'flag: Sint Maarten',
'🇸🇾': 'flag: Syria',
'🇸🇿': 'flag: Eswatini',
'🇹🇦': 'flag: Tristan da Cunha',
'🇹🇨': 'flag: Turks & Caicos Islands',
'🇹🇩': 'flag: Chad',
'🇹🇫': 'flag: French Southern Territories',
'🇹🇬': 'flag: Togo',
'🇹🇭': 'flag: Thailand',
'🇹🇯': 'flag: Tajikistan',
'🇹🇰': 'flag: Tokelau',
'🇹🇱': 'flag: Timor-Leste',
'🇹🇲': 'flag: Turkmenistan',
'🇹🇳': 'flag: Tunisia',
'🇹🇴': 'flag: Tonga',
'🇹🇷': 'flag: Turkey',
'🇹🇹': 'flag: Trinidad & Tobago',
'🇹🇻': 'flag: Tuvalu',
'🇹🇼': 'flag: Taiwan',
'🇹🇿': 'flag: Tanzania',
'🇺🇦': 'flag: Ukraine',
'🇺🇬': 'flag: Uganda',
'🇺🇲': 'flag: U.S. Outlying Islands',
'🇺🇳': 'flag: United Nations',
'🇺🇸': 'flag: United States',
'🇺🇾': 'flag: Uruguay',
'🇺🇿': 'flag: Uzbekistan',
'🇻🇦': 'flag: Vatican City',
'🇻🇨': 'flag: St. Vincent & Grenadines',
'🇻🇪': 'flag: Venezuela',
'🇻🇬': 'flag: British Virgin Islands',
'🇻🇮': 'flag: U.S. Virgin Islands',
'🇻🇳': 'flag: Vietnam',
'🇻🇺': 'flag: Vanuatu',
'🇼🇫': 'flag: Wallis & Futuna',
'🇼🇸': 'flag: Samoa',
'🇽🇰': 'flag: Kosovo',
'🇾🇪': 'flag: Yemen',
'🇾🇹': 'flag: Mayotte',
'🇿🇦': 'flag: South Africa',
'🇿🇲': 'flag: Zambia',
'🇿🇼': 'flag: Zimbabwe',
'🏴󠁧󠁢󠁥󠁮󠁧󠁿': 'flag: England',
'🏴󠁧󠁢󠁳󠁣󠁴󠁿': 'flag: Scotland',
'🏴󠁧󠁢󠁷󠁬󠁳󠁿': 'flag: Wales',
};
const isEmoji = (str) => {
const ranges = [
'(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])', // U+1F680 to U+1F6FF
];
if (str.match(ranges.join('|'))) {
return true;
}
else {
return false;
}
};
var emoji = {
shortNames,
isEmoji,
};
// Default obsidian file icon.
const DEFAULT_FILE_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-file"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>';
// Default obsidian folder icon.
const DEFAULT_FOLDER_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-folder"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"></path></svg>';
/**
* Tries to read the file synchronously.
* @param file File that will be read.
* @returns A promise that will resolve to a string which is the content of the file.
*/
const readFileSync = (file) => __awaiter(void 0, void 0, void 0, function* () {
const content = yield new Promise((resolve) => {
const reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = (readerEvent) => resolve(readerEvent.target.result);
});
return content;
});
/**
* Gets all the currently opened files by getting the markdown leaves and then checking
* for the `file` property in the view.
* @param plugin Instance of the IconFolderPlugin.
* @returns An array of {@link TFile} objects.
*/
const getAllOpenedFiles = (plugin) => {
return plugin.app.workspace.getLeavesOfType('markdown').reduce((prev, curr) => {
const file = curr.view.file;
if (file) {
prev.push(file);
}
return prev;
}, []);
};
/**
* Gets the file item title element by either accessing `titleEl` or `selfEl`.
* @param fileItem FileItem which will be used to retrieve the title element from.
* @returns HTMLElement which is the title element.
*/
const getFileItemTitleEl = (fileItem) => {
var _a;
return (_a = fileItem.titleEl) !== null && _a !== void 0 ? _a : fileItem.selfEl;
};
/**
* Gets the file item inner title element by either accessing `titleInnerEl` or `innerEl`.
* @param fileItem FileItem which will be used to retrieve the inner title element from.
* @returns HTMLElement which is the inner title element.
*/
const getFileItemInnerTitleEl = (fileItem) => {
var _a;
return (_a = fileItem.titleInnerEl) !== null && _a !== void 0 ? _a : fileItem.innerEl;
};
/**
* A utility function which will add the icon to the icon pack and then extract the icon
* to the icon pack.
* @param plugin IconFolderPlugin that will be used for extracting the icon.
* @param iconNameWithPrefix String that will be used to add the icon to the icon pack.
*/
const saveIconToIconPack = (plugin, iconNameWithPrefix) => {
const iconNextIdentifier = nextIdentifier(iconNameWithPrefix);
const iconName = iconNameWithPrefix.substring(iconNextIdentifier);
const iconPrefix = iconNameWithPrefix.substring(0, iconNextIdentifier);
const possibleIcon = getSvgFromLoadedIcon(iconPrefix, iconName);
if (!possibleIcon) {
console.error(`Icon ${iconNameWithPrefix} could not be found.`);
return;
}
const iconPackName = getIconPackNameByPrefix(iconPrefix);
const icon = addIconToIconPack(iconPackName, `${iconName}.svg`, possibleIcon);
extractIconToIconPack(plugin, icon, possibleIcon);
};
/**
* A utility function which will remove the icon from the icon pack by removing the icon
* file from the icon pack directory.
* @param plugin IconFolderPlugin that will be used for removing the icon.
* @param iconNameWithPrefix String that will be used to remove the icon from the icon pack.
*/
const removeIconFromIconPack = (plugin, iconNameWithPrefix) => {
const identifier = nextIdentifier(iconNameWithPrefix);
const prefix = iconNameWithPrefix.substring(0, identifier);
const iconName = iconNameWithPrefix.substring(identifier);
const iconPackName = getIconPackNameByPrefix(prefix);
const duplicatedIcon = plugin.getDataPathByValue(iconNameWithPrefix);
if (!duplicatedIcon) {
removeIconFromIconPackDirectory(plugin, iconPackName, iconName);
}
};
// This library file does not include any other dependency and is a standalone file that
/**
* Sets the margin for a specific node.
* @param el Node where the margin will be set.
* @param margin Margin that will be applied to the node.
* @returns The modified node with the applied margin.
*/
const setMargin = (el, margin) => {
el.style.margin = `${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px`;
return el;
};
/**
* Applies all stylings to the specified svg icon string and applies styling to the node
* (container). The styling to the specified element is only modified when it is an emoji
* or extra margin is defined in the settings.
* @param plugin Instance of the IconFolderPlugin.
* @param iconString SVG that will be used to apply the svg styles to.
* @param el Node for manipulating the style.
* @returns Icon svg string with the manipulate style attributes.
*/
const applyAll = (plugin, iconString, container) => {
iconString = svg.setFontSize(iconString, plugin.getSettings().fontSize);
container.style.color = plugin.getSettings().iconColor;
iconString = svg.colorize(iconString, plugin.getSettings().iconColor);
// Sets the margin of an element.
const margin = plugin.getSettings().extraMargin;
const normalizedMargin = {
top: margin.top !== undefined ? margin.top : 4,
right: margin.right !== undefined ? margin.right : 4,
left: margin.left !== undefined ? margin.left : 4,
bottom: margin.bottom !== undefined ? margin.bottom : 4,
};
if (plugin.getSettings().extraMargin) {
setMargin(container, normalizedMargin);
}
if (emoji.isEmoji(iconString)) {
container.style.fontSize = `${plugin.getSettings().fontSize}px`;
container.style.lineHeight = `${plugin.getSettings().fontSize}px`;
}
return iconString;
};
/**
* Refreshes all the styles of all the applied icons where a `.obsidian-icon-folder-icon`
* class is defined. This function only modifies the styling of the node.
* @param plugin Instance of the IconFolderPlugin.
*/
const refreshIconNodes = (plugin) => {
const fileExplorers = plugin.app.workspace.getLeavesOfType('file-explorer');
for (const fileExplorer of fileExplorers) {
Object.keys(plugin.getData()).forEach((path) => {
const fileItem = fileExplorer.view.fileItems[path];
if (fileItem) {
const titleEl = getFileItemTitleEl(fileItem);
const iconNode = titleEl.querySelector('.obsidian-icon-folder-icon');
if (iconNode) {
iconNode.innerHTML = applyAll(plugin, iconNode.innerHTML, iconNode);
}
}
});
}
};
var style = {
applyAll,
setMargin,
refreshIconNodes,
};
/**
* Removes the `obsidian-icon-folder-icon` icon node from the provided HTMLElement.
* @param el HTMLElement from which the icon node will be removed.
*/
const removeIconInNode = (el) => {
const iconNode = el.querySelector('.obsidian-icon-folder-icon');
if (!iconNode) {
return;
}
iconNode.remove();
};
/**
* Removes the 'obsidian-icon-folder-icon' icon node from the HTMLElement corresponding
* to the specified file path.
* @param path File path for which the icon node will be removed.
*/
const removeIconInPath = (path, options) => {
var _a;
const node = (_a = options === null || options === void 0 ? void 0 : options.container) !== null && _a !== void 0 ? _a : document.querySelector(`[data-path="${path}"]`);
if (!node) {
console.error('element with data path not found', path);
return;
}
removeIconInNode(node);
};
/**
* Sets an icon or emoji for an HTMLElement based on the specified icon name and color.
* The function manipulates the specified node inline.
* @param plugin Instance of the IconFolderPlugin.
* @param iconName Name of the icon or emoji to add.
* @param node HTMLElement to which the icon or emoji will be added.
* @param color Optional color of the icon to add.
*/
const setIconForNode = (plugin, iconName, node, color) => {
// Gets the possible icon based on the icon name.
const iconNextIdentifier = nextIdentifier(iconName);
const possibleIcon = getSvgFromLoadedIcon(iconName.substring(0, iconNextIdentifier), iconName.substring(iconNextIdentifier));
if (possibleIcon) {
// The icon is possibly not an emoji.
let iconContent = style.applyAll(plugin, possibleIcon, node);
if (color) {
node.style.color = color;
iconContent = svg.colorize(possibleIcon, color);
}
node.innerHTML = iconContent;
}
else {
// The icon is an emoji.
let emoji = '';
switch (plugin.getSettings().emojiStyle) {
case 'twemoji':
emoji = twemoji.parse(iconName, {
base: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/',
folder: 'svg',
ext: '.svg',
attributes: () => ({
width: '16px',
height: '16px',
}),
});
break;
case 'native':
emoji = iconName;
}
node.innerHTML = style.applyAll(plugin, emoji, node);
}
};
/**
* Creates an icon node for the specified path and inserts it to the DOM.
* @param plugin Instance of the IconFolderPlugin.
* @param path Path for which the icon node will be created.
* @param iconName Name of the icon or emoji to add.
* @param color Optional color of the icon to add.
*/
const createIconNode = (plugin, path, iconName, options) => {
var _a;
// TODO: Refactor to more efficient solution.
if (plugin.getData()[path]) {
removeIconInPath(path, { container: options === null || options === void 0 ? void 0 : options.container });
}
// Get the container from the provided options or try to find the node that has the
// path from the document itself.
const node = (_a = options === null || options === void 0 ? void 0 : options.container) !== null && _a !== void 0 ? _a : document.querySelector(`[data-path="${path}"]`);
if (!node) {
console.error('element with data path not found', path);
return;
}
// Get the folder or file title node.
let titleNode = node.querySelector('.nav-folder-title-content');
if (!titleNode) {
titleNode = node.querySelector('.nav-file-title-content');
if (!titleNode) {
console.error('element with title not found');
return;
}
}
// Check for possible inheritance and remove the inherited icon node.
const possibleInheritanceIcon = node.querySelector('.obsidian-icon-folder-icon');
if (possibleInheritanceIcon) {
possibleInheritanceIcon.remove();
}
// Creates a new icon node and inserts it to the DOM.
const iconNode = document.createElement('div');
iconNode.classList.add('obsidian-icon-folder-icon');
setIconForNode(plugin, iconName, iconNode, options === null || options === void 0 ? void 0 : options.color);
node.insertBefore(iconNode, titleNode);
};
var dom = {
setIconForNode,
createIconNode,
removeIconInNode,
removeIconInPath,
};
class IconsPickerModal extends obsidian.FuzzySuggestModal {
constructor(app, plugin, path) {
super(app);
this.renderIndex = 0;
this.plugin = plugin;
this.path = path;
this.limit = 150;
const pluginRecentltyUsedItems = [...plugin.getSettings().recentlyUsedIcons];
this.recentlyUsedItems = new Set(pluginRecentltyUsedItems.reverse().filter((iconName) => {
return doesIconExists(iconName) || emoji.isEmoji(iconName);
}));
this.resultContainerEl.classList.add('obsidian-icon-folder-modal');
}
onOpen() {
super.onOpen();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
getItemText(item) {
return `${item.name} (${item.prefix})`;
}
getItems() {
const iconKeys = [];
if (this.inputEl.value.length === 0) {
this.renderIndex = 0;
this.recentlyUsedItems.forEach((iconName) => {
if (this.plugin.isSomeEmojiStyleActive() && emoji.isEmoji(iconName)) {
iconKeys.push({
name: emoji.shortNames[iconName],
prefix: 'Emoji',
displayName: iconName,
iconPackName: null,
});
return;
}
const nextLetter = nextIdentifier(iconName);
const iconPrefix = iconName.substring(0, nextLetter);
const iconPackName = getIconPackNameByPrefix(iconPrefix);
iconKeys.push({
name: iconName.substring(nextLetter),
prefix: iconPrefix,
displayName: iconName,
iconPackName: iconPackName,
});
});
}
for (const icon of getAllLoadedIconNames()) {
iconKeys.push({
name: icon.name,
prefix: icon.prefix,
displayName: icon.prefix + icon.name,
iconPackName: icon.iconPackName,
});
}
if (this.plugin.isSomeEmojiStyleActive()) {
Object.entries(emoji.shortNames).forEach(([unicode, shortName]) => {
iconKeys.push({
name: shortName,
prefix: 'Emoji',
displayName: unicode,
iconPackName: null,
});
iconKeys.push({
name: unicode,
prefix: 'Emoji',
displayName: unicode,
iconPackName: null,
});
});
}
return iconKeys;
}
onChooseItem(item) {
var _a;
const iconNameWithPrefix = typeof item === 'object' ? item.displayName : item;
dom.createIconNode(this.plugin, this.path, iconNameWithPrefix);
(_a = this.onSelect) === null || _a === void 0 ? void 0 : _a.call(this, iconNameWithPrefix);
this.plugin.addFolderIcon(this.path, item);
// Extracts the icon file to the icon pack.
if (typeof item === 'object') {
saveIconToIconPack(this.plugin, iconNameWithPrefix);
}
this.plugin.notifyPlugins();
}
renderSuggestion(item, el) {
super.renderSuggestion(item, el);
// if (getAllIconPacks().length === 0) {
// this.resultContainerEl.style.display = 'block';
// this.resultContainerEl.innerHTML = '<div class="suggestion-empty">You need to create an icon pack.</div>';
// return;
// }
// Render subheadlines for modal.
if (this.recentlyUsedItems.size !== 0 && this.inputEl.value.length === 0) {
if (this.renderIndex === 0) {
const subheadline = this.resultContainerEl.createDiv();
subheadline.classList.add('obsidian-icon-folder-subheadline');
subheadline.innerText = 'Recently used Icons:';
this.resultContainerEl.prepend(subheadline);
}
else if (this.renderIndex === this.recentlyUsedItems.size - 1) {
const subheadline = this.resultContainerEl.createDiv();
subheadline.classList.add('obsidian-icon-folder-subheadline');
subheadline.innerText = 'All Icons:';
this.resultContainerEl.append(subheadline);
}
}
if (item.item.name !== 'default') {
if (item.item.prefix === 'Emoji') {
let displayName = '';
switch (this.plugin.getSettings().emojiStyle) {
case 'twemoji':
displayName = twemoji.parse(item.item.displayName, {
base: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/',
});
break;
case 'native':
displayName = item.item.displayName;
break;
}
el.innerHTML = `<div>${el.innerHTML}</div><div class="obsidian-icon-folder-icon-preview">${displayName}</div>`;
}
else {
el.innerHTML = `<div>${el.innerHTML}</div><div class="obsidian-icon-folder-icon-preview">${getSvgFromLoadedIcon(item.item.prefix, item.item.name)}</div>`;
}
}
this.renderIndex++;
}
}
const DEFAULT_SETTINGS = {
migrated: 1,
iconPacksPath: '.obsidian/plugins/obsidian-icon-folder/icons',
fontSize: 16,
emojiStyle: 'none',
iconColor: null,
recentlyUsedIcons: [],
recentlyUsedIconsSize: 5,
rules: [],
extraMargin: {
top: 0,
right: 4,
bottom: 0,
left: 0,
},
iconInTabsEnabled: false,
};
/**
* Checks if the file type is equal to the `for` property of the custom rule.
* @param rule Custom rule that will be checked.
* @param fileType File type that will be checked.
* @returns Boolean whether the custom rule `for` matches the file type or not.
*/
const doesMatchFileType = (rule, fileType) => {
return (rule.for === 'everything' ||
(rule.for === 'files' && fileType === 'file') ||
(rule.for === 'folders' && fileType === 'folder'));
};
/**
* Determines whether a given file matches a specified custom rule.
* @param plugin Plugin object containing the app and other plugin data.
* @param rule Custom rule to check against the file.
* @param file File to check against the custom rule.
* @returns A promise that resolves to true if the file matches the rule, false otherwise.
*/
const isApplicable = (plugin, rule, file) => __awaiter(void 0, void 0, void 0, function* () {
// Gets the file type based on the specified file path.
const fileType = (yield plugin.app.vault.adapter.stat(file.path)).type;
try {
// Rule is in some sort of regex.
const regex = new RegExp(rule.rule);
if (!file.name.match(regex)) {
return false;
}
return doesMatchFileType(rule, fileType);
}
catch (_a) {
// Rule is not in some sort of regex, check for basic string match.
return file.name.includes(rule.rule) && doesMatchFileType(rule, fileType);
}
});
/**
* Removes the icon from the custom rule from all the files, if applicable.
* @param plugin Instance of the IconFolderPlugin.
* @param rule Custom rule where the nodes will be removed based on this rule.
*/
const removeFromAllFiles = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () {
for (const fileExplorer of plugin.getRegisteredFileExplorers()) {
const files = Object.entries(fileExplorer.fileItems);
for (const [path, fileItem] of files) {
const fileType = (yield plugin.app.vault.adapter.stat(path)).type;
// Gets the icon name of the inheritance object or by the value directly.
let iconName = plugin.getData()[path];
if (typeof plugin.getData()[path] === 'object') {
iconName = plugin.getData()[path].iconName;
}
if (!iconName && doesExistInPath$1(rule, path) && doesMatchFileType(rule, fileType)) {
dom.removeIconInNode(getFileItemTitleEl(fileItem));
}
}
}
});
/**
* Really dumb way to sort the custom rules. At the moment, it only sorts the custom rules
* based on the `localCompare` function.
* @param plugin Instance of IconFolderPlugin.
* @returns An array of sorted custom rules.
*/
const getSortedRules = (plugin) => {
return plugin.getSettings().rules.sort((a, b) => a.rule.localeCompare(b.rule));
};
/**
* Tries to apply all custom rules to all files. This function iterates over all the saved
* custom rules and calls {@link addToAllFiles}.
* @param plugin Instance of the IconFolderPlugin.
*/
const addAll$1 = (plugin) => __awaiter(void 0, void 0, void 0, function* () {
for (const rule of getSortedRules(plugin)) {
yield addToAllFiles(plugin, rule);
}
});
/**
* Tries to add all specific custom rule icon to all registered files. It does that by
* calling the {@link add} function.
* @param plugin Instance of the IconFolderPlugin.
* @param rule Custom rule that will be applied, if applicable, to all files.
*/
const addToAllFiles = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () {
for (const fileExplorer of plugin.getRegisteredFileExplorers()) {
const files = Object.values(fileExplorer.fileItems);
for (const fileItem of files) {
yield add$2(plugin, rule, fileItem.file, getFileItemTitleEl(fileItem));
}
}
});
/**
* Tries to add the icon of the custom rule to a file or folder. This function also checks
* if the file type matches the `for` property of the custom rule.
* @param plugin Instance of the IconFolderPlugin.
* @param rule Custom rule that will be used to check if the rule is applicable to the file.
* @param file File or folder that will be used to possibly create the icon for.
* @param container Optional element where the icon will be added if the custom rules matches.
*/
const add$2 = (plugin, rule, file, container) => __awaiter(void 0, void 0, void 0, function* () {
// Gets the type of the file.
const fileType = (yield plugin.app.vault.adapter.stat(file.path)).type;
const hasIcon = plugin.getData()[file.path];
if (!doesMatchFileType(rule, fileType) || hasIcon) {
return;
}
try {
// Rule is in some sort of regex.
const regex = new RegExp(rule.rule);
if (file.name.match(regex)) {
dom.createIconNode(plugin, file.path, rule.icon, { color: rule.color, container });
}
}
catch (_b) {
// Rule is not applicable to a regex format.
if (file.name.includes(rule.rule)) {
dom.createIconNode(plugin, file.path, rule.icon, { color: rule.color, container });
}
}
});
/**
* Determines whether a given rule exists in a given path.
* @param rule Rule to check for.
* @param path Path to check in.
* @returns True if the rule exists in the path, false otherwise.
*/
const doesExistInPath$1 = (rule, path) => {
const name = path.split('/').pop();
try {
// Rule is in some sort of regex.
const regex = new RegExp(rule.rule);
if (name.match(regex)) {
return true;
}
}
catch (_a) {
// Rule is not in some sort of regex, check for basic string match.
return name.includes(rule.rule);
}
return false;
};
/**
* Gets a custom rule by its path.
* @param plugin Instance of the plugin.
* @param path Path to check for.
* @returns The custom rule if it exists, undefined otherwise.
*/
const getByPath$2 = (plugin, path) => {
if (path === 'settings' || path === 'migrated') {
return undefined;
}
return getSortedRules(plugin).find((rule) => !emoji.isEmoji(rule.icon) && doesExistInPath$1(rule, path));
};
/**
* Gets all the files and directories that can be applied to the specific custom rule.
* @param plugin Instance of IconFolderPlugin.
* @param rule Custom rule that will be checked for.
* @returns An array of files and directories that match the custom rule.
*/
const getFiles$1 = (plugin, rule) => {
const result = [];
for (const fileExplorer of plugin.getRegisteredFileExplorers()) {
const files = Object.values(fileExplorer.fileItems);
for (const fileItem of files) {
if (doesExistInPath$1(rule, fileItem.file.path)) {
result.push(fileItem.file);
}
}
}
return result;
};
var customRule = {
getFiles: getFiles$1,
doesExistInPath: doesExistInPath$1,
getSortedRules,
getByPath: getByPath$2,
removeFromAllFiles,
add: add$2,
addAll: addAll$1,
addToAllFiles,
isApplicable,
};
/**
* Get all icon containers of all open tabs. The icon container mostly relies next to the
* element with the actual name of the file.
* @param filename String that will be used to get the icon container.
* @returns An array of HTMLElement of the icon containers.
*/
const getIconContainers = (filename) => {
// Gets all tab header elements with the `aria-label` attribute.
const nodes = document.querySelectorAll(`[aria-label="${filename}"]`);
const containers = [];
nodes.forEach((node) => {
if (!node.hasAttribute('draggable') || node.children.length === 0) {
return;
}
// Gets the inner header container of the tab.
const headerInnerContainer = node.children[0];
if (!headerInnerContainer || headerInnerContainer.children.length === 0) {
return;
}
// Gets the icon container inside of the inner header container.
const iconContainer = headerInnerContainer.children[0];
if (!iconContainer) {
return;
}
containers.push(iconContainer);
});
return containers;
};
const add$1 = (plugin, file, options) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const iconContainers = getIconContainers(file.basename);
if (iconContainers.length === 0) {
return;
}
const iconColor = (_a = options === null || options === void 0 ? void 0 : options.iconColor) !== null && _a !== void 0 ? _a : plugin.getSettings().iconColor;
const data = Object.entries(plugin.getData());
for (const iconContainer of iconContainers) {
// Removes the `display: none` from the obsidian styling.
iconContainer.style.display = 'flex';
// Only add the icon name manually when it is defined in the options.
if (options === null || options === void 0 ? void 0 : options.iconName) {
dom.setIconForNode(plugin, options.iconName, iconContainer, iconColor);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
continue;
}
// Files can also have custom icons inside of inheritance folders.
const hasIcon = plugin.getData()[file.path];
if (!hasIcon) {
// Add icons to tabs if there is some sort of inheritance going on.
const inheritanceData = data.filter(([key, value]) => typeof value === 'object' && key !== 'settings');
for (const [inheritancePath, inheritance] of inheritanceData) {
if (!inheritance.inheritanceIcon) {
continue;
}
if (!file.path.includes(inheritancePath)) {
continue;
}
dom.setIconForNode(plugin, inheritance.inheritanceIcon, iconContainer, iconColor);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
break;
}
}
// Add icons to tabs if a custom rule is applicable.
for (const rule of customRule.getSortedRules(plugin)) {
const isApplicable = yield customRule.isApplicable(plugin, rule, file);
if (isApplicable) {
dom.setIconForNode(plugin, rule.icon, iconContainer, rule.color);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
}
}
// Add icons to tabs if there is an icon set.
const iconData = data.find(([dataPath]) => dataPath === file.path);
// Check if data was not found or name of icon is not a string.
if (!iconData || typeof iconData[1] !== 'string') {
continue;
}
dom.setIconForNode(plugin, iconData[1], iconContainer, iconColor);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
}
});
const update = (plugin, file, iconName) => {
const iconContainers = getIconContainers(file.basename);
if (iconContainers.length === 0) {
return;
}
for (const iconContainer of iconContainers) {
dom.setIconForNode(plugin, iconName, iconContainer);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
}
};
const remove$1 = (file, options) => {
const iconContainers = getIconContainers(file.basename);
if (iconContainers.length === 0) {
return;
}
for (const iconContainer of iconContainers) {
if (!(options === null || options === void 0 ? void 0 : options.replaceWithDefaultIcon)) {
// Removes the display of the icon container to remove the icons from the tabs.
iconContainer.style.display = 'none';
}
else {
iconContainer.innerHTML = DEFAULT_FILE_ICON;
}
}
};
var iconTabs = {
add: add$1,
update,
remove: remove$1,
};
/**
* Gets all the inheritance folder from the data as an object which consists of the path
* as a key and the value as the object. It does that by including all objects (except
* the settings).
* @param plugin IconFolderPlugin that will be used to get the data from.
* @returns An object where the keys are the paths and the values are the objects.
*/
const getFolders = (plugin) => {
return Object.entries(plugin.getData())
.filter(([k, v]) => k !== 'settings' && typeof v === 'object')
.reduce((prev, [path, value]) => {
prev[path] = value;
return prev;
}, {});
};
/**
* Gets all the files where that file path includes the specified folder path.
* @param plugin Instance of IconFolderPlugin.
* @param folderPath Folder path that will be used to check if the file includes this.
* @returns An array of files that include the folder path.
*/
const getFiles = (plugin, folderPath) => {
return plugin.app.vault.getAllLoadedFiles().filter((file) => file.path.includes(folderPath));
};
const add = (plugin, folderPath, iconName, options) => {
var _a;
const folder = plugin.getData()[folderPath];
// Checks if data exists and if the data is some kind of object type.
if (!folder || typeof folder !== 'object') {
return;
}
// A inner function that helps to add the inheritance icon to the DOM.
const addIcon = (fileItem) => {
var _a;
const titleEl = getFileItemTitleEl(fileItem);
const innerTitleEl = getFileItemInnerTitleEl(fileItem);
const iconNode = titleEl.createDiv();
iconNode.classList.add('obsidian-icon-folder-icon');
dom.setIconForNode(plugin, iconName, iconNode);
titleEl.insertBefore(iconNode, innerTitleEl);
(_a = options === null || options === void 0 ? void 0 : options.onAdd) === null || _a === void 0 ? void 0 : _a.call(options, fileItem.file);
};
for (const fileExplorer of plugin.getRegisteredFileExplorers()) {
if (options === null || options === void 0 ? void 0 : options.file) {
// Handles the addition of the inheritance icon for only one file.
const fileItem = fileExplorer.fileItems[options.file.path];
const inFolder = options.file.path.includes(folderPath);
const hasIcon = fileItem && plugin.getData()[fileItem.file.path];
if (!fileItem || !inFolder || hasIcon) {
continue;
}
addIcon(fileItem);
}
else {
// Handles the addition of a completely new inheritance for a folder.
for (const [path, fileItem] of Object.entries(fileExplorer.fileItems)) {
// Checks if the file is in the folder and not a directory.
if (((_a = fileItem.file.parent) === null || _a === void 0 ? void 0 : _a.path) !== folderPath) {
continue;
}
const isFolder = fileItem.file.children !== undefined;
const inFolder = path.includes(folderPath);
const hasIcon = plugin.getData()[fileItem.file.path];
if (!inFolder || hasIcon || isFolder) {
continue;
}
addIcon(fileItem);
}
}
}
};
const remove = (plugin, folderPath, options) => {
var _a, _b;
const folder = plugin.getData()[folderPath];
// Checks if data exists and if the data is some kind of object type.
if (!folder || typeof folder !== 'object') {
return;
}
// Gets all files that include the folder path of the currently opened vault.
const files = getFiles(plugin, folderPath);
for (const file of files) {
if (((_a = file.parent) === null || _a === void 0 ? void 0 : _a.path) !== folderPath) {
continue;
}
// When the file path is not registered in the data it should remove the icon.
if (!plugin.getData()[file.path]) {
dom.removeIconInPath(file.path);
(_b = options === null || options === void 0 ? void 0 : options.onRemove) === null || _b === void 0 ? void 0 : _b.call(options, file);
}
}
};
const getByPath$1 = (plugin, path) => {
const folders = getFolders(plugin);
const foundFolderIcon = Object.entries(folders).find(([folderPath]) => path.includes(folderPath));
return foundFolderIcon === null || foundFolderIcon === void 0 ? void 0 : foundFolderIcon[1]; // Returns the folder icon when defined.
};
const doesExistInPath = (plugin, path) => {
const folders = getFolders(plugin);
return Object.keys(folders).some((folderPath) => path.includes(folderPath));
};
const getFolderPathByFilePath = (plugin, filePath) => {
const folders = getFolders(plugin);
const foundFolderIcon = Object.entries(folders).find(([folderPath]) => filePath.includes(folderPath));
return foundFolderIcon === null || foundFolderIcon === void 0 ? void 0 : foundFolderIcon[0]; // Returns the folder path when defined.
};
var inheritance = {
add,
remove,
getFolders,
getFiles,
getByPath: getByPath$1,
getFolderPathByFilePath,
doesExistInPath,
};
/**
* This function adds all the possible icons to the corresponding nodes. It adds the icons,
* that are defined in the data as a basic string to the nodes, the inheritance folder
* icons, and also the custom rule icons.
* @param plugin Instance of IconFolderPlugin.
* @param data Data that will be used to add all the icons to the nodes.
* @param registeredFileExplorers A WeakSet of file explorers that are being used as a
* cache for already handled file explorers.
* @param callback Callback is being called whenever the icons are added to one file
* explorer.
*/
const addAll = (plugin, data, registeredFileExplorers, callback) => {
const fileExplorers = plugin.app.workspace.getLeavesOfType('file-explorer');
for (const fileExplorer of fileExplorers) {
if (registeredFileExplorers.has(fileExplorer.view)) {
continue;
}
registeredFileExplorers.add(fileExplorer.view);
// Adds icons to already open file tabs.
if (plugin.getSettings().iconInTabsEnabled) {
for (const leaf of plugin.app.workspace.getLeavesOfType('markdown')) {
const file = leaf.view.file;
if (file) {
iconTabs.add(plugin, file);
}
}
}
for (const [dataPath, value] of data) {
const fileItem = fileExplorer.view.fileItems[dataPath];
if (fileItem) {
const titleEl = getFileItemTitleEl(fileItem);
const titleInnerEl = getFileItemInnerTitleEl(fileItem);
// Need to check this because refreshing the plugin will duplicate all the icons.
if (titleEl.children.length === 2 || titleEl.children.length === 1) {
// Gets the icon name directly or from the inheritance folder.
const iconName = typeof value === 'string' ? value : value.iconName;
if (iconName) {
// Removes a possible existing icon.
const existingIcon = titleEl.querySelector('.obsidian-icon-folder-icon');
if (existingIcon) {
existingIcon.remove();
}
// Creates the new node with the icon inside.
const iconNode = titleEl.createDiv();
iconNode.classList.add('obsidian-icon-folder-icon');
dom.setIconForNode(plugin, iconName, iconNode);
titleEl.insertBefore(iconNode, titleInnerEl);
}
// Handle possible inheritance for the folder.
if (typeof value === 'object' && value.inheritanceIcon) {
inheritance.add(plugin, dataPath, value.inheritanceIcon);
}
}
}
}
// Callback function to register other events to this file explorer.
callback === null || callback === void 0 ? void 0 : callback();
}
// Handles the custom rules.
customRule.addAll(plugin);
};
/**
* Gets the icon of a given path. This function returns the first occurrence of an icon.
* @param plugin Instance of the IconFolderPlugin.
* @param path Path to get the icon of.
* @returns The icon of the path if it exists, undefined otherwise.
*/
const getByPath = (plugin, path) => {
if (path === 'settings' || path === 'migrated') {
return undefined;
}
const value = plugin.getData()[path];
if (typeof value === 'string' && !emoji.isEmoji(value)) {
// If the value is a plain icon name, return it.
return value;
}
else if (typeof value === 'object') {
// Additional checks for inheritance folders.
const v = value;
// If the inheritance folder contains a custom icon for itself, return it.
if (v.iconName !== null && !emoji.isEmoji(v.iconName)) {
return v.iconName;
}
}
// Tries to get the inheritance icon for the path and returns its inheritance icon if
// it exists.
const inheritanceIcon = inheritance.getByPath(plugin, path);
if (inheritanceIcon) {
return inheritanceIcon.inheritanceIcon;
}
// Tries to get the custom rule for the path and returns its icon if it exists.
const rule = customRule.getByPath(plugin, path);
if (rule) {
return rule.icon;
}
return undefined;
};
/**
* Gets all the icons with their paths as an object.
* @param plugin Instance of the IconFolderPlugin.
* @returns An object that consists of the path and the icon name for the data, inheritance,
* or custom rule.
*/
const getAllWithPath = (plugin) => {
const result = [];
Object.keys(plugin.getData()).forEach((path) => {
if (path === 'settings' || path === 'migrated') {
return;
}
const icon = getByPath(plugin, path);
if (icon && !emoji.isEmoji(icon)) {
result.push({ path, icon });
}
// Check for inheritance folder and insert the inheritance icon.
const inheritanceFolder = inheritance.getByPath(plugin, path);
if (inheritanceFolder && !emoji.isEmoji(inheritanceFolder.inheritanceIcon)) {
result.push({ path, icon: inheritanceFolder.inheritanceIcon });
}
});
// Add all icons for the custom rules with the rule as the path.
for (const rule of plugin.getSettings().rules) {
if (!emoji.isEmoji(rule.icon)) {
result.push({ path: rule.rule, icon: rule.icon });
}
}
return result;
};
var icon = {
addAll,
getByPath,
getAllWithPath,
};
const migrationMap = [
{
oldIconPackPrefix: 'Fa',
identifier: 'Brands',
transformation: 'Fab',
},
{
oldIconPackPrefix: 'Fa',
identifier: 'Line',
transformation: 'Far',
},
{
oldIconPackPrefix: 'Fa',
identifier: 'Fill',
transformation: 'Fas',
},
];
const migrateIcons = (plugin) => {
const data = Object.assign({}, plugin.getData());
const entries = icon.getAllWithPath(plugin);
entries.forEach((entry) => {
if (entry) {
const { path, icon } = entry;
const migration = migrationMap.find((migration) => icon.substring(0, 2) === migration.oldIconPackPrefix && icon.includes(migration.identifier));
if (migration) {
data[path] =
migration.transformation +
icon.substring(migration.oldIconPackPrefix.length, icon.indexOf(migration.identifier));
}
}
});
return data;
};
class IconFolderSetting {
constructor(plugin, containerEl) {
this.plugin = plugin;
this.containerEl = containerEl;
}
}
class CustomIconPackSetting extends IconFolderSetting {
constructor(plugin, containerEl, refreshDisplay) {
super(plugin, containerEl);
this.refreshDisplay = refreshDisplay;
this.dragOverElement = document.createElement('div');
this.dragOverElement.addClass('obsidian-icon-folder-dragover-el');
this.dragOverElement.style.display = 'hidden';
this.dragOverElement.innerHTML = '<p>Drop to add icon.</p>';
}
normalizeIconPackName(value) {
return value.toLowerCase().replace(/\s/g, '-');
}
preventDefaults(event) {
event.preventDefault();
event.stopPropagation();
}
highlight(el) {
clearTimeout(this.closeTimer);
if (!this.dragTargetElement) {
el.appendChild(this.dragOverElement);
el.classList.add('obsidian-icon-folder-dragover');
this.dragTargetElement = el;
}
}
unhighlight(target, el) {
if (this.dragTargetElement && this.dragTargetElement !== target) {
this.dragTargetElement.removeChild(this.dragOverElement);
this.dragTargetElement.classList.remove('obsidian-icon-folder-dragover');
this.dragTargetElement = undefined;
}
clearTimeout(this.closeTimer);
this.closeTimer = setTimeout(() => {
if (this.dragTargetElement) {
el.removeChild(this.dragOverElement);
el.classList.remove('obsidian-icon-folder-dragover');
this.dragTargetElement = undefined;
}
}, 100);
}
display() {
new obsidian.Setting(this.containerEl)
.setName('Add custom icon pack')
.setDesc('Add a custom icon pack')
.addText((text) => {
text.setPlaceholder('Your icon pack name');
this.textComponent = text;
})
.addButton((btn) => {
btn.setButtonText('Add icon pack');
btn.buttonEl.style.marginLeft = '12px';
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
const name = this.textComponent.getValue();
if (name.length === 0) {
return;
}
const normalizedName = this.normalizeIconPackName(this.textComponent.getValue());
if (yield doesIconPackExist(this.plugin, normalizedName)) {
new obsidian.Notice('Icon pack already exists.');
return;
}
yield createCustomIconPackDirectory(this.plugin, normalizedName);
this.textComponent.setValue('');
this.refreshDisplay();
new obsidian.Notice('Icon pack successfully created.');
}));
});
getAllIconPacks().forEach((iconPack) => {
const iconPackSetting = new obsidian.Setting(this.containerEl)
.setName(iconPack.name)
.setDesc(`Total icons: ${iconPack.icons.length}`);
// iconPackSetting.addButton((btn) => {
// btn.setIcon('broken-link');
// btn.setTooltip('Try to fix icon pack');
// btn.onClick(async () => {
// new Notice('Try to fix icon pack...');
// getIconPack(iconPack.name).icons = [];
// const icons = await getFilesInDirectory(this.plugin, `${getPath()}/${iconPack.name}`);
// for (let i = 0; i < icons.length; i++) {
// const filePath = icons[i];
// const fileName = filePath.split('/').pop();
// const file = await this.plugin.app.vault.adapter.read(filePath);
// const iconContent = file
// .replace(/stroke="#fff"/g, 'stroke="currentColor"')
// .replace(/fill="#fff"/g, 'fill="currentColor"');
// await this.plugin.app.vault.adapter.write(filePath, iconContent);
// await normalizeFileName(this.plugin, filePath);
// addIconToIconPack(iconPack.name, fileName, iconContent);
// }
// new Notice('...tried to fix icon pack');
// // Refreshes the DOM.
// Object.entries(this.plugin.getData()).forEach(async ([k, v]) => {
// const doesPathExist = await this.plugin.app.vault.adapter.exists(k, true);
// if (doesPathExist && typeof v === 'string') {
// // dom.removeIconInPath(k);
// dom.createIconNode(this.plugin, k, v);
// }
// });
// });
// });
iconPackSetting.addButton((btn) => {
btn.setIcon('plus');
btn.setTooltip('Add an icon');
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
const fileSelector = document.createElement('input');
fileSelector.setAttribute('type', 'file');
fileSelector.setAttribute('multiple', 'multiple');
fileSelector.setAttribute('accept', '.svg');
fileSelector.click();
fileSelector.onchange = (e) => __awaiter(this, void 0, void 0, function* () {
const target = e.target;
for (let i = 0; i < target.files.length; i++) {
const file = target.files[i];
const content = yield readFileSync(file);
yield createFile(this.plugin, iconPack.name, file.name, content);
addIconToIconPack(iconPack.name, file.name, content);
iconPackSetting.setDesc(`Total icons: ${iconPack.icons.length} (added: ${file.name})`);
}
new obsidian.Notice('Icons successfully added.');
});
}));
});
iconPackSetting.addButton((btn) => {
btn.setIcon('trash');
btn.setTooltip('Remove the icon pack');
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
yield deleteIconPack(this.plugin, iconPack.name);
this.refreshDisplay();
new obsidian.Notice('Icon pack successfully deleted.');
}));
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((event) => {
iconPackSetting.settingEl.addEventListener(event, this.preventDefaults, false);
});
['dragenter', 'dragover'].forEach((event) => {
iconPackSetting.settingEl.addEventListener(event, () => this.highlight(iconPackSetting.settingEl), false);
});
['dragleave', 'drop'].forEach((event) => {
iconPackSetting.settingEl.addEventListener(event, (event) => this.unhighlight(event.currentTarget, iconPackSetting.settingEl), false);
});
iconPackSetting.settingEl.addEventListener('drop', (event) => __awaiter(this, void 0, void 0, function* () {
const files = event.dataTransfer.files;
let successful = false;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type !== 'image/svg+xml') {
new obsidian.Notice(`File ${file.name} is not a XML file.`);
continue;
}
successful = true;
const content = yield readFileSync(file);
yield createFile(this.plugin, iconPack.name, file.name, content);
addIconToIconPack(iconPack.name, file.name, content);
iconPackSetting.setDesc(`Total icons: ${iconPack.icons.length} (added: ${file.name})`);
}
if (successful) {
new obsidian.Notice('Icons successfully added.');
}
}), false);
});
}
}
class CustomIconRuleSetting extends IconFolderSetting {
constructor(plugin, containerEl, app, refreshDisplay) {
super(plugin, containerEl);
this.app = app;
this.refreshDisplay = refreshDisplay;
}
/**
* Updates all the open files based on the custom rule that was specified.
* @param rule Rule that will be used to update all the icons for all opened files.
* @param remove Whether to remove the icons that are applicable to the rule or not.
*/
updateIconTabs(rule, remove) {
return __awaiter(this, void 0, void 0, function* () {
if (this.plugin.getSettings().iconInTabsEnabled) {
for (const openedFile of getAllOpenedFiles(this.plugin)) {
const applicable = yield customRule.isApplicable(this.plugin, rule, openedFile);
if (!applicable) {
continue;
}
if (remove) {
iconTabs.remove(openedFile, { replaceWithDefaultIcon: true });
}
else {
iconTabs.add(this.plugin, openedFile, { iconName: rule.icon, iconColor: rule.color });
}
}
}
});
}
createDescriptionEl(container, text) {
const description = container.createEl('p', {
text,
cls: 'setting-item-description',
});
description.style.marginBottom = 'var(--size-2-2)';
}
display() {
new obsidian.Setting(this.containerEl)
.setName('Add icon rule')
.setDesc('Will add the icon based on the specific string.')
.addText((text) => {
text.onChange((value) => {
this.chooseIconBtn.setDisabled(value.length === 0);
this.chooseIconBtn.buttonEl.style.cursor = value.length === 0 ? 'not-allowed' : 'default';
this.chooseIconBtn.buttonEl.style.opacity = value.length === 0 ? '50%' : '100%';
});
text.setPlaceholder('regex or simple string');
this.textComponent = text;
})
.addButton((btn) => {
btn.setDisabled(true);
btn.setButtonText('Choose icon');
btn.buttonEl.style.marginLeft = '12px';
btn.buttonEl.style.cursor = 'not-allowed';
btn.buttonEl.style.opacity = '50%';
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
if (this.textComponent.getValue().length === 0) {
return;
}
const modal = new IconsPickerModal(this.app, this.plugin, '');
modal.onChooseItem = (item) => __awaiter(this, void 0, void 0, function* () {
const icon = getNormalizedName(typeof item === 'object' ? item.displayName : item);
const rule = { rule: this.textComponent.getValue(), icon, for: 'everything' };
this.plugin.getSettings().rules = [...this.plugin.getSettings().rules, rule];
yield this.plugin.saveIconFolderData();
this.refreshDisplay();
new obsidian.Notice('Icon rule added.');
this.textComponent.setValue('');
saveIconToIconPack(this.plugin, rule.icon);
yield customRule.addToAllFiles(this.plugin, rule);
this.updateIconTabs(rule, false);
});
modal.open();
}));
this.chooseIconBtn = btn;
});
this.plugin.getSettings().rules.forEach((rule) => {
// Keeping track of the old rule so that we can get a reference to it for old values.
const oldRule = Object.assign({}, rule);
const settingRuleEl = new obsidian.Setting(this.containerEl).setName(rule.rule).setDesc(`Icon: ${rule.icon}`);
// Add the configuration button for configuring where the custom rule gets applied to.
settingRuleEl.addButton((btn) => {
var _a;
const isFor = (_a = rule.for) !== null && _a !== void 0 ? _a : 'everything';
if (isFor === 'folders') {
btn.setIcon('folder');
}
else if (isFor === 'files') {
btn.setIcon('document');
}
else {
btn.setIcon('documents');
}
btn.setTooltip(`Icon applicable to: ${isFor}`);
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
this.updateIconTabs(rule, true);
yield customRule.removeFromAllFiles(this.plugin, Object.assign(Object.assign({}, rule), { for: isFor }));
if (isFor === 'folders') {
rule.for = 'everything';
}
else if (isFor === 'files') {
rule.for = 'folders';
}
else {
rule.for = 'files';
}
yield customRule.addToAllFiles(this.plugin, rule);
this.updateIconTabs(rule, false);
yield this.plugin.saveIconFolderData();
this.refreshDisplay();
customRule.getSortedRules(this.plugin).forEach((previousRule) => __awaiter(this, void 0, void 0, function* () {
yield customRule.addToAllFiles(this.plugin, previousRule);
this.updateIconTabs(previousRule, false);
}));
}));
});
// Add the edit custom rule button.
settingRuleEl.addButton((btn) => {
btn.setIcon('pencil');
btn.setTooltip('Edit the custom rule');
btn.onClick(() => {
var _a;
// Create modal and its children elements.
const modal = new obsidian.Modal(this.plugin.app);
modal.contentEl.style.display = 'block';
modal.modalEl.classList.add('obsidian-icon-folder-custom-rule-modal');
modal.titleEl.createEl('h3', { text: 'Edit custom rule' });
// Create the input for the rule.
this.createDescriptionEl(modal.contentEl, 'Regex or simple string');
const input = new obsidian.TextComponent(modal.contentEl);
input.setValue(rule.rule);
input.onChange((value) => __awaiter(this, void 0, void 0, function* () {
rule.rule = value;
}));
// Create the change icon button with icon preview.
this.createDescriptionEl(modal.contentEl, 'Custom rule icon');
const iconContainer = modal.contentEl.createDiv();
iconContainer.style.display = 'flex';
iconContainer.style.alignItems = 'center';
iconContainer.style.justifyContent = 'space-between';
const iconEl = iconContainer.createDiv();
const iconPreviewEl = iconEl.createDiv();
dom.setIconForNode(this.plugin, rule.icon, iconPreviewEl);
iconEl.style.display = 'flex';
iconEl.style.alignItems = 'center';
iconEl.style.justifyContent = 'space-between';
iconEl.style.margin = null;
iconPreviewEl.innerHTML = svg.setFontSize(iconPreviewEl.innerHTML, 20);
const iconNameEl = iconEl.createEl('div', { cls: 'setting-item-description' });
iconNameEl.style.paddingTop = '0';
iconNameEl.style.marginLeft = 'var(--size-2-2)';
iconNameEl.innerText = rule.icon;
const changeIconBtn = new obsidian.ButtonComponent(iconContainer);
changeIconBtn.setButtonText('Change icon');
changeIconBtn.onClick(() => __awaiter(this, void 0, void 0, function* () {
const modal = new IconsPickerModal(this.app, this.plugin, rule.icon);
modal.onChooseItem = (item) => __awaiter(this, void 0, void 0, function* () {
const icon = typeof item === 'object' ? item.displayName : item;
rule.icon = icon;
dom.setIconForNode(this.plugin, rule.icon, iconPreviewEl);
iconPreviewEl.innerHTML = svg.setFontSize(iconPreviewEl.innerHTML, 20);
iconNameEl.innerText = getNormalizedName(rule.icon);
});
modal.open();
}));
// Create the color picker for the rule.
this.createDescriptionEl(modal.contentEl, 'Color of the icon');
const colorContainer = modal.contentEl.createDiv();
colorContainer.style.display = 'flex';
colorContainer.style.alignItems = 'center';
colorContainer.style.justifyContent = 'space-between';
const colorPicker = new obsidian.ColorComponent(colorContainer).setValue((_a = rule.color) !== null && _a !== void 0 ? _a : '#000000').onChange((value) => {
rule.color = value;
});
const defaultColorButton = new obsidian.ButtonComponent(colorContainer);
defaultColorButton.setTooltip('Set color to the default one');
defaultColorButton.setButtonText('Default');
defaultColorButton.onClick(() => {
colorPicker.setValue('#000000');
rule.color = undefined;
});
// Create the save button.
const button = new obsidian.ButtonComponent(modal.contentEl);
button.buttonEl.style.marginTop = 'var(--size-4-4)';
button.buttonEl.style.float = 'right';
button.setButtonText('Save Changes');
button.onClick(() => __awaiter(this, void 0, void 0, function* () {
// Tries to remove the previously used icon from the icon pack.
removeIconFromIconPack(this.plugin, oldRule.icon);
// Tries to add the newly used icon to the icon pack.
saveIconToIconPack(this.plugin, rule.icon);
rule.icon = getNormalizedName(rule.icon);
this.refreshDisplay();
new obsidian.Notice('Custom rule updated.');
// Refresh the DOM.
yield customRule.removeFromAllFiles(this.plugin, rule);
this.updateIconTabs(rule, true);
this.plugin.getSettings().rules.forEach((rule) => __awaiter(this, void 0, void 0, function* () {
yield customRule.addToAllFiles(this.plugin, rule);
this.updateIconTabs(rule, false);
}));
yield this.plugin.saveIconFolderData();
modal.close();
}));
modal.open();
});
});
// Add the delete custom rule button.
settingRuleEl.addButton((btn) => {
btn.setIcon('trash');
btn.setTooltip('Remove the custom rule');
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
const newRules = this.plugin
.getSettings()
.rules.filter((r) => rule.rule !== r.rule || rule.color !== r.color || rule.icon !== r.icon || rule.for !== r.for);
this.plugin.getSettings().rules = newRules;
yield this.plugin.saveIconFolderData();
this.refreshDisplay();
new obsidian.Notice('Custom rule deleted.');
yield customRule.removeFromAllFiles(this.plugin, rule);
removeIconFromIconPack(this.plugin, rule.icon);
this.updateIconTabs(rule, true);
const previousRules = this.plugin.getSettings().rules.filter((r) => rule.for === r.for);
previousRules.forEach((previousRule) => __awaiter(this, void 0, void 0, function* () {
yield customRule.addToAllFiles(this.plugin, previousRule);
this.updateIconTabs(previousRule, false);
}));
}));
});
});
}
}
class EmojiStyleSetting extends IconFolderSetting {
display() {
const emojiStyle = new obsidian.Setting(this.containerEl).setName('Emoji Style').setDesc('Change the style of your emojis.');
emojiStyle.addDropdown((dropdown) => {
dropdown.addOption('none', 'None');
dropdown.addOption('native', 'Native');
dropdown.addOption('twemoji', 'Twemoji');
dropdown.setValue(this.plugin.getSettings().emojiStyle);
dropdown.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().emojiStyle = value;
this.updateDOM();
yield this.plugin.saveIconFolderData();
}));
});
}
updateDOM() {
getAllOpenedFiles(this.plugin);
for (const fileExplorer of this.plugin.getRegisteredFileExplorers()) {
const fileItems = Object.entries(fileExplorer.fileItems);
for (const [path, fileItem] of fileItems) {
let iconName = this.plugin.getData()[path];
if (!iconName) {
continue;
}
if (typeof this.plugin.getData()[path] === 'object') {
const inheritanceData = this.plugin.getData()[path];
iconName = inheritanceData.iconName;
// Handle updating the emoji style for the inheritance icon.
if (emoji.isEmoji(inheritanceData.inheritanceIcon)) {
for (const file of inheritance.getFiles(this.plugin, path)) {
dom.createIconNode(this.plugin, file.path, inheritanceData.inheritanceIcon);
iconTabs.update(this.plugin, file, inheritanceData.inheritanceIcon);
}
}
}
// `iconName` is `null` indicates that for the inheritance object the icon name
// on the node itself does not exist.
if (!iconName) {
continue;
}
if (emoji.isEmoji(iconName)) {
dom.createIconNode(this.plugin, path, iconName);
iconTabs.update(this.plugin, fileItem.file, iconName);
}
}
}
customRule.addAll(this.plugin);
}
}
/**
* Helper function that refreshes the style of all the icons that are defined, in some
* sort of inheritance, or in a custom rule involved.
* @param plugin Instance of the IconFolderPlugin.
*/
const refreshStyleOfIcons = (plugin) => {
// Refreshes the icon style for all normally added icons.
style.refreshIconNodes(plugin);
const fileExplorers = plugin.app.workspace.getLeavesOfType('file-explorer');
for (const fileExplorer of fileExplorers) {
// Refreshes the icon style for all inheritance folders.
for (const folderPath of Object.keys(inheritance.getFolders(plugin))) {
// Apply style for the icon node itself.
const folderItem = fileExplorer.view.fileItems[folderPath];
if (folderItem) {
const titleEl = getFileItemTitleEl(folderItem);
const iconNode = titleEl.querySelector('.obsidian-icon-folder-icon');
if (iconNode) {
iconNode.innerHTML = style.applyAll(plugin, iconNode.innerHTML, iconNode);
}
}
// Apply style for all files in this inheritance.
const files = inheritance.getFiles(plugin, folderPath);
for (const file of files) {
const fileItem = fileExplorer.view.fileItems[file.path];
const titleEl = getFileItemTitleEl(fileItem);
const iconNode = titleEl.querySelector('.obsidian-icon-folder-icon');
if (iconNode) {
iconNode.innerHTML = style.applyAll(plugin, iconNode.innerHTML, iconNode);
}
}
}
// Refreshes the icon style for all custom icon rules, when the color of the rule is
// not defined.
for (const rule of customRule.getSortedRules(plugin)) {
const files = customRule.getFiles(plugin, rule);
for (const file of files) {
if (rule.color) {
continue;
}
const fileItem = fileExplorer.view.fileItems[file.path];
const titleEl = getFileItemTitleEl(fileItem);
const iconNode = titleEl.querySelector('.obsidian-icon-folder-icon');
iconNode.innerHTML = style.applyAll(plugin, iconNode.innerHTML, iconNode);
}
}
}
};
var helper = {
refreshStyleOfIcons,
};
class ExtraMarginSetting extends IconFolderSetting {
display() {
var _a, _b;
const extraMarginSetting = new obsidian.Setting(this.containerEl)
.setName('Extra margin (in pixels)')
.setDesc('Change the margin of the icons.')
.setClass('obsidian-icon-folder-setting');
const extraMarginDropdown = new obsidian.DropdownComponent(extraMarginSetting.controlEl).addOptions({
top: 'Top',
right: 'Right',
bottom: 'Bottom',
left: 'Left',
});
const extraMarginSlider = new obsidian.SliderComponent(extraMarginSetting.controlEl)
.setLimits(-24, 24, 1)
.setDynamicTooltip()
.setValue((_b = (_a = this.plugin.getSettings().extraMargin) === null || _a === void 0 ? void 0 : _a.top) !== null && _b !== void 0 ? _b : 2)
.onChange((val) => __awaiter(this, void 0, void 0, function* () {
const dropdownValue = extraMarginDropdown.getValue();
if (this.plugin.getSettings().extraMargin) {
this.plugin.getSettings().extraMargin[dropdownValue] = val;
}
else {
this.plugin.getSettings().extraMargin = {
[dropdownValue]: val,
};
}
yield this.plugin.saveIconFolderData();
helper.refreshStyleOfIcons(this.plugin);
}));
extraMarginDropdown.onChange((val) => {
var _a;
if (this.plugin.getSettings().extraMargin) {
extraMarginSlider.setValue((_a = this.plugin.getSettings().extraMargin[val]) !== null && _a !== void 0 ? _a : 2);
}
else {
extraMarginSlider.setValue(2);
}
});
extraMarginSetting.components.push(extraMarginDropdown, extraMarginSlider);
}
}
class IconColorSetting extends IconFolderSetting {
display() {
var _a;
const colorCustomization = new obsidian.Setting(this.containerEl)
.setName('Icon color')
.setDesc('Change the color of the displayed icons.');
const colorPicker = new obsidian.ColorComponent(colorCustomization.controlEl)
.setValue((_a = this.plugin.getSettings().iconColor) !== null && _a !== void 0 ? _a : '#000000')
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconColor = value;
yield this.plugin.saveIconFolderData();
helper.refreshStyleOfIcons(this.plugin);
}));
colorCustomization.addButton((button) => {
button
.setButtonText('Default')
.setTooltip('Set color to the default one')
.onClick(() => __awaiter(this, void 0, void 0, function* () {
colorPicker.setValue('#000000');
this.plugin.getSettings().iconColor = null;
yield this.plugin.saveIconFolderData();
helper.refreshStyleOfIcons(this.plugin);
}));
});
colorCustomization.components.push(colorPicker);
}
}
class IconFontSizeSetting extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Icon font size (in pixels)')
.setDesc('Change the font size of the displayed icons.')
.addSlider((slider) => {
var _a;
slider
.setLimits(10, 24, 1)
.setDynamicTooltip()
.setValue((_a = this.plugin.getSettings().fontSize) !== null && _a !== void 0 ? _a : DEFAULT_SETTINGS.fontSize)
.onChange((val) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().fontSize = val;
yield this.plugin.saveIconFolderData();
helper.refreshStyleOfIcons(this.plugin);
}));
});
}
}
class IconPacksPathSetting extends IconFolderSetting {
display() {
const iconPacksPathSetting = new obsidian.Setting(this.containerEl)
.setName('Icon Packs folder path')
.setDesc('Change the default icon packs folder path');
iconPacksPathSetting.addText((text) => {
this.iconPacksSettingTextComp = text;
text.setValue(this.plugin.getSettings().iconPacksPath);
});
iconPacksPathSetting.addButton((btn) => {
btn.setButtonText('Save');
btn.buttonEl.style.marginLeft = '12px';
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
const newPath = this.iconPacksSettingTextComp.getValue();
const oldPath = this.plugin.getSettings().iconPacksPath;
if (oldPath === this.iconPacksSettingTextComp.getValue()) {
return;
}
new obsidian.Notice('Saving in progress...');
setPath(newPath);
yield createDefaultDirectory(this.plugin);
yield moveIconPackDirectories(this.plugin, oldPath, newPath);
this.plugin.getSettings().iconPacksPath = newPath;
yield this.plugin.saveIconFolderData();
new obsidian.Notice('...saved successfully');
}));
});
}
}
var iconPacks = {
faBrands: {
name: 'font-awesome-brands',
displayName: 'FontAwesome Brands',
path: 'fontawesome-free-6.0.0-web/svgs/brands/',
downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.0.0/fontawesome-free-6.0.0-web.zip',
},
faRegular: {
name: 'font-awesome-regular',
displayName: 'FontAwesome Regular',
path: 'fontawesome-free-6.0.0-web/svgs/regular/',
downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.0.0/fontawesome-free-6.0.0-web.zip',
},
faSolid: {
name: 'font-awesome-solid',
displayName: 'FontAwesome Solid',
path: 'fontawesome-free-6.0.0-web/svgs/solid/',
downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.0.0/fontawesome-free-6.0.0-web.zip',
},
remixIcons: {
name: 'remix-icons',
displayName: 'Remix Icons',
path: '',
downloadLink: 'https://github.com/Remix-Design/RemixIcon/releases/download/v2.5.0/RemixIcon_SVG_v2.5.0.zip',
},
iconBrew: {
name: 'icon-brew',
displayName: 'Icon Brew',
path: '',
downloadLink: 'https://github.com/FlorianWoelki/obsidian-icon-folder/raw/main/iconPacks/icon-brew.zip',
},
/* https://simpleicons.org/ */
simpleIcons: {
name: 'simple-icons',
displayName: 'Simple Icons',
path: 'icons',
downloadLink: 'https://github.com/simple-icons/simple-icons/archive/refs/tags/7.15.0.zip',
},
lucide: {
name: 'lucide-icons',
displayName: 'Lucide',
path: '',
downloadLink: 'https://github.com/lucide-icons/lucide/releases/download/v0.122.0/lucide-icons-0.122.0.zip',
},
tablerIcons: {
name: 'tabler-icons',
displayName: 'Tabler Icons',
path: 'svg',
downloadLink: 'https://github.com/tabler/tabler-icons/releases/download/v2.17.0/tabler-icons-2.17.0.zip',
},
};
class IconPackBrowserModal extends obsidian.FuzzySuggestModal {
constructor(app, plugin) {
super(app);
this.plugin = plugin;
this.resultContainerEl.classList.add('obsidian-icon-folder-browse-modal');
this.inputEl.placeholder = 'Select to download icon pack';
}
onAddedIconPack() { }
onOpen() {
super.onOpen();
}
onClose() {
this.contentEl.empty();
}
getItemText(item) {
const prefix = createIconPackPrefix(item.name);
return `${item.displayName} (${prefix})`;
}
getItems() {
const predefinedIconPacks = Object.values(iconPacks);
const allIconPacks = getAllIconPacks();
return predefinedIconPacks.filter((iconPack) => allIconPacks.find((ip) => iconPack.name === ip.name) === undefined);
}
onChooseItem(item, _event) {
return __awaiter(this, void 0, void 0, function* () {
new obsidian.Notice(`Adding ${item.displayName}...`);
const arrayBuffer = yield downloadZipFile(item.downloadLink);
yield createZipFile(this.plugin, `${item.name}.zip`, arrayBuffer);
yield registerIconPack(item.name, arrayBuffer);
new obsidian.Notice(`...${item.displayName} added`);
this.onAddedIconPack();
});
}
renderSuggestion(item, el) {
super.renderSuggestion(item, el);
el.innerHTML = `<div>${el.innerHTML}</div>`;
}
}
class PredefinedIconPacksSetting extends IconFolderSetting {
constructor(plugin, containerEl, app, refreshDisplay) {
super(plugin, containerEl);
this.app = app;
this.refreshDisplay = refreshDisplay;
}
display() {
new obsidian.Setting(this.containerEl)
.setName('Add predefined icon pack')
.setDesc('Add an icon pack like FontAwesome or Remixicons')
.addButton((btn) => {
btn.setButtonText('Browse icon packs');
btn.onClick(() => {
const modal = new IconPackBrowserModal(this.app, this.plugin);
modal.onAddedIconPack = () => {
this.refreshDisplay();
};
modal.open();
});
});
}
}
class RecentlyUsedIconsSetting extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Recently used Icons limit')
.setDesc('Change the limit for the recently used icons displayed in the icon modal.')
.addSlider((slider) => {
var _a;
slider
.setLimits(1, 15, 1)
.setDynamicTooltip()
.setValue((_a = this.plugin.getSettings().recentlyUsedIconsSize) !== null && _a !== void 0 ? _a : DEFAULT_SETTINGS.recentlyUsedIconsSize)
.onChange((val) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().recentlyUsedIconsSize = val;
yield this.plugin.checkRecentlyUsedIcons();
yield this.plugin.saveIconFolderData();
}));
});
}
}
class ToggleIconInTabs extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Toggle Icon in Tabs')
.setDesc('Toggles the visibility of an icon for a file in the tab bar.')
.addToggle((toggle) => {
toggle.setValue(this.plugin.getSettings().iconInTabsEnabled).onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconInTabsEnabled = enabled;
yield this.plugin.saveIconFolderData();
// Updates the already opened files.
this.plugin.app.workspace.getLeavesOfType('markdown').forEach((leaf) => {
const file = leaf.view.file;
if (file) {
if (enabled) {
// Adds the icons to already opened files.
iconTabs.add(this.plugin, file);
}
else {
// Removes the icons from already opened files.
iconTabs.remove(file);
}
}
});
}));
});
}
}
class IconFolderSettings extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { plugin, containerEl, app } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Icon Folder Settings' });
new RecentlyUsedIconsSetting(plugin, containerEl).display();
new IconPacksPathSetting(plugin, containerEl).display();
new EmojiStyleSetting(plugin, containerEl).display();
new ToggleIconInTabs(plugin, containerEl).display();
containerEl.createEl('h3', { text: 'Icon Packs' });
new PredefinedIconPacksSetting(plugin, containerEl, app, () => this.display()).display();
new CustomIconPackSetting(plugin, containerEl, () => this.display()).display();
containerEl.createEl('h3', { text: 'Icon Customization' });
new IconFontSizeSetting(plugin, containerEl).display();
new IconColorSetting(plugin, containerEl).display();
new ExtraMarginSetting(plugin, containerEl).display();
containerEl.createEl('h3', { text: 'Custom Icon Rules' });
new CustomIconRuleSetting(plugin, containerEl, app, () => this.display()).display();
}
}
function around(obj, factories) {
const removers = Object.keys(factories).map(key => around1(obj, key, factories[key]));
return removers.length === 1 ? removers[0] : function () { removers.forEach(r => r()); };
}
function around1(obj, method, createWrapper) {
const original = obj[method], hadOwn = obj.hasOwnProperty(method);
let current = createWrapper(original);
// Let our wrapper inherit static props from the wrapping method,
// and the wrapping method, props from the original method
if (original)
Object.setPrototypeOf(current, original);
Object.setPrototypeOf(wrapper, current);
obj[method] = wrapper;
// Return a callback to allow safe removal
return remove;
function wrapper(...args) {
// If we have been deactivated and are no longer wrapped, remove ourselves
if (current === original && obj[method] === wrapper)
remove();
return current.apply(this, args);
}
function remove() {
// If no other patches, just do a direct removal
if (obj[method] === wrapper) {
if (hadOwn)
obj[method] = original;
else
delete obj[method];
}
if (current === original)
return;
// Else pass future calls through, and remove wrapper from the prototype chain
current = original;
Object.setPrototypeOf(wrapper, original || Function);
}
}
class InternalPluginInjector {
constructor(plugin) {
this.plugin = plugin;
}
get fileExplorers() {
return this.plugin.app.workspace.getLeavesOfType('file-explorer');
}
onMount() { }
}
/**
* @deprecated After obsidian 1.2.6 in favor of the bookmarks plugin.
*/
class StarredInternalPlugin extends InternalPluginInjector {
constructor(plugin) {
super(plugin);
}
get starred() {
return this.plugin.app.internalPlugins.getPluginById('starred');
}
get enabled() {
return this.plugin.app.internalPlugins.getPluginById('starred').enabled;
}
get leaf() {
const leaf = this.plugin.app.workspace.getLeavesOfType('starred');
if (!leaf) {
return undefined;
}
if (leaf.length === 1) {
return leaf[0].view;
}
return undefined;
}
setIcon(filePath, node) {
const iconName = icon.getByPath(this.plugin, filePath);
const iconNode = node.querySelector('.nav-file-icon');
if (!iconNode || !iconName) {
return;
}
dom.setIconForNode(this.plugin, iconName, iconNode);
}
computeNodesWithPath(callback) {
const { itemLookup, containerEl } = this.leaf;
const navFileEls = containerEl.querySelectorAll('.nav-file');
navFileEls.forEach((navFileEl) => {
const lookupFile = itemLookup.get(navFileEl);
if (!lookupFile) {
return;
}
callback(navFileEl, lookupFile.path);
});
}
onMount() {
const nodesWithPath = {};
this.computeNodesWithPath((node, filePath) => {
nodesWithPath[filePath] = node;
});
Object.entries(nodesWithPath).forEach(([filePath, node]) => this.setIcon(filePath, node));
}
register() {
if (!this.plugin.app.internalPlugins.getPluginById('file-explorer').enabled) {
console.info(`[${MetaData.pluginName}/Starred] Skipping starred internal plugin registration because file-explorer is not enabled.`);
return;
}
if (!this.enabled) {
console.info(`[${MetaData.pluginName}/Starred] Skipping starred internal plugin registration because it's not enabled.`);
return;
}
const self = this;
this.plugin.register(around(this.starred.instance, {
addItem: function (next) {
return function (file) {
next.call(this, file);
self.onMount();
};
},
removeItem: function (next) {
return function (file) {
next.call(this, file);
self.onMount();
};
},
}));
}
}
class BookmarkInternalPlugin extends InternalPluginInjector {
constructor(plugin) {
super(plugin);
}
get bookmark() {
return this.plugin.app.internalPlugins.getPluginById('bookmarks');
}
get enabled() {
return this.plugin.app.internalPlugins.getPluginById('bookmarks').enabled;
}
get leaf() {
const leaf = this.plugin.app.workspace.getLeavesOfType('bookmarks');
if (!leaf) {
return undefined;
}
if (leaf.length === 1) {
return leaf[0].view;
}
return undefined;
}
setIconOrRemove(filePath, node) {
const iconName = icon.getByPath(this.plugin, filePath);
let iconNode = node.querySelector('.tree-item-icon');
if (!iconName) {
if (iconNode) {
// Reset the icon to the default obsidian icon.
const items = this.bookmark.instance.items;
const item = items.find((item) => item.path === filePath);
if ((item === null || item === void 0 ? void 0 : item.type) === 'file') {
iconNode.innerHTML = DEFAULT_FILE_ICON;
}
else if ((item === null || item === void 0 ? void 0 : item.type) === 'folder') {
iconNode.innerHTML = DEFAULT_FOLDER_ICON;
}
}
return;
}
// If the icon node is not defined, then we need to recreate it.
if (!iconNode) {
// Get the tree-item-self element where the original icon is set.
const treeItemSelf = node.querySelector('.tree-item-self');
if (!treeItemSelf) {
return;
}
iconNode = node.createDiv({ cls: 'tree-item-icon' });
// Prepends the icon to the tree-item-self element as a first child.
treeItemSelf.prepend(iconNode);
}
const defaultMargin = iconNode.style.margin;
dom.setIconForNode(this.plugin, iconName, iconNode);
// Reset the margin to the default value to prevent overlapping with the text.
iconNode.style.margin = defaultMargin;
}
computeNodesWithPath(callback) {
if (!this.leaf) {
return;
}
/**
* Retrieves the lookup item from the bookmark plugin and calls the callback with the
* element and the path of the item.
* @param item BookmarkItem object which can be a folder or a file.
* @param itemDoms WeakMap of the bookmark plugin which contains the lookup item.
*/
const retrieveLookupItem = (item, itemDoms) => {
const lookupItem = itemDoms.get(item);
if (!lookupItem) {
return;
}
if (item.items) {
// If the item is a folder, then we need to retrieve all the items inside it.
for (const subItem of item.items) {
retrieveLookupItem(subItem, itemDoms);
}
}
// If the item is a `file` or a `folder` (not of type `group`), then we can call the callback.
if (item.type === 'file' || item.type === 'folder') {
callback(lookupItem.el, item.path);
}
};
const { itemDoms, containerEl } = this.leaf;
// Retrieves all the items of the bookmark plugin which areo objects.
const items = this.bookmark.instance.items;
items.forEach((item) => {
retrieveLookupItem(item, itemDoms);
});
}
onMount() {
const nodesWithPath = {};
this.computeNodesWithPath((node, filePath) => {
nodesWithPath[filePath] = node;
});
Object.entries(nodesWithPath).forEach(([filePath, node]) => this.setIconOrRemove(filePath, node));
}
register() {
if (!this.plugin.app.internalPlugins.getPluginById('file-explorer').enabled) {
console.info(`[${MetaData.pluginName}/Bookmarks] Skipping bookmark internal plugin registration because file-explorer is not enabled.`);
return;
}
if (!this.enabled) {
console.info(`[${MetaData.pluginName}/Bookmarks] Skipping bookmark internal plugin registration because it's not enabled.`);
return;
}
const self = this;
this.plugin.register(around(this.bookmark.instance, {
addItem: function (next) {
return function (...args) {
next.call(this, ...args);
// TODO: Remove in the future, I could not think of a better way to do this.
setTimeout(() => {
self.onMount();
}, 1000);
};
},
removeItem: function (next) {
return function (...args) {
next.call(this, ...args);
self.onMount();
};
},
}));
}
}
class IconFolderPlugin extends obsidian.Plugin {
constructor() {
super(...arguments);
this.registeredFileExplorers = new Set();
this.modifiedInternalPlugins = [];
}
migrate() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.getSettings().migrated) {
console.log('migrating icons...');
this.data = migrateIcons(this);
this.getSettings().migrated++;
console.log('...icons migrated');
}
// @ts-ignore
if (this.getSettings().migrated === true) {
this.getSettings().migrated = 1;
}
// Migration for new syncing mechanism.
if (this.getSettings().migrated === 1) {
new obsidian.Notice('Please delete your old icon packs and redownload your icon packs to use the new syncing mechanism.', 20000);
this.getSettings().migrated++;
}
const extraPadding = this.getSettings().extraPadding;
if (extraPadding) {
if (extraPadding.top !== 2 || extraPadding.bottom !== 2 || extraPadding.left !== 2 || extraPadding.right !== 2) {
this.getSettings().extraMargin = extraPadding;
delete this.getSettings()['extraPadding'];
}
}
yield this.saveIconFolderData();
});
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
MetaData.pluginName = this.manifest.id;
console.log(`loading ${MetaData.pluginName}`);
// Registers all modified internal plugins.
// Only adds star plugin for obsidian under v0.12.6.
if (!obsidian.requireApiVersion('0.12.6')) {
this.modifiedInternalPlugins.push(new StarredInternalPlugin(this));
}
else if (obsidian.requireApiVersion('1.2.0')) {
this.modifiedInternalPlugins.push(new BookmarkInternalPlugin(this));
}
yield this.loadIconFolderData();
setPath(this.getSettings().iconPacksPath);
yield createDefaultDirectory(this);
yield this.checkRecentlyUsedIcons();
yield this.migrate();
const usedIconNames = icon.getAllWithPath(this).map((value) => value.icon);
yield loadUsedIcons(this, usedIconNames);
initIconPacks(this);
this.app.workspace.onLayoutReady(() => this.handleChangeLayout());
this.registerEvent(this.app.workspace.on('layout-change', () => this.handleChangeLayout()));
this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => {
const addIconMenuItem = (item) => {
item.setTitle('Change icon');
item.setIcon('hashtag');
item.onClick(() => {
const modal = new IconsPickerModal(this.app, this, file.path);
modal.open();
// Update icon in tab when setting is enabled.
if (this.getSettings().iconInTabsEnabled) {
modal.onSelect = (iconName) => {
iconTabs.update(this, file, iconName);
};
}
});
};
const removeIconMenuItem = (item) => {
item.setTitle('Remove icon');
item.setIcon('trash');
item.onClick(() => {
this.removeFolderIcon(file.path);
dom.removeIconInPath(file.path);
this.notifyPlugins();
// Remove icon in tab when setting is enabled.
if (this.getSettings().iconInTabsEnabled) {
iconTabs.remove(file, { replaceWithDefaultIcon: true });
}
// Check for possible inheritance and add the icon if an inheritance exists.
if (inheritance.doesExistInPath(this, file.path)) {
const folderPath = inheritance.getFolderPathByFilePath(this, file.path);
const folderInheritance = inheritance.getByPath(this, file.path);
const iconName = folderInheritance.inheritanceIcon;
inheritance.add(this, folderPath, iconName, {
file,
onAdd: (file) => {
if (this.getSettings().iconInTabsEnabled) {
iconTabs.add(this, file, { iconName });
}
},
});
}
customRule.addAll(this);
});
};
menu.addItem(addIconMenuItem);
const filePathData = this.getData()[file.path];
const inheritanceFolderHasIcon = typeof filePathData === 'object' && filePathData.iconName !== null;
// Only add remove icon menu item when the file path exists in the data.
// We do not want to show this menu item for e.g. inheritance or custom rules.
if (filePathData && (typeof filePathData === 'string' || inheritanceFolderHasIcon)) {
menu.addItem(removeIconMenuItem);
}
const inheritIcon = (item) => {
const iconData = this.data[file.path];
if (typeof iconData === 'object') {
item.setTitle('Remove inherit icon');
item.onClick(() => {
inheritance.remove(this, file.path, {
onRemove: (file) => {
// Removes the icons from the file tabs inside of the inheritance.
if (this.getSettings().iconInTabsEnabled) {
iconTabs.remove(file, { replaceWithDefaultIcon: true });
}
},
});
this.saveInheritanceData(file.path, null);
removeIconFromIconPack(this, iconData.inheritanceIcon);
});
}
else {
item.setTitle('Inherit icon');
item.onClick(() => {
const modal = new IconsPickerModal(this.app, this, file.path);
modal.open();
// manipulate `onChooseItem` method to get custom functionality for inheriting icons
modal.onChooseItem = (icon) => {
this.saveInheritanceData(file.path, icon);
const iconName = typeof icon === 'string' ? icon : icon.displayName;
saveIconToIconPack(this, iconName);
inheritance.add(this, file.path, iconName, {
onAdd: (file) => {
if (this.getSettings().iconInTabsEnabled) {
iconTabs.add(this, file, { iconName });
}
},
});
};
});
}
item.setIcon('vertical-three-dots');
};
menu.addItem(inheritIcon);
}));
// deleting event
this.registerEvent(this.app.vault.on('delete', (file) => {
const path = file.path;
this.removeFolderIcon(path);
}));
// renaming event
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => {
this.renameFolder(file.path, oldPath);
}));
this.addSettingTab(new IconFolderSettings(this.app, this));
});
}
isSomeEmojiStyleActive() {
return this.getSettings().emojiStyle !== 'none';
}
notifyPlugins() {
this.modifiedInternalPlugins.forEach((internalPlugin) => {
if (internalPlugin.enabled) {
internalPlugin.onMount();
}
});
}
handleChangeLayout() {
// Transform data that are objects to single strings.
const data = Object.entries(this.data);
this.modifiedInternalPlugins.forEach((internalPlugin) => {
if (internalPlugin.enabled) {
internalPlugin.onMount();
internalPlugin.register();
}
});
icon.addAll(this, data, this.registeredFileExplorers, () => {
//const searchLeaveDom = this.getSearchLeave().dom;
//searchLeaveDom.changed = () => this.addIconsToSearch();
// Register rename event for adding icons with custom rules to the DOM and updating
// inheritance when file was moved to another directory.
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => {
customRule.getSortedRules(this).forEach((rule) => __awaiter(this, void 0, void 0, function* () {
if (customRule.doesExistInPath(rule, oldPath)) {
dom.removeIconInPath(file.path);
}
yield customRule.add(this, rule, file, undefined);
}));
if (inheritance.doesExistInPath(this, file.path)) {
const folderPath = inheritance.getFolderPathByFilePath(this, file.path);
const folderInheritance = inheritance.getByPath(this, file.path);
const iconName = folderInheritance.inheritanceIcon;
dom.removeIconInPath(file.path);
inheritance.add(this, folderPath, iconName, {
file,
onAdd: (file) => {
if (this.getSettings().iconInTabsEnabled) {
iconTabs.add(this, file, { iconName });
}
},
});
}
}));
// Register create event for checking inheritance functionality.
this.registerEvent(this.app.vault.on('create', (file) => {
const inheritanceFolders = Object.entries(this.data).filter(([k, v]) => k !== 'settings' && typeof v === 'object');
if (!file.parent || file.parent.path === '/')
return;
inheritanceFolders.forEach(([path, obj]) => {
inheritance.add(this, path, obj.inheritanceIcon, {
file,
onAdd: (file) => {
if (this.getSettings().iconInTabsEnabled) {
iconTabs.add(this, file, { iconName: obj.inheritanceIcon });
}
},
});
});
}));
// Register active leaf change event for adding icon of file to tab.
this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {
if (!this.getSettings().iconInTabsEnabled) {
return;
}
if (leaf.view.getViewType() !== 'markdown') {
return;
}
const explorerLeaf = leaf;
if (explorerLeaf.view.file) {
iconTabs.add(this, explorerLeaf.view.file);
}
}));
});
}
saveInheritanceData(folderPath, icon) {
const currentValue = this.data[folderPath];
// if icon is null, it will remove the inheritance icon from the data
if (icon === null && currentValue && typeof currentValue === 'object') {
const folderObject = currentValue;
if (folderObject.iconName) {
this.data[folderPath] = getNormalizedName(folderObject.iconName);
}
else {
delete this.data[folderPath];
}
}
// icon is not null, so it will add inheritance data
else {
// check if data already exists
if (currentValue) {
// check if current value is already an icon name
if (typeof currentValue === 'string') {
this.data[folderPath] = {
iconName: currentValue,
inheritanceIcon: getNormalizedName(typeof icon === 'object' ? icon.displayName : icon),
};
}
// check if it has already a inheritance icon
else if (folderPath !== 'settings') {
this.data[folderPath] = Object.assign(Object.assign({}, currentValue), { inheritanceIcon: getNormalizedName(typeof icon === 'object' ? icon.displayName : icon) });
}
}
else {
this.data[folderPath] = {
iconName: null,
inheritanceIcon: getNormalizedName(typeof icon === 'object' ? icon.displayName : icon),
};
}
}
this.saveIconFolderData();
}
onunload() {
console.log('unloading obsidian-icon-folder');
}
renameFolder(newPath, oldPath) {
if (!this.data[oldPath] || newPath === oldPath) {
return;
}
Object.defineProperty(this.data, newPath, Object.getOwnPropertyDescriptor(this.data, oldPath));
delete this.data[oldPath];
this.saveIconFolderData();
}
removeFolderIcon(path) {
if (!this.data[path]) {
return;
}
// Saves the icon name with prefix to remove it from the icon pack directory later.
const iconData = this.data[path];
if (typeof this.data[path] === 'object') {
const currentValue = this.data[path];
this.data[path] = Object.assign(Object.assign({}, currentValue), { iconName: null });
}
else {
delete this.data[path];
}
// Removes the icon from the icon pack directory if it is not used as an icon somewhere
// else.
if (iconData) {
let iconNameWithPrefix = iconData;
if (typeof iconData === 'object') {
iconNameWithPrefix = iconData.iconName;
}
else {
iconNameWithPrefix = iconData;
}
removeIconFromIconPack(this, iconNameWithPrefix);
}
//this.addIconsToSearch();
this.saveIconFolderData();
}
addFolderIcon(path, icon) {
const iconName = getNormalizedName(typeof icon === 'object' ? icon.displayName : icon);
// Check if inheritance is active for this path.
if (typeof this.data[path] === 'object') {
const currentValue = this.data[path];
this.data[path] = Object.assign(Object.assign({}, currentValue), { iconName });
}
else {
this.data[path] = iconName;
}
// Update recently used icons.
if (!this.getSettings().recentlyUsedIcons.includes(iconName)) {
if (this.getSettings().recentlyUsedIcons.length >= this.getSettings().recentlyUsedIconsSize) {
this.getSettings().recentlyUsedIcons = this.getSettings().recentlyUsedIcons.slice(0, this.getSettings().recentlyUsedIconsSize - 1);
}
this.getSettings().recentlyUsedIcons.unshift(iconName);
this.checkRecentlyUsedIcons();
}
//this.addIconsToSearch();
this.saveIconFolderData();
}
getSettings() {
return this.data.settings;
}
loadIconFolderData() {
return __awaiter(this, void 0, void 0, function* () {
const data = yield this.loadData();
if (data) {
Object.entries(DEFAULT_SETTINGS).forEach(([k, v]) => {
if (!data.settings[k]) {
data.settings[k] = v;
}
});
}
this.data = Object.assign({ settings: Object.assign({}, DEFAULT_SETTINGS) }, {}, data);
});
}
saveIconFolderData() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.data);
});
}
checkRecentlyUsedIcons() {
return __awaiter(this, void 0, void 0, function* () {
if (this.getSettings().recentlyUsedIcons.length > this.getSettings().recentlyUsedIconsSize) {
this.getSettings().recentlyUsedIcons = this.getSettings().recentlyUsedIcons.slice(0, this.getSettings().recentlyUsedIconsSize);
yield this.saveIconFolderData();
}
});
}
getData() {
return this.data;
}
getRegisteredFileExplorers() {
return this.registeredFileExplorers;
}
/**
* Returns a possible data path by the given value. This function checks for direct icon,
* inheritance icon and custom rules.
* @param value String that will be used to find the data path.
* @returns String that is the data path or `undefined` if no data path was found.
*/
getDataPathByValue(value) {
return Object.entries(this.data).find(([k, v]) => {
if (typeof v === 'string') {
if (value === v) {
return k;
}
}
else if (typeof v === 'object') {
// Check for custom rules.
if (k === 'settings') {
// `rules` are defined in the settings object.
const rules = v.rules;
return rules.find((rule) => rule.icon === value);
}
// Check for inheritance icons.
v = v;
if (value === v.iconName || value === v.inheritanceIcon) {
return k;
}
}
});
}
}
module.exports = IconFolderPlugin;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzLy5wbnBtL0Byb2xsdXArcGx1Z2luLXR5cGVzY3JpcHRAOC41LjBfcm9sbHVwQDIuNzkuMV90c2xpYkAyLjYuMl90eXBlc2NyaXB0QDQuOS41L25vZGVfbW9kdWxlcy90c2xpYi90c2xpYi5lczYuanMiLCJzcmMvTWV0YURhdGEudHMiLCJzcmMvbGliL3V0aWwvc3ZnLnRzIiwibm9kZV9tb2R1bGVzLy5wbnBtL2pzemlwQDMuMTAuMS9ub2RlX21vZHVsZXMvanN6aXAvZGlzdC9qc3ppcC5taW4uanMiLCJzcmMvemlwVXRpbC50cyIsInNyYy9pY29uUGFja01hbmFnZXIudHMiLCJub2RlX21vZHVsZXMvLnBucG0vdHdlbW9qaUAxNC4wLjIvbm9kZV9tb2R1bGVzL3R3ZW1vamkvZGlzdC90d2Vtb2ppLmVzbS5qcyIsInNyYy9lbW9qaS50cyIsInNyYy91dGlsLnRzIiwic3JjL2xpYi91dGlsL3N0eWxlLnRzIiwic3JjL2xpYi91dGlsL2RvbS50cyIsInNyYy9pY29uc1BpY2tlck1vZGFsLnRzIiwic3JjL3NldHRpbmdzL2RhdGEudHMiLCJzcmMvbGliL2N1c3RvbVJ1bGUudHMiLCJzcmMvbGliL2ljb25UYWJzLnRzIiwic3JjL2xpYi9pbmhlcml0YW5jZS50cyIsInNyYy9saWIvaWNvbi50cyIsInNyYy9taWdyYXRpb24udHMiLCJzcmMvc2V0dGluZ3MvdWkvaWNvbkZvbGRlclNldHRpbmcudHMiLCJzcmMvc2V0dGluZ3MvdWkvY3VzdG9tSWNvblBhY2sudHMiLCJzcmMvc2V0dGluZ3MvdWkvY3VzdG9tSWNvblJ1bGUudHMiLCJzcmMvc2V0dGluZ3MvdWkvZW1vamlTdHlsZS50cyIsInNyYy9zZXR0aW5ncy9oZWxwZXIudHMiLCJzcmMvc2V0dGluZ3MvdWkvZXh0cmFNYXJnaW4udHMiLCJzcmMvc2V0dGluZ3MvdWkvaWNvbkNvbG9yLnRzIiwic3JjL3NldHRpbmdzL3VpL2ljb25Gb250U2l6ZS50cyIsInNyYy9zZXR0aW5ncy91aS9pY29uUGFja3NQYXRoLnRzIiwic3JjL2ljb25QYWNrcy50cyIsInNyYy9pY29uUGFja0Jyb3dzZXJNb2RhbC50cyIsInNyYy9zZXR0aW5ncy91aS9wcmVkZWZpbmVkSWNvblBhY2tzLnRzIiwic3JjL3NldHRpbmdzL3VpL3JlY2VudGx5VXNlZEljb25zLnRzIiwic3JjL3NldHRpbmdzL3VpL3RvZ2dsZUljb25JblRhYnMudHMiLCJzcmMvc2V0dGluZ3MvdWkvaW5kZXgudHMiLCJub2RlX21vZHVsZXMvLnBucG0vbW9ua2V5LWFyb3VuZEAyLjMuMC9ub2RlX21vZHVsZXMvbW9ua2V5LWFyb3VuZC9tanMvaW5kZXguanMiLCJzcmMvQHR5cGVzL2ludGVybmFsUGx1Z2luSW5qZWN0b3IudHMiLCJzcmMvaW50ZXJuYWwtcGx1Z2lucy9zdGFycmVkLnRzIiwic3JjL2ludGVybmFsLXBsdWdpbnMvYm9va21hcmsudHMiLCJzcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6bnVsbCwibmFtZXMiOlsicmVxdWlyZSIsImdsb2JhbCIsInJlcXVlc3RVcmwiLCJKU1ppcCIsIk5vdGljZSIsImljb25QYWNrcyIsIkZ1enp5U3VnZ2VzdE1vZGFsIiwiZG9lc0V4aXN0SW5QYXRoIiwiYWRkQWxsIiwiYWRkIiwiZ2V0QnlQYXRoIiwiZ2V0RmlsZXMiLCJyZW1vdmUiLCJTZXR0aW5nIiwiTW9kYWwiLCJUZXh0Q29tcG9uZW50IiwiQnV0dG9uQ29tcG9uZW50IiwiQ29sb3JDb21wb25lbnQiLCJEcm9wZG93bkNvbXBvbmVudCIsIlNsaWRlckNvbXBvbmVudCIsIlBsdWdpblNldHRpbmdUYWIiLCJQbHVnaW4iLCJyZXF1aXJlQXBpVmVyc2lvbiIsIkljb25Gb2xkZXJTZXR0aW5nc1VJIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBb0dBO0FBQ08sU0FBUyxTQUFTLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFO0FBQzdELElBQUksU0FBUyxLQUFLLENBQUMsS0FBSyxFQUFFLEVBQUUsT0FBTyxLQUFLLFlBQVksQ0FBQyxHQUFHLEtBQUssR0FBRyxJQUFJLENBQUMsQ0FBQyxVQUFVLE9BQU8sRUFBRSxFQUFFLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFO0FBQ2hILElBQUksT0FBTyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLEVBQUUsVUFBVSxPQUFPLEVBQUUsTUFBTSxFQUFFO0FBQy9ELFFBQVEsU0FBUyxTQUFTLENBQUMsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRTtBQUNuRyxRQUFRLFNBQVMsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRTtBQUN0RyxRQUFRLFNBQVMsSUFBSSxDQUFDLE1BQU0sRUFBRSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsUUFBUSxDQUFDLENBQUMsRUFBRTtBQUN0SCxRQUFRLElBQUksQ0FBQyxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUMsS0FBSyxDQUFDLE9BQU8sRUFBRSxVQUFVLElBQUksRUFBRSxDQUFDLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUM5RSxLQUFLLENBQUMsQ0FBQztBQUNQLENBQUM7QUFnTUQ7QUFDdUIsT0FBTyxlQUFlLEtBQUssVUFBVSxHQUFHLGVBQWUsR0FBRyxVQUFVLEtBQUssRUFBRSxVQUFVLEVBQUUsT0FBTyxFQUFFO0FBQ3ZILElBQUksSUFBSSxDQUFDLEdBQUcsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUM7QUFDL0IsSUFBSSxPQUFPLENBQUMsQ0FBQyxJQUFJLEdBQUcsaUJBQWlCLEVBQUUsQ0FBQyxDQUFDLEtBQUssR0FBRyxLQUFLLEVBQUUsQ0FBQyxDQUFDLFVBQVUsR0FBRyxVQUFVLEVBQUUsQ0FBQyxDQUFDO0FBQ3JGOztBQzdUYyxNQUFPLFFBQVEsQ0FBQTtBQUU1Qjs7QUN