digital-garden/.obsidian/plugins/obsidian-icon-folder/main.js

6608 lines
946 KiB
JavaScript
Raw Normal View History

2024-06-13 21:01:37 +03: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');
var view = require('@codemirror/view');
var state = require('@codemirror/state');
var language = require('@codemirror/language');
/******************************************************************************
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;
};
// 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');
}
const possibleTitle = svg.querySelector('title');
if (possibleTitle) {
possibleTitle.remove();
}
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 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.');
}
var jszip_min = {exports: {}};
/*!
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
*/
jszip_min.exports;
(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(
} (jszip_min, jszip_min.exports));
var jszip_minExports = jszip_min.exports;
/**
* 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_1, ...args_1) => __awaiter(void 0, [bytes_1, ...args_1], void 0, function* (bytes, extraPath = '') {
const unzippedFiles = yield jszip_minExports.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;
});
});
const PLUGIN_NAME = 'iconize';
const TITLE_ICON_CLASS = 'iconize-title-icon';
const INLINE_TITLE_WRAPPER_CLASS = 'iconize-inline-title-wrapper';
/**
* The name of the attribute that is used to store the icon name in the node.
* The value of this attributes contains the prefix and the name of the icon.
*/
const ICON_ATTRIBUTE_NAME = 'data-icon';
var config = {
PLUGIN_NAME,
TITLE_ICON_CLASS,
INLINE_TITLE_WRAPPER_CLASS,
ICON_ATTRIBUTE_NAME,
};
class ConsoleLogger {
constructor(projectPrefix, enabled = false) {
this.logLevels = {
log: { label: 'LOG:' },
info: { label: 'INFO:' },
warn: { label: 'WARN:' },
error: { label: 'ERROR:' },
};
this.projectPrefix = projectPrefix;
this.enabled = enabled;
}
formatMessage(level, message, optionalParams) {
const timestamp = new Date().toISOString();
const { label } = this.logLevels[level];
return [
`${this.projectPrefix}: [${timestamp}] ${label} ${message}`,
...optionalParams,
];
}
log(message, ...optionalParams) {
if (this.enabled) {
console.log(...this.formatMessage('log', message, optionalParams));
}
}
info(message, ...optionalParams) {
if (this.enabled) {
console.info(...this.formatMessage('info', message, optionalParams));
}
}
warn(message, ...optionalParams) {
if (this.enabled) {
console.warn(...this.formatMessage('warn', message, optionalParams));
}
}
error(message, ...optionalParams) {
if (this.enabled) {
console.error(...this.formatMessage('error', message, optionalParams));
}
}
toggleLogging(enabled) {
this.enabled = enabled;
}
}
const logger = new ConsoleLogger(config.PLUGIN_NAME);
const iconPacks$1 = {
faBrands: {
name: 'font-awesome-brands',
displayName: 'FontAwesome Brands',
path: 'fontawesome-free-6.5.1-web/svgs/brands/',
downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.1/fontawesome-free-6.5.1-web.zip',
},
faRegular: {
name: 'font-awesome-regular',
displayName: 'FontAwesome Regular',
path: 'fontawesome-free-6.5.1-web/svgs/regular/',
downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.1/fontawesome-free-6.5.1-web.zip',
},
faSolid: {
name: 'font-awesome-solid',
displayName: 'FontAwesome Solid',
path: 'fontawesome-free-6.5.1-web/svgs/solid/',
downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.1/fontawesome-free-6.5.1-web.zip',
},
remixIcons: {
name: 'remix-icons',
displayName: 'Remix Icons',
path: '',
downloadLink: 'https://github.com/Remix-Design/RemixIcon/releases/download/v4.2.0/RemixIcon_Svg_v4.2.0.zip',
},
iconBrew: {
name: 'icon-brew',
displayName: 'Icon Brew',
path: '',
downloadLink: 'https://github.com/FlorianWoelki/obsidian-iconize/raw/main/iconPacks/icon-brew.zip',
},
/** @source https://simpleicons.org/ */
simpleIcons: {
name: 'simple-icons',
displayName: 'Simple Icons',
path: 'simple-icons-11.10.0/icons/',
downloadLink: 'https://github.com/simple-icons/simple-icons/archive/refs/tags/11.10.0.zip',
},
lucide: {
name: 'lucide-icons',
displayName: 'Lucide',
path: '',
downloadLink: 'https://github.com/lucide-icons/lucide/releases/download/0.363.0/lucide-icons-0.363.0.zip',
},
tablerIcons: {
name: 'tabler-icons',
displayName: 'Tabler Icons',
path: 'svg',
downloadLink: 'https://github.com/tabler/tabler-icons/releases/download/v3.1.0/tabler-icons-3.1.0.zip',
},
/** @source https://boxicons.com/ */
boxicons: {
name: 'boxicons',
displayName: 'Boxicons',
path: 'svg',
downloadLink: 'https://github.com/FlorianWoelki/obsidian-iconize/raw/main/iconPacks/boxicons.zip',
},
/** @source http://nagoshiashumari.github.io/Rpg-Awesome/ */
rpgAwesome: {
name: 'rpg-awesome',
displayName: 'RPG Awesome',
path: '',
downloadLink: 'https://github.com/FlorianWoelki/obsidian-iconize/raw/main/iconPacks/rpg-awesome.zip',
},
/** @source https://coolicons.cool/ */
coolicons: {
name: 'coolicons',
displayName: 'Coolicons',
path: 'cooliocns SVG',
downloadLink: 'https://github.com/krystonschwarze/coolicons/releases/download/v4.1/coolicons.v4.1.zip',
},
/** @source https://feathericons.com/ */
feathericons: {
name: 'feather-icons',
displayName: 'Feather Icons',
path: 'feather-4.29.1/icons/',
downloadLink: 'https://github.com/feathericons/feather/archive/refs/tags/v4.29.1.zip',
},
/** @source https://github.com/primer/octicons */
octicons: {
name: 'octicons',
displayName: 'Octicons',
path: 'octicons-19.8.0/icons/',
downloadLink: 'https://github.com/primer/octicons/archive/refs/tags/v19.8.0.zip',
},
};
/**
* Returns a possible path to the icon pack.
* @param name String of the icon pack name.
* @returns String of the path to the icon pack or undefined if the icon pack does not
* exist.
*/
const getExtraPath = (iconPackName) => {
var _a;
const path = (_a = Object.values(iconPacks$1).find((iconPack) => iconPack.name === iconPackName)) === null || _a === void 0 ? void 0 : _a.path;
return (path === null || path === void 0 ? void 0 : path.length) === 0 ? undefined : path;
};
let path;
const getPath = () => {
return path;
};
const setPath = (newPath) => {
if (newPath === 'plugins/obsidian-icon-folder/icons') {
newPath = '.obsidian/plugins/obsidian-icon-folder/icons';
new obsidian.Notice(`[${config.PLUGIN_NAME}] 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;
};
let preloadedIcons = [];
const getPreloadedIcons = () => {
return preloadedIcons;
};
const resetPreloadedIcons = () => {
preloadedIcons = [];
};
let iconPacks = [];
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.length; i++) {
const iconPack = iconPacks[i];
if (yield plugin.app.vault.adapter.exists(`${from}/${iconPack.name}`)) {
// 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.length; i++) {
const iconPack = iconPacks[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.push({ name: dir, icons: [], prefix, custom: true });
});
const deleteIconPack = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () {
iconPacks = iconPacks.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.remove(`${path}/${dir}.zip`);
}
});
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);
logger.info(`Renamed old file ${normalizedFilename} to ${newFilename} due to duplication`);
new obsidian.Notice(`[${config.PLUGIN_NAME}] Renamed ${normalizedFilename} to ${newFilename} to avoid duplication.`, 8000);
}
else {
logger.warn(`Could not create icons with duplicated file names (file name: ${normalizedFilename})`);
new obsidian.Notice(`[${config.PLUGIN_NAME}] 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;
};
const getFilesInDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () {
if (!(yield plugin.app.vault.adapter.exists(dir))) {
return [];
}
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)) {
logger.info(`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) {
logger.info(`Skipping icon with invalid svg content: ${iconName}`);
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.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) {
// Ignore because background check automatically adds the icons and icon pack
// directories.
if (!plugin.getSettings().iconsBackgroundCheckEnabled) {
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))) {
logger.info(`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 = getNormalizedName(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.push({
name: folderName,
icons: loadedIcons,
prefix,
custom: true,
});
logger.info(`Loaded icon pack '${folderName}' (amount of icons: ${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.push({
name: zipFile,
icons: loadedIcons,
prefix,
custom: false,
});
logger.info(`Loaded icon pack '${zipFile}' (amount of icons: ${loadedIcons.length})`);
}
});
const getLoadedIconsFromZipFile = (iconPackName, files) => __awaiter(void 0, void 0, void 0, function* () {
const loadedIcons = [];
const extraPath = getExtraPath(iconPackName);
for (let j = 0; j < files.length; j++) {
// Checks if the icon pack has an extra path. Also ignores files which do not start
// with the extra path.
if (extraPath && !files[j].name.startsWith(extraPath)) {
continue;
}
const file = yield getFileFromJSZipFile(files[j]);
const iconContent = yield file.text();
const iconName = getNormalizedName(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) {
logger.warn(`Icon could not be generated (icon: ${iconName}, content: ${iconContent})`);
return undefined;
}
const iconPack = iconPacks.find((iconPack) => iconPack.name === iconPackName);
if (!iconPack) {
logger.warn(`Iconpack with name '${iconPackName}' was not found`);
return undefined;
}
iconPack.icons.push(icon);
return icon;
};
const removeIconFromIconPackDirectory = (plugin, iconPackName, iconName) => {
const iconPack = iconPacks.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.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.push({ name, icons: loadedIcons, prefix, custom: false });
logger.info(`Loaded icon pack ${name} (amount of icons: ${loadedIcons.length})`);
});
const doesIconExists = (iconName) => {
const icons = getAllLoadedIconNames();
return (icons.find((icon) => icon.name === iconName || icon.prefix + icon.name === iconName) !== undefined);
};
const getIconFromIconPack = (iconPackName, iconPrefix, iconName) => {
const foundIcon = preloadedIcons.find((icon) => icon.prefix.toLowerCase() === iconPrefix.toLowerCase() &&
icon.name.toLowerCase() === iconName.toLowerCase());
if (foundIcon) {
return foundIcon;
}
const iconPack = iconPacks.find((iconPack) => iconPack.name === iconPackName);
if (!iconPack) {
return undefined;
}
return iconPack.icons.find((icon) => getNormalizedName(icon.name) === iconName);
};
const getSvgFromLoadedIcon = (iconPrefix, iconName) => {
let icon = '';
let foundIcon = preloadedIcons.find((icon) => icon.prefix.toLowerCase() === iconPrefix.toLowerCase() &&
icon.name.toLowerCase() === iconName.toLowerCase());
if (!foundIcon) {
iconPacks.forEach((iconPack) => {
const icon = iconPack.icons.find((icon) => {
return (icon.prefix.toLowerCase() === iconPrefix.toLowerCase() &&
getNormalizedName(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://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/",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\u20
const getRegex = () => {
return new RegExp(/[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD
};
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 regex = getRegex();
const emojiMatches = str.match(regex);
const emojiString = emojiMatches ? emojiMatches.join('') : '';
return !/\d/.test(str) && emojiString === str;
};
const parseEmoji = (style, str, size = 16) => {
switch (style) {
case 'twemoji':
return twemoji.parse(str, {
folder: 'svg',
ext: '.svg',
attributes: () => ({
width: `${size}px`,
height: `${size}px`,
}),
});
case 'native':
return str;
default:
return null;
}
};
/**
* Gets the shortcode for a given emoji by the name of the emoji. This function replaces
* spaces with underscores and removes colons.
* @param key String to replace with shortcode.
* @returns String with shortcode, or `undefined` if no shortcode exists.
*/
const getShortcode = (key) => {
var _a;
// Removable of colons is necessary for the flag shortcodes.
return (_a = shortNames[key]) === null || _a === void 0 ? void 0 : _a.replace(/\s/g, '_').replace(/:/g, '').toLowerCase();
};
var emoji = {
shortNames,
isEmoji,
getShortcode,
parseEmoji,
getRegex,
};
// 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. This also returns the leaf of the file.
* @param plugin Instance of the IconFolderPlugin.
* @returns An array of {@link FileWithLeaf} objects.
*/
const getAllOpenedFiles = (plugin) => {
return plugin.app.workspace
.getLeavesOfType('markdown')
.reduce((prev, curr) => {
const file = curr.view.file;
if (file) {
prev.push(Object.assign(Object.assign({}, file), { leaf: curr, pinned: false }));
}
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) {
throw new Error(`Icon ${iconNameWithPrefix} could not be found.`);
}
const iconPackName = getIconPackNameByPrefix(iconPrefix);
const icon = getIconFromIconPack(iconPackName, iconPrefix, iconName);
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);
}
};
/**
* A utility function which will convert a string to a hexadecimal color.
* @param str String that will be converted to a hexadecimal color.
* @returns A string which is the hexadecimal color.
*/
const stringToHex = (str) => {
const validHex = str.replace(/[^0-9a-fA-F]/g, '');
const hex = validHex.padStart(6, '0').substring(0, 6);
return `#${hex}`;
};
/**
* A utility function which will check if a string is a hexadecimal color.
* @param str String that will be checked if it is a hexadecimal color.
* @param includeHash Boolean which will include the hash in the check.
* @returns A boolean which is true if the string is a hexadecimal color.
*/
const isHexadecimal = (str, includeHash = false) => {
const regex = new RegExp(`^${includeHash ? '#' : ''}[0-9A-Fa-f]{1,6}$`);
return regex.test(str);
};
// 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 `.iconize-icon`
* class is defined. This function only modifies the styling of the node.
* @param plugin Instance of the IconFolderPlugin.
* @param applyStyles Function that is getting called when the icon node is found and
* typically applies all the styles to the icon.
*/
const refreshIconNodes = (plugin, applyStyles = applyAll) => {
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('.iconize-icon');
if (iconNode) {
const pathValue = plugin.getData()[path];
const hasIndividualColor = typeof pathValue === 'object' && pathValue.iconColor;
iconNode.innerHTML = applyStyles(plugin, iconNode.innerHTML, iconNode);
if (hasIndividualColor) {
iconNode.style.color = pathValue.iconColor;
const colorizedInnerHtml = svg.colorize(iconNode.innerHTML, pathValue.iconColor);
iconNode.innerHTML = colorizedInnerHtml;
}
}
}
});
}
};
var style = {
applyAll,
setMargin,
refreshIconNodes,
};
/**
* Removes the `iconize-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('.iconize-icon');
if (!iconNode) {
return;
}
iconNode.remove();
};
/**
* Removes the 'iconize-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) {
logger.warn(`Element with data path not found (path: ${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) => {
var _a;
// 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(iconContent, color);
}
node.innerHTML = iconContent;
}
else {
const parsedEmoji = (_a = emoji.parseEmoji(plugin.getSettings().emojiStyle, iconName)) !== null && _a !== void 0 ? _a : iconName;
node.innerHTML = style.applyAll(plugin, parsedEmoji, node);
}
node.setAttribute('title', iconName);
};
/**
* 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;
// 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) {
logger.warn(`Element with data path not found (path: ${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) {
logger.warn(`Element with title node not found (path: ${path})`);
return;
}
}
let iconNode = node.querySelector('.iconize-icon');
// If the icon is already set in the path, we do not need to create a new div element.
if (iconNode) {
setIconForNode(plugin, iconName, iconNode, options === null || options === void 0 ? void 0 : options.color);
}
else {
// Creates a new icon node and inserts it to the DOM.
iconNode = document.createElement('div');
iconNode.setAttribute(config.ICON_ATTRIBUTE_NAME, iconName);
iconNode.classList.add('iconize-icon');
setIconForNode(plugin, iconName, iconNode, options === null || options === void 0 ? void 0 : options.color);
node.insertBefore(iconNode, titleNode);
}
};
/**
* Checks if the element has an icon node by checking if the element has a child with the
* class `iconize-icon`.
* @param element HTMLElement which will be checked if it has an icon.
* @returns Boolean whether the element has an icon node or not.
*/
const doesElementHasIconNode = (element) => {
return element.querySelector('.iconize-icon') !== null;
};
/**
* Gets the icon name of the element if it has an icon node.
* @param element HTMLElement parent which includes a node with the icon.
* @returns String with the icon name if the element has an icon, `undefined` otherwise.
*/
const getIconFromElement = (element) => {
const iconNode = element.querySelector('.iconize-icon');
const existingIcon = iconNode === null || iconNode === void 0 ? void 0 : iconNode.getAttribute(config.ICON_ATTRIBUTE_NAME);
return existingIcon;
};
const getIconNodeFromPath = (path) => {
var _a;
return (_a = document
.querySelector(`[data-path="${path}"]`)) === null || _a === void 0 ? void 0 : _a.querySelector('[data-icon]');
};
var dom = {
setIconForNode,
createIconNode,
doesElementHasIconNode,
getIconFromElement,
getIconNodeFromPath,
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('iconize-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 (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,
});
}
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' && !emoji.isEmoji(iconNameWithPrefix)) {
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('iconize-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('iconize-subheadline');
subheadline.innerText = 'All Icons:';
this.resultContainerEl.append(subheadline);
}
}
if (item.item.name !== 'default') {
if (item.item.prefix === 'Emoji') {
const displayName = emoji.parseEmoji(this.plugin.getSettings().emojiStyle, item.item.displayName);
if (!displayName) {
return;
}
el.innerHTML = `<div>${el.innerHTML}</div><div class="iconize-icon-preview">${displayName}</div>`;
}
else {
el.innerHTML = `<div>${el.innerHTML}</div><div class="iconize-icon-preview">${getSvgFromLoadedIcon(item.item.prefix, item.item.name)}</div>`;
}
}
this.renderIndex++;
}
}
var IconInTitlePosition;
(function (IconInTitlePosition) {
IconInTitlePosition["Above"] = "above";
IconInTitlePosition["Inline"] = "inline";
})(IconInTitlePosition || (IconInTitlePosition = {}));
const DEFAULT_SETTINGS = {
migrated: 2,
iconPacksPath: '.obsidian/icons',
fontSize: 16,
emojiStyle: 'native',
iconColor: null,
recentlyUsedIcons: [],
recentlyUsedIconsSize: 5,
rules: [],
extraMargin: {
top: 0,
right: 4,
bottom: 0,
left: 0,
},
iconInTabsEnabled: false,
iconInTitleEnabled: false,
iconInTitlePosition: IconInTitlePosition.Above,
iconInFrontmatterEnabled: false,
iconInFrontmatterFieldName: 'icon',
iconColorInFrontmatterFieldName: 'iconColor',
iconsBackgroundCheckEnabled: false,
iconsInNotesEnabled: true,
iconsInLinksEnabled: true,
iconIdentifier: ':',
debugMode: false,
};
function migrate$4(plugin) {
return __awaiter(this, void 0, void 0, function* () {
// Migration for new syncing mechanism.
if (plugin.getSettings().migrated === 1) {
new obsidian.Notice('Please delete your old icon packs and redownload your icon packs to use the new syncing mechanism.', 20000);
plugin.getSettings().migrated++;
}
});
}
function migrate$3(plugin) {
return __awaiter(this, void 0, void 0, function* () {
// Migration for new order functionality of custom rules.
if (plugin.getSettings().migrated === 2) {
// Sorting alphabetically was the default behavior before.
plugin
.getSettings()
.rules.sort((a, b) => a.rule.localeCompare(b.rule))
.forEach((rule, i) => {
rule.order = i;
});
plugin.getSettings().migrated++;
}
});
}
class IconCache {
constructor() {
this.cache = new Map();
this.set = (path, result) => {
this.cache.set(path, result);
};
this.invalidate = (path) => {
this.cache.delete(path);
};
this.clear = () => {
this.cache.clear();
};
this.get = (path) => {
var _a;
return (_a = this.cache.get(path)) !== null && _a !== void 0 ? _a : null;
};
this.doesRecordExist = (path) => {
return this.get(path) !== null;
};
if (IconCache.instance) {
throw new Error('Error: Instantiation failed: Use `IconCache.getInstance()` instead of new.');
}
IconCache.instance = this;
}
}
IconCache.instance = new IconCache();
IconCache.getInstance = () => {
return IconCache.instance;
};
/**
* Checks if the file type is equal to the `for` property of the custom rule.
* @param rule CustomRule that will be checked.
* @param fileType CustomRuleFileType that will be checked. Can be either `file` or `folder`.
* @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 or folder matches a specified custom rule.
* @param plugin Plugin instance.
* @param rule CustomRule to check against the file or folder.
* @param file TAbstractFile to check against the custom rule.
* @returns 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* () {
const metadata = yield plugin.app.vault.adapter.stat(file.path);
if (!metadata) {
return false;
}
const fileType = metadata.type;
const doesMatch = doesMatchFileType(rule, fileType);
if (!doesMatch) {
return false;
}
return doesMatchPath(rule, file.path);
});
/**
* Removes the icon from the custom rule from all the files and folders, if applicable.
* @param plugin IconFolderPlugin instance.
* @param rule CustomRule where the icons will be removed based on this rule.
*/
const removeFromAllFiles = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () {
const nodesWithIcon = document.querySelectorAll(`[${config.ICON_ATTRIBUTE_NAME}="${rule.icon}"]`);
for (let i = 0; i < nodesWithIcon.length; i++) {
const node = nodesWithIcon[i];
// Parent element is the node which contains the data path.
const parent = node.parentElement;
if (!parent) {
continue;
}
const dataPath = parent.getAttribute('data-path');
if (!dataPath) {
continue;
}
const fileType = (yield plugin.app.vault.adapter.stat(dataPath)).type;
if (doesMatchPath(rule, dataPath) && doesMatchFileType(rule, fileType)) {
dom.removeIconInNode(parent);
IconCache.getInstance().invalidate(dataPath);
}
}
});
/**
* Gets all the custom rules sorted by their order property in ascending order.
* @param plugin IconFolderPlugin instance.
* @returns CustomRule array sorted by their order property in ascending order.
*/
const getSortedRules = (plugin) => {
return plugin.getSettings().rules.sort((a, b) => a.order - b.order);
};
/**
* Tries to add all specific custom rule icons to all registered files and directories.
* It does that by calling the {@link add} function. Custom rules should have the lowest
* priority and will get ignored if an icon already exists in the file or directory.
* @param plugin IconFolderPlugin instance.
* @param rule CustomRule that will be applied, if applicable, to all files and folders.
*/
const addToAllFiles = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () {
const fileItems = yield getFileItems(plugin, rule);
for (const fileItem of fileItems) {
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 IconFolderPlugin instance.
* @param rule CustomRule that will be used to check if the rule is applicable to the file
* or directory.
* @param file TAbstractFile that will be used to possibly create the icon for.
* @param container HTMLElement where the icon will be added if the custom rules matches.
* @returns A promise that resolves to `true` if the icon was added, `false` otherwise.
*/
const add$2 = (plugin, rule, file, container) => __awaiter(void 0, void 0, void 0, function* () {
if (container && dom.doesElementHasIconNode(container)) {
return false;
}
// Checks if the file or directory already has an icon.
const hasIcon = plugin.getIconNameFromPath(file.path);
if (hasIcon) {
return false;
}
const doesMatch = yield isApplicable(plugin, rule, file);
if (doesMatch) {
IconCache.getInstance().set(file.path, {
iconNameWithPrefix: rule.icon,
inCustomRule: true,
});
dom.createIconNode(plugin, file.path, rule.icon, {
color: rule.color,
container,
});
return true;
}
return false;
});
/**
* 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 doesMatchPath = (rule, path) => {
const toMatch = rule.useFilePath ? path : path.split('/').pop();
try {
// Rule is in some sort of regex.
const regex = new RegExp(rule.rule);
if (toMatch.match(regex)) {
return true;
}
}
catch (_a) {
// Rule is not in some sort of regex, check for basic string match.
return toMatch.includes(rule.rule);
}
return false;
};
/**
* Gets all the file items that can be applied to the specific custom rule.
* @param plugin Instance of IconFolderPlugin.
* @param rule Custom rule that will be checked for.
* @returns A promise that resolves to an array of file items that match the custom rule.
*/
const getFileItems = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () {
const result = [];
for (const fileExplorer of plugin.getRegisteredFileExplorers()) {
const files = Object.values(fileExplorer.fileItems);
for (const fileItem of files) {
if (yield isApplicable(plugin, rule, fileItem.file)) {
result.push(fileItem);
}
}
}
return result;
});
var customRule = {
getFileItems,
doesMatchPath,
doesMatchFileType,
getSortedRules,
removeFromAllFiles,
add: add$2,
addToAllFiles,
isApplicable,
};
function migrate$2(plugin) {
return __awaiter(this, void 0, void 0, function* () {
// Migration for inheritance to custom rule.
if (plugin.getSettings().migrated === 3) {
let hasRemovedInheritance = false;
for (const [key, value] of Object.entries(plugin.getData())) {
if (key === 'settings' || typeof value !== 'object') {
continue;
}
const folderData = value;
const inheritanceIcon = folderData.inheritanceIcon;
if (!inheritanceIcon) {
continue;
}
const folderIconName = folderData.iconName;
// Clean up old data.
if (folderData.iconColor && folderIconName) {
delete folderData.inheritanceIcon;
}
else if (folderIconName) {
delete plugin.getData()[key];
plugin.getData()[key] = folderIconName;
}
else if (!folderIconName) {
delete plugin.getData()[key];
}
const folderPath = key + '\\/[\\w\\d\\s]+';
const newRule = {
icon: inheritanceIcon,
rule: `${folderPath}\\.(?:\\w+\\.)*\\w+`,
for: 'files',
order: 0,
useFilePath: true,
};
// Reorder existing custom rules so that the new inheritance custom rule
// is at the top.
plugin.getSettings().rules.map((rule) => {
rule.order++;
});
plugin.getSettings().rules.unshift(newRule);
// Apply the custom rule.
yield customRule.addToAllFiles(plugin, newRule);
hasRemovedInheritance = true;
}
if (hasRemovedInheritance) {
new obsidian.Notice(`[${config.PLUGIN_NAME}] Inheritance has been removed and replaced with custom rules.`);
}
plugin.getSettings().migrated++;
}
});
}
function migrate$1(plugin) {
return __awaiter(this, void 0, void 0, function* () {
if (plugin.getSettings().migrated === 4) {
if (plugin.getSettings().emojiStyle === 'none') {
plugin.getSettings().emojiStyle = 'native';
}
plugin.getSettings().migrated++;
}
});
}
const migrate = (plugin) => __awaiter(void 0, void 0, void 0, function* () {
// eslint-disable-next-line
// @ts-ignore - Required because an older version of the plugin saved the `migrated`
// property as a boolean instead of a number.
if (plugin.getSettings().migrated === true) {
plugin.getSettings().migrated = 1;
}
yield migrate$4(plugin);
yield migrate$3(plugin);
yield migrate$2(plugin);
yield migrate$1(plugin);
yield plugin.saveIconFolderData();
});
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('iconize-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('iconize-dragover');
this.dragTargetElement = el;
}
}
unhighlight(target, el) {
if (this.dragTargetElement && this.dragTargetElement !== target) {
this.dragTargetElement.removeChild(this.dragOverElement);
this.dragTargetElement.classList.remove('iconize-dragover');
this.dragTargetElement = undefined;
}
clearTimeout(this.closeTimer);
this.closeTimer = setTimeout(() => {
if (this.dragTargetElement) {
el.removeChild(this.dragOverElement);
el.classList.remove('iconize-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.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} (${iconPack.prefix})`)
.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 SVG 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);
});
}
}
/**
* Gets the tab leaves of a specific file path by looping through all opened files and
* checking if the file path matches.
* @param plugin IconFolderPlugin instance.
* @param path String of the file path to get the tab leaf of.
* @returns TabHeaderLeaf array that includes all tab leaves of the file path.
*/
const getTabLeavesOfFilePath = (plugin, path) => {
const openedFiles = getAllOpenedFiles(plugin);
const openedFile = openedFiles.filter((openedFile) => openedFile.path === path);
const leaves = openedFile.map((openedFile) => openedFile.leaf);
return leaves;
};
/**
* Adds an icon to the tab and its container. This function respects the
* custom rules and individually icon set.
* @param plugin IconFolderPlugin instance.
* @param file TFile instance of the file to add the icon to.
* @param iconContainer HTMLElement where the icon will be added to.
* @param options AddOptions for the add function which can optionally be used.
*/
const add$1 = (plugin, file, iconContainer, options) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
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());
// 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;
return;
}
// 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;
break;
}
}
// Add icons to tabs if there is an icon set.
const iconData = data.find(([dataPath]) => dataPath === file.path);
if (!iconData) {
return;
}
const value = iconData[1];
if (typeof value !== 'string' && typeof value !== 'object') {
return;
}
let iconName;
if (typeof value === 'object') {
const v = value;
if (v.iconName === null) {
return;
}
iconName = v.iconName;
}
else {
iconName = value;
}
dom.setIconForNode(plugin, iconName, iconContainer, iconColor);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
});
/**
* Updates the icon in the tab and container by setting calling the `setIconForNode`
* function and removing the margin from the icon container.
* @param plugin IconFolderPlugin instance.
* @param iconName String of the icon name to update to.
* @param iconContainer HTMLElement where the icon is located and will be updated.
*/
const update = (plugin, iconName, iconContainer) => {
dom.setIconForNode(plugin, iconName, iconContainer);
// TODO: Refactor to include option to `insertIconToNode` function.
iconContainer.style.margin = null;
};
/**
* Removes the icon from the tab and container by setting the `display` style property
* to `none`. Optionally, the icon can be replaced with the default obsidian icon.
* @param iconContainer HTMLElement where the icon is located and will be removed from.
* @param options RemoveOptions for the remove function which can optionally be used.
*/
const remove$1 = (iconContainer, options) => {
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,
getTabLeavesOfFilePath,
};
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_1, remove_1) {
return __awaiter(this, arguments, void 0, function* (rule, remove, cachedPaths = []) {
if (this.plugin.getSettings().iconInTabsEnabled) {
for (const openedFile of getAllOpenedFiles(this.plugin)) {
if (cachedPaths.includes(openedFile.path)) {
continue;
}
const applicable = yield customRule.isApplicable(this.plugin, rule, openedFile);
if (!applicable) {
continue;
}
const leaf = openedFile.leaf;
if (remove) {
iconTabs.remove(leaf.tabHeaderInnerIconEl, {
replaceWithDefaultIcon: true,
});
}
else {
iconTabs.add(this.plugin, openedFile, leaf.tabHeaderInnerIconEl, {
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 defined rule (as a plain string or in regex format).')
.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.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',
order: this.plugin.getSettings().rules.length,
};
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}`);
const currentOrder = rule.order;
/**
* Re-orders the custom rule based on the value that is passed in.
* @param valueForReorder Number that will be used to determine whether to swap the
* custom rule with the next rule or the previous rule.
*/
const orderCustomRules = (valueForReorder) => __awaiter(this, void 0, void 0, function* () {
const otherRule = this.plugin.getSettings().rules[currentOrder + valueForReorder];
// Swap the current rule with the next rule.
otherRule.order = otherRule.order - valueForReorder;
rule.order = currentOrder + valueForReorder;
// Refreshes the DOM.
yield customRule.removeFromAllFiles(this.plugin, oldRule);
yield this.plugin.saveIconFolderData();
const addedPaths = [];
for (const fileExplorer of this.plugin.getRegisteredFileExplorers()) {
const files = Object.values(fileExplorer.fileItems);
for (const rule of customRule.getSortedRules(this.plugin)) {
// Removes the icon tabs from all opened files.
this.updateIconTabs(rule, true, addedPaths);
// Adds the icon tabs to all opened files.
this.updateIconTabs(rule, false, addedPaths);
for (const fileItem of files) {
if (addedPaths.includes(fileItem.file.path)) {
continue;
}
const added = yield customRule.add(this.plugin, rule, fileItem.file, getFileItemTitleEl(fileItem));
if (added) {
addedPaths.push(fileItem.file.path);
}
}
}
}
this.refreshDisplay();
});
// Add the move down custom rule button to re-order the custom rule.
settingRuleEl.addExtraButton((btn) => {
const isFirstOrder = currentOrder === 0;
btn.setDisabled(isFirstOrder);
btn.extraSettingsEl.style.cursor = isFirstOrder
? 'not-allowed'
: 'default';
btn.extraSettingsEl.style.opacity = isFirstOrder ? '50%' : '100%';
btn.setIcon('arrow-up');
btn.setTooltip('Prioritize the custom rule');
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
yield orderCustomRules(-1);
}));
});
// Add the move up custom rule button to re-order the custom rule.
settingRuleEl.addExtraButton((btn) => {
const isLastOrder = currentOrder === this.plugin.getSettings().rules.length - 1;
btn.setDisabled(isLastOrder);
btn.extraSettingsEl.style.cursor = isLastOrder
? 'not-allowed'
: 'default';
btn.extraSettingsEl.style.opacity = isLastOrder ? '50%' : '100%';
btn.setIcon('arrow-down');
btn.setTooltip('Deprioritize the custom rule');
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
yield orderCustomRules(1);
}));
});
// Add the edit custom rule button.
settingRuleEl.addButton((btn) => {
btn.setIcon('pencil');
btn.setTooltip('Edit the custom rule');
btn.onClick(() => {
var _a, _b;
// Create modal and its children elements.
const modal = new obsidian.Modal(this.plugin.app);
modal.contentEl.style.display = 'block';
modal.modalEl.classList.add('iconize-custom-modal');
modal.titleEl.setText('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;
}));
const useFilePathContainer = modal.contentEl.createDiv();
useFilePathContainer.style.display = 'flex';
useFilePathContainer.style.alignItems = 'center';
useFilePathContainer.style.justifyContent = 'space-between';
useFilePathContainer.style.marginTop = 'var(--size-4-5)';
const useFilePathDescription = useFilePathContainer.createEl('p', {
text: 'Include folders and files that are part of the path.',
cls: 'setting-item-description',
});
useFilePathDescription.style.margin = '0';
useFilePathDescription.style.marginBottom = 'var(--size-2-2)';
new obsidian.ToggleComponent(useFilePathContainer)
.setValue(rule.useFilePath === true)
.onChange((value) => {
rule.useFilePath = value;
});
// Create the toggle for changing the rule type.
const ruleTypeContainer = modal.contentEl.createDiv();
ruleTypeContainer.style.display = 'flex';
ruleTypeContainer.style.alignItems = 'center';
ruleTypeContainer.style.justifyContent = 'space-between';
ruleTypeContainer.style.marginTop = 'var(--size-4-5)';
const ruleTypeDescription = ruleTypeContainer.createEl('p', {
text: 'Where the custom rule gets applied to.',
cls: 'setting-item-description',
});
ruleTypeDescription.style.margin = '0';
ruleTypeDescription.style.marginBottom = 'var(--size-2-2)';
const ruleTypeButton = new obsidian.ButtonComponent(ruleTypeContainer);
const setButtonContent = (isFor) => {
if (isFor === 'folders') {
ruleTypeButton.setIcon('folder');
}
else if (isFor === 'files') {
ruleTypeButton.setIcon('document');
}
else {
ruleTypeButton.setIcon('documents');
}
ruleTypeButton.setTooltip(`Icon applicable to: ${isFor}`);
};
setButtonContent((_a = rule.for) !== null && _a !== void 0 ? _a : 'everything');
ruleTypeButton.onClick(() => __awaiter(this, void 0, void 0, function* () {
var _c;
const isFor = (_c = rule.for) !== null && _c !== void 0 ? _c : 'everything';
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';
}
setButtonContent(rule.for);
}));
// 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((_b = rule.color) !== null && _b !== void 0 ? _b : '#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* () {
if (!emoji.isEmoji(oldRule.icon)) {
// Tries to remove the previously used icon from the icon pack.
removeIconFromIconPack(this.plugin, oldRule.icon);
}
if (!emoji.isEmoji(rule.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, oldRule);
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);
}));
}));
});
});
}
}
const getTitleIcon = (leaf) => {
return leaf.querySelector(`.${config.TITLE_ICON_CLASS}`);
};
const add = (plugin, inlineTitleEl, svgElement, options) => {
var _a;
if (!inlineTitleEl.parentElement) {
return;
}
if (options === null || options === void 0 ? void 0 : options.fontSize) {
svgElement = svg.setFontSize(svgElement, options.fontSize);
}
let titleIcon = getTitleIcon(inlineTitleEl.parentElement);
if (!titleIcon) {
titleIcon = document.createElement('div');
}
const isInline = plugin.getSettings().iconInTitlePosition === IconInTitlePosition.Inline;
if (isInline) {
titleIcon.style.display = 'inline-block';
titleIcon.style.removeProperty('margin-inline');
titleIcon.style.removeProperty('width');
}
else {
titleIcon.style.display = 'block';
titleIcon.style.width = 'var(--line-width)';
titleIcon.style.marginInline = '0';
}
titleIcon.classList.add(config.TITLE_ICON_CLASS);
// Checks if the passed element is an emoji.
if (emoji.isEmoji(svgElement) && options.fontSize) {
svgElement =
(_a = emoji.parseEmoji(plugin.getSettings().emojiStyle, svgElement, options.fontSize)) !== null && _a !== void 0 ? _a : svgElement;
titleIcon.style.fontSize = `${options.fontSize}px`;
}
titleIcon.innerHTML = svgElement;
let wrapperElement = inlineTitleEl.parentElement;
// Checks the parent and selects the correct wrapper element.
// This should only happen in the beginning.
if (wrapperElement &&
!wrapperElement.classList.contains(config.INLINE_TITLE_WRAPPER_CLASS)) {
wrapperElement = wrapperElement.querySelector(`.${config.INLINE_TITLE_WRAPPER_CLASS}`);
}
// Whenever there is no correct wrapper element, we create one.
if (!wrapperElement) {
wrapperElement = inlineTitleEl.parentElement.createDiv();
wrapperElement.classList.add(config.INLINE_TITLE_WRAPPER_CLASS);
}
// Avoiding adding the same nodes together when changing the title.
if (wrapperElement !== inlineTitleEl.parentElement) {
inlineTitleEl.parentElement.prepend(wrapperElement);
}
if (isInline) {
wrapperElement.style.display = 'flex';
wrapperElement.style.alignItems = 'flex-start';
if (emoji.isEmoji(svgElement)) {
titleIcon.style.transform = 'translateY(-9%)';
}
else {
titleIcon.style.transform = 'translateY(9%)';
}
}
else {
wrapperElement.style.display = 'block';
titleIcon.style.transform = 'translateY(9%)';
}
wrapperElement.append(titleIcon);
wrapperElement.append(inlineTitleEl);
};
const updateStyle = (inlineTitleEl, options) => {
if (!inlineTitleEl.parentElement) {
return;
}
const titleIcon = getTitleIcon(inlineTitleEl.parentElement);
if (!titleIcon) {
return;
}
if (options.fontSize) {
if (!emoji.isEmoji(titleIcon.innerHTML)) {
titleIcon.innerHTML = svg.setFontSize(titleIcon.innerHTML, options.fontSize);
}
else {
titleIcon.style.fontSize = `${options.fontSize}px`;
}
}
};
/**
* Hides the title icon from the provided HTMLElement.
* @param contentEl HTMLElement to hide the title icon from.
*/
const hide = (inlineTitleEl) => {
if (!inlineTitleEl.parentElement) {
return;
}
const titleIconContainer = getTitleIcon(inlineTitleEl.parentElement);
if (!titleIconContainer) {
return;
}
titleIconContainer.style.display = 'none';
};
const remove = (inlineTitleEl) => {
if (!inlineTitleEl.parentElement) {
return;
}
const titleIconContainer = getTitleIcon(inlineTitleEl.parentElement);
if (!titleIconContainer) {
return;
}
titleIconContainer.remove();
};
var titleIcon = {
add,
updateStyle,
hide,
remove,
};
const calculateFontTextSize = () => {
var _a;
let fontSize = parseFloat((_a = getComputedStyle(document.body).getPropertyValue('--font-text-size')) !== null && _a !== void 0 ? _a : '0');
if (!fontSize) {
fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
}
return fontSize;
};
const calculateInlineTitleSize = () => {
const fontSize = calculateFontTextSize();
const inlineTitleSize = parseFloat(getComputedStyle(document.body).getPropertyValue('--inline-title-size'));
return fontSize * inlineTitleSize;
};
const isHeader = (value) => {
return /^h[1-6]$/.test(value);
};
const calculateHeaderSize = (header) => {
const fontSize = calculateFontTextSize();
const headerSize = parseFloat(getComputedStyle(document.body).getPropertyValue(`--${header}-size`));
return fontSize * headerSize;
};
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('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() {
for (const fileExplorer of this.plugin.getRegisteredFileExplorers()) {
const fileItems = Object.entries(fileExplorer.fileItems);
for (const [path, _] of fileItems) {
let iconName = this.plugin.getData()[path];
if (!iconName) {
continue;
}
const data = this.plugin.getData()[path];
if (typeof data === 'object') {
const data = this.plugin.getData()[path];
if (data.iconName) {
iconName = data.iconName;
}
}
if (emoji.isEmoji(iconName)) {
dom.createIconNode(this.plugin, path, iconName);
if (this.plugin.getSettings().iconInTabsEnabled) {
const tabLeaves = iconTabs.getTabLeavesOfFilePath(this.plugin, path);
for (const tabLeaf of tabLeaves) {
iconTabs.update(this.plugin, iconName, tabLeaf.tabHeaderInnerIconEl);
}
}
if (this.plugin.getSettings().iconInTitleEnabled) {
for (const openedFile of getAllOpenedFiles(this.plugin)) {
const activeView = openedFile.leaf.view;
if (activeView instanceof obsidian.MarkdownView &&
openedFile.path === path) {
titleIcon.add(this.plugin, activeView.inlineTitleEl, iconName, {
fontSize: calculateInlineTitleSize(),
});
}
}
}
}
}
}
for (const rule of customRule.getSortedRules(this.plugin)) {
customRule.addToAllFiles(this.plugin, rule);
}
}
}
/**
* Helper function that refreshes the style of all the icons that are defined
* or in a custom rule involved.
* @param plugin Instance of the IconFolderPlugin.
*/
const refreshStyleOfIcons = (plugin) => __awaiter(void 0, void 0, void 0, function* () {
// Refreshes the icon style for all normally added icons.
style.refreshIconNodes(plugin);
// 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 fileItems = yield customRule.getFileItems(plugin, rule);
for (const fileItem of fileItems) {
const titleEl = getFileItemTitleEl(fileItem);
const iconNode = titleEl.querySelector('.iconize-icon');
let iconContent = iconNode.innerHTML;
iconContent = style.applyAll(plugin, iconContent, iconNode);
if (rule.color) {
iconContent = svg.colorize(iconContent, rule.color);
iconNode.style.color = rule.color;
}
iconNode.innerHTML = iconContent;
}
}
});
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('iconize-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.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');
}));
});
}
}
class IconPacksBackgroundChecker extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Icons background check')
.setDesc('Check in the background on every load of Obsidian, if icons are missing and it will try to add them to the specific icon pack.')
.addToggle((toggle) => {
toggle
.setValue(this.plugin.getSettings().iconsBackgroundCheckEnabled)
.onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconsBackgroundCheckEnabled = enabled;
yield this.plugin.saveIconFolderData();
if (enabled) {
new obsidian.Notice('You need to reload Obsidian for this to take effect.', 10000);
}
}));
});
}
}
class IconPackBrowserModal extends obsidian.FuzzySuggestModal {
constructor(app, plugin) {
super(app);
this.plugin = plugin;
this.resultContainerEl.classList.add('iconize-browse-modal');
this.inputEl.placeholder = 'Select to download icon pack';
}
// eslint-disable-next-line
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$1);
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 a predefined icon pack that is officially supported.')
.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 selection modal.')
.addSlider((slider) => {
var _a;
slider
.setLimits(1, 25, 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) {
const tabHeaderLeaf = leaf;
if (enabled) {
// Adds the icons to already opened files.
iconTabs.add(this.plugin, file, tabHeaderLeaf.tabHeaderInnerIconEl);
}
else {
// Removes the icons from already opened files.
iconTabs.remove(tabHeaderLeaf.tabHeaderInnerIconEl);
}
}
});
}));
});
}
}
const checkMissingIcons = (plugin, data) => __awaiter(void 0, void 0, void 0, function* () {
const missingIcons = new Set();
const allIcons = new Map();
const getMissingIcon = (iconNameWithPrefix) => __awaiter(void 0, void 0, void 0, function* () {
const iconNextIdentifier = nextIdentifier(iconNameWithPrefix);
const iconName = iconNameWithPrefix.substring(iconNextIdentifier);
const iconPrefix = iconNameWithPrefix.substring(0, iconNextIdentifier);
const iconPackName = getIconPackNameByPrefix(iconPrefix);
const icon = getIconFromIconPack(iconPackName, iconPrefix, iconName);
if (!icon) {
logger.error(`Icon file with name ${iconNameWithPrefix} could not be found`);
return null;
}
const doesIconFileExists = yield plugin.app.vault.adapter.exists(`${getPath()}/${iconPackName}/${iconName}.svg`);
if (!doesIconFileExists) {
const possibleIcon = getSvgFromLoadedIcon(iconPrefix, iconName);
if (!possibleIcon) {
logger.error(`Icon SVG with name ${iconNameWithPrefix} could not be found`);
return null;
}
yield extractIconToIconPack(plugin, icon, possibleIcon);
return icon;
}
return null;
});
for (const rule of plugin.getSettings().rules) {
if (!emoji.isEmoji(rule.icon)) {
allIcons.set(rule.icon, true);
const icon = yield getMissingIcon(rule.icon);
if (icon) {
missingIcons.add(icon);
}
}
}
for (const [_, value] of data) {
// Check for missing icon names.
let iconNameWithPrefix = value;
if (typeof value === 'object') {
iconNameWithPrefix = value.iconName;
}
if (iconNameWithPrefix && !emoji.isEmoji(iconNameWithPrefix)) {
allIcons.set(iconNameWithPrefix, true);
const icon = yield getMissingIcon(iconNameWithPrefix);
if (icon) {
missingIcons.add(icon);
}
}
}
// Show notice that background check is running.
if (missingIcons.size !== 0) {
new obsidian.Notice(`[${config.PLUGIN_NAME}] Background Check: found missing icons. Adding missing icons...`, 10000);
}
// Iterates over all the missing icons with its path and adds the icon to the node.
for (const icon of missingIcons) {
const normalizedName = getNormalizedName(icon.prefix + icon.name);
const nodesWithIcon = document.querySelectorAll(`[${config.ICON_ATTRIBUTE_NAME}="${normalizedName}"]`);
nodesWithIcon.forEach((node) => {
dom.setIconForNode(plugin, normalizedName, node);
});
}
// Show notice that background check was finished.
if (missingIcons.size !== 0) {
new obsidian.Notice(`[${config.PLUGIN_NAME}] Background Check: added missing icons`, 10000);
}
// Remove all icon files that can not be found in the data.
for (const iconPack of getAllIconPacks()) {
// Checks if the icon pack exists.
const doesIconPackExist = yield plugin.app.vault.adapter.exists(`${getPath()}/${iconPack.name}`);
if (!doesIconPackExist) {
continue;
}
const iconFiles = yield plugin.app.vault.adapter.list(`${getPath()}/${iconPack.name}`);
for (const iconFilePath of iconFiles.files) {
const iconNameWithExtension = iconFilePath.split('/').pop();
// Removes the file extension.
const iconName = iconNameWithExtension === null || iconNameWithExtension === void 0 ? void 0 : iconNameWithExtension.substring(0, iconNameWithExtension.length - 4);
const iconNameWithPrefix = iconPack.prefix + iconName;
const doesIconExist = allIcons.get(iconNameWithPrefix);
if (!doesIconExist) {
const path = `${getPath()}/${iconPack.name}/${iconName}.svg`;
const doesPathExist = yield plugin.app.vault.adapter.exists(path);
if (doesPathExist) {
logger.info(`Removing icon with path '${path}' because it is not used anymore`);
// Removes the icon file.
yield plugin.app.vault.adapter.remove(`${getPath()}/${iconPack.name}/${iconName}.svg`);
}
}
}
}
});
/**
* 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
* and 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) {
const tabHeaderLeaf = leaf;
const iconColor = plugin.getIconColor(file.path);
iconTabs.add(plugin, file, tabHeaderLeaf.tabHeaderInnerIconEl, {
iconColor,
});
}
}
}
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) {
const iconName = typeof value === 'string' ? value : value.iconName;
const iconColor = typeof value === 'string' ? undefined : value.iconColor;
if (iconName) {
// Removes a possible existing icon.
const existingIcon = titleEl.querySelector('.iconize-icon');
if (existingIcon) {
existingIcon.remove();
}
// Creates the new node with the icon inside.
const iconNode = titleEl.createDiv();
iconNode.setAttribute(config.ICON_ATTRIBUTE_NAME, iconName);
iconNode.classList.add('iconize-icon');
IconCache.getInstance().set(dataPath, {
iconNameWithPrefix: iconName,
});
dom.setIconForNode(plugin, iconName, iconNode, iconColor);
titleEl.insertBefore(iconNode, titleInnerEl);
}
}
}
}
// Callback function to register other events to this file explorer.
callback === null || callback === void 0 ? void 0 : callback();
}
// Handles the custom rules.
for (const rule of customRule.getSortedRules(plugin)) {
customRule.addToAllFiles(plugin, rule);
}
};
/**
* 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') {
// If the value is a plain icon name, return it.
return value;
}
else if (typeof value === 'object') {
const v = value;
if (v.iconName !== null) {
return v.iconName;
}
}
// Tries to get the custom rule for the path and returns its icon if it exists.
const rule = customRule.getSortedRules(plugin).find((rule) => {
return customRule.doesMatchPath(rule, 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
* 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 });
}
});
// 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;
};
/**
* Returns the {@link Icon} for the given icon name. It is important, that the icon name
* contains the icon pack prefix.
* @param iconNameWithPrefix String that contains the icon pack prefix combined with the
* icon name.
* @returns Icon if it exists, `null` otherwise.
*/
const getIconByName = (iconNameWithPrefix) => {
const iconNextIdentifier = nextIdentifier(iconNameWithPrefix);
const iconName = iconNameWithPrefix.substring(iconNextIdentifier);
const iconPrefix = iconNameWithPrefix.substring(0, iconNextIdentifier);
const iconPackName = getIconPackNameByPrefix(iconPrefix);
const icon = getIconFromIconPack(iconPackName, iconPrefix, iconName);
if (!icon) {
return null;
}
return icon;
};
/**
* Returns the {@link Icon} for the given path.
* @param plugin IconFolderPlugin instance.
* @param path String which is the path to get the icon of.
* @returns Icon or Emoji as string if it exists, `null` otherwise.
*/
const getIconByPath = (plugin, path) => {
const iconNameWithPrefix = getByPath(plugin, path);
if (!iconNameWithPrefix) {
return null;
}
if (emoji.isEmoji(iconNameWithPrefix)) {
return iconNameWithPrefix;
}
return getIconByName(iconNameWithPrefix);
};
var icon = {
addAll,
getByPath,
getAllWithPath,
getIconByPath,
getIconByName,
checkMissingIcons,
};
class ToggleIconInTitle extends IconFolderSetting {
updateLeaves(options) {
this.plugin.app.workspace.getLeavesOfType('markdown').forEach((leaf) => {
const view = leaf.view;
if (view instanceof obsidian.MarkdownView) {
const foundIcon = icon.getIconByPath(this.plugin, view.file.path);
if (foundIcon && options.enabled) {
if (options.removeBeforeReAdd) {
// Remove the icon before re-adding it. This is needed to update the DOM because
// the icon node will be inserted in the beginning inline title node.
titleIcon.remove(view.contentEl);
}
const content = typeof foundIcon === 'string' ? foundIcon : foundIcon.svgElement;
titleIcon.add(this.plugin, view.inlineTitleEl, content, {
fontSize: calculateInlineTitleSize(),
});
}
else {
titleIcon.remove(view.contentEl);
}
}
});
}
display() {
new obsidian.Setting(this.containerEl)
.setName('Toggle icon in title')
.setDesc('Toggles the visibility of an icon above the title of a file.')
.addDropdown((dropdown) => {
this.dropdown = dropdown;
dropdown.setDisabled(!this.plugin.getSettings().iconInTitleEnabled);
dropdown.addOptions({
above: 'Above title',
inline: 'Next to title',
});
dropdown.setValue(this.plugin.getSettings().iconInTitlePosition);
dropdown.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconInTitlePosition =
value;
yield this.plugin.saveIconFolderData();
this.updateLeaves({ enabled: true, removeBeforeReAdd: true });
}));
})
.addToggle((toggle) => {
toggle
.setValue(this.plugin.getSettings().iconInTitleEnabled)
.onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
if (this.dropdown) {
this.dropdown.setDisabled(!enabled);
}
this.plugin.getSettings().iconInTitleEnabled = enabled;
yield this.plugin.saveIconFolderData();
this.updateLeaves({ enabled });
}));
});
}
}
class FrontmatterOptions extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Use icon in frontmatter')
.setDesc('Toggles whether to set the icon based on the frontmatter property `icon`.')
.addToggle((toggle) => {
toggle
.setValue(this.plugin.getSettings().iconInFrontmatterEnabled)
.onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconInFrontmatterEnabled = enabled;
yield this.plugin.saveIconFolderData();
}));
});
new obsidian.Setting(this.containerEl)
.setName('Frontmatter icon field name')
.setDesc('Sets the name of the frontmatter field which contains the icon.')
.addText((text) => {
this.iconFieldNameTextComp = text;
text.setValue(this.plugin.getSettings().iconInFrontmatterFieldName);
})
.addButton((button) => {
button.setButtonText('Save');
button.onClick(() => __awaiter(this, void 0, void 0, function* () {
const newValue = this.iconFieldNameTextComp.getValue();
const oldValue = this.plugin.getSettings().iconInFrontmatterFieldName;
if (newValue === oldValue) {
return;
}
this.plugin.getSettings().iconInFrontmatterFieldName = newValue;
yield this.plugin.saveIconFolderData();
new obsidian.Notice('...saved successfully');
}));
});
new obsidian.Setting(this.containerEl)
.setName('Frontmatter icon color field name')
.setDesc('Sets the name of the frontmatter field which contains the icon color.')
.addText((text) => {
this.iconColorFieldNameTextComp = text;
text.setValue(this.plugin.getSettings().iconColorInFrontmatterFieldName);
})
.addButton((button) => {
button.setButtonText('Save');
button.onClick(() => __awaiter(this, void 0, void 0, function* () {
const newValue = this.iconColorFieldNameTextComp.getValue();
const oldValue = this.plugin.getSettings().iconColorInFrontmatterFieldName;
if (newValue === oldValue) {
return;
}
this.plugin.getSettings().iconColorInFrontmatterFieldName = newValue;
yield this.plugin.saveIconFolderData();
new obsidian.Notice('...saved successfully');
}));
});
}
}
class ToggleIconsInEditor extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Toggle icons while editing notes')
.setDesc('Toggles whether you are able to add and see icons in your notes and editor (e.g., ability to have :LiSofa: as an icon in your notes).')
.addToggle((toggle) => {
toggle
.setValue(this.plugin.getSettings().iconsInNotesEnabled)
.onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconsInNotesEnabled = enabled;
yield this.plugin.saveIconFolderData();
new obsidian.Notice(`[${config.PLUGIN_NAME}] Obsidian has to be restarted for this change to take effect.`);
}));
});
}
}
class ToggleIconsInLinks extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Toggle icons in links')
.setDesc('Toggles whether you are able to see icons in the links to other notes')
.addToggle((toggle) => {
toggle
.setValue(this.plugin.getSettings().iconsInLinksEnabled)
.onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().iconsInLinksEnabled = enabled;
yield this.plugin.saveIconFolderData();
new obsidian.Notice(`[${config.PLUGIN_NAME}] Obsidian has to be restarted for this change to take effect.`);
}));
});
}
}
class IconIdentifierSetting extends IconFolderSetting {
display() {
const setting = new obsidian.Setting(this.containerEl)
.setName('Icon identifier')
.setDesc('Change the icon identifier used in notes.')
.setClass('iconize-setting');
setting.addText((text) => {
this.textComp = text;
text.setValue(this.plugin.getSettings().iconIdentifier);
});
setting.addButton((btn) => {
btn.setButtonText('Save');
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
const newIdentifier = this.textComp.getValue();
const oldIdentifier = this.plugin.getSettings().iconIdentifier;
if (newIdentifier === oldIdentifier) {
return;
}
this.plugin.getSettings().iconIdentifier = newIdentifier;
yield this.plugin.saveIconFolderData();
new obsidian.Notice('...saved successfully');
}));
});
}
}
class DebugMode extends IconFolderSetting {
display() {
new obsidian.Setting(this.containerEl)
.setName('Toggle Debug Mode')
.setDesc('Toggle debug mode to see more detailed logs in the console. Do not touch this unless you know what you are doing.')
.addToggle((toggle) => {
toggle
.setValue(this.plugin.getSettings().debugMode)
.onChange((enabled) => __awaiter(this, void 0, void 0, function* () {
this.plugin.getSettings().debugMode = enabled;
yield this.plugin.saveIconFolderData();
}));
});
}
}
class IconFolderSettings extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { plugin, containerEl, app } = this;
containerEl.empty();
containerEl.createEl('h1', { text: 'General' });
new RecentlyUsedIconsSetting(plugin, containerEl).display();
new IconPacksPathSetting(plugin, containerEl).display();
new IconPacksBackgroundChecker(plugin, containerEl).display();
new EmojiStyleSetting(plugin, containerEl).display();
new IconIdentifierSetting(plugin, containerEl).display();
new DebugMode(plugin, containerEl).display();
containerEl.createEl('h3', { text: 'Visibility of icons' });
new ToggleIconInTabs(plugin, containerEl).display();
new ToggleIconInTitle(plugin, containerEl).display();
new FrontmatterOptions(plugin, containerEl).display();
new ToggleIconsInEditor(plugin, containerEl).display();
new ToggleIconsInLinks(plugin, containerEl).display();
containerEl.createEl('h1', {
text: 'Icon customization for files/folders',
});
new IconFontSizeSetting(plugin, containerEl).display();
new IconColorSetting(plugin, containerEl).display();
new ExtraMarginSetting(plugin, containerEl).display();
containerEl.createEl('h1', { text: 'Custom icon rules' });
new CustomIconRuleSetting(plugin, containerEl, app, () => this.display()).display();
containerEl.createEl('h1', { text: 'Icon packs' });
new PredefinedIconPacksSetting(plugin, containerEl, app, () => this.display()).display();
new CustomIconPackSetting(plugin, containerEl, () => 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');
}
// eslint-disable-next-line
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(`[${config.PLUGIN_NAME}/Starred] Skipping starred internal plugin registration because file-explorer is not enabled.`);
return;
}
if (!this.enabled) {
console.info(`[${config.PLUGIN_NAME}/Starred] Skipping starred internal plugin registration because it's not enabled.`);
return;
}
// eslint-disable-next-line
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) {
var _a;
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;
const iconColor = (_a = this.plugin.getIconColor(filePath)) !== null && _a !== void 0 ? _a : this.plugin.getSettings().iconColor;
dom.setIconForNode(this.plugin, iconName, iconNode, iconColor);
// 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 } = 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(`[${config.PLUGIN_NAME}/Bookmarks] Skipping bookmark internal plugin registration because file-explorer is not enabled.`);
return;
}
if (!this.enabled) {
console.info(`[${config.PLUGIN_NAME}/Bookmarks] Skipping bookmark internal plugin registration because it's not enabled.`);
return;
}
// eslint-disable-next-line
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 SuggestionIcon extends obsidian.EditorSuggest {
constructor(app, plugin) {
super(app);
this.plugin = plugin;
}
onTrigger(cursor, editor) {
// Isolate shortcode starting position closest to the cursor.
const shortcodeStart = editor
.getLine(cursor.line)
.substring(0, cursor.ch)
.lastIndexOf(this.plugin.getSettings().iconIdentifier);
// `onTrigger` needs to return `null` as soon as possible to save processing performance.
if (shortcodeStart === -1) {
return null;
}
// Regex for checking if the shortcode is not done yet.
const regex = new RegExp(`^(${this.plugin.getSettings().iconIdentifier})\\w+$`, 'g');
const regexOngoingShortcode = editor
.getLine(cursor.line)
.substring(shortcodeStart, cursor.ch)
.match(regex);
if (regexOngoingShortcode === null) {
return null;
}
const startingIndex = editor
.getLine(cursor.line)
.indexOf(regexOngoingShortcode[0]);
return {
start: {
line: cursor.line,
ch: startingIndex,
},
end: {
line: cursor.line,
ch: startingIndex + regexOngoingShortcode[0].length,
},
query: regexOngoingShortcode[0],
};
}
getSuggestions(context) {
const queryLowerCase = context.query
.substring(this.plugin.getSettings().iconIdentifier.length)
.toLowerCase();
// Store all icons corresponding to the current query.
const iconsNameArray = getAllLoadedIconNames()
.filter((iconObject) => {
const name = iconObject.prefix.toLowerCase() + iconObject.name.toLowerCase();
return name.toLowerCase().includes(queryLowerCase);
})
.map((iconObject) => iconObject.prefix + iconObject.name);
// Store all emojis correspoding to the current query - parsing whitespaces and
// colons for shortcodes compatibility.
const emojisNameArray = Object.keys(emoji.shortNames).filter((e) => { var _a; return (_a = emoji.getShortcode(e)) === null || _a === void 0 ? void 0 : _a.includes(queryLowerCase); });
return [...iconsNameArray, ...emojisNameArray];
}
renderSuggestion(value, el) {
const iconObject = icon.getIconByName(value);
el.style.display = 'flex';
el.style.alignItems = 'center';
el.style.gap = '0.25rem';
if (iconObject) {
// Suggest an icon.
el.innerHTML = `${iconObject.svgElement} <span>${value}</span>`;
}
else {
// Suggest an emoji - display its shortcode version.
const shortcode = emoji.getShortcode(value);
if (shortcode) {
el.innerHTML = `<span>${value}</span> <span>${shortcode}</span>`;
}
}
}
selectSuggestion(value) {
const isEmoji = emoji.isEmoji(value.replace(/_/g, ' '));
if (!isEmoji) {
saveIconToIconPack(this.plugin, value);
}
// Replace query with iconNameWithPrefix or emoji unicode directly.
const updatedValue = isEmoji
? value
: `${this.plugin.getSettings().iconIdentifier}${value}${this.plugin.getSettings().iconIdentifier}`;
this.context.editor.replaceRange(updatedValue, this.context.start, this.context.end);
}
}
class IconInTextWidget extends view.WidgetType {
constructor(plugin, id) {
super();
this.plugin = plugin;
this.id = id;
this.start = -1;
this.end = -1;
}
setPosition(start, end) {
this.start = start;
this.end = end;
}
eq(other) {
return other instanceof IconInTextWidget && other.id === this.id;
}
getSize(view) {
let fontSize = calculateFontTextSize();
const line = view.state.doc.lineAt(this.end);
const headerMatch = line.text.match(/^#{1,6}\s/);
if (headerMatch && headerMatch[0].trim()) {
const mapping = {
'#': 'h1',
'##': 'h2',
'###': 'h3',
'####': 'h4',
'#####': 'h5',
'######': 'h6',
};
const header = mapping[headerMatch[0].trim()];
fontSize = calculateHeaderSize(header);
}
return fontSize;
}
toDOM(view) {
const wrap = createSpan({
cls: 'cm-iconize-icon',
attr: {
'aria-label': this.id,
'data-icon': this.id,
'aria-hidden': 'true',
},
});
const foundIcon = icon.getIconByName(this.id);
const fontSize = this.getSize(view);
if (foundIcon) {
const svgElement = svg.setFontSize(foundIcon.svgElement, fontSize);
wrap.style.display = 'inline-flex';
wrap.style.transform = 'translateY(13%)';
wrap.innerHTML = svgElement;
}
else if (emoji.isEmoji(this.id)) {
wrap.innerHTML = emoji.parseEmoji(this.plugin.getSettings().emojiStyle, this.id, fontSize);
}
else {
wrap.append(`${this.plugin.getSettings().iconIdentifier}${this.id}${this.plugin.getSettings().iconIdentifier}`);
}
return wrap;
}
ignoreEvent() {
return false;
}
}
class IconInLinkWidget extends view.WidgetType {
constructor(plugin, iconData, path) {
super();
this.plugin = plugin;
this.iconData = iconData;
this.path = path;
}
toDOM() {
var _a;
const iconNode = document.createElement('span');
const iconName = typeof this.iconData === 'string'
? this.iconData
: this.iconData.prefix + this.iconData.name;
iconNode.style.color =
(_a = this.plugin.getIconColor(this.path)) !== null && _a !== void 0 ? _a : this.plugin.getSettings().iconColor;
iconNode.setAttribute('title', iconName);
iconNode.classList.add('iconize-icon-in-link');
if (typeof this.iconData === 'string') {
iconNode.style.transform = 'translateY(0)';
}
let innerHTML = typeof this.iconData === 'string'
? this.iconData
: this.iconData.svgElement;
if (emoji.isEmoji(innerHTML)) {
innerHTML = emoji.parseEmoji(this.plugin.getSettings().emojiStyle, innerHTML);
}
iconNode.innerHTML = innerHTML;
return iconNode;
}
ignoreEvent() {
return true;
}
}
const buildLinkDecorations = (view$1, plugin) => {
const builder = new state.RangeSetBuilder();
const mdView = view$1.state.field(obsidian.editorInfoField);
for (const { from, to } of view$1.visibleRanges) {
language.syntaxTree(view$1.state).iterate({
from,
to,
enter: (node) => {
const tokenProps = node.type.prop(language.tokenClassNodeProp);
if (tokenProps) {
const props = new Set(tokenProps.split(' '));
const isLink = props.has('hmd-internal-link');
if (isLink) {
let linkText = view$1.state.doc.sliceString(node.from, node.to);
linkText = linkText.split('#')[0];
const file = plugin.app.metadataCache.getFirstLinkpathDest(linkText, mdView.file.basename);
if (file) {
const possibleIcon = icon.getIconByPath(plugin, file.path);
if (possibleIcon) {
const iconDecoration = view.Decoration.widget({
widget: new IconInLinkWidget(plugin, possibleIcon, file.path),
});
builder.add(node.from, node.from, iconDecoration);
}
}
}
}
},
});
}
return builder.finish();
};
const buildTextDecorations = (view$1, plugin) => {
const ranges = [];
const iconInfo = view$1.state.field(plugin.positionField);
for (const { from, to } of view$1.visibleRanges) {
iconInfo.between(from - 1, to + 1, (from, to, { iconId }) => {
ranges.push([iconId, from, to]);
});
}
return view.Decoration.set(ranges.map(([code, from, to]) => {
const widget = new IconInTextWidget(plugin, code);
widget.setPosition(from, to);
if (view$1.state.field(obsidian.editorLivePreviewField)) {
return view.Decoration.replace({
widget,
side: -1,
}).range(from, to);
}
return view.Decoration.widget({
widget,
side: -1,
}).range(to);
}), true);
};
const buildIconInTextPlugin = (plugin) => {
return view.ViewPlugin.fromClass(class IconPlugin {
constructor(view) {
this.plugin = plugin;
this.decorations = buildTextDecorations(view, plugin);
}
update(update) {
this.decorations = buildTextDecorations(update.view, this.plugin);
}
}, {
decorations: (v) => v.decorations,
provide: (plugin) => view.EditorView.atomicRanges.of((view$1) => {
const value = view$1.plugin(plugin);
return value ? value.decorations : view.Decoration.none;
}),
});
};
const buildIconInLinksPlugin = (plugin) => {
return view.ViewPlugin.fromClass(class {
constructor(view) {
this.plugin = plugin;
this.decorations = buildLinkDecorations(view, plugin);
}
destroy() { }
update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = buildLinkDecorations(update.view, this.plugin);
}
}
}, {
decorations: (v) => v.decorations,
});
};
// TODO: Optimize the code to reduce the number of iterations and improve the
function checkForSourceMode(plugin) {
let isSourceMode = false;
// Iterate over all leaves to check if any is in source mode
plugin.app.workspace.iterateAllLeaves((leaf) => {
var _a;
if (!isSourceMode && leaf.view.getViewType() === 'markdown') {
if ((_a = leaf.getViewState().state) === null || _a === void 0 ? void 0 : _a.source) {
isSourceMode = true;
}
}
});
return isSourceMode;
}
class IconPosition extends state.RangeValue {
constructor(text) {
super();
this.text = text;
}
get iconId() {
return this.text;
}
eq(other) {
return other instanceof IconPosition && other.text === this.text;
}
}
/**
* Builds a position field for the editor state. This field will track the
* positions of the icons in the document.
**/
const buildPositionField = (plugin) => {
/**
* Checks the ranges of the icons in the document. If the range is not
* excluded, the range is added to the range set. If the range is excluded,
* the range is removed from the range set.
* @param state EditorState to get the ranges from.
* @param excludeFrom Number to exclude from the ranges.
* @param excludeTo Number to exclude to the ranges.
* @param updateRange Function callback to update the range.
*/
const checkRanges = (state, excludeFrom, excludeTo, updateRange) => {
const isSourceMode = checkForSourceMode(plugin);
const text = state.doc.sliceString(0, state.doc.length);
const identifier = plugin.getSettings().iconIdentifier;
const regex = new RegExp(`(${identifier})((\\w{1,64}:\\d{17,18})|(\\w{1,64}))(${identifier})`, 'g');
for (const { 0: rawCode, index: offset } of text.matchAll(regex)) {
const iconName = rawCode.substring(identifier.length, rawCode.length - identifier.length);
if (!icon.getIconByName(iconName)) {
continue;
}
const from = offset;
const to = offset + rawCode.length;
if (!isNodeInRangeAccepted(state, from, to)) {
continue;
}
if (offset < excludeFrom || offset > excludeTo) {
updateRange(from, to, new IconPosition(iconName), isSourceMode);
continue;
}
updateRange(from, to, new IconPosition(iconName), true);
}
for (const { 0: emojiName, index: offset } of text.matchAll(emoji.getRegex())) {
if (!emoji.isEmoji(emojiName)) {
continue;
}
const from = offset;
const to = offset + emojiName.length;
if (!isNodeInRangeAccepted(state, from, to)) {
continue;
}
if (offset < excludeFrom || offset > excludeTo) {
updateRange(from, to, new IconPosition(emojiName), isSourceMode);
continue;
}
updateRange(from, to, new IconPosition(emojiName), true);
}
};
const isNodeInRangeAccepted = (state, from, to) => {
let isRangeAccepted = true;
language.syntaxTree(state).iterate({
from,
to,
enter: ({ type }) => {
var _a;
if (type.name === 'Document') {
return;
}
const allowedNodeTypes = [
'header',
'strong',
'em',
'quote',
'link',
'list-1',
'list-2',
'list-3',
'highlight',
'footref',
'comment',
'link-alias',
];
const excludedNodeTypes = [
'formatting',
'hmd-codeblock',
'inline-code',
'hr',
];
const nodeProps = (_a = type.prop(language.tokenClassNodeProp)) !== null && _a !== void 0 ? _a : '';
const s = new Set(nodeProps.split(' '));
if (excludedNodeTypes.some((t) => s.has(t)) &&
allowedNodeTypes.every((t) => !s.has(t))) {
isRangeAccepted = false;
}
},
});
return isRangeAccepted;
};
return state.StateField.define({
create: (state$1) => {
const rangeSet = new state.RangeSetBuilder();
const changedLines = [];
checkRanges(state$1, -1, -1, (from, to, iconPosition) => {
changedLines.push({ from, to, iconPosition });
});
changedLines.sort((a, b) => a.from - b.from);
for (const { from, to, iconPosition } of changedLines) {
rangeSet.add(from, to, iconPosition);
}
return rangeSet.finish();
},
update: (rangeSet, transaction) => {
const newRanges = [];
if (!transaction.docChanged) {
if (transaction.selection) {
const from = transaction.selection.ranges[0].from;
const to = transaction.selection.ranges[0].to;
const lineEnd = transaction.state.doc.lineAt(to).length;
const lineStart = transaction.state.doc.lineAt(from).from;
// Checks the ranges of the icons in the document except for the
// excluded line start and end.
checkRanges(transaction.state, lineStart, lineStart + lineEnd, (from, to, value, removed) => {
rangeSet = rangeSet.update({
filterFrom: from,
filterTo: to,
filter: () => false,
});
if (!removed) {
newRanges.push(value.range(from, to));
}
});
}
else {
checkRanges(transaction.state, -1, -1, (from, to, value, removed) => {
rangeSet = rangeSet.update({
filterFrom: from,
filterTo: to,
filter: () => false,
});
if (!removed) {
newRanges.push(value.range(from, to));
}
});
}
newRanges.sort((a, b) => a.from - b.from);
rangeSet = rangeSet.update({ add: newRanges });
return rangeSet;
}
rangeSet = rangeSet.map(transaction.changes);
const changedLines = [];
transaction.changes.iterChangedRanges((_f, _t, from, to) => {
changedLines.push([
transaction.state.doc.lineAt(from).number,
transaction.state.doc.lineAt(to).number,
]);
});
for (const [start, end] of changedLines) {
const from = transaction.state.doc.line(start).from;
const to = transaction.state.doc.line(end).to;
rangeSet = rangeSet.update({
filterFrom: from,
filterTo: to,
filter: () => false,
});
const lineEnd = transaction.state.doc.line(end).length;
const lineStart = transaction.state.doc.line(end).from;
// Checks the ranges of the icons in the document except for the excluded
// line start and end.
checkRanges(transaction.state, lineStart, lineStart + lineEnd, (from, to, value, removed) => {
if (!removed) {
newRanges.push(value.range(from, to));
}
});
}
newRanges.sort((a, b) => a.from - b.from);
rangeSet = rangeSet.update({ add: newRanges });
return rangeSet;
},
});
};
// TODO: Optimize the code to reduce the number of iterations and improve the
const createIconShortcodeRegex = (plugin) => {
return new RegExp(`(${plugin.getSettings().iconIdentifier})((\\w{1,64}:\\d{17,18})|(\\w{1,64}))(${plugin.getSettings().iconIdentifier})`, 'g');
};
const createTreeWalker = (plugin, root) => {
return document.createTreeWalker(root, NodeFilter.SHOW_ALL, {
acceptNode: function (node) {
if (node.nodeName === 'CODE') {
return NodeFilter.FILTER_REJECT;
}
else if (node.nodeName === '#text') {
if (node.nodeValue &&
(emoji.getRegex().test(node.nodeValue) ||
createIconShortcodeRegex(plugin).test(node.nodeValue))) {
return NodeFilter.FILTER_ACCEPT;
}
else {
return NodeFilter.FILTER_REJECT;
}
}
return NodeFilter.FILTER_SKIP;
},
});
};
const checkForTextNodes = (treeWalker, match, cb) => {
let currentNode = treeWalker.currentNode;
while (currentNode) {
if (currentNode.nodeType === Node.TEXT_NODE) {
const text = currentNode;
const textNodes = [...Array.from(text.parentElement.childNodes)].filter((n) => n instanceof Text);
for (const text of textNodes) {
for (const code of [...text.wholeText.matchAll(match)]
.sort((a, b) => b.index - a.index)
.map((arr) => ({ text: arr[0], index: arr.index }))) {
if (!text.textContent) {
continue;
}
cb(text, code);
}
}
}
currentNode = treeWalker.nextNode();
}
};
const processIconInTextMarkdown = (plugin, element) => {
// Ignore if codeblock
const codeElement = element.querySelector('pre > code');
if (codeElement) {
return;
}
const iconTreeWalker = createTreeWalker(plugin, element);
const iconShortcodeRegex = createIconShortcodeRegex(plugin);
const iconIdentifierLength = plugin.getSettings().iconIdentifier.length;
checkForTextNodes(iconTreeWalker, iconShortcodeRegex, (text, code) => {
var _a, _b, _c, _d;
const shortcode = code.text;
const iconName = shortcode.slice(iconIdentifierLength, shortcode.length - iconIdentifierLength);
const iconObject = icon.getIconByName(iconName);
if (iconObject) {
const toReplace = text.splitText(code.index);
const rootSpan = createSpan({
cls: 'cm-iconize-icon',
attr: {
'aria-label': iconName,
'data-icon': iconName,
'aria-hidden': 'true',
},
});
rootSpan.style.display = 'inline-flex';
rootSpan.style.transform = 'translateY(13%)';
const tagName = (_c = (_b = (_a = toReplace.parentElement) === null || _a === void 0 ? void 0 : _a.tagName) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : '';
let fontSize = calculateFontTextSize();
if (isHeader(tagName)) {
fontSize = calculateHeaderSize(tagName);
const svgElement = svg.setFontSize(iconObject.svgElement, fontSize);
rootSpan.innerHTML = svgElement;
}
else {
const svgElement = svg.setFontSize(iconObject.svgElement, fontSize);
rootSpan.innerHTML = svgElement;
}
(_d = toReplace.parentElement) === null || _d === void 0 ? void 0 : _d.insertBefore(rootSpan, toReplace);
toReplace.textContent = toReplace.wholeText.substring(code.text.length);
}
});
const emojiTreeWalker = createTreeWalker(plugin, element);
checkForTextNodes(emojiTreeWalker, emoji.getRegex(), (text, code) => {
var _a, _b, _c, _d;
if (!emoji.isEmoji(code.text)) {
return;
}
if (plugin.getSettings().emojiStyle === 'twemoji') {
const toReplace = text.splitText(code.index);
const tagName = (_c = (_b = (_a = toReplace.parentElement) === null || _a === void 0 ? void 0 : _a.tagName) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : '';
let fontSize = calculateFontTextSize();
if (isHeader(tagName)) {
fontSize = calculateHeaderSize(tagName);
}
const emojiValue = emoji.parseEmoji(plugin.getSettings().emojiStyle, code.text, fontSize);
if (!emojiValue) {
return;
}
const emojiNode = createSpan();
emojiNode.innerHTML = emojiValue;
(_d = toReplace.parentElement) === null || _d === void 0 ? void 0 : _d.insertBefore(emojiNode, toReplace);
toReplace.textContent = toReplace.wholeText.substring(code.text.length);
}
});
};
const processIconInLinkMarkdown = (plugin, element, ctx) => {
const linkElements = element.querySelectorAll('a');
if (!linkElements || linkElements.length === 0) {
return;
}
linkElements.forEach((linkElement) => {
var _a, _b;
// Skip if the link element e.g., is a tag.
if (!linkElement.hasAttribute('data-href')) {
return;
}
const linkHref = linkElement.getAttribute('href');
if (!linkHref) {
logger.warn('Link element does not have an `href` attribute');
return;
}
const file = plugin.app.metadataCache.getFirstLinkpathDest(linkHref, ctx.sourcePath);
if (!file) {
logger.warn('Link element does not have a linkpath to a file');
return;
}
const path = file.path;
const iconValue = icon.getIconByPath(plugin, path);
if (!iconValue) {
return;
}
const iconName = typeof iconValue === 'string'
? iconValue
: iconValue.prefix + iconValue.name;
const rootSpan = createSpan({
cls: 'iconize-icon-in-link',
attr: {
title: iconName,
'aria-label': iconName,
'data-icon': iconName,
'aria-hidden': 'true',
},
});
rootSpan.style.color =
(_a = plugin.getIconColor(path)) !== null && _a !== void 0 ? _a : plugin.getSettings().iconColor;
if (emoji.isEmoji(iconName)) {
const parsedEmoji = (_b = emoji.parseEmoji(plugin.getSettings().emojiStyle, iconName)) !== null && _b !== void 0 ? _b : iconName;
rootSpan.innerHTML = parsedEmoji;
}
else {
const svg = icon.getIconByName(iconName).svgElement;
if (svg) {
rootSpan.innerHTML = svg;
}
}
linkElement.prepend(rootSpan);
});
};
class ChangeColorModal extends obsidian.Modal {
constructor(app, plugin, path) {
var _a;
super(app);
this.plugin = plugin;
this.path = path;
this.usedColor = this.plugin.getIconColor(this.path);
this.contentEl.style.display = 'block';
this.modalEl.classList.add('iconize-custom-modal');
this.titleEl.setText('Change color');
const description = this.contentEl.createEl('p', {
text: 'Select a color for this icon',
cls: 'setting-item-description',
});
description.style.marginBottom = 'var(--size-2-2)';
const colorContainer = this.contentEl.createDiv();
colorContainer.style.display = 'flex';
colorContainer.style.alignItems = 'center';
colorContainer.style.justifyContent = 'space-between';
const colorPicker = new obsidian.ColorComponent(colorContainer)
.setValue((_a = this.usedColor) !== null && _a !== void 0 ? _a : '#000000')
.onChange((value) => {
this.usedColor = value;
});
const defaultColorButton = new obsidian.ButtonComponent(colorContainer);
defaultColorButton.setTooltip('Set color to the default one');
defaultColorButton.setButtonText('Reset');
defaultColorButton.onClick(() => {
colorPicker.setValue('#000000');
this.usedColor = undefined;
});
// Save button.
const button = new obsidian.ButtonComponent(this.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* () {
var _b;
new obsidian.Notice('Color of icon changed.');
if (this.usedColor) {
this.plugin.addIconColor(this.path, this.usedColor);
}
else {
this.plugin.removeIconColor(this.path);
}
// Refresh the DOM.
const iconNode = dom.getIconNodeFromPath(this.path);
iconNode.style.color = (_b = this.usedColor) !== null && _b !== void 0 ? _b : null;
const colorizedInnerHtml = svg.colorize(iconNode.innerHTML, this.usedColor);
iconNode.innerHTML = colorizedInnerHtml;
this.close();
}));
}
onOpen() {
super.onOpen();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class IconFolderPlugin extends obsidian.Plugin {
constructor() {
super(...arguments);
this.registeredFileExplorers = new Set();
this.modifiedInternalPlugins = [];
this.positionField = buildPositionField(this);
this.frontmatterCache = new Set();
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
console.log(`loading ${config.PLUGIN_NAME}`);
// 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();
logger.toggleLogging(this.getSettings().debugMode);
setPath(this.getSettings().iconPacksPath);
yield createDefaultDirectory(this);
yield this.checkRecentlyUsedIcons();
yield migrate(this);
const usedIconNames = icon.getAllWithPath(this).map((value) => value.icon);
yield loadUsedIcons(this, usedIconNames);
this.app.workspace.onLayoutReady(() => this.handleChangeLayout());
this.addCommand({
id: 'iconize:set-icon-for-file',
name: 'Set icon for file',
hotkeys: [
{
modifiers: ['Mod', 'Shift'],
key: 'j',
},
],
editorCallback: (editor) => __awaiter(this, void 0, void 0, function* () {
var _a;
const file = (_a = editor.editorComponent) === null || _a === void 0 ? void 0 : _a.file;
if (!file) {
logger.warn(`'editor.editorComponent?.file' is undefined for file: ${file}`);
return;
}
const modal = new IconsPickerModal(this.app, this, file.path);
modal.open();
modal.onSelect = (iconName) => {
IconCache.getInstance().set(file.path, {
iconNameWithPrefix: iconName,
});
// Update icon in tab when setting is enabled.
if (this.getSettings().iconInTabsEnabled) {
const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path);
for (const tabLeaf of tabLeaves) {
iconTabs.update(this, iconName, tabLeaf.tabHeaderInnerIconEl);
}
}
// Update icon in title when setting is enabled.
if (this.getSettings().iconInTitleEnabled) {
this.addIconInTitle(iconName);
}
};
}),
});
this.registerEvent(
// Registering file menu event for listening to file pinning and unpinning.
this.app.workspace.on('file-menu', (menu, file) => {
// I've researched other ways of doing this. However, there is no other way to listen to file pinning and unpinning.
menu.onHide(() => {
const path = file.path;
if (this.getSettings().iconInTabsEnabled) {
for (const openedFile of getAllOpenedFiles(this)) {
if (openedFile.path === path) {
const possibleIcon = IconCache.getInstance().get(path);
if (!possibleIcon) {
return;
}
const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path);
for (const tabLeaf of tabLeaves) {
// Add timeout to ensure that the default icon is already set.
setTimeout(() => {
iconTabs.add(this, file, tabLeaf.tabHeaderInnerIconEl);
}, 5);
}
}
}
}
});
}));
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();
modal.onSelect = (iconName) => {
IconCache.getInstance().set(file.path, {
iconNameWithPrefix: iconName,
});
// Update icon in tab when setting is enabled.
if (this.getSettings().iconInTabsEnabled) {
const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path);
for (const tabLeaf of tabLeaves) {
iconTabs.update(this, iconName, tabLeaf.tabHeaderInnerIconEl);
}
}
// Update icon in title when setting is enabled.
if (this.getSettings().iconInTitleEnabled) {
this.addIconInTitle(iconName);
}
};
});
};
const removeIconMenuItem = (item) => {
item.setTitle('Remove icon');
item.setIcon('trash');
item.onClick(() => __awaiter(this, void 0, void 0, function* () {
yield this.removeSingleIcon(file);
}));
};
const changeColorOfIcon = (item) => {
item.setTitle('Change color of icon');
item.setIcon('palette');
item.onClick(() => {
const modal = new ChangeColorModal(this.app, this, file.path);
modal.open();
});
};
menu.addItem(addIconMenuItem);
const filePathData = this.getData()[file.path];
const hasNestedIcon = 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. custom rules.
if (filePathData &&
(typeof filePathData === 'string' || hasNestedIcon)) {
const icon = typeof filePathData === 'string'
? filePathData
: filePathData.iconName;
if (!emoji.isEmoji(icon)) {
menu.addItem(changeColorOfIcon);
}
menu.addItem(removeIconMenuItem);
}
}));
// 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) => {
// Check if the file was moved and had an icon before.
const dataPoint = this.data[oldPath];
if (dataPoint && oldPath !== 'settings') {
const iconNameWithPrefix = typeof dataPoint === 'object'
? dataPoint.iconName
: dataPoint;
dom.createIconNode(this, file.path, iconNameWithPrefix);
}
this.renameFolder(file.path, oldPath);
}));
if (this.getSettings().iconsInNotesEnabled) {
this.registerMarkdownPostProcessor((el) => processIconInTextMarkdown(this, el));
this.registerEditorSuggest(new SuggestionIcon(this.app, this));
this.registerEditorExtension([
this.positionField,
buildIconInTextPlugin(this),
]);
}
if (this.getSettings().iconsInLinksEnabled) {
this.registerMarkdownPostProcessor((el, ctx) => processIconInLinkMarkdown(this, el, ctx));
this.registerEditorExtension([
this.positionField,
buildIconInLinksPlugin(this),
]);
}
this.addSettingTab(new IconFolderSettings(this.app, this));
});
}
notifyPlugins() {
this.modifiedInternalPlugins.forEach((internalPlugin) => {
if (internalPlugin.enabled) {
internalPlugin.onMount();
}
});
}
removeSingleIcon(file) {
return __awaiter(this, void 0, void 0, function* () {
this.removeFolderIcon(file.path);
dom.removeIconInPath(file.path);
IconCache.getInstance().invalidate(file.path);
this.notifyPlugins();
let didUpdate = false;
// Refreshes the icon tab and title icon for custom rules.
for (const rule of customRule.getSortedRules(this)) {
const applicable = yield customRule.isApplicable(this, rule, file);
if (applicable) {
customRule.add(this, rule, file);
this.addIconInTitle(rule.icon);
const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path);
for (const tabLeaf of tabLeaves) {
iconTabs.add(this, file, tabLeaf.tabHeaderInnerIconEl, {
iconName: rule.icon,
});
}
didUpdate = true;
break;
}
}
// Only remove icon above titles and icon in tabs if no custom rule was found.
if (!didUpdate) {
// Refreshes icons above title and icons in tabs.
for (const openedFile of getAllOpenedFiles(this)) {
if (this.getSettings().iconInTitleEnabled) {
titleIcon.remove(openedFile.leaf.view.inlineTitleEl);
}
if (this.getSettings().iconInTabsEnabled) {
const leaf = openedFile.leaf;
iconTabs.remove(leaf.tabHeaderInnerIconEl, {
replaceWithDefaultIcon: true,
});
}
}
}
});
}
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, () => {
// After initialization of the icon packs, checks the vault for missing icons and
// adds them.
initIconPacks(this).then(() => __awaiter(this, void 0, void 0, function* () {
if (this.getSettings().iconsBackgroundCheckEnabled) {
const data = Object.entries(this.data);
yield icon.checkMissingIcons(this, data);
resetPreloadedIcons();
}
}));
if (this.getSettings().iconInFrontmatterEnabled) {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
this.frontmatterCache.add(activeFile.path);
}
}
// Adds the title icon to the active leaf view.
if (this.getSettings().iconInTitleEnabled) {
for (const openedFile of getAllOpenedFiles(this)) {
const iconName = icon.getByPath(this, openedFile.path);
const activeView = openedFile.leaf.view;
if (activeView instanceof obsidian.MarkdownView && iconName) {
let possibleIcon = iconName;
if (!emoji.isEmoji(iconName)) {
const iconNextIdentifier = nextIdentifier(iconName);
possibleIcon = getSvgFromLoadedIcon(iconName.substring(0, iconNextIdentifier), iconName.substring(iconNextIdentifier));
}
if (possibleIcon) {
titleIcon.add(this, activeView.inlineTitleEl, possibleIcon, {
fontSize: calculateInlineTitleSize(),
});
}
}
}
}
// Register rename event for adding icons with custom rules to the DOM
// when file was moved to another directory.
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => __awaiter(this, void 0, void 0, function* () {
const sortedRules = customRule.getSortedRules(this);
// Removes possible icons from the renamed file.
sortedRules.forEach((rule) => {
if (customRule.doesMatchPath(rule, oldPath)) {
dom.removeIconInPath(file.path);
}
});
// Adds possible icons to the renamed file.
sortedRules.forEach((rule) => {
if (customRule.doesMatchPath(rule, oldPath)) {
return;
}
customRule.add(this, rule, file, undefined);
});
// Updates icon tabs for the renamed file.
for (const rule of customRule.getSortedRules(this)) {
const applicable = yield customRule.isApplicable(this, rule, file);
if (!applicable) {
continue;
}
const openedFiles = getAllOpenedFiles(this);
const openedFile = openedFiles.find((openedFile) => openedFile.path === file.path);
if (openedFile) {
const leaf = openedFile.leaf;
iconTabs.update(this, rule.icon, leaf.tabHeaderInnerIconEl);
}
break;
}
})));
// Register `layout-change` event for adding icons to tabs when moving a pane or
// enabling reading mode.
this.registerEvent(this.app.workspace.on('layout-change', () => {
var _a, _b;
if (this.getSettings().iconInTitleEnabled) {
const activeView = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView);
if (activeView) {
const file = activeView.file;
const view = activeView.leaf.view.currentMode
.view;
const iconNameWithPrefix = icon.getByPath(this, file.path);
if (!iconNameWithPrefix) {
titleIcon.hide(view.inlineTitleEl);
return;
}
let foundIcon = iconNameWithPrefix;
if (!emoji.isEmoji(foundIcon)) {
foundIcon = (_a = icon.getIconByName(iconNameWithPrefix)) === null || _a === void 0 ? void 0 : _a.svgElement;
// Check for preloaded icons if no icon was found when the start up was faster
// than the loading of the icons.
if (!foundIcon && getPreloadedIcons().length > 0) {
foundIcon = (_b = getPreloadedIcons().find((icon) => icon.prefix + icon.name === iconNameWithPrefix)) === null || _b === void 0 ? void 0 : _b.svgElement;
}
}
if (foundIcon) {
// Removes the node because the editor markdown content is being rerendered
// when the content mode changes back to editing.
titleIcon.remove(view.inlineTitleEl);
titleIcon.add(this, view.inlineTitleEl, foundIcon, {
fontSize: calculateInlineTitleSize(),
});
}
}
}
if (!this.getSettings().iconInTabsEnabled) {
return;
}
for (const openedFile of getAllOpenedFiles(this)) {
const leaf = openedFile.leaf;
const iconColor = this.getIconColor(leaf.view.file.path);
iconTabs.add(this, openedFile, leaf.tabHeaderInnerIconEl, {
iconColor,
});
}
}));
// Register `file-open` event for adding icon to title.
this.registerEvent(this.app.workspace.on('file-open', (file) => {
var _a, _b;
if (!this.getSettings().iconInTitleEnabled) {
return;
}
for (const openedFile of getAllOpenedFiles(this)) {
if (openedFile.path !== file.path) {
continue;
}
const leaf = openedFile.leaf.view;
const iconNameWithPrefix = icon.getByPath(this, file.path);
if (!iconNameWithPrefix) {
titleIcon.hide(leaf.inlineTitleEl);
return;
}
let foundIcon = iconNameWithPrefix;
if (!emoji.isEmoji(foundIcon)) {
foundIcon = (_a = icon.getIconByName(iconNameWithPrefix)) === null || _a === void 0 ? void 0 : _a.svgElement;
// Check for preloaded icons if no icon was found when the start up was faster
// than the loading of the icons.
if (!foundIcon && getPreloadedIcons().length > 0) {
foundIcon = (_b = getPreloadedIcons().find((icon) => icon.prefix + icon.name === iconNameWithPrefix)) === null || _b === void 0 ? void 0 : _b.svgElement;
}
}
if (foundIcon) {
titleIcon.add(this, leaf.inlineTitleEl, foundIcon, {
fontSize: calculateInlineTitleSize(),
});
}
else {
titleIcon.hide(leaf.inlineTitleEl);
}
}
}));
// Register event for frontmatter icon registration.
this.registerEvent(this.app.metadataCache.on('resolve', (file) => __awaiter(this, void 0, void 0, function* () {
if (!this.getSettings().iconInFrontmatterEnabled) {
return;
}
const fileCache = this.app.metadataCache.getFileCache(file);
const iconFrontmatterName = this.getSettings().iconInFrontmatterFieldName;
const iconColorFrontmatterName = this.getSettings().iconColorInFrontmatterFieldName;
if (fileCache === null || fileCache === void 0 ? void 0 : fileCache.frontmatter) {
const { [iconFrontmatterName]: newIconName, [iconColorFrontmatterName]: newIconColor, } = fileCache.frontmatter;
// If `icon` property is empty, we will remove it from the data and remove the icon.
if (!newIconName) {
if (this.frontmatterCache.has(file.path)) {
yield this.removeSingleIcon(file);
this.frontmatterCache.delete(file.path);
}
return;
}
if (typeof newIconName !== 'string') {
new obsidian.Notice(`[${config.PLUGIN_NAME}] Frontmatter property type \`icon\` has to be of type \`text\`.`);
return;
}
if (newIconColor && typeof newIconColor !== 'string') {
new obsidian.Notice(`[${config.PLUGIN_NAME}] Frontmatter property type \`iconColor\` has to be of type \`text\`.`);
return;
}
let iconColor = newIconColor;
if (isHexadecimal(iconColor)) {
iconColor = stringToHex(iconColor);
}
const cachedIcon = IconCache.getInstance().get(file.path);
if (newIconName === (cachedIcon === null || cachedIcon === void 0 ? void 0 : cachedIcon.iconNameWithPrefix) &&
iconColor === (cachedIcon === null || cachedIcon === void 0 ? void 0 : cachedIcon.iconColor)) {
return;
}
this.frontmatterCache.add(file.path);
try {
if (!emoji.isEmoji(newIconName)) {
saveIconToIconPack(this, newIconName);
}
}
catch (e) {
logger.warn(`Something went wrong while saving icon to icon pack (error: ${e})`);
new obsidian.Notice(e.message);
return;
}
dom.createIconNode(this, file.path, newIconName, {
color: iconColor,
});
this.addFolderIcon(file.path, newIconName);
this.addIconColor(file.path, iconColor);
IconCache.getInstance().set(file.path, {
iconNameWithPrefix: newIconName,
iconColor,
});
// Update icon in tab when setting is enabled.
if (this.getSettings().iconInTabsEnabled) {
const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path);
for (const tabLeaf of tabLeaves) {
iconTabs.update(this, newIconName, tabLeaf.tabHeaderInnerIconEl);
}
}
// Update icon in title when setting is enabled.
if (this.getSettings().iconInTitleEnabled) {
this.addIconInTitle(newIconName);
}
}
})));
// 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;
}
// TODO: Maybe change in the future to a more optimal solution.
// Fixes a problem when the file was clicked twice in the same tab.
// See https://github.com/FlorianWoelki/obsidian-iconize/issues/208.
if (leaf.view.getViewType() === 'file-explorer') {
for (const openedFile of getAllOpenedFiles(this)) {
const leaf = openedFile.leaf;
const iconColor = this.getIconColor(leaf.view.file.path);
iconTabs.add(this, openedFile, leaf.tabHeaderInnerIconEl, {
iconColor,
});
}
return;
}
if (leaf.view.getViewType() !== 'markdown') {
return;
}
const tabHeaderLeaf = leaf;
if (tabHeaderLeaf.view.file) {
const iconColor = this.getIconColor(tabHeaderLeaf.view.file.path);
iconTabs.add(this, tabHeaderLeaf.view.file, tabHeaderLeaf.tabHeaderInnerIconEl, {
iconColor,
});
}
}));
this.registerEvent(this.app.workspace.on('css-change', () => {
for (const openedFile of getAllOpenedFiles(this)) {
const activeView = openedFile.leaf.view;
if (activeView instanceof obsidian.MarkdownView) {
titleIcon.updateStyle(activeView.inlineTitleEl, {
fontSize: calculateInlineTitleSize(),
});
}
}
}));
});
}
addIconInTitle(iconName) {
var _a;
for (const openedFile of getAllOpenedFiles(this)) {
const activeView = openedFile.leaf.view;
if (activeView instanceof obsidian.MarkdownView) {
let possibleIcon = iconName;
if (!emoji.isEmoji(iconName)) {
possibleIcon = (_a = icon.getIconByName(iconName)) === null || _a === void 0 ? void 0 : _a.svgElement;
}
if (possibleIcon) {
titleIcon.add(this, activeView.inlineTitleEl, possibleIcon, {
fontSize: calculateInlineTitleSize(),
});
}
}
}
}
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();
}
addIconColor(path, iconColor) {
const pathData = this.getData()[path];
if (typeof pathData === 'string') {
this.getData()[path] = {
iconName: pathData,
iconColor,
};
}
else {
pathData.iconColor = iconColor;
}
this.saveIconFolderData();
}
getIconColor(path) {
const pathData = this.getData()[path];
if (!pathData) {
return undefined;
}
if (typeof pathData === 'string') {
return undefined;
}
return pathData.iconColor;
}
removeIconColor(path) {
const pathData = this.getData()[path];
if (typeof pathData === 'string') {
return;
}
const currentValue = pathData;
this.getData()[path] = currentValue.iconName;
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];
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;
}
if (!emoji.isEmoji(iconNameWithPrefix)) {
removeIconFromIconPack(this, iconNameWithPrefix);
}
}
//this.addIconsToSearch();
this.saveIconFolderData();
}
addFolderIcon(path, icon) {
const iconName = getNormalizedName(typeof icon === 'object' ? icon.displayName : icon);
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] === undefined) {
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;
}
getIconNameFromPath(path) {
if (typeof this.getData()[path] === 'object') {
return this.getData()[path].iconName;
}
return this.getData()[path];
}
getRegisteredFileExplorers() {
return this.registeredFileExplorers;
}
/**
* Returns a possible data path by the given value. This function checks for
* direct 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);
}
v = v;
if (value === v.iconName) {
return k;
}
}
});
}
}
module.exports = IconFolderPlugin;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzLy5wbnBtL0Byb2xsdXArcGx1Z2luLXR5cGVzY3JpcHRAMTEuMS42X3JvbGx1cEAyLjc5LjFfdHNsaWJAMi42LjJfdHlwZXNjcmlwdEA1LjQuMi9ub2RlX21vZHVsZXMvdHNsaWIvdHNsaWIuZXM2LmpzIiwic3JjL2xpYi91dGlsL3N2Zy50cyIsIm5vZGVfbW9kdWxlcy8ucG5wbS9qc3ppcEAzLjEwLjEvbm9kZV9tb2R1bGVzL2pzemlwL2Rpc3QvanN6aXAubWluLmpzIiwic3JjL3ppcC11dGlsLnRzIiwic3JjL2NvbmZpZy9pbmRleC50cyIsInNyYy9saWIvbG9nZ2VyLnRzIiwic3JjL2ljb24tcGFja3MudHMiLCJzcmMvaWNvbi1wYWNrLW1hbmFnZXIudHMiLCJub2RlX21vZHVsZXMvLnBucG0vQHR3ZW1vamkrYXBpQDE1LjEuMC9ub2RlX21vZHVsZXMvQHR3ZW1vamkvYXBpL2Rpc3QvdHdlbW9qaS5lc20uanMiLCJzcmMvZW1vamkudHMiLCJzcmMvdXRpbC50cyIsInNyYy9saWIvdXRpbC9zdHlsZS50cyIsInNyYy9saWIvdXRpbC9kb20udHMiLCJzcmMvdWkvaWNvbnMtcGlja2VyLW1vZGFsLnRzIiwic3JjL3NldHRpbmdzL2RhdGEudHMiLCJzcmMvbWlncmF0aW9ucy8wMDAxLWNoYW5nZS1taWdyYXRlZC10cnVlLXRvLTEudHMiLCJzcmMvbWlncmF0aW9ucy8wMDAyLW9yZGVyLWN1c3RvbS1ydWxlcy50cyIsInNyYy9saWIvaWNvbi1jYWNoZS50cyIsInNyYy9saWIvY3VzdG9tLXJ1bGUudHMiLCJzcmMvbWlncmF0aW9ucy8wMDAzLWluaGVyaXRhbmNlLXRvLWN1c3RvbS1ydWxlLnRzIiwic3JjL21pZ3JhdGlvbnMvMDAwNC1yZW1vdmUtbm9uZS1lbW9qaS1vcHRpb24udHMiLCJzcmMvbWlncmF0aW9ucy9pbmRleC50cyIsInNyYy9zZXR0aW5ncy91aS9pY29uRm9sZGVyU2V0dGluZy50cyIsInNyYy9zZXR0aW5ncy91aS9jdXN0b21JY29uUGFjay50cyIsInNyYy9saWIvaWNvbi10YWJzLnRzIiwic3JjL3NldHRpbmdzL3VpL2N1c3RvbUljb25SdWxlLnRzIiwic3JjL2xpYi9pY29uLXRpdGxlLnRzIiwic3JjL2xpYi91dGlsL3RleHQudHMiLCJzcmMvc2V0dGluZ3MvdWkvZW1vamlTdHlsZS50cyIsInNyYy9zZXR0aW5ncy9oZWxwZXIudHMiLCJzcmMvc2V0dGluZ3MvdWkvZXh0cmFNYXJnaW4udHMiLCJzcmMvc2V0dGluZ3MvdWkvaWNvbkNvbG9yLnRzIiwic3JjL3NldHRpbmdzL3VpL2ljb25Gb250U2l6ZS50cyIsInNyYy9zZXR0aW5ncy91aS9pY29uUGFja3NQYXRoLnRzIiwic3JjL3NldHRpbmdzL3VpL2ljb25QYWNrc0JhY2tncm91bmRDaGVja2VyLnRzIiwic3JjL3VpL2ljb24tcGFjay1icm93c2VyLW1vZGFsLnRzIiwic3JjL3NldHRpbmdzL3VpL3ByZWRlZmluZWRJY29uUGFja3MudHMiLCJzcmMvc2V0dGluZ3MvdWkvcmVjZW50bHlVc2VkSWNvbnMudHMiLCJzcmMvc2V0dGluZ3MvdWkvdG9nZ2xlSWNvbkluVGFicy50cyIsInNyYy9saWIvaWNvbi50cyIsInNyYy9zZXR0aW5ncy91aS90b2dnbGVJY29uSW5UaXRsZS50cyIsInNyYy9zZXR0aW5ncy91aS9mcm9udG1hdHRlck9wdGlvbnMudHMiLCJzcmMvc2V0dGluZ3MvdWkvdG9nZ2xlSWNvbnNJbk5vdGVzLnRzIiwic3JjL3NldHRpbmdzL3VpL3RvZ2dsZUljb25zSW5MaW5rcy50cyIsInNyYy9zZXR0aW5ncy91aS9pY29uSWRlbnRpZmllci50cyIsInNyYy9zZXR0aW5ncy91aS9kZWJ1Z01vZGUudHMiLCJzcmMvc2V0dGluZ3MvdWkvaW5kZXgudHMiLCJub2RlX21vZHVsZXMvLnBucG0vbW9ua2V5LWFyb3VuZEAyLjMuMC9ub2RlX21vZHVsZXMvbW9ua2V5LWFyb3VuZC9tanMvaW5kZXguanMiLCJzcmMvQHR5cGVzL2ludGVybmFsLXBsdWdpbi1pbmplY3Rvci50cyIsInNyYy9pbnRlcm5hbC1wbHVnaW5zL3N0YXJyZWQudHMiLCJzcmMvaW50ZXJuYWwtcGx1Z2lucy9ib29rbWFyay50cyIsInNyYy9lZGl0b3IvaWNvbnMtc3VnZ2VzdGlvbi50cyIsInNyYy9lZGl0b3IvbGl2ZS1wcmV2aWV3L3dpZGdldHMvaWNvbi1pbi10ZXh0LnRzIiwic3JjL2VkaXRvci9saXZlLXByZXZpZXcvd2lkZ2V0cy9pY29uLWluLWxpbmsudHMiLCJzcmMvZWRpdG9yL2xpdmUtcHJldmlldy9kZWNvcmF0aW9ucy9idWlsZC1saW5rLWRlY29yYXRpb25zLnRzIiwic3JjL2VkaXRvci9saXZlLXByZXZpZXcvZGVjb3JhdGlvbnMvYnVpbGQtdGV4dC1kZWNvcmF0aW9ucy50cyIsInNyYy9lZGl0b3IvbGl2ZS1wcmV2aWV3L3BsdWdpbnMvaWNvbi1pbi10ZXh0LnRzIiwic3JjL2VkaXRvci9saXZlLXByZXZpZXcvcGx1Z2lucy9pY29uLWluLWxpbmtzLnRzIiwic3JjL2VkaXRvci9saXZlLXByZXZpZXcvc3RhdGUudHMiLCJzcmMvZWRpdG9yL21hcmtkb3duLXByb2Nlc3NvcnMvaWNvbi1pbi10ZXh0LnRzIiwic3JjL2VkaXRvci9tYXJrZG93bi1wcm9jZXNzb3JzL2ljb24taW4tbGluay50cyIsInNyYy91aS9jaGFuZ2UtY29sb3ItbW9kYWwudHMiLCJzcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6bnVsbCwibmFtZXMiOlsicmVxdWlyZSIsImdsb2JhbCIsInJlcXVlc3RVcmwiLCJsb2FkQXN5bmMiLCJpY29uUGFja3MiLCJOb3RpY2UiLCJGdXp6eVN1Z2dlc3RNb2RhbCIsIm1pZ3JhdGUiLCJhZGQiLCJtaWdyYXRlMDAwMSIsIm1pZ3JhdGUwMDAyIiwibWlncmF0ZTAwMDMiLCJtaWdyYXRlMDAwNCIsIlNldHRpbmciLCJyZW1vdmUiLCJNb2RhbCIsIlRleHRDb21wb25lbnQiLCJUb2dnbGVDb21wb25lbnQiLCJCdXR0b25Db21wb25lbnQiLCJDb2xvckNvbXBvbmVudCIsIk1hcmtkb3duVmlldyIsIkRyb3Bkb3duQ29tcG9uZW50IiwiU2xpZGVyQ29tcG9uZW50IiwiUGx1Z2luU2V0dGluZ1RhYiIsIlRvZ2dsZUljb25zSW5Ob3RlcyIsIkVkaXRvclN1Z2dlc3QiLCJXaWRnZXRUeXBlIiwidmlldyIsIlJhbmdlU2V0QnVpbGRlciIsImVkaXRvckluZm9GaWVsZCIsInN5bnRheFRyZWUiLCJ0b2tlbkNsYXNzTm9kZVByb3AiLCJEZWNvcmF0aW9uIiwiZWRpdG9yTGl2ZVByZXZpZXdGaWVsZCIsIlZpZXdQbHVnaW4iLCJFZGl0b3JWaWV3IiwiUmFuZ2VWYWx1ZSIsIlN0YXRlRmllbGQiLCJzdGF0ZSIsIlBsdWdpbiI