From e973cec353a656112f9310c64afb7780ba8590c3 Mon Sep 17 00:00:00 2001 From: valentine195 <38669521+valentine195@users.noreply.github.com> Date: Fri, 25 Feb 2022 14:59:26 -0500 Subject: [PATCH] feat: Added Icon Packs (close #181) --- README.md | 46 ++++-- package-lock.json | 1 - src/@types/index.ts | 18 ++- src/assets/main.css | 10 +- src/icons/manager.ts | 149 +++++++++++++++++++ src/icons/packs.ts | 6 + src/main.ts | 114 +++++++++----- src/modal/confirm.ts | 58 ++++++++ src/modal/index.ts | 37 +++-- src/settings.ts | 337 ++++++++++++++++++++++++++++-------------- src/util/constants.ts | 13 +- src/util/icons.ts | 0 src/util/index.ts | 1 - src/util/util.ts | 33 +++++ 14 files changed, 633 insertions(+), 190 deletions(-) create mode 100644 src/icons/manager.ts create mode 100644 src/icons/packs.ts create mode 100644 src/modal/confirm.ts create mode 100644 src/util/icons.ts diff --git a/README.md b/README.md index cae6464..67890c9 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Please note that by default, the background color of the title is simply the col ### Images as Icons -Images can be uploaded to use as an admonition icon instead of an icon from Font Awesome or RPG Awesome. +Images can be uploaded to use as an admonition icon instead of an icon from a downloaded icon set. These images will be resized to 24px x 24px to be stored in the plugin's saved data. @@ -507,7 +507,6 @@ As of v6.8.0, an additional non-code block syntax can be used that is inspired b ![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/msdocs.png) - This syntax can also be used on indented code blocks: ```md @@ -515,7 +514,6 @@ This syntax can also be used on indented code blocks: This is an admonition! ``` - ### Title A title can be added to the MSDoc-style admonition by appending it after the type. @@ -525,7 +523,7 @@ A title can be added to the MSDoc-style admonition by appending it after the typ > This is an admonition! ``` -Like the code block syntax, providing an empty title will remove the title from the rendered admonition. +Like the code block syntax, providing an empty title will remove the title from the rendered admonition. ```md > [!quote:] @@ -557,6 +555,18 @@ Instructions: Please note that I can give no guarantees of stability on your publish site. Other JavaScript you include may conflict with this file. If you run into an issue using it, please create an issue on this repository and I will try to help you. +## Icon Packs + +Additional icon packs can be downloaded in settings. + +### Adding Icon Packs + +Want to add an existing icon pack? Make a pull request with the following: + +1. Add a new folder in the [icons](./icons) folder with the name of your icon set. +2. Create an `icons.json` file that has the icons defined as an Object. Please see the [Octicons json](./icons/octicons/icons.json) for reference. +3. Put your icon pack's information in the two variables in the [Icon Packs](./src/icons/packs.ts) file. + # Settings ## Custom Admonition Types @@ -577,7 +587,7 @@ If this setting is off, rendered admonitions will receive the `.no-drop` class. All admonitions will be collapsible by default, unless `collapse: none` is set in the admonition parameters. -### Default Collapse Type +### Default Collapse Type > :warning: This setting is only available when Collapsible By Default is true. @@ -601,16 +611,32 @@ Turn this off to totally control color via CSS. Admonitions with no content are hidden by default. -> :warning: Please note that this only works for Admonitions that have *no text content whatsoever*. +> :warning: Please note that this only works for Admonitions that have _no text content whatsoever_. + +## Icon Packs + +### Use Font Awesome Icons + +The plugin comes pre-bundled with the entire [Font Awesome Free](https://fontawesome.com/search?m=free&s=brands%2Cregular%2Csolid) icon set. Turn this setting off to not include them in the icon picker. + +Existing custom Admonitions that use Font Awesome icons will continue to work. + +### Additional Icon Packs + +Additional icon packs can be downloaded to supplement the included Font Awesome Free icon set. + +**Downloading an icon pack requires an internet connection.** + +Current additional icon packs available are the [Octicons](https://primer.style/octicons/) set and the [RPG Awesome](https://nagoshiashumari.github.io/Rpg-Awesome/) set. + +> :pencil: For backwards compability, if an Admonition was created prior to version **7.0.0** using an RPG Awesome icon, the pack will try to be downloaded. ## Additional Syntaxes ### Enable Non-codeblock Admonitions -> :heavy_exclamation_mark: This syntax will be removed in a future version! +> :heavy_exclamation_mark: This setting has been removed as of version **7.0.0**. > -> It is no longer possible to enable this setting. Legacy support will continue until version **7.0.0**. -> > It is recommended to use the [Microsoft Document Syntax](#microsoft-document-syntax) instead. Enabled use of `!!! ad-` style admonitions. No longer supported, will be removed in a future version. @@ -647,7 +673,7 @@ If you require links to be fully synced, it is recommended to use the [Microsoft ### Generate JS for Publish -Use this setting to enable Admonitions on custom-domain Obsidian Publish websites. +Use this setting to enable Admonitions on custom-domain Obsidian Publish websites. See [Publish](#publish) for more information. diff --git a/package-lock.json b/package-lock.json index ad23d21..934e812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "obsidian-admonition", "version": "6.12.1", "license": "MIT", "dependencies": { diff --git a/src/@types/index.ts b/src/@types/index.ts index 15d7900..be06642 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -1,5 +1,5 @@ -import { MarkdownPostProcessorContext, Plugin_2 } from "obsidian"; -import { IconName } from "src/util"; +import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { DownloadableIconPack } from "src/icons/manager"; export interface Admonition { type: string; @@ -29,7 +29,6 @@ export interface AdmonitionSettings { defaultCollapseType: "open" | "closed"; syncLinks: boolean; version: string; - warnedAboutNC: boolean; injectColor: boolean; parseTitles: boolean; allowMSSyntax: boolean; @@ -37,11 +36,20 @@ export interface AdmonitionSettings { livePreviewMS: boolean; dropShadow: boolean; hideEmpty: boolean; + icons: Array; + useFontAwesome: boolean; + rpgDownloadedOnce: boolean; + open: { + admonitions: boolean; + icons: boolean; + other: boolean; + advanced: boolean; + }; } export type AdmonitionIconDefinition = { - type?: "font-awesome" | "rpg" | "image"; - name?: IconName | RPGIconName | string; + type?: "font-awesome" | "image" | DownloadableIconPack; + name?: IconName | string; }; export type AdmonitionIconName = AdmonitionIconDefinition["name"]; diff --git a/src/assets/main.css b/src/assets/main.css index 4a5ad55..a94f97a 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -311,6 +311,14 @@ input.is-invalid { .admonition-settings details[open] > summary > .collapser > .handle { transform: rotate(90deg); } +.admonition-setting-warning { + display: flex; + gap: 0.25rem; + align-items: center; +} +.admonition-setting-warning.text-warning { + color: var(--text-error); +} .admonitions-nested-settings .setting-item { border: 0px; @@ -376,4 +384,4 @@ input.is-invalid { .is-live-preview .admonition-content .math-block > mjx-container { padding: 0; -} \ No newline at end of file +} diff --git a/src/icons/manager.ts b/src/icons/manager.ts new file mode 100644 index 0000000..f5bc4a5 --- /dev/null +++ b/src/icons/manager.ts @@ -0,0 +1,149 @@ +import { faCopy, far, IconPrefix } from "@fortawesome/free-regular-svg-icons"; +import { fas } from "@fortawesome/free-solid-svg-icons"; +import { fab } from "@fortawesome/free-brands-svg-icons"; +import { + IconDefinition, + findIconDefinition, + icon as getFAIcon, + library +} from "@fortawesome/fontawesome-svg-core"; + +import type { IconName } from "@fortawesome/fontawesome-svg-core"; + +/* import { RPG } from "./rpgawesome"; */ +import type { AdmonitionIconDefinition } from "src/@types"; +import type ObsidianAdmonition from "src/main"; +import { Notice } from "obsidian"; +import { type DownloadableIconPack, DownloadableIcons } from "./packs"; + +export { type DownloadableIconPack, DownloadableIcons }; + +/** Load Font Awesome Library */ +library.add(fas, far, fab, faCopy); + +export class IconManager { + DOWNLOADED: { + [key in DownloadableIconPack]?: Record; + } = {}; + FONT_AWESOME_MAP = new Map( + [Object.values(fas), Object.values(far), Object.values(fab)] + .flat() + .map((i: IconDefinition) => { + return [ + i.iconName, + { + name: i.iconName, + type: "font-awesome" as "font-awesome" + } + ]; + }) + ); + constructor(public plugin: ObsidianAdmonition) {} + async load() { + for (const icon of this.plugin.data.icons) { + const exists = await this.plugin.app.vault.adapter.exists( + this.localIconPath(icon) + ); + if (!exists) { + await this.downloadIcon(icon); + } else { + this.DOWNLOADED[icon] = JSON.parse( + await this.plugin.app.vault.adapter.read( + `${this.plugin.app.plugins.getPluginFolder()}/obsidian-admonition/${icon}.json` + ) + ); + } + } + } + iconDefinitions: AdmonitionIconDefinition[] = []; + setIconDefinitions() { + const downloaded: AdmonitionIconDefinition[] = []; + for (const pack of this.plugin.data.icons) { + if (!(pack in this.DOWNLOADED)) continue; + const icons = this.DOWNLOADED[pack]; + downloaded.push( + ...Object.keys(icons).map((name) => { + return { type: pack, name }; + }) + ); + } + this.iconDefinitions = [ + ...(this.plugin.data.useFontAwesome + ? this.FONT_AWESOME_MAP.values() + : []), + ...downloaded + ]; + } + iconPath(pack: DownloadableIconPack) { + return `https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/icons/${pack}/icons.json`; + } + localIconPath(pack: DownloadableIconPack) { + return `${this.plugin.app.plugins.getPluginFolder()}/obsidian-admonition/${pack}.json`; + } + async downloadIcon(pack: DownloadableIconPack) { + try { + const icons: Record = await ( + await fetch(this.iconPath(pack)) + ).json(); + this.plugin.data.icons.push(pack); + this.plugin.data.icons = [...new Set(this.plugin.data.icons)]; + await this.plugin.app.vault.adapter.write( + this.localIconPath(pack), + JSON.stringify(icons) + ); + this.DOWNLOADED[pack] = icons; + await this.plugin.saveSettings(); + this.setIconDefinitions(); + + new Notice(`${DownloadableIcons[pack]} successfully downloaded.`); + } catch (e) { + console.error(e); + new Notice("Could not download icon pack"); + } + } + async removeIcon(pack: DownloadableIconPack) { + await this.plugin.app.vault.adapter.remove(this.localIconPath(pack)); + delete this.DOWNLOADED[pack]; + this.plugin.data.icons.remove(pack); + this.plugin.data.icons = [...new Set(this.plugin.data.icons)]; + await this.plugin.saveSettings(); + this.setIconDefinitions(); + } + getIconType(str: string): "font-awesome" | DownloadableIconPack { + if (findIconDefinition({ iconName: str as IconName, prefix: "fas" })) + return "font-awesome"; + if (findIconDefinition({ iconName: str as IconName, prefix: "far" })) + return "font-awesome"; + if (findIconDefinition({ iconName: str as IconName, prefix: "fab" })) + return "font-awesome"; + for (const [pack, icons] of Object.entries(this.DOWNLOADED)) { + if (Object.keys(icons).find((icon) => icon == str)) + return pack as DownloadableIconPack; + } + } + getIconModuleName(icon: AdmonitionIconDefinition) { + if (icon.type === "font-awesome") return "Font Awesome"; + if (icon.type === "image") return; + if (icon.type in DownloadableIcons) return DownloadableIcons[icon.type]; + } + getIconNode(icon: AdmonitionIconDefinition): Element { + if (icon.type === "image") { + const img = new Image(); + img.src = icon.name; + return img; + } + if (this.DOWNLOADED[icon.type as DownloadableIconPack]?.[icon.name]) { + const el = createDiv(); + el.innerHTML = + this.DOWNLOADED[icon.type as DownloadableIconPack]?.[icon.name]; + return el.children[0]; + } + for (const prefix of ["fas", "far", "fab"] as IconPrefix[]) { + const definition = findIconDefinition({ + iconName: icon.name as IconName, + prefix + }); + if (definition) return getFAIcon(definition).node[0]; + } + } +} diff --git a/src/icons/packs.ts b/src/icons/packs.ts new file mode 100644 index 0000000..0dd5888 --- /dev/null +++ b/src/icons/packs.ts @@ -0,0 +1,6 @@ +export type DownloadableIconPack = "octicons" | "rpg"; + +export const DownloadableIcons: Record = { + octicons: "Octicons", + rpg: "RPG Awesome" +} as const; diff --git a/src/main.ts b/src/main.ts index 30a856b..f068ee5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,6 @@ import { MarkdownPreviewRenderer, MarkdownRenderChild, MarkdownRenderer, - MarkdownSectionInformation, MarkdownView, Notice, Plugin, @@ -32,7 +31,14 @@ import { AdmonitionSettings, AdmonitionIconDefinition } from "./@types"; -import { getParametersFromSource, MSDOCREGEX } from "./util"; +import { + COPY_ICON, + COPY_ICON_NAME, + getParametersFromSource, + MSDOCREGEX, + WARNING_ICON, + WARNING_ICON_NAME +} from "./util"; import { ADMONITION_MAP, ADD_ADMONITION_COMMAND_ICON, @@ -50,6 +56,7 @@ declare global { } //add commands to app interface + declare module "obsidian" { interface App { commands: { @@ -61,6 +68,9 @@ declare module "obsidian" { executeCommandById(id: string): void; findCommand(id: string): Command; }; + plugins: { + getPluginFolder(): string; + }; } interface MarkdownPreviewView { renderer: MarkdownPreviewRenderer; @@ -72,22 +82,17 @@ declare module "obsidian" { } import AdmonitionSetting from "./settings"; -import { - IconName, - COPY_BUTTON_ICON, - iconDefinitions, - getIconNode -} from "./util/icons"; +import { DownloadableIconPack, IconManager } from "./icons/manager"; import { InsertAdmonitionModal } from "./modal"; import "./assets/main.css"; -import { isLivePreview, rangesInclude } from "./util/livepreview"; +import { isLivePreview, rangesInclude } from "./util"; +import { IconName } from "@fortawesome/fontawesome-svg-core"; const DEFAULT_APP_SETTINGS: AdmonitionSettings = { userAdmonitions: {}, syntaxHighlight: false, copyButton: false, version: "", - warnedAboutNC: false, autoCollapse: false, defaultCollapseType: "open", syncLinks: true, @@ -97,7 +102,16 @@ const DEFAULT_APP_SETTINGS: AdmonitionSettings = { msSyntaxIndented: true, livePreviewMS: true, dropShadow: true, - hideEmpty: false + hideEmpty: false, + open: { + admonitions: true, + icons: true, + other: true, + advanced: false + }, + icons: [], + useFontAwesome: true, + rpgDownloadedOnce: false }; export default class ObsidianAdmonition extends Plugin { @@ -106,6 +120,8 @@ export default class ObsidianAdmonition extends Plugin { postprocessors: Map = new Map(); + iconManager = new IconManager(this); + get types() { return Object.keys(this.admonitions); } @@ -120,25 +136,27 @@ export default class ObsidianAdmonition extends Plugin { async onload(): Promise { console.log("Obsidian Admonition loaded"); + await this.loadSettings(); - Object.keys(this.admonitions).forEach((type) => { - const processor = this.registerMarkdownCodeBlockProcessor( - `ad-${type}`, - (src, el, ctx) => this.postprocessor(type, src, el, ctx) - ); - this.postprocessors.set(type, processor); - if (this.admonitions[type].command) { - this.registerCommandsFor(this.admonitions[type]); - } - }); + await this.iconManager.load(); this.app.workspace.onLayoutReady(async () => { + Object.keys(this.admonitions).forEach((type) => { + const processor = this.registerMarkdownCodeBlockProcessor( + `ad-${type}`, + (src, el, ctx) => this.postprocessor(type, src, el, ctx) + ); + this.postprocessors.set(type, processor); + if (this.admonitions[type].command) { + this.registerCommandsFor(this.admonitions[type]); + } + }); + this.addSettingTab(new AdmonitionSetting(this.app, this)); - addIcon(ADD_COMMAND_NAME.toString(), ADD_ADMONITION_COMMAND_ICON); - addIcon( - REMOVE_COMMAND_NAME.toString(), - REMOVE_ADMONITION_COMMAND_ICON - ); + addIcon(ADD_COMMAND_NAME, ADD_ADMONITION_COMMAND_ICON); + addIcon(REMOVE_COMMAND_NAME, REMOVE_ADMONITION_COMMAND_ICON); + addIcon(WARNING_ICON_NAME, WARNING_ICON); + addIcon(COPY_ICON_NAME, COPY_ICON); if (this.data.syntaxHighlight) { this.turnOnSyntaxHighlighting(); @@ -225,6 +243,13 @@ export default class ObsidianAdmonition extends Plugin { this.enableMSSyntax(); }); } + async downloadIcon(pack: DownloadableIconPack) { + this.iconManager.downloadIcon(pack); + } + + async removeIcon(pack: DownloadableIconPack) { + this.iconManager.removeIcon(pack); + } async postprocessor( type: string, @@ -256,8 +281,9 @@ export default class ObsidianAdmonition extends Plugin { let admonitionElement = this.getAdmonitionElement( type, title, - iconDefinitions.find(({ name }) => icon === name) ?? - admonition.icon, + this.iconManager.iconDefinitions.find( + ({ name }) => icon === name + ) ?? admonition.icon, color ?? (admonition.injectColor ?? this.data.injectColor ? admonition.color @@ -410,7 +436,7 @@ export default class ObsidianAdmonition extends Plugin { content, ctx, ctx.sourcePath, - text.join('\n') + text.join("\n") ); el.replaceWith(admonition); @@ -777,7 +803,9 @@ export default class ObsidianAdmonition extends Plugin { "admonition-title-icon" ); if (icon && icon.name && icon.type) { - iconEl.appendChild(getIconNode(icon)); + iconEl.appendChild( + this.iconManager.getIconNode(icon) ?? createDiv() + ); } //add markdown children back @@ -897,9 +925,8 @@ export default class ObsidianAdmonition extends Plugin { ); const contentEl = contentHolder.createDiv("admonition-content"); if (this.admonitions[type].copy ?? this.data.copyButton) { - let copy = contentHolder - .createDiv("admonition-content-copy") - .appendChild(COPY_BUTTON_ICON.cloneNode(true)); + let copy = contentHolder.createDiv("admonition-content-copy"); + setIcon(copy, COPY_ICON_NAME); copy.addEventListener("click", () => { navigator.clipboard.writeText(content.trim()).then(async () => { new Notice("Admonition content copied to clipboard."); @@ -1037,6 +1064,7 @@ ${editor.getDoc().getSelection()} async saveSettings() { this.data.version = this.manifest.version; + await this.saveData(this.data); } async loadSettings() { @@ -1066,14 +1094,18 @@ ${editor.getDoc().getSelection()} } } - if (loaded != null && !this.data.warnedAboutNC) { - if (Number(this.data.version.split(".")[0]) < 7) { - new Notice( - "Admonitions: Use of the !!!-style Admonitions will be removed in a future version.\n\nPlease update them to the MSDoc-style syntax.", - 0 - ); - } - this.data.warnedAboutNC = true; + if ( + !this.data.rpgDownloadedOnce && + this.data.userAdmonitions && + Object.values(this.data.userAdmonitions).some((admonition) => { + if (admonition.icon.type == "rpg") return true; + }) && + !this.data.icons.includes("rpg") + ) { + try { + await this.downloadIcon("rpg"); + this.data.rpgDownloadedOnce = true; + } catch (e) {} } this.admonitions = { diff --git a/src/modal/confirm.ts b/src/modal/confirm.ts new file mode 100644 index 0000000..61f6ad9 --- /dev/null +++ b/src/modal/confirm.ts @@ -0,0 +1,58 @@ +import { App, ButtonComponent, Modal } from "obsidian"; + +export async function confirmWithModal( + app: App, + text: string, + buttons: { cta: string; secondary: string } = { + cta: "Yes", + secondary: "No" + } +): Promise { + return new Promise((resolve, reject) => { + try { + const modal = new ConfirmModal(app, text, buttons); + modal.onClose = () => { + resolve(modal.confirmed); + }; + modal.open(); + } catch (e) { + reject(); + } + }); +} + +export class ConfirmModal extends Modal { + constructor( + app: App, + public text: string, + public buttons: { cta: string; secondary: string } + ) { + super(app); + } + confirmed: boolean = false; + async display() { + this.contentEl.empty(); + this.contentEl.addClass("confirm-modal"); + this.contentEl.createEl("p", { + text: this.text + }); + const buttonEl = this.contentEl.createDiv( + "fantasy-calendar-confirm-buttons" + ); + new ButtonComponent(buttonEl) + .setButtonText(this.buttons.cta) + .setCta() + .onClick(() => { + this.confirmed = true; + this.close(); + }); + new ButtonComponent(buttonEl) + .setButtonText(this.buttons.secondary) + .onClick(() => { + this.close(); + }); + } + onOpen() { + this.display(); + } +} diff --git a/src/modal/index.ts b/src/modal/index.ts index 5e9853d..6f3d70c 100644 --- a/src/modal/index.ts +++ b/src/modal/index.ts @@ -13,7 +13,6 @@ import { } from "obsidian"; import { createPopper, Instance as PopperInstance } from "@popperjs/core"; -import { getIconModuleName, getIconNode, iconDefinitions } from "../util"; import { Admonition, AdmonitionIconDefinition } from "src/@types"; import ObsidianAdmonition from "src/main"; @@ -241,9 +240,13 @@ export class IconSuggestionModal extends SuggestionModal m[0] === i); if (match) { let element = matchElements[matches.matches.indexOf(match)]; - text.appendChild(element); + content.appendChild(element); element.appendText(item.name.substring(match[0], match[1])); i += match[1] - match[0] - 1; continue; } - text.appendText(item.name[i]); + content.appendText(item.name[i]); } const iconDiv = createDiv("suggestion-flair admonition-suggester-icon"); - iconDiv.appendChild(getIconNode(item)); - content.appendChild(iconDiv); + iconDiv.appendChild(this.plugin.iconManager.getIconNode(item)); + content.prepend(iconDiv); content.createDiv({ cls: "suggestion-note", - text: getIconModuleName(item) + text: this.plugin.iconManager.getIconModuleName(item) }); } getItems() { @@ -320,8 +323,12 @@ class AdmonitionSuggestionModal extends SuggestionModal { admonitions: Admonition[]; admonition: Admonition; text: TextComponent; - constructor(app: App, input: TextComponent, items: Admonition[]) { - super(app, input.inputEl, items); + constructor( + public plugin: ObsidianAdmonition, + input: TextComponent, + items: Admonition[] + ) { + super(plugin.app, input.inputEl, items); this.admonitions = [...items]; this.text = input; @@ -381,7 +388,7 @@ class AdmonitionSuggestionModal extends SuggestionModal { const iconDiv = createDiv("suggestion-flair admonition-suggester-icon"); iconDiv - .appendChild(getIconNode(item.icon)) + .appendChild(this.plugin.iconManager.getIconNode(item.icon)) .setAttribute("color", `rgb(${item.color})`); content.prepend(iconDiv); @@ -416,7 +423,7 @@ export class InsertAdmonitionModal extends Modal { typeSetting.setName("Admonition Type").addText((t) => { t.setPlaceholder("Admonition Type").setValue(this.type); const modal = new AdmonitionSuggestionModal( - this.app, + this.plugin, t, this.plugin.admonitionArray ); diff --git a/src/settings.ts b/src/settings.ts index 8af7e93..2de88be 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,7 +5,8 @@ import { ButtonComponent, Modal, TextComponent, - Notice + Notice, + setIcon } from "obsidian"; import { Admonition, @@ -14,16 +15,21 @@ import { AdmonitionIconType } from "./@types"; -import { getIconNode, getIconType, WARNING_ICON } from "./util"; - -import { ADD_COMMAND_NAME, REMOVE_COMMAND_NAME } from "./util"; +import { + ADD_COMMAND_NAME, + REMOVE_COMMAND_NAME, + WARNING_ICON_NAME +} from "./util"; import { IconSuggestionModal } from "./modal"; //@ts-expect-error import CONTENT from "../publish/publish.admonition.txt"; + import { t } from "src/lang/helpers"; import ObsidianAdmonition from "./main"; +import { confirmWithModal } from "./modal/confirm"; +import { DownloadableIconPack, DownloadableIcons } from "./icons/packs"; /** Taken from https://stackoverflow.com/questions/34849001/check-if-css-selector-is-valid/42149818 */ const isSelectorValid = ((dummyElement) => (selector: string) => { @@ -87,7 +93,15 @@ export default class AdmonitionSetting extends PluginSettingTab { this.containerEl.createEl("details", { cls: "admonitions-nested-settings", attr: { - open: "open" + ...(this.plugin.data.open.admonitions ? { open: true } : {}) + } + }) + ); + this.buildIcons( + this.containerEl.createEl("details", { + cls: "admonitions-nested-settings", + attr: { + ...(this.plugin.data.open.icons ? { open: true } : {}) } }) ); @@ -95,7 +109,7 @@ export default class AdmonitionSetting extends PluginSettingTab { this.containerEl.createEl("details", { cls: "admonitions-nested-settings", attr: { - open: "open" + ...(this.plugin.data.open.other ? { open: true } : {}) } }) ); @@ -103,7 +117,7 @@ export default class AdmonitionSetting extends PluginSettingTab { this.containerEl.createEl("details", { cls: "admonitions-nested-settings", attr: { - open: "open" + ...(this.plugin.data.open.advanced ? { open: true } : {}) } }) ); @@ -120,6 +134,10 @@ export default class AdmonitionSetting extends PluginSettingTab { async buildAdmonitions(containerEl: HTMLDetailsElement) { containerEl.empty(); + containerEl.ontoggle = () => { + this.plugin.data.open.admonitions = containerEl.open; + this.plugin.saveSettings(); + }; const summary = containerEl.createEl("summary"); new Setting(summary).setHeading().setName("Admonitions"); summary.createDiv("collapser").createDiv("handle"); @@ -247,109 +265,105 @@ export default class AdmonitionSetting extends PluginSettingTab { ); } - buildAdvanced(containerEl: HTMLDetailsElement) { + buildIcons(containerEl: HTMLDetailsElement) { containerEl.empty(); + containerEl.ontoggle = () => { + this.plugin.data.open.icons = containerEl.open; + this.plugin.saveSettings(); + }; const summary = containerEl.createEl("summary"); - new Setting(summary).setHeading().setName("Advanced Settings"); + new Setting(summary).setHeading().setName("Icon Packs"); summary.createDiv("collapser").createDiv("handle"); new Setting(containerEl) - .setName(t("Markdown Syntax Highlighting")) + .setName("Use Font Awesome Icons") .setDesc( - t( - "Use Obsidian's markdown syntax highlighter in admonition code blocks. This setting is experimental and could cause errors." - ) + "Font Awesome Free icons will be available in the item picker. Existing Admonitions defined using Font Awesome icons will continue to work." ) .addToggle((t) => { - t.setValue(this.plugin.data.syntaxHighlight); - t.onChange(async (v) => { - this.plugin.data.syntaxHighlight = v; - if (v) { - this.plugin.turnOnSyntaxHighlighting(); - } else { - this.plugin.turnOffSyntaxHighlighting(); - } - await this.plugin.saveSettings(); + t.setValue(this.plugin.data.useFontAwesome).onChange((v) => { + this.plugin.data.useFontAwesome = v; + this.plugin.iconManager.setIconDefinitions(); + this.plugin.saveSettings(); }); }); + let selected: DownloadableIconPack; + const possibilities = Object.entries(DownloadableIcons).filter( + ([icon]) => !this.plugin.data.icons.includes(icon) + ); new Setting(containerEl) - .setName( - createFragment((e) => { - e.appendChild(WARNING_ICON.cloneNode(true)); - e.createSpan({ - text: t(" Sync Links to Metadata Cache") - }); - }) - ) + .setName("Load Additional Icons") .setDesc( - t( - "Try to sync internal links to the metadata cache to display in graph view. This setting could have unintended consequences. Use at your own risk." - ) + "Load an additional icon pack. This requires an internet connection." ) - .addToggle((t) => { - t.setValue(this.plugin.data.syncLinks).onChange(async (v) => { - this.plugin.data.syncLinks = v; - this.display(); - await this.plugin.saveSettings(); - }); - }); + .addDropdown((d) => { + if (!possibilities.length) { + d.setDisabled(true); + return; + } + for (const [icon, display] of possibilities) { + d.addOption(icon, display); + } + d.onChange((v: DownloadableIconPack) => (selected = v)); + selected = d.getValue() as DownloadableIconPack; + }) + .addExtraButton((b) => { + b.setIcon("plus-with-circle") + .setTooltip("Load") + .onClick(async () => { + if (!selected || !selected.length) return; - new Setting(containerEl) - .setName("Generate JS for Publish") - .setDesc( - createFragment((f) => { - f.createSpan({ - text: "Generate a javascript file to place in your " + await this.plugin.iconManager.downloadIcon(selected); + this.buildIcons(containerEl); }); - f.createEl("code", { text: "publish.js" }); - f.createSpan({ text: "file." }); - f.createEl("br"); - f.createEl("strong", { - text: "Please note that this can only be done on custom domain publish sites." - }); - }) - ) - .addButton((b) => { - b.setButtonText("Generate"); - b.onClick((evt) => { - const admonition_icons: { - [admonition_type: string]: { - icon: string; - color: string; - }; - } = {}; + if (!possibilities.length) b.setDisabled(true); + }); - for (let key in this.plugin.admonitions) { - const value = this.plugin.admonitions[key]; + const iconsEl = containerEl.createDiv("admonitions-nested-settings"); + new Setting(iconsEl); + for (const icon of this.plugin.data.icons) { + new Setting(iconsEl) + .setName(DownloadableIcons[icon]) + .addExtraButton((b) => { + b.setIcon("reset") + .setTooltip("Redownload") + .onClick(async () => { + await this.plugin.iconManager.removeIcon(icon); + await this.plugin.iconManager.downloadIcon(icon); + this.buildIcons(containerEl); + }); + }) + .addExtraButton((b) => { + b.setIcon("trash").onClick(async () => { + if ( + Object.values( + this.plugin.data.userAdmonitions + ).find((admonition) => admonition.icon.type == icon) + ) { + if ( + !(await confirmWithModal( + this.plugin.app, + "You have Admonitions using icons from this pack. Are you sure you want to remove it?" + )) + ) + return; + } - admonition_icons[key] = { - icon: getIconNode(value.icon).outerHTML, - color: value.color - }; - } + await this.plugin.iconManager.removeIcon(icon); - const js = CONTENT.replace( - "const ADMONITION_ICON_MAP = {}", - "const ADMONITION_ICON_MAP = " + - JSON.stringify(admonition_icons) - ); - let csvFile = new Blob([js], { - type: "text/javascript" + this.buildIcons(containerEl); }); - let downloadLink = document.createElement("a"); - downloadLink.download = "publish.admonition.js"; - downloadLink.href = window.URL.createObjectURL(csvFile); - downloadLink.style.display = "none"; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); }); - }); + } } buildOtherSyntaxes(containerEl: HTMLDetailsElement) { containerEl.empty(); + containerEl.ontoggle = () => { + this.plugin.data.open.other = containerEl.open; + this.plugin.saveSettings(); + }; const summary = containerEl.createEl("summary"); new Setting(summary).setHeading().setName("Additional Syntaxes"); summary.createDiv("collapser").createDiv("handle"); @@ -376,6 +390,7 @@ export default class AdmonitionSetting extends PluginSettingTab { }); if (this.plugin.data.allowMSSyntax) { new Setting(containerEl) + .setClass("admonition-setting-warning") .setName( "Use Microsoft Document Syntax for Indented Codeblocks" ) @@ -390,10 +405,14 @@ export default class AdmonitionSetting extends PluginSettingTab { }); e.createEl("br"); - const strong = e.createEl("strong"); + const strong = e.createSpan( + "admonition-setting-warning text-warning" + ); - strong.appendChild(WARNING_ICON.cloneNode(true)); - strong.createSpan({text: "This syntax will not work in Live Preview."}) + setIcon(strong.createSpan(), WARNING_ICON_NAME); + strong.createSpan({ + text: "This syntax will not work in Live Preview." + }); }) ) .addToggle((t) => { @@ -428,6 +447,113 @@ export default class AdmonitionSetting extends PluginSettingTab { }); } } + buildAdvanced(containerEl: HTMLDetailsElement) { + containerEl.empty(); + containerEl.ontoggle = () => { + this.plugin.data.open.advanced = containerEl.open; + this.plugin.saveSettings(); + }; + const summary = containerEl.createEl("summary"); + new Setting(summary).setHeading().setName("Advanced Settings"); + summary.createDiv("collapser").createDiv("handle"); + + new Setting(containerEl) + .setName(t("Markdown Syntax Highlighting")) + .setDesc( + t( + "Use Obsidian's markdown syntax highlighter in admonition code blocks. This setting is experimental and could cause errors." + ) + ) + .addToggle((t) => { + t.setValue(this.plugin.data.syntaxHighlight); + t.onChange(async (v) => { + this.plugin.data.syntaxHighlight = v; + if (v) { + this.plugin.turnOnSyntaxHighlighting(); + } else { + this.plugin.turnOffSyntaxHighlighting(); + } + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName( + createFragment((e) => { + const name = e.createSpan("admonition-setting-warning"); + setIcon(name, WARNING_ICON_NAME); + name.createSpan({ + text: t(" Sync Links to Metadata Cache") + }); + }) + ) + .setDesc( + t( + "Try to sync internal links to the metadata cache to display in graph view. This setting could have unintended consequences. Use at your own risk." + ) + ) + .addToggle((t) => { + t.setValue(this.plugin.data.syncLinks).onChange(async (v) => { + this.plugin.data.syncLinks = v; + this.display(); + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName("Generate JS for Publish") + .setDesc( + createFragment((f) => { + f.createSpan({ + text: "Generate a javascript file to place in your " + }); + f.createEl("code", { text: "publish.js" }); + f.createSpan({ text: "file." }); + f.createEl("br"); + f.createEl("strong", { + text: "Please note that this can only be done on custom domain publish sites." + }); + }) + ) + .addButton((b) => { + b.setButtonText("Generate"); + b.onClick((evt) => { + const admonition_icons: { + [admonition_type: string]: { + icon: string; + color: string; + }; + } = {}; + + for (let key in this.plugin.admonitions) { + const value = this.plugin.admonitions[key]; + + admonition_icons[key] = { + icon: this.plugin.iconManager.getIconNode( + value.icon + ).outerHTML, + color: value.color + }; + } + + const js = CONTENT.replace( + "const ADMONITION_ICON_MAP = {}", + "const ADMONITION_ICON_MAP = " + + JSON.stringify(admonition_icons) + ); + let csvFile = new Blob([js], { + type: "text/javascript" + }); + let downloadLink = document.createElement("a"); + downloadLink.download = "publish.admonition.js"; + downloadLink.href = window.URL.createObjectURL(csvFile); + downloadLink.style.display = "none"; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + }); + }); + } async buildTypes() { this.additionalEl.empty(); @@ -666,33 +792,14 @@ class SettingsModal extends Modal { let iconText: TextComponent; new Setting(settingDiv) .setName(t("Admonition Icon")) - .setDesc( - createFragment((desc) => { - desc.createEl("a", { - text: "Font Awesome Icon", - href: "https://fontawesome.com/icons?d=gallery&p=2&s=solid&m=free", - attr: { - tabindex: -1 - } - }); - desc.createSpan({ text: " or " }); - desc.createEl("a", { - text: "RPG Awesome Icon", - href: "https://nagoshiashumari.github.io/Rpg-Awesome/", - attr: { - tabindex: -1 - } - }); - desc.createSpan({ text: " to use next to the title." }); - }) - ) + .setDesc("Icon to display next to the title.") .addText((text) => { iconText = text; if (this.icon.type !== "image") text.setValue(this.icon.name); const validate = async () => { const v = text.inputEl.value; - let ic = getIconType(v); + let ic = this.plugin.iconManager.getIconType(v); if (!ic) { SettingsModal.setValidationError( text, @@ -720,17 +827,19 @@ class SettingsModal extends Modal { ".admonition-title-icon" ); - iconEl.innerHTML = getIconNode(this.icon).outerHTML; + iconEl.innerHTML = this.plugin.iconManager.getIconNode( + this.icon + ).outerHTML; }; - const modal = new IconSuggestionModal(this.app, text); + const modal = new IconSuggestionModal(this.plugin, text); modal.onClose = validate; text.inputEl.onblur = validate; }) .addButton((b) => { - b.setButtonText(t("Upload Image")).setIcon('image-file'); + b.setButtonText(t("Upload Image")).setIcon("image-file"); b.buttonEl.addClass("admonition-file-upload"); b.buttonEl.appendChild(input); b.onClick(() => input.click()); @@ -821,7 +930,9 @@ class SettingsModal extends Modal { } if ( - !getIconType(iconText.inputEl.value) && + !this.plugin.iconManager.getIconType( + iconText.inputEl.value + ) && this.icon.type !== "image" ) { SettingsModal.setValidationError( diff --git a/src/util/constants.ts b/src/util/constants.ts index 9232ada..d169d9e 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -1,12 +1,19 @@ import { Admonition } from "../@types"; export const ADD_ADMONITION_COMMAND_ICON = ``; -export const ADD_COMMAND_NAME = Symbol("add-command"); +export const ADD_COMMAND_NAME = "admonition-add-command"; export const REMOVE_ADMONITION_COMMAND_ICON = ``; -export const REMOVE_COMMAND_NAME = Symbol("remove-command"); +export const REMOVE_COMMAND_NAME = "admonition-remove-command"; -export const MSDOCREGEX = /^(?:> |\t|[ ]{4})\[!(\w+)(?::[ ]?(.+)?)?\](x|\+|\-)?/; +export const COPY_ICON = ``; +export const COPY_ICON_NAME = "admonition-copy-content"; + +export const WARNING_ICON = ``; +export const WARNING_ICON_NAME = "admonition-warning"; + +export const MSDOCREGEX = + /^(?:> |\t|[ ]{4})\[!(\w+)(?::[ ]?(.+)?)?\](x|\+|\-)?/; export const ADMONITION_MAP: Record = { note: { diff --git a/src/util/icons.ts b/src/util/icons.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/util/index.ts b/src/util/index.ts index 2451184..28e2734 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,3 +1,2 @@ -export * from "./icons"; export * from "./util"; export * from "./constants"; diff --git a/src/util/util.ts b/src/util/util.ts index e94e3ea..a20aa5d 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,5 +1,11 @@ import { Notice } from "obsidian"; import { Admonition } from "../@types"; +import type { SelectionRange, EditorState } from "@codemirror/state"; +import { + editorLivePreviewField, + editorViewField, + requireApiVersion +} from "obsidian"; function startsWithAny(str: string, needles: string[]) { for (let i = 0; i < needles.length; i++) { @@ -83,3 +89,30 @@ export function getParametersFromSource( return { title, collapse, content, icon, color }; } + +export const rangesInclude = ( + ranges: readonly SelectionRange[], + from: number, + to: number +) => { + for (const range of ranges) { + const { from: rFrom, to: rTo } = range; + if (rFrom >= from && rFrom <= to) return true; + if (rTo >= from && rTo <= to) return true; + if (rFrom < from && rTo > to) return true; + } + return false; +}; + +export const isLivePreview = (state: EditorState) => { + if (requireApiVersion && requireApiVersion("0.13.23")) { + return state.field(editorLivePreviewField); + } else { + const md = state.field(editorViewField); + const { state: viewState } = md.leaf.getViewState() ?? {}; + + return ( + viewState && viewState.mode == "source" && viewState.source == false + ); + } +};