diff --git a/README.md b/README.md index dd0058f..81b6da2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Obsidian Admonition -Adds admonition block-styled content to Obsidian.md +Adds admonition block-styled content to Obsidian.md, styled after [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) + +![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/all.gif) ## Usage @@ -12,9 +14,19 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. ``` ```` -This will then render as the admonition: +Becomes: + +![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/default.png) - +## Options + +````markdown +``` # Admonition type. See below for a list of available types. +title: # Admonition title. Leave blank to remove the title element and display only the content. +collapse: # Create a collapsible admonition. Use "open" to initially render the admonition open. +content: # Actual text of admonition. Only required if "title" or "collapse" is used. +``` +```` ### Titles @@ -27,9 +39,10 @@ content: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euism ``` ```` - +![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/title.png) Leave the title field blank to only display the admonition. + ````markdown ```note title: @@ -37,17 +50,21 @@ content: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euism ``` ```` - +![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/no-title.png) -**Please note that when the title is included, you _must_ specificy the content as well.** +**Please note that when the title is included, you _must_ specify the content as well.** ### Collapsible -Use `collapse: open` or `collapse: closed` to create a collapsible admonition. +Use the `collapse` parameter to create a collapsible admonition. + +`collapse: open` will start the admonition opened on render, but allow collapse on click. + +If a blank title is provided, the collapse parameter will not do anything. -`collapse: open` will startthe admonition opened on render. +![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/collapse.gif) - +**Please note that when the title is included, you _must_ specify the content as well.** ## Admonition Types diff --git a/manifest.json b/manifest.json index 7d5b38f..6caace0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-admonition", "name": "Admonition", - "version": "0.0.5", + "version": "0.1.0", "minAppVersion": "0.11.0", "description": "Admonition block-styled content for Obsidian.md", "author": "Jeremy Valentine", diff --git a/package.json b/package.json index 2eda7ec..fce6d4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-admonition", - "version": "0.0.5", + "version": "0.1.0", "description": "Admonition block-styled content for Obsidian.md", "main": "main.js", "scripts": { diff --git a/src/admonitions.ts b/src/admonitions.ts deleted file mode 100644 index 25b37a6..0000000 --- a/src/admonitions.ts +++ /dev/null @@ -1,63 +0,0 @@ -interface Admonition { - [admonitionType: string]: string; -} -const admonitions = [ - "note", - "seealso", - "abstract", - "summary", - "info", - "todo", - "tip", - "hint", - "important", - "success", - "check", - "done", - "question", - "help", - "faq", - "warning", - "caution", - "attention", - "failure", - "fail", - "missing", - "danger", - "error", - "bug", - "example", - "quote", - "cite" -] as const; - -const ADMONITION_MAP: Record = { - note: "note", - seealso: "note", - abstract: "abstract", - summary: "abstract", - info: "info", - todo: "todo", - tip: "tip", - hint: "tip", - important: "tip", - success: "success", - check: "check", - done: "done", - question: "question", - help: "question", - faq: "question", - warning: "warning", - caution: "warning", - attention: "warning", - failure: "failure", - fail: "failure", - missing: "failure", - danger: "danger", - error: "danger", - bug: "bug", - example: "example", - quote: "quote", - cite: "quote" -}; -export default ADMONITION_MAP as Admonition; diff --git a/src/main.ts b/src/main.ts index 8602aba..996a083 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,48 +1,112 @@ -import { MarkdownPostProcessorContext, Plugin } from "obsidian"; +import { MarkdownPostProcessorContext, Notice, Plugin } from "obsidian"; import "./main.css"; -import ADMONITION_MAP from "./admonitions"; -export default class Admonition extends Plugin { +const ADMONITION_MAP: { + [admonitionType: string]: string; +} = { + note: "note", + seealso: "note", + abstract: "abstract", + summary: "abstract", + info: "info", + todo: "todo", + tip: "tip", + hint: "tip", + important: "tip", + success: "success", + check: "check", + done: "done", + question: "question", + help: "question", + faq: "question", + warning: "warning", + caution: "warning", + attention: "warning", + failure: "failure", + fail: "failure", + missing: "failure", + danger: "danger", + error: "danger", + bug: "bug", + example: "example", + quote: "quote", + cite: "quote" +}; +const classMap = Object.keys(ADMONITION_MAP).map((k) => `language-${k}`); + +/** Fast Intersection taken from + * https://codeburst.io/optimizing-array-analytics-in-javascript-part-two-search-intersection-and-cross-products-79b4a6d68da0 + */ +const fastIntersection = (...arrays: any[]) => { + // if we process the arrays from shortest to longest + // then we will identify failure points faster, i.e. when + // one item is not in all arrays + const ordered = + arrays.length === 1 + ? arrays + : arrays.sort((a1, a2) => a1.length - a2.length), + shortest = ordered[0], + set = new Set(), // used for bookeeping, Sets are faster + result = []; // the intersection, conversion from Set is slow + // for each item in the shortest array + for (let i = 0; i < shortest.length; i++) { + const item = shortest[i]; + // see if item is in every subsequent array + let every = true; // don't use ordered.every ... it is slow + for (let j = 1; j < ordered.length; j++) { + if (ordered[j].includes(item)) continue; + every = false; + break; + } + // ignore if not in every other array, or if already captured + if (!every || set.has(item)) continue; + // otherwise, add to bookeeping set and the result + set.add(item); + result[result.length] = item; + } + return result; +}; +export default class ObsidianAdmonition extends Plugin { async onload(): Promise { console.log("Obsidian Admonition loaded"); this.registerMarkdownPostProcessor(this.postprocessor.bind(this)); } postprocessor(el: HTMLElement, ctx: MarkdownPostProcessorContext) { - /* */ + //don't process if no code elements in element; let codeBlocks = el.querySelectorAll("code"); if (!codeBlocks.length) return; - const classMap = Object.keys(ADMONITION_MAP).map( - (k) => `language-${k}` - ); - codeBlocks = Array.prototype.map - .call( - codeBlocks, - (element: HTMLElement): HTMLElement => { - if (element) { - const classList = Array.prototype.filter.call( - element.classList, - (cls: string) => classMap.includes(cls) - ); - if (classList.length) return element; - } - } - ) - .filter((b: HTMLElement) => b); + //don't process if the code block is not an admonition type + codeBlocks = Array.prototype.filter.call( + codeBlocks, + (element: HTMLElement) => + element && + fastIntersection( + Array.prototype.slice.call(element.classList), + classMap + ).length > 0 + ); if (!codeBlocks.length) return; + //render admonition element codeBlocks.forEach((block) => { if (block) { - let classType = Array.prototype.find.call( - block.classList, - (cls: string) => classMap.includes(cls) - ); - if (!classType) return; let type = - ADMONITION_MAP[classType.split("language-").pop().trim()]; - if (!type) return; + ADMONITION_MAP[ + Array.prototype.find + .call(block.classList, (cls: string) => + classMap.includes(cls) + ) + .split("language-") + .pop() + .trim() + ]; + if (!type) { + new Notice("There was an error rendering the admonition."); + return; + } let params = Object.fromEntries( block.innerText .split("\n") @@ -53,13 +117,6 @@ export default class Admonition extends Plugin { content = block.innerText, collapse } = params; - console.log( - "🚀 ~ file: main.ts ~ line 56 ~ Admonition ~ codeBlocks.forEach ~ params", - params, - block.innerText - .split("\n") - .map((l) => l.split(":").map((s) => s.trim())) - ); if ( Object.prototype.hasOwnProperty.call(params, "title") && @@ -71,14 +128,12 @@ export default class Admonition extends Plugin { if ( Object.prototype.hasOwnProperty.call(params, "collapse") && (params.collapse.length == 0 || - params.collapse === undefined) + params.collapse === undefined || + collapse !== "open") ) { collapse = "closed"; } - console.log( - "🚀 ~ file: main.ts ~ line 69 ~ Admonition ~ codeBlocks.forEach ~ params.collapse", - collapse - ); + this.buildAdmonition( block.parentElement, type, @@ -97,15 +152,9 @@ export default class Admonition extends Plugin { collapse?: string ) { let attrs, - els = [ - "div" as keyof HTMLElementTagNameMap, - "div" as keyof HTMLElementTagNameMap - ]; - if (collapse && ["open", "closed"].includes(collapse)) { - els = [ - "details" as keyof HTMLElementTagNameMap, - "summary" as keyof HTMLElementTagNameMap - ]; + els: Array = ["div", "div"]; + if (collapse) { + els = ["details", "summary"]; attrs = { [collapse]: true }; diff --git a/versions.json b/versions.json index fdb2e69..cca8d52 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "0.0.5": "0.11.0" + "0.1.0": "0.11.0" }