diff --git a/package.json b/package.json index 642201b..eca2287 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,11 @@ "@rollup/plugin-commonjs": "^15.1.0", "@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-typescript": "^6.0.0", + "@types/html": "^1.0.0", "@types/node": "^14.14.2", "@types/object.fromentries": "^2.0.0", + "html": "^1.0.0", + "nanoid": "^3.1.23", "object.fromentries": "^2.0.4", "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", "rollup": "^2.32.1", diff --git a/src/main.ts b/src/main.ts index 024fad1..829ff3f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,9 +8,13 @@ import { Plugin, TFile } from "obsidian"; +import { prettyPrint as html } from "html"; + import { Admonition, ObsidianAdmonitionPlugin, ISettingsData } from "./@types"; import { getAdmonitionElement, + getAdmonitionElementAsync, + getID, getMatches, getParametersFromSource } from "./util"; @@ -82,9 +86,8 @@ export default class ObsidianAdmonition implements ObsidianAdmonitionPlugin { admonitions: { [admonitionType: string]: Admonition } = {}; - /* userAdmonitions: { [admonitionType: string]: Admonition } = {}; - syntaxHighlight: boolean; */ data: ISettingsData; + contextMap: Map = new Map(); get types() { return Object.keys(this.admonitions); } @@ -179,6 +182,115 @@ export default class ObsidianAdmonition addIcon(ADD_COMMAND_NAME.toString(), ADD_ADMONITION_COMMAND_ICON); addIcon(REMOVE_COMMAND_NAME.toString(), REMOVE_ADMONITION_COMMAND_ICON); + const TYPE_REGEX = new RegExp( + `(!{3,}|\\?{3,}\\+?) ad-(${this.types.join("|")})` + ); + const END_REGEX = new RegExp(`\\-{3,} admonition`); + + let push = false, + id: string; + const elementMap: Map< + MarkdownRenderChild, + { contentEl: HTMLElement; elements: Element[]; loaded: boolean } + > = new Map(); + const childMap: Map = new Map(); + this.registerMarkdownPostProcessor(async (el, ctx) => { + if (END_REGEX.test(el.textContent) && push) { + push = false; + el.children[0].detach(); + return; + } + if (!TYPE_REGEX.test(el.textContent) && !push) return; + + if (!push) { + push = true; + let child = new MarkdownRenderChild(el); + id = getID(); + childMap.set(id, child); + + elementMap.set(child, { + contentEl: null, + elements: [], + loaded: false + }); + + child.onload = async () => { + const source = el.textContent; + + const [, col, type] = source.match(TYPE_REGEX) ?? []; + if (!type) return; + let collapse; + if (/\?{3,}/.test(col)) { + collapse = /\+/.test(col) ? "open" : "closed"; + } + + const admonitionElement = await getAdmonitionElementAsync( + type, + type, + this.admonitions[type].icon, + this.admonitions[type].color, + collapse + ); + + const contentEl = admonitionElement.createDiv({ + cls: "admonition-content" + }); + + child.containerEl.appendChild(admonitionElement); + + console.log(elementMap.get(child).elements); + for (let element of elementMap.get(child)?.elements) { + contentEl.appendChild(element); + } + + elementMap.set(child, { + ...elementMap.get(child), + contentEl: contentEl, + loaded: true + }); + }; + + child.onunload = () => { + childMap.delete(id); + elementMap.delete(child); + }; + + ctx.addChild(child); + + el.children[0].detach(); + + return; + } + + if (id && childMap.get(id)) { + const child = childMap.get(id); + elementMap.set(child, { + ...elementMap.get(child), + elements: [ + ...elementMap.get(child).elements, + ...Array.from(el.children) + ] + }); + if (elementMap.get(child)?.loaded) { + for (let element of elementMap.get(child)?.elements) { + elementMap.get(child).contentEl.appendChild(element); + } + } + } + + /* const child = elementMap.get(child); + + elementMap.set(child, { + ...elementMap.get(child), + elements: [ + ...elementMap.get(child).elements, + ...Array.from(el.children) + ] + }); + + console.log(childMap.elements); */ + }); + Object.keys(this.admonitions).forEach((type) => { this.registerMarkdownCodeBlockProcessor( `ad-${type}`, @@ -192,6 +304,7 @@ export default class ObsidianAdmonition this.turnOnSyntaxHighlighting(); } + /** Add generic commands. */ this.addCommand({ id: "collapse-admonitions", name: "Collapse Admonitions in Note", @@ -199,8 +312,9 @@ export default class ObsidianAdmonition let view = this.app.workspace.getActiveViewOfType(MarkdownView); if (!view || !(view instanceof MarkdownView)) return; - let admonitions = - view.contentEl.querySelectorAll("details[open]"); + let admonitions = view.contentEl.querySelectorAll( + "details[open].admonition-plugin" + ); for (let i = 0; i < admonitions.length; i++) { let admonition = admonitions[i]; admonition.removeAttribute("open"); @@ -215,7 +329,7 @@ export default class ObsidianAdmonition if (!view || !(view instanceof MarkdownView)) return; let admonitions = view.contentEl.querySelectorAll( - "details:not([open])" + "details:not([open]).admonition-plugin" ); for (let i = 0; i < admonitions.length; i++) { let admonition = admonitions[i]; @@ -223,6 +337,50 @@ export default class ObsidianAdmonition } } }); + this.addCommand({ + id: "replace-with-html", + name: "Replace Admonitions with HTML", + callback: async () => { + let view = this.app.workspace.getActiveViewOfType(MarkdownView); + if ( + !view || + !(view instanceof MarkdownView) || + view.getMode() !== "preview" + ) + return; + + let admonitions = + view.contentEl.querySelectorAll( + ".admonition-plugin" + ); + + let content = ( + (await this.app.vault.read(view.file)) ?? "" + ).split("\n"); + if (!content) return; + for (let admonition of Array.from(admonitions).reverse()) { + if (admonition.id && this.contextMap.has(admonition.id)) { + const ctx = this.contextMap.get(admonition.id); + const { lineStart, lineEnd } = + ctx.getSectionInfo(admonition) ?? {}; + if (!lineStart || !lineEnd) continue; + + const element = admonition.cloneNode( + true + ) as HTMLElement; + + element.removeAttribute("id"); + + content.splice( + lineStart, + lineEnd - lineStart + 1, + html(element.outerHTML) + ); + } + } + await this.app.vault.modify(view.file, content.join("\n")); + } + }); this.registerEvent( this.app.metadataCache.on("resolve", (file) => { @@ -404,14 +562,16 @@ title: } else if (collapse && collapse.trim() === "none") { collapse = ""; } - + const id = getID(); let admonitionElement = getAdmonitionElement( type, title, this.admonitions[type].icon, this.admonitions[type].color, - collapse + collapse, + id ); + /** * Create a unloadable component. */ @@ -420,6 +580,14 @@ title: ); markdownRenderChild.containerEl = admonitionElement; + markdownRenderChild.onload = () => { + this.contextMap.set(id, ctx); + }; + markdownRenderChild.onunload = () => { + this.contextMap.delete(id); + }; + ctx.addChild(markdownRenderChild); + let admonitionContent = admonitionElement.createDiv({ cls: "admonition-content" }); diff --git a/src/util/util.ts b/src/util/util.ts index d0cb9de..c8aa968 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,7 +1,14 @@ import { MarkdownRenderer, Notice } from "obsidian"; + +import { nanoid } from "nanoid"; + import { getIconNode } from "./icons"; import { AdmonitionIconDefinition, INestedAdmonition } from "../@types"; +export function getID() { + return `ID_${nanoid()}`; +} + export function getMatches( src: string, from: number, @@ -103,7 +110,8 @@ export /* async */ function getAdmonitionElement( title: string, icon: AdmonitionIconDefinition, color: string, - collapse?: string + collapse?: string, + id?: string ): HTMLElement { let admonition, titleEl, @@ -131,6 +139,10 @@ export /* async */ function getAdmonitionElement( }); } + if (id) { + admonition.id = id; + } + if (title && title.length) { /** * Title structure @@ -147,11 +159,107 @@ export /* async */ function getAdmonitionElement( MarkdownRenderer.renderMarkdown(title, markdownHolder, "", null); //admonition-title-content is first child of rendered markdown - const admonitionTitleContent = markdownHolder.children[0]; + + const admonitionTitleContent = + markdownHolder.children[0].tagName === "P" + ? createDiv() + : markdownHolder.children[0]; + + //get children of markdown element, then remove them + const markdownElements = Array.from( + markdownHolder.children[0]?.childNodes || [] + ); + admonitionTitleContent.innerHTML = ""; + admonitionTitleContent.addClass("admonition-title-content"); + + //build icon element + const iconEl = admonitionTitleContent.createDiv( + "admonition-title-icon" + ); + if (icon && icon.name && icon.type) { + iconEl.appendChild(getIconNode(icon)); + } + + //add markdown children back + const admonitionTitleMarkdown = admonitionTitleContent.createDiv( + "admonition-title-markdown" + ); + for (let i = 0; i < markdownElements.length; i++) { + admonitionTitleMarkdown.appendChild(markdownElements[i]); + } + titleEl.appendChild(admonitionTitleContent || createDiv()); + } + + //add them to title element + + if (collapse) { + titleEl.createDiv("collapser").createDiv("handle"); + } + return admonition; +} +export async function getAdmonitionElementAsync( + type: string, + title: string, + icon: AdmonitionIconDefinition, + color: string, + collapse?: string, + id?: string +): Promise { + let admonition, + titleEl, + attrs: { style: string; open?: string } = { + style: `--admonition-color: ${color};` + }; + if (collapse) { + if (collapse === "open") { + attrs.open = "open"; + } + admonition = createEl("details", { + cls: `admonition admonition-${type} admonition-plugin admonition-plugin-async`, + attr: attrs + }); + titleEl = admonition.createEl("summary", { + cls: `admonition-title ${!title.trim().length ? "no-title" : ""}` + }); + } else { + admonition = createDiv({ + cls: `admonition admonition-${type} admonition-plugin`, + attr: attrs + }); + titleEl = admonition.createDiv({ + cls: `admonition-title ${!title.trim().length ? "no-title" : ""}` + }); + } + + if (id) { + admonition.id = id; + } + + if (title && title.length) { + // + // Title structure + // .admonition-title + // .admonition-title-content - Rendered Markdown top-level element (e.g. H1/2/3 etc, p) + // div.admonition-title-icon + // svg + // div.admonition-title-markdown - Container of rendered markdown + // ...rendered markdown children... + // + + //get markdown + const markdownHolder = createDiv(); + await MarkdownRenderer.renderMarkdown(title, markdownHolder, "", null); + + //admonition-title-content is first child of rendered markdown + + const admonitionTitleContent = + markdownHolder.children[0].tagName === "P" + ? createDiv() + : markdownHolder.children[0]; //get children of markdown element, then remove them const markdownElements = Array.from( - admonitionTitleContent?.childNodes || [] + markdownHolder.children[0]?.childNodes || [] ); admonitionTitleContent.innerHTML = ""; admonitionTitleContent.addClass("admonition-title-content");