diff --git a/docs/API/attributes_functions_overview.md b/docs/API/attributes_functions_overview.md index 35b3507..092257d 100644 --- a/docs/API/attributes_functions_overview.md +++ b/docs/API/attributes_functions_overview.md @@ -176,6 +176,11 @@ export interface ExcalidrawAutomate { b: readonly [number, number], gap?: number, //if given, element is inflated by this value ): Point[]; + + //See OCR plugin for example on how to use scriptSettings + activeScript: string; //Set automatically by the ScriptEngine + getScriptSettings(): {}; //Returns script settings. Saves settings in plugin settings, under the activeScript key + setScriptSettings(settings:any):Promise; //sets script settings. } ``` diff --git a/ea-scripts/OCR - Optical Character Recognition.md b/ea-scripts/OCR - Optical Character Recognition.md index b62914c..44dd186 100644 --- a/ea-scripts/OCR - Optical Character Recognition.md +++ b/ea-scripts/OCR - Optical Character Recognition.md @@ -5,12 +5,12 @@ Download this file and save to your Obsidian Vault including the first line, or ![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg) +THIS SCRIPT REQUIRES EXCALIDRAW 1.5.15 + The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element -⚠ Don't forget to paste your token into the script after the first run. ⚠ - I recommend also installing the [Transfer TextElements to Excalidraw markdown metadata](Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md) script as well. The script is based on [@schlundd](https://github.com/schlundd)'s [Obsidian-OCR-Plugin](https://github.com/schlundd/obsidian-ocr-plugin) @@ -20,11 +20,14 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h ```javascript */ -let token = ""; //paste token in-between the quotation marks "xxxxxxxxxx" +const curVersion = app.plugins.manifests["obsidian-excalidraw-plugin"].version; +if(curVersion < "1.5.15") new Notice("please update Excalidraw plugin to the latest version"); + +let token = ea.getScriptSettings()?.token; const BASE_URL = "https://ocr.taskbone.com"; //get new token if token was not provided -if (token==="") { +if (!token) { const tokenResponse = await fetch( BASE_URL + "/get-new-token", { method: 'post' @@ -32,9 +35,7 @@ if (token==="") { if (tokenResponse.status === 200) { jsonResponse = await tokenResponse.json(); token = jsonResponse.token; - navigator.clipboard.writeText(token); - notice("Please update the ScriptEngine script with the Token.\n\nToken is on the clipboard and in Developer Console."); - console.log({token}); + ea.setScriptSettings({token}); } else { notice(`Taskbone OCR Error: ${tokenResponse.status}\nPlease try again later.`); return; diff --git a/ea-scripts/index.md b/ea-scripts/index.md index facf960..fc51541 100644 --- a/ea-scripts/index.md +++ b/ea-scripts/index.md @@ -171,7 +171,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea ```excalidraw-script-install https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/OCR%20-%20Optical%20Character%20Recognition.md ``` -
Author@zsviczian
SourceFile on GitHub
DescriptionThe script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element.
⚠ Note that you will need to manually paste your token into the script after the first run! ⚠

+
Author@zsviczian
SourceFile on GitHub
DescriptionREQUIRES EXCALIDRAW 1.5.15
The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element.
⚠ Note that you will need to manually paste your token into the script after the first run! ⚠

## Reverse arrows ```excalidraw-script-install diff --git a/manifest.json b/manifest.json index 4ecff8e..8d36d52 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "1.5.14", + "version": "1.5.15", "minAppVersion": "0.12.16", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 87cc60b..704807d 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -211,9 +211,11 @@ export interface ExcalidrawAutomate { b: readonly [number, number], gap?: number, //if given, element is inflated by this value ): Point[]; - activeScript: string; - getScriptSettings(): {}; - setScriptSettings(settings:any):Promise; + + //See OCR plugin for example on how to use scriptSettings + activeScript: string; //Set automatically by the ScriptEngine + getScriptSettings(): {}; //Returns script settings. Saves settings in plugin settings, under the activeScript key + setScriptSettings(settings:any):Promise; //sets script settings. } declare let window: any; diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 01e56e5..12f15f5 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -46,6 +46,7 @@ import { } from "./ExcalidrawData"; import { checkAndCreateFolder, + checkExcalidrawVersion, //debug, download, embedFontsInSVG, @@ -723,6 +724,7 @@ export default class ExcalidrawView extends TextFileView { private isLoaded: boolean = false; async setViewData(data: string, clear: boolean = false) { + checkExcalidrawVersion(this.app); this.isLoaded = false; if (clear) { this.clear(); diff --git a/src/MarkdownPostProcessor.ts b/src/MarkdownPostProcessor.ts new file mode 100644 index 0000000..6896296 --- /dev/null +++ b/src/MarkdownPostProcessor.ts @@ -0,0 +1,481 @@ +import { settings } from "cluster"; +import { MarkdownPostProcessorContext, MetadataCache, TFile, Vault } from "obsidian"; +import { CTRL_OR_CMD, RERENDER_EVENT } from "./constants"; +import { EmbeddedFilesLoader } from "./EmbeddedFileLoader"; +import { createPNG, createSVG } from "./ExcalidrawAutomate"; +import { ExportSettings } from "./ExcalidrawView"; +import ExcalidrawPlugin from "./main"; +import { embedFontsInSVG, getIMGFilename, isObsidianThemeDark, splitFolderAndFilename, svgToBase64 } from "./Utils"; + +interface imgElementAttributes { + file?: TFile; + fname: string; //Excalidraw filename + fwidth: string; //Display width of image + fheight: string; //Display height of image + style: string; //css style to apply to IMG element +} + +let plugin: ExcalidrawPlugin; +let vault: Vault; +let metadataCache: MetadataCache; + +export const initializeMarkdownPostProcessor = (p:ExcalidrawPlugin) => { + plugin = p; + vault = p.app.vault; + metadataCache = p.app.metadataCache; +} + + + +/** + * Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings) + * @param parts {imgElementAttributes} - display properties of the image + * @returns {Promise} - the IMG HTML element containing the image + */ +const getIMG = async ( + imgAttributes: imgElementAttributes, +): Promise => { + let file = imgAttributes.file; + if (!imgAttributes.file) { + const f = vault.getAbstractFileByPath(imgAttributes.fname); + if (!(f && f instanceof TFile)) { + return null; + } + file = f; + } + + const exportSettings: ExportSettings = { + withBackground: plugin.settings.exportWithBackground, + withTheme: plugin.settings.exportWithTheme, + }; + const img = createEl("img"); + let style = `max-width:${imgAttributes.fwidth}px !important; width:100%;`; + if (imgAttributes.fheight) { + style += `height:${imgAttributes.fheight}px;`; + } + img.setAttribute("style", style); + img.addClass(imgAttributes.style); + + const theme = plugin.settings.previewMatchObsidianTheme + ? isObsidianThemeDark() + ? "dark" + : "light" + : !plugin.settings.exportWithTheme + ? "light" + : undefined; + if (theme) { + exportSettings.withTheme = true; + } + const loader = new EmbeddedFilesLoader( + plugin, + theme ? theme === "dark" : undefined, + ); + + if (!plugin.settings.displaySVGInPreview) { + const width = parseInt(imgAttributes.fwidth); + let scale = 1; + if (width >= 600) { + scale = 2; + } + if (width >= 1200) { + scale = 3; + } + if (width >= 1800) { + scale = 4; + } + if (width >= 2400) { + scale = 5; + } + const png = await createPNG( + file.path, + scale, + exportSettings, + loader, + theme, + null, + null, + [], + plugin, + ); + //const png = await getPNG(JSON_parse(scene),exportSettings, scale); + if (!png) { + return null; + } + img.src = URL.createObjectURL(png); + return img; + } + const svgSnapshot = ( + await createSVG( + file.path, + true, + exportSettings, + loader, + theme, + null, + null, + [], + plugin, + ) + ).outerHTML; + let svg: SVGSVGElement = null; + const el = document.createElement("div"); + el.innerHTML = svgSnapshot; + const firstChild = el.firstChild; + if (firstChild instanceof SVGSVGElement) { + svg = firstChild; + } + if (!svg) { + return null; + } + svg = embedFontsInSVG(svg); + svg.removeAttribute("width"); + svg.removeAttribute("height"); + img.setAttribute("src", svgToBase64(svg.outerHTML)); + return img; +}; + +const createImageDiv = async ( + attr: imgElementAttributes, +): Promise => { + const img = await getIMG(attr); + return createDiv(attr.style, (el) => { + el.append(img); + el.setAttribute("src", attr.file.path); + if (attr.fwidth) { + el.setAttribute("w", attr.fwidth); + } + if (attr.fheight) { + el.setAttribute("h", attr.fheight); + } + el.onClickEvent((ev) => { + if ( + ev.target instanceof Element && + ev.target.tagName.toLowerCase() != "img" + ) { + return; + } + const src = el.getAttribute("src"); + if (src) { + plugin.openDrawing( + vault.getAbstractFileByPath(src) as TFile, + ev[CTRL_OR_CMD], + ); + } //.ctrlKey||ev.metaKey); + }); + el.addEventListener(RERENDER_EVENT, async (e) => { + e.stopPropagation(); + el.empty(); + const img = await getIMG({ + fname: el.getAttribute("src"), + fwidth: el.getAttribute("w"), + fheight: el.getAttribute("h"), + style: el.getAttribute("class"), + }); + el.append(img); + }); + }); +}; + + + +const processInternalEmbeds = async ( + embeddedItems:NodeListOf|[HTMLElement], + ctx: MarkdownPostProcessorContext, +) => { + //if not, then we are processing a non-excalidraw file in reading mode + //in that cases embedded files will be displayed in an .internal-embed container + const attr: imgElementAttributes = { + fname: "", + fheight: "", + fwidth: "", + style: "", + }; + let alt: string; + let parts; + let file: TFile; + + //Iterating through all the containers to check which one is an excalidraw drawing + //This is a for loop instead of embeddedItems.forEach() because createImageDiv at the end + //is awaited, otherwise excalidraw images would not display in the Kanban plugin + for (const maybeDrawing of embeddedItems) { + //check to see if the file in the src attribute exists + attr.fname = maybeDrawing.getAttribute("src"); + file = metadataCache.getFirstLinkpathDest( + attr.fname?.split("#")[0], + ctx.sourcePath, + ); + + //if the embeddedFile exits and it is an Excalidraw file + //then lets replace the .internal-embed with the generated PNG or SVG image + if (file && file instanceof TFile && plugin.isExcalidrawFile(file)) { + attr.fwidth = maybeDrawing.getAttribute("width") + ? maybeDrawing.getAttribute("width") + : plugin.settings.width; + attr.fheight = maybeDrawing.getAttribute("height"); + alt = maybeDrawing.getAttribute("alt"); + if (alt == attr.fname) { + alt = ""; + } //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text + attr.style = "excalidraw-svg"; + if (alt) { + //for some reason Obsidian renders ![]() in a DIV and ![[]] in a SPAN + //also the alt-text of the DIV does not include the alt-text of the image + //thus need to add an additional "|" character when its a SPAN + if (maybeDrawing.tagName.toLowerCase() == "span") { + alt = `|${alt}`; + } + //1:width, 2:height, 3:style 1 2 3 + parts = alt.match(/[^\|]*\|?(\d*%?)x?(\d*%?)\|?(.*)/); + attr.fwidth = parts[1] ? parts[1] : plugin.settings.width; + attr.fheight = parts[2]; + if (parts[3] != attr.fname) { + attr.style = `excalidraw-svg${parts[3] ? `-${parts[3]}` : ""}`; + } + } + + attr.fname = file?.path; + attr.file = file; + const div = await createImageDiv(attr); + maybeDrawing.parentElement.replaceChild(div, maybeDrawing); + } + } +} + + +const tmpObsidianWYSIWYG = async ( + el: HTMLElement, + ctx: MarkdownPostProcessorContext, +) => { + if (!ctx.frontmatter) { + return; + } + if (!ctx.frontmatter.hasOwnProperty("excalidraw-plugin")) { + return; + } + //@ts-ignore + if (ctx.remainingNestLevel < 4) { + return; + } + if (!el.querySelector(".frontmatter")) { + el.style.display = "none"; + return; + } + const attr: imgElementAttributes = { + fname: ctx.sourcePath, + fheight: "", + fwidth: plugin.settings.width, + style: "excalidraw-svg", + }; + + attr.file = metadataCache.getFirstLinkpathDest( + ctx.sourcePath, + "", + ); + + el.empty(); + + if(!plugin.settings.experimentalLivePreview) { + el.appendChild(await createImageDiv(attr)); + return; + } + + const div = createDiv(); + el.appendChild(div); + + //The timeout gives time for obsidian to attach el to the displayed document + //Once the element is attached, I can traverse up the dom tree to find .internal-embed + //If internal embed is not found, it means the that the excalidraw.md file + //is being rendered in "reading" mode. In that case, the image with the default width + //specified in setting should be displayed + //if .internal-embed is found, then contents is replaced with the image using the + //alt, width, and height attributes of .internal-embed to size and style the image + setTimeout(async ()=>{ + let internalEmbedDiv:HTMLElement = div; + while(!internalEmbedDiv.hasClass("internal-embed") && internalEmbedDiv.parentElement) { + internalEmbedDiv = internalEmbedDiv.parentElement; + } + + if(!internalEmbedDiv.hasClass("internal-embed")) { + el.empty(); + el.appendChild(await createImageDiv(attr)); + return; + } + + internalEmbedDiv.empty(); + + const basename = splitFolderAndFilename(attr.fname).basename; + const setAttr = () => { + const hasWidth = internalEmbedDiv.getAttribute("width")!==""; + const hasHeight = internalEmbedDiv.getAttribute("height")!==""; + if(hasWidth) + attr.fwidth = internalEmbedDiv.getAttribute("width"); + if(hasHeight) + attr.fheight = internalEmbedDiv.getAttribute("height"); + const alt = internalEmbedDiv.getAttribute("alt"); + const hasAttr = alt && alt!=="" && alt!==basename; + if(hasAttr) { + //1:width, 2:height, 3:style 1 2 3 + const parts = alt.match(/(\d*%?)x?(\d*%?)\|?(.*)/); + attr.fwidth = parts[1] ? parts[1] : plugin.settings.width; + attr.fheight = parts[2]; + if (parts[3] != attr.fname) { + attr.style = `excalidraw-svg${parts[3] ? `-${parts[3]}` : ""}`; + } + } + if(!hasWidth && !hasHeight && !hasAttr) { + attr.fheight = ""; + attr.fwidth = plugin.settings.width; + attr.style = "excalidraw-svg"; + } + } + + const createImgElement = async () => { + setAttr(); + const imgDiv = await createImageDiv(attr); + internalEmbedDiv.appendChild(imgDiv); + } + await createImgElement(); + + //timer to avoid the image flickering when the user is typing + let timer:NodeJS.Timeout = null; + const observer = new MutationObserver((m) => { + if(!["alt","width","height"].contains(m[0]?.attributeName)) { + return; + } + if(timer) clearTimeout(timer); + timer = setTimeout(() => { + timer = null; + setAttr(); + internalEmbedDiv.empty(); + createImgElement(); + },500); + }); + observer.observe(internalEmbedDiv, { + attributes: true, //configure it to listen to attribute changes + }); + },300); +}; + + +/** + * + * @param el + * @param ctx + */ +export const markdownPostProcessor = async ( + el: HTMLElement, + ctx: MarkdownPostProcessorContext, +) => { + //check to see if we are rendering in editing mode of live preview + //if yes, then there should be no .internal-embed containers + const embeddedItems = el.querySelectorAll(".internal-embed"); + if (embeddedItems.length === 0) { + tmpObsidianWYSIWYG(el, ctx); + return; + } + + //If the file being processed is an excalidraw file, + //then I want to hide all embedded items as these will be + //transcluded text element or some other transcluded content inside the Excalidraw file + //in reading mode these elements should be hidden + if (ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")) { + el.style.display = "none"; + return; + } + + await processInternalEmbeds( embeddedItems,ctx); + +}; + +/** + * internal-link quick preview + * @param e + * @returns + */ +export const hoverEvent = (e: any) => { + if (!e.linktext) { + plugin.hover.linkText = null; + return; + } + plugin.hover.linkText = e.linktext; + plugin.hover.sourcePath = e.sourcePath; +}; + +//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree +export const observer = new MutationObserver(async (m) => { + if (m.length == 0) { + return; + } + if (!plugin.hover.linkText) { + return; + } + const file = metadataCache.getFirstLinkpathDest( + plugin.hover.linkText, + plugin.hover.sourcePath ? plugin.hover.sourcePath : "", + ); + if (!file) { + return; + } + if (!(file instanceof TFile)) { + return; + } + if (file.extension !== "excalidraw") { + return; + } + + const svgFileName = getIMGFilename(file.path, "svg"); + const svgFile = vault.getAbstractFileByPath(svgFileName); + if (svgFile && svgFile instanceof TFile) { + return; + } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview + + const pngFileName = getIMGFilename(file.path, "png"); + const pngFile = vault.getAbstractFileByPath(pngFileName); + if (pngFile && pngFile instanceof TFile) { + return; + } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview + + if (!plugin.hover.linkText) { + return; + } + if (m.length != 1) { + return; + } + if (m[0].addedNodes.length != 1) { + return; + } + if ( + //@ts-ignore + m[0].addedNodes[0].className != + "popover hover-popover file-embed is-loaded" + ) { + return; + } + const node = m[0].addedNodes[0]; + node.empty(); + + //this div will be on top of original DIV. By stopping the propagation of the click + //I prevent the default Obsidian feature of openning the link in the native app + const img = await getIMG({ + file, + fname: file.path, + fwidth: "300", + fheight: null, + style: "excalidraw-svg", + }); + const div = createDiv("", async (el) => { + el.appendChild(img); + el.setAttribute("src", file.path); + el.onClickEvent((ev) => { + ev.stopImmediatePropagation(); + const src = el.getAttribute("src"); + if (src) { + plugin.openDrawing( + vault.getAbstractFileByPath(src) as TFile, + ev[CTRL_OR_CMD], + ); + } //.ctrlKey||ev.metaKey); + }); + }); + node.appendChild(div); +}); diff --git a/src/Utils.ts b/src/Utils.ts index 32a7d72..43ed118 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -2,6 +2,7 @@ import { exportToSvg, exportToBlob } from "@zsviczian/excalidraw"; import { App, normalizePath, + Notice, TAbstractFile, TFolder, Vault, @@ -26,6 +27,21 @@ declare module "obsidian" { } } + +let versionUpdateChecked = false; +export const checkExcalidrawVersion = async (app:App) => { + if(versionUpdateChecked) return; + versionUpdateChecked = true; + //@ts-ignore + const manifest = app.plugins.manifests["obsidian-excalidraw-plugin"]; + //@ts-ignore + const latestVersion = await app.plugins.getLatestVersion("obsidian-excalidraw-plugin",manifest) + if(latestVersion>manifest.version) { + new Notice(`A newer version of Excalidraw is available in Community Plugins. You are using ${manifest.version}. The latest version is ${latestVersion}`); + } + setTimeout(()=>versionUpdateChecked=false,28800000);//reset after 8 hours +} + /** * Splits a full path including a folderpath and a filename into separate folderpath and filename components * @param filepath diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 95a4c31..523d6ed 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -271,6 +271,10 @@ export default { FILETAG_NAME: "Set the type indicator for excalidraw.md files", FILETAG_DESC: "The text or emojii to display as type indicator.", INSERT_EMOJI: "Insert an emoji", + LIVEPREVIEW_NAME: "Immersive image embedding in live preview editing mode", + LIVEPREVIEW_DESC: "Turn this on to support image embedding styles such as ![[drawing|width|style]] in live preview editing mode. " + + "The setting will not effect the currently open documents. You need close the open documents and re-open them for the change " + + "to take effect.", //openDrawings.ts SELECT_FILE: "Select a file then press enter.", diff --git a/src/main.ts b/src/main.ts index b13ed3e..42b21f4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -72,12 +72,14 @@ import { getNewUniqueFilepath, isObsidianThemeDark, log, + splitFolderAndFilename, svgToBase64, } from "./Utils"; import { OneOffs } from "./OneOffs"; import { FileId } from "@zsviczian/excalidraw/types/element/types"; import { EmbeddedFilesLoader } from "./EmbeddedFileLoader"; import { ScriptEngine } from "./Scripts"; +import { hoverEvent, initializeMarkdownPostProcessor, markdownPostProcessor, observer } from "./MarkdownPostProcessor"; declare module "obsidian" { interface App { @@ -368,380 +370,14 @@ export default class ExcalidrawPlugin extends Plugin { * Displays a transcluded .excalidraw image in markdown preview mode */ private addMarkdownPostProcessor() { - interface imgElementAttributes { - file?: TFile; - fname: string; //Excalidraw filename - fwidth: string; //Display width of image - fheight: string; //Display height of image - style: string; //css style to apply to IMG element - } - - /** - * Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings) - * @param parts {imgElementAttributes} - display properties of the image - * @returns {Promise} - the IMG HTML element containing the image - */ - const getIMG = async ( - imgAttributes: imgElementAttributes, - ): Promise => { - let file = imgAttributes.file; - if (!imgAttributes.file) { - const f = this.app.vault.getAbstractFileByPath(imgAttributes.fname); - if (!(f && f instanceof TFile)) { - return null; - } - file = f; - } - - const exportSettings: ExportSettings = { - withBackground: this.settings.exportWithBackground, - withTheme: this.settings.exportWithTheme, - }; - const img = createEl("img"); - let style = `max-width:${imgAttributes.fwidth}px !important; width:100%;`; - if (imgAttributes.fheight) { - style += `height:${imgAttributes.fheight}px;`; - } - img.setAttribute("style", style); - img.addClass(imgAttributes.style); - - const theme = this.settings.previewMatchObsidianTheme - ? isObsidianThemeDark() - ? "dark" - : "light" - : !this.settings.exportWithTheme - ? "light" - : undefined; - if (theme) { - exportSettings.withTheme = true; - } - const loader = new EmbeddedFilesLoader( - this, - theme ? theme === "dark" : undefined, - ); - - if (!this.settings.displaySVGInPreview) { - const width = parseInt(imgAttributes.fwidth); - let scale = 1; - if (width >= 600) { - scale = 2; - } - if (width >= 1200) { - scale = 3; - } - if (width >= 1800) { - scale = 4; - } - if (width >= 2400) { - scale = 5; - } - const png = await createPNG( - file.path, - scale, - exportSettings, - loader, - theme, - null, - null, - [], - this, - ); - //const png = await getPNG(JSON_parse(scene),exportSettings, scale); - if (!png) { - return null; - } - img.src = URL.createObjectURL(png); - return img; - } - const svgSnapshot = ( - await createSVG( - file.path, - true, - exportSettings, - loader, - theme, - null, - null, - [], - this, - ) - ).outerHTML; - let svg: SVGSVGElement = null; - const el = document.createElement("div"); - el.innerHTML = svgSnapshot; - const firstChild = el.firstChild; - if (firstChild instanceof SVGSVGElement) { - svg = firstChild; - } - if (!svg) { - return null; - } - svg = embedFontsInSVG(svg); - svg.removeAttribute("width"); - svg.removeAttribute("height"); - img.setAttribute("src", svgToBase64(svg.outerHTML)); - return img; - }; - - const createImageDiv = async ( - attr: imgElementAttributes, - ): Promise => { - const img = await getIMG(attr); - return createDiv(attr.style, (el) => { - el.append(img); - el.setAttribute("src", attr.file.path); - if (attr.fwidth) { - el.setAttribute("w", attr.fwidth); - } - if (attr.fheight) { - el.setAttribute("h", attr.fheight); - } - el.onClickEvent((ev) => { - if ( - ev.target instanceof Element && - ev.target.tagName.toLowerCase() != "img" - ) { - return; - } - const src = el.getAttribute("src"); - if (src) { - this.openDrawing( - this.app.vault.getAbstractFileByPath(src) as TFile, - ev[CTRL_OR_CMD], - ); - } //.ctrlKey||ev.metaKey); - }); - el.addEventListener(RERENDER_EVENT, async (e) => { - e.stopPropagation(); - el.empty(); - const img = await getIMG({ - fname: el.getAttribute("src"), - fwidth: el.getAttribute("w"), - fheight: el.getAttribute("h"), - style: el.getAttribute("class"), - }); - el.append(img); - }); - }); - }; - - const tmpObsidianWYSIWYG = async ( - el: HTMLElement, - ctx: MarkdownPostProcessorContext, - ) => { - if (!ctx.frontmatter) { - return; - } - if (!ctx.frontmatter.hasOwnProperty("excalidraw-plugin")) { - return; - } - //@ts-ignore - if (ctx.remainingNestLevel < 4) { - return; - } - if (!el.querySelector(".frontmatter")) { - el.style.display = "none"; - return; - } - const attr: imgElementAttributes = { - fname: ctx.sourcePath, - fheight: "", - fwidth: this.settings.width, - style: "excalidraw-svg", - }; - - attr.file = this.app.metadataCache.getFirstLinkpathDest( - ctx.sourcePath, - "", - ); - const div = await createImageDiv(attr); - el.childNodes.forEach( - (child: HTMLElement) => (child.style.display = "none"), - ); - el.appendChild(div); - }; - - /** - * - * @param el - * @param ctx - */ - const markdownPostProcessor = async ( - el: HTMLElement, - ctx: MarkdownPostProcessorContext, - ) => { - //check to see if we are rendering in editing mode of live preview - //if yes, then there should be no .internal-embed containers - const embeddedItems = el.querySelectorAll(".internal-embed"); - if (embeddedItems.length === 0) { - tmpObsidianWYSIWYG(el, ctx); - return; - } - - //If the file being processed is an excalidraw file, - //then I want to hide all embedded items as these will be - //transcluded text element or some other transcluded content inside the Excalidraw file - //in reading mode these elements should be hidden - if (ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")) { - el.style.display = "none"; - return; - } - - //if not, then we are processing a non-excalidraw file in reading mode - //in that cases embedded files will be displayed in an .internal-embed container - const attr: imgElementAttributes = { - fname: "", - fheight: "", - fwidth: "", - style: "", - }; - let alt: string; - let parts; - let file: TFile; - - //Iterating through all the containers to check which one is an excalidraw drawing - //This is a for loop instead of embeddedItems.forEach() because createImageDiv at the end - //is awaited, otherwise excalidraw images would not display in the Kanban plugin - for (const maybeDrawing of embeddedItems) { - //check to see if the file in the src attribute exists - attr.fname = maybeDrawing.getAttribute("src"); - file = this.app.metadataCache.getFirstLinkpathDest( - attr.fname?.split("#")[0], - ctx.sourcePath, - ); - - //if the embeddedFile exits and it is an Excalidraw file - //then lets replace the .internal-embed with the generated PNG or SVG image - if (file && file instanceof TFile && this.isExcalidrawFile(file)) { - attr.fwidth = maybeDrawing.getAttribute("width") - ? maybeDrawing.getAttribute("width") - : this.settings.width; - attr.fheight = maybeDrawing.getAttribute("height"); - alt = maybeDrawing.getAttribute("alt"); - if (alt == attr.fname) { - alt = ""; - } //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text - attr.style = "excalidraw-svg"; - if (alt) { - //for some reason Obsidian renders ![]() in a DIV and ![[]] in a SPAN - //also the alt-text of the DIV does not include the alt-text of the image - //thus need to add an additional "|" character when its a SPAN - if (maybeDrawing.tagName.toLowerCase() == "span") { - alt = `|${alt}`; - } - //1:width, 2:height, 3:style 1 2 3 - parts = alt.match(/[^\|]*\|?(\d*%?)x?(\d*%?)\|?(.*)/); - attr.fwidth = parts[1] ? parts[1] : this.settings.width; - attr.fheight = parts[2]; - if (parts[3] != attr.fname) { - attr.style = `excalidraw-svg${parts[3] ? `-${parts[3]}` : ""}`; - } - } - - attr.fname = file?.path; - attr.file = file; - const div = await createImageDiv(attr); - maybeDrawing.parentElement.replaceChild(div, maybeDrawing); - } - } - }; - + initializeMarkdownPostProcessor(this); this.registerMarkdownPostProcessor(markdownPostProcessor); - /** - * internal-link quick preview - * @param e - * @returns - */ - const hoverEvent = (e: any) => { - if (!e.linktext) { - this.hover.linkText = null; - return; - } - this.hover.linkText = e.linktext; - this.hover.sourcePath = e.sourcePath; - }; + // internal-link quick preview this.registerEvent(this.app.workspace.on("hover-link", hoverEvent)); //monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree - this.observer = new MutationObserver(async (m) => { - if (m.length == 0) { - return; - } - if (!this.hover.linkText) { - return; - } - const file = this.app.metadataCache.getFirstLinkpathDest( - this.hover.linkText, - this.hover.sourcePath ? this.hover.sourcePath : "", - ); - if (!file) { - return; - } - if (!(file instanceof TFile)) { - return; - } - if (file.extension !== "excalidraw") { - return; - } - - const svgFileName = getIMGFilename(file.path, "svg"); - const svgFile = this.app.vault.getAbstractFileByPath(svgFileName); - if (svgFile && svgFile instanceof TFile) { - return; - } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview - - const pngFileName = getIMGFilename(file.path, "png"); - const pngFile = this.app.vault.getAbstractFileByPath(pngFileName); - if (pngFile && pngFile instanceof TFile) { - return; - } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview - - if (!this.hover.linkText) { - return; - } - if (m.length != 1) { - return; - } - if (m[0].addedNodes.length != 1) { - return; - } - if ( - //@ts-ignore - m[0].addedNodes[0].className != - "popover hover-popover file-embed is-loaded" - ) { - return; - } - const node = m[0].addedNodes[0]; - node.empty(); - - //this div will be on top of original DIV. By stopping the propagation of the click - //I prevent the default Obsidian feature of openning the link in the native app - const img = await getIMG({ - file, - fname: file.path, - fwidth: "300", - fheight: null, - style: "excalidraw-svg", - }); - const div = createDiv("", async (el) => { - el.appendChild(img); - el.setAttribute("src", file.path); - el.onClickEvent((ev) => { - ev.stopImmediatePropagation(); - const src = el.getAttribute("src"); - if (src) { - this.openDrawing( - this.app.vault.getAbstractFileByPath(src) as TFile, - ev[CTRL_OR_CMD], - ); - } //.ctrlKey||ev.metaKey); - }); - }); - node.appendChild(div); - }); - + this.observer = observer; this.observer.observe(document, { childList: true, subtree: true }); } diff --git a/src/settings.ts b/src/settings.ts index d779e0e..a76023e 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -45,6 +45,7 @@ export interface ExcalidrawSettings { compatibilityMode: boolean; experimentalFileType: boolean; experimentalFileTag: string; + experimentalLivePreview: boolean; loadCount: number; //version 1.2 migration counter drawingOpenCount: number; library: string; @@ -95,6 +96,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = { syncExcalidraw: false, experimentalFileType: false, experimentalFileTag: "✏️", + experimentalLivePreview: true, compatibilityMode: false, loadCount: 0, drawingOpenCount: 0, @@ -828,5 +830,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(); }), ); + + new Setting(containerEl) + .setName(t("LIVEPREVIEW_NAME")) + .setDesc(t("LIVEPREVIEW_DESC")) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.experimentalLivePreview) + .onChange(async (value) => { + this.plugin.settings.experimentalLivePreview = value; + this.applySettingsUpdate(); + }), + ); + } } diff --git a/styles.css b/styles.css index 309d100..5b4cab7 100644 --- a/styles.css +++ b/styles.css @@ -35,6 +35,10 @@ img.excalidraw-svg-right { float: right; } +.excalidraw-svg-center { + text-align: center; +} + img.excalidraw-svg-left { float: left; } diff --git a/versions.json b/versions.json index 9746cfa..c965c1a 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,4 @@ { - "1.5.14": "0.12.16", + "1.5.15": "0.12.16", "1.4.2": "0.11.13" }