diff --git a/package.json b/package.json index 773ef4c..454c48c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/node": "^15.12.4", "@types/react-dom": "^17.0.9", "cross-env": "^7.0.3", + "html2canvas": "^1.3.2", "js-beautify": "1.13.3", "nanoid": "^3.1.23", "obsidian": "^0.12.16", diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 6b17bc5..8295c6a 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -10,156 +10,154 @@ import { TFile } from "obsidian" import ExcalidrawView, { TextMode } from "./ExcalidrawView"; -import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData"; +import { ExcalidrawData} from "./ExcalidrawData"; import { FRONTMATTER, nanoid, - JSON_parse, VIEW_TYPE_EXCALIDRAW, - MAX_IMAGE_SIZE + MAX_IMAGE_SIZE, } from "./constants"; -import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils"; +import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, tex2dataURL, wrapText } from "./Utils"; import { AppState } from "@zsviczian/excalidraw/types/types"; declare type ConnectionPoint = "top"|"bottom"|"left"|"right"; -export interface ExcalidrawAutomate extends Window { - ExcalidrawAutomate: { - plugin: ExcalidrawPlugin; - elementsDict: {}; - imagesDict: {}; - style: { - strokeColor: string; - backgroundColor: string; - angle: number; - fillStyle: FillStyle; - strokeWidth: number; - storkeStyle: StrokeStyle; - roughness: number; - opacity: number; - strokeSharpness: StrokeSharpness; - fontFamily: number; - fontSize: number; - textAlign: string; - verticalAlign: string; - startArrowHead: string; - endArrowHead: string; - } - canvas: { - theme: string, - viewBackgroundColor: string, - gridSize: number - }; - setFillStyle (val:number): void; - setStrokeStyle (val:number): void; - setStrokeSharpness (val:number): void; - setFontFamily (val:number): void; - setTheme (val:number): void; - addToGroup (objectIds:[]):string; - toClipboard (templatePath?:string): void; - getElements ():ExcalidrawElement[]; - getElement (id:string):ExcalidrawElement; - create ( - params?: { - filename?: string, - foldername?:string, - templatePath?:string, - onNewPane?: boolean, - frontmatterKeys?:{ - "excalidraw-plugin"?: "raw"|"parsed", - "excalidraw-link-prefix"?: string, - "excalidraw-link-brackets"?: boolean, - "excalidraw-url-prefix"?: string - } - } - ):Promise; - createSVG (templatePath?:string, embedFont?:boolean):Promise; - createPNG (templatePath?:string):Promise; - wrapText (text:string, lineLen:number):string; - addRect (topX:number, topY:number, width:number, height:number):string; - addDiamond (topX:number, topY:number, width:number, height:number):string; - addEllipse (topX:number, topY:number, width:number, height:number):string; - addBlob (topX:number, topY:number, width:number, height:number):string; - addText ( - topX:number, - topY:number, - text:string, - formatting?: { - wrapAt?:number, - width?:number, - height?:number, - textAlign?: string, - box?: boolean|"box"|"blob"|"ellipse"|"diamond", - boxPadding?: number - }, - id?:string - ):string; - addLine(points: [[x:number,y:number]]):string; - addArrow ( - points: [[x:number,y:number]], - formatting?: { - startArrowHead?:string, - endArrowHead?:string, - startObjectId?:string, - endObjectId?:string - } - ):string ; - addImage(topX:number, topY:number, imageFile: TFile):Promise; - connectObjects ( - objectA: string, - connectionA: ConnectionPoint, - objectB: string, - connectionB: ConnectionPoint, - formatting?: { - numberOfPoints?: number, - startArrowHead?:string, - endArrowHead?:string, - padding?: number - } - ):void; - clear (): void; - reset (): void; - isExcalidrawFile (f:TFile): boolean; - //view manipulation - targetView: ExcalidrawView; - setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView; - getExcalidrawAPI ():any; - getViewElements ():ExcalidrawElement[]; - deleteViewElements (el: ExcalidrawElement[]):boolean; - getViewSelectedElement ():ExcalidrawElement; - getViewSelectedElements ():ExcalidrawElement[]; - viewToggleFullScreen (forceViewMode?:boolean):void; - connectObjectWithViewSelectedElement ( - objectA:string, - connectionA: ConnectionPoint, - connectionB: ConnectionPoint, - formatting?: { - numberOfPoints?: number, - startArrowHead?:string, - endArrowHead?:string, - padding?: number - } - ):boolean; - addElementsToView (repositionToCursor:boolean, save:boolean):Promise; - onDropHook (data: { - ea: ExcalidrawAutomate, - event: React.DragEvent, - draggable: any, //Obsidian draggable object - type: "file"|"text"|"unknown", - payload: { - files: TFile[], //TFile[] array of dropped files - text: string, //string - }, - excalidrawFile: TFile, //the file receiving the drop event - view: ExcalidrawView, //the excalidraw view receiving the drop - pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop - }):boolean; +export interface ExcalidrawAutomate { + plugin: ExcalidrawPlugin; + elementsDict: {}; + imagesDict: {}; + style: { + strokeColor: string; + backgroundColor: string; + angle: number; + fillStyle: FillStyle; + strokeWidth: number; + storkeStyle: StrokeStyle; + roughness: number; + opacity: number; + strokeSharpness: StrokeSharpness; + fontFamily: number; + fontSize: number; + textAlign: string; + verticalAlign: string; + startArrowHead: string; + endArrowHead: string; + } + canvas: { + theme: string, + viewBackgroundColor: string, + gridSize: number }; + setFillStyle (val:number): void; + setStrokeStyle (val:number): void; + setStrokeSharpness (val:number): void; + setFontFamily (val:number): void; + setTheme (val:number): void; + addToGroup (objectIds:[]):string; + toClipboard (templatePath?:string): void; + getElements ():ExcalidrawElement[]; + getElement (id:string):ExcalidrawElement; + create ( + params?: { + filename?: string, + foldername?:string, + templatePath?:string, + onNewPane?: boolean, + frontmatterKeys?:{ + "excalidraw-plugin"?: "raw"|"parsed", + "excalidraw-link-prefix"?: string, + "excalidraw-link-brackets"?: boolean, + "excalidraw-url-prefix"?: string + } + } + ):Promise; + createSVG (templatePath?:string, embedFont?:boolean):Promise; + createPNG (templatePath?:string):Promise; + wrapText (text:string, lineLen:number):string; + addRect (topX:number, topY:number, width:number, height:number):string; + addDiamond (topX:number, topY:number, width:number, height:number):string; + addEllipse (topX:number, topY:number, width:number, height:number):string; + addBlob (topX:number, topY:number, width:number, height:number):string; + addText ( + topX:number, + topY:number, + text:string, + formatting?: { + wrapAt?:number, + width?:number, + height?:number, + textAlign?: string, + box?: boolean|"box"|"blob"|"ellipse"|"diamond", + boxPadding?: number + }, + id?:string + ):string; + addLine(points: [[x:number,y:number]]):string; + addArrow ( + points: [[x:number,y:number]], + formatting?: { + startArrowHead?:string, + endArrowHead?:string, + startObjectId?:string, + endObjectId?:string + } + ):string ; + addImage(topX:number, topY:number, imageFile: TFile):Promise; + addLaTex(topX:number, topY:number, tex: string, color?:string):Promise; + connectObjects ( + objectA: string, + connectionA: ConnectionPoint, + objectB: string, + connectionB: ConnectionPoint, + formatting?: { + numberOfPoints?: number, + startArrowHead?:string, + endArrowHead?:string, + padding?: number + } + ):void; + clear (): void; + reset (): void; + isExcalidrawFile (f:TFile): boolean; + //view manipulation + targetView: ExcalidrawView; + setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView; + getExcalidrawAPI ():any; + getViewElements ():ExcalidrawElement[]; + deleteViewElements (el: ExcalidrawElement[]):boolean; + getViewSelectedElement ():ExcalidrawElement; + getViewSelectedElements ():ExcalidrawElement[]; + viewToggleFullScreen (forceViewMode?:boolean):void; + connectObjectWithViewSelectedElement ( + objectA:string, + connectionA: ConnectionPoint, + connectionB: ConnectionPoint, + formatting?: { + numberOfPoints?: number, + startArrowHead?:string, + endArrowHead?:string, + padding?: number + } + ):boolean; + addElementsToView (repositionToCursor:boolean, save:boolean):Promise; + onDropHook (data: { + ea: ExcalidrawAutomate, + event: React.DragEvent, + draggable: any, //Obsidian draggable object + type: "file"|"text"|"unknown", + payload: { + files: TFile[], //TFile[] array of dropped files + text: string, //string + }, + excalidrawFile: TFile, //the file receiving the drop event + view: ExcalidrawView, //the excalidraw view receiving the drop + pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop + }):boolean; } -declare let window: ExcalidrawAutomate; +declare let window: any; -export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { +export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise { window.ExcalidrawAutomate = { plugin: plugin, elementsDict: {}, @@ -249,7 +247,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { return id; }, async toClipboard(templatePath?:string) { - const template = templatePath ? (await getTemplate(templatePath)) : null; + const template = templatePath ? (await getTemplate(this.plugin,templatePath)) : null; let elements = template ? template.elements : []; elements = elements.concat(this.getElements()); navigator.clipboard.writeText( @@ -283,7 +281,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { } } ):Promise { - const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null; + const template = params?.templatePath ? (await getTemplate(this.plugin,params.templatePath,true)) : null; let elements = template ? template.elements : []; elements = elements.concat(this.getElements()); let frontmatter:string; @@ -340,7 +338,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { }, async createSVG(templatePath?:string,embedFont:boolean = false):Promise { const automateElements = this.getElements(); - const template = templatePath ? (await getTemplate(templatePath,true)) : null; + const template = templatePath ? (await getTemplate(this.plugin,templatePath,true)) : null; let elements = template ? template.elements : []; elements = elements.concat(automateElements); const svg = await getSVG( @@ -364,7 +362,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { }, async createPNG(templatePath?:string, scale:number=1) { const automateElements = this.getElements(); - const template = templatePath ? (await getTemplate(templatePath,true)) : null; + const template = templatePath ? (await getTemplate(this.plugin,templatePath,true)) : null; let elements = template ? template.elements : []; elements = elements.concat(automateElements); return getPNG( @@ -477,12 +475,13 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding); } } + const ea = window.ExcalidrawAutomate; this.elementsDict[id] = { text: text, - fontSize: window.ExcalidrawAutomate.style.fontSize, - fontFamily: window.ExcalidrawAutomate.style.fontFamily, - textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign, - verticalAlign: window.ExcalidrawAutomate.style.verticalAlign, + fontSize: ea.style.fontSize, + fontFamily: ea.style.fontFamily, + textAlign: formatting?.textAlign ? formatting.textAlign : ea.style.textAlign, + verticalAlign: ea.style.verticalAlign, baseline: baseline, ... boxedElement(id,"text",topX,topY,width,height) }; @@ -529,14 +528,15 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { }, async addImage(topX:number, topY:number, imageFile: TFile):Promise { const id = nanoid(); - const image = await getObsidianImage(this.plugin.app,imageFile); + const image = await getObsidianImage(this.plugin,imageFile); if(!image) return null; this.imagesDict[image.fileId] = { mimeType: image.mimeType, id: image.fileId, dataURL: image.dataURL, created: image.created, - file: imageFile.path + file: imageFile.path, + tex: null } if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) { const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height); @@ -548,6 +548,23 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { this.elementsDict[id].scale = [1,1]; return id; }, + async addLaTex(topX:number, topY:number, tex:string, color:string = "black"):Promise { + const id = nanoid(); + const image = await tex2dataURL(tex, color); + if(!image) return null; + this.imagesDict[image.fileId] = { + mimeType: image.mimeType, + id: image.fileId, + dataURL: image.dataURL, + created: image.created, + file: null, + tex: tex + } + this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height); + this.elementsDict[id].fileId = image.fileId; + this.elementsDict[id].scale = [1,1]; + return id; + }, connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void { if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) { return; @@ -723,6 +740,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { onDropHook:null, }; await initFonts(); + return window.ExcalidrawAutomate; } export function destroyExcalidrawAutomate() { @@ -738,6 +756,7 @@ function normalizeLinePoints(points:[[x:number,y:number]],box:{x:number,y:number } function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number) { + const ea = window.ExcalidrawAutomate; return { id: id, type: eltype, @@ -745,15 +764,15 @@ function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number) y: y, width: w, height: h, - angle: window.ExcalidrawAutomate.style.angle, - strokeColor: window.ExcalidrawAutomate.style.strokeColor, - backgroundColor: window.ExcalidrawAutomate.style.backgroundColor, - fillStyle: window.ExcalidrawAutomate.style.fillStyle, - strokeWidth: window.ExcalidrawAutomate.style.strokeWidth, - storkeStyle: window.ExcalidrawAutomate.style.storkeStyle, - roughness: window.ExcalidrawAutomate.style.roughness, - opacity: window.ExcalidrawAutomate.style.opacity, - strokeSharpness: window.ExcalidrawAutomate.style.strokeSharpness, + angle: ea.style.angle, + strokeColor: ea.style.strokeColor, + backgroundColor: ea.style.backgroundColor, + fillStyle: ea.style.fillStyle, + strokeWidth: ea.style.strokeWidth, + storkeStyle: ea.style.storkeStyle, + roughness: ea.style.roughness, + opacity: ea.style.opacity, + strokeSharpness: ea.style.strokeSharpness, seed: Math.floor(Math.random() * 100000), version: 1, versionNounce: 1, @@ -816,19 +835,19 @@ export function measureText (newText:string, fontSize:number, fontFamily:number) return {w: width, h: height, baseline: baseline }; }; -async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{ +async function getTemplate(plugin: ExcalidrawPlugin, fileWithPath:string, loadFiles:boolean = false):Promise<{ elements: any, appState: any, frontmatter: string, files: any, svgSnapshot: string }> { - const app = window.ExcalidrawAutomate.plugin.app; + const app = plugin.app; const vault = app.vault; const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),''); if(file && file instanceof TFile) { const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n"); - let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin); + let excalidrawData:ExcalidrawData = new ExcalidrawData(plugin); if(file.extension === "excalidraw") { await excalidrawData.loadLegacyData(data,file); @@ -848,7 +867,7 @@ async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promi if(trimLocation == -1) trimLocation = data.search("# Drawing\n"); if(loadFiles) { - await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{ + await loadSceneFiles(plugin,excalidrawData.files, excalidrawData.equations, (fileArray:any)=>{ for(const f of fileArray) { excalidrawData.scene.files[f.id] = f; } diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 20e7f22..8db33ad 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -114,12 +114,14 @@ export class ExcalidrawData { private plugin: ExcalidrawPlugin; public loaded: boolean = false; public files:Map = null; //fileId, path + public equations:Map = null; //fileId, path private compatibilityMode:boolean = false; constructor(plugin: ExcalidrawPlugin) { this.plugin = plugin; this.app = plugin.app; this.files = new Map(); + this.equations = new Map(); } /** @@ -132,6 +134,7 @@ export class ExcalidrawData { this.file = file; this.textElements = new Map(); this.files.clear(); + this.equations.clear(); this.compatibilityMode = false; //I am storing these because if the settings change while a drawing is open parsing will run into errors during save @@ -207,14 +210,21 @@ export class ExcalidrawData { } + data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length); //Load Embedded files const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm; - data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length); res = data.matchAll(REG_FILEID_FILEPATH); while(!(parts = res.next()).done) { this.files.set(parts.value[1] as FileId,parts.value[2]); } + //Load Equations + const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$(.*)(\$\$\s*\n)$/gm; + res = data.matchAll(REG_FILEID_EQUATION); + while(!(parts = res.next()).done) { + this.equations.set(parts.value[1] as FileId,parts.value[2]); + } + //Check to see if there are text elements in the JSON that were missed from the # Text Elements section //e.g. if the entire text elements section was deleted. this.findNewTextElementsInScene(); @@ -238,6 +248,7 @@ export class ExcalidrawData { this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light"; } this.files.clear(); + this.equations.clear(); this.findNewTextElementsInScene(); await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode. return true; @@ -503,13 +514,20 @@ export class ExcalidrawData { for(const key of this.textElements.keys()){ outString += this.textElements.get(key).raw+' ^'+key+'\n\n'; } + + outString += (this.equations.size>0 || this.files.size>0) ? '\n# Embedded files\n' : ''; + if(this.equations.size>0) { + for(const key of this.equations.keys()) { + outString += key +': $$'+this.equations.get(key) + '$$\n'; + } + } if(this.files.size>0) { - outString += '\n# Embedded files\n'; for(const key of this.files.keys()) { outString += key +': [['+this.files.get(key) + ']]\n'; } - outString += '\n'; } + outString += (this.equations.size>0 || this.files.size>0) ? '\n' : ''; + const sceneJSONstring = JSON.stringify(this.scene,null,"\t"); return outString + getMarkdownDrawingSection(sceneJSONstring,this.svgSnapshot); @@ -531,7 +549,7 @@ export class ExcalidrawData { if(!scene.files || scene.files == {}) return false; for(const key of Object.keys(scene.files)) { - if(!this.files.has(key as FileId)) { + if(!(this.files.has(key as FileId) || this.equations.has(key as FileId))) { dirty = true; let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS"); switch(scene.files[key].mimeType) { diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index a47e57f..e36bfcb 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -38,8 +38,6 @@ import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, get import { Prompt } from "./Prompt"; import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard"; -declare let window: ExcalidrawAutomate; - export enum TextMode { parsed, raw @@ -444,7 +442,7 @@ export default class ExcalidrawView extends TextFileView { if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) { this.excalidrawWrapperRef.current.focus(); } - loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files)); + loadSceneFiles(this.plugin,this.excalidrawData.files, this.excalidrawData.equations, (files:any)=>this.addFiles(files)); } else { this.instantiateExcalidraw({ elements: excalidrawData.elements, @@ -640,7 +638,7 @@ export default class ExcalidrawView extends TextFileView { React.useEffect(() => { excalidrawRef.current.readyPromise.then((api) => { this.excalidrawAPI = api; - loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files)); + loadSceneFiles(this.plugin,this.excalidrawData.files,this.excalidrawData.equations, (files:any)=>this.addFiles(files)); }); }, [excalidrawRef]); @@ -719,14 +717,15 @@ export default class ExcalidrawView extends TextFileView { } const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements(); const st: AppState = this.excalidrawAPI.getAppState(); - window.ExcalidrawAutomate.reset(); - window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor; - window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity; - window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily; - window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize; - window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign; - const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text); - this.addElements(window.ExcalidrawAutomate.getElements(),false,true); + const ea = this.plugin.ea; + ea.reset(); + ea.style.strokeColor = st.currentItemStrokeColor; + ea.style.opacity = st.currentItemOpacity; + ea.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily; + ea.style.fontSize = st.currentItemFontSize; + ea.style.textAlign = st.currentItemTextAlign; + const id:string = ea.addText(currentPosition.x, currentPosition.y, text); + this.addElements(ea.getElements(),false,true); } this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise => { @@ -759,7 +758,12 @@ export default class ExcalidrawView extends TextFileView { dataURL: images[k].dataURL, created: images[k].created }); - this.excalidrawData.files.set(images[k].id,images[k].file); + if(images[k].file) { + this.excalidrawData.files.set(images[k].id,images[k].file); + } + if(images[k].tex) { + this.excalidrawData.equations.set(images[k].id,images[k].tex); + } }); this.excalidrawAPI.addFiles(files); } @@ -1070,11 +1074,11 @@ export default class ExcalidrawView extends TextFileView { const draggable = (this.app as any).dragManager.draggable; const onDropHook = (type:"file"|"text"|"unknown", files:TFile[], text:string):boolean => { - if (window.ExcalidrawAutomate.onDropHook) { + if (this.plugin.ea.onDropHook) { try { - return window.ExcalidrawAutomate.onDropHook({ + return this.plugin.ea.onDropHook({ //@ts-ignore - ea: window.ExcalidrawAutomate, //the Excalidraw Automate object + ea: this.plugin.ea, //the Excalidraw Automate object event: event, //React.DragEvent draggable: draggable, //Obsidian draggable object type: type, //"file"|"text" @@ -1106,7 +1110,7 @@ export default class ExcalidrawView extends TextFileView { const f = draggable.file; const topX = currentPosition.x; const topY = currentPosition.y; - const ea = window.ExcalidrawAutomate; + const ea = this.plugin.ea; ea.reset(); ea.setView(this); (async () => { diff --git a/src/InsertImageDialog.ts b/src/InsertImageDialog.ts index 632b2cf..1242c19 100644 --- a/src/InsertImageDialog.ts +++ b/src/InsertImageDialog.ts @@ -7,16 +7,18 @@ import { IMAGE_TYPES } from "./constants"; import { ExcalidrawAutomate } from "./ExcalidrawAutomate"; import ExcalidrawView from "./ExcalidrawView"; import {t} from './lang/helpers' +import ExcalidrawPlugin from "./main"; -declare let window: ExcalidrawAutomate; export class InsertImageDialog extends FuzzySuggestModal { public app: App; + public plugin: ExcalidrawPlugin; private view: ExcalidrawView; - constructor(app: App) { - super(app); - this.app = app; + constructor(plugin: ExcalidrawPlugin) { + super(plugin.app); + this.plugin = plugin; + this.app = plugin.app; this.limit = 20; this.setInstructions([{ command: t("SELECT_FILE"), @@ -37,7 +39,7 @@ export class InsertImageDialog extends FuzzySuggestModal { onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void { - const ea = window.ExcalidrawAutomate; + const ea = this.plugin.ea; ea.reset(); ea.setView(this.view); (async () => { diff --git a/src/Utils.ts b/src/Utils.ts index d39661f..adb8abe 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -2,14 +2,13 @@ import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw"; import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian"; import { Random } from "roughjs/bin/math"; import { BinaryFileData, DataURL, Zoom } from "@zsviczian/excalidraw/types/types"; -import { nanoid } from "nanoid"; -import { CASCADIA_FONT, IMAGE_TYPES, VIRGIL_FONT } from "./constants"; -import {ExcalidrawAutomate} from './ExcalidrawAutomate'; +import { CASCADIA_FONT, fileid, IMAGE_TYPES, VIRGIL_FONT } from "./constants"; import ExcalidrawPlugin from "./main"; import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types"; import { ExportSettings } from "./ExcalidrawView"; import { ExcalidrawSettings } from "./settings"; import { html_beautify } from "js-beautify"; +import html2canvas from "html2canvas"; declare module "obsidian" { interface Workspace { @@ -20,7 +19,7 @@ declare module "obsidian" { } } -declare let window: ExcalidrawAutomate; +declare let window: any; export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream"; @@ -190,7 +189,7 @@ export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLe return plugin.app.workspace.createLeafBySplit(leaf); } -export const getObsidianImage = async (app: App, file: TFile) +export const getObsidianImage = async (plugin: ExcalidrawPlugin, file: TFile) :Promise<{ mimeType: MimeType, fileId: FileId, @@ -198,14 +197,15 @@ export const getObsidianImage = async (app: App, file: TFile) created: number, size: {height: number, width: number}, }> => { - if(!app || !file) return null; - const isExcalidrawFile = window.ExcalidrawAutomate.isExcalidrawFile(file); + if(!plugin || !file) return null; + const app = plugin.app; + const isExcalidrawFile = plugin.ea.isExcalidrawFile(file); if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) { return null; } const ab = await app.vault.readBinary(file); const excalidrawSVG = isExcalidrawFile - ? svgToBase64((await window.ExcalidrawAutomate.createSVG(file.path,true)).outerHTML) as DataURL + ? svgToBase64((await plugin.ea.createSVG(file.path,true)).outerHTML) as DataURL : null; let mimeType:MimeType = "image/svg+xml"; if (!isExcalidrawFile) { @@ -263,7 +263,7 @@ const generateIdFromFile = async (file: ArrayBuffer):Promise => { .join("") as FileId; } catch (error) { console.error(error); - id = nanoid(40) as FileId; + id = fileid() as FileId; } return id; }; @@ -363,14 +363,15 @@ export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => { } -export const loadSceneFiles = async (app:App, filesMap: Map,addFiles:Function) => { - const entries = filesMap.entries(); +export const loadSceneFiles = async (plugin:ExcalidrawPlugin, filesMap: Map, equationsMap: Map, addFiles:Function) => { + const app = plugin.app; + let entries = filesMap.entries(); let entry; let files:BinaryFileData[] = []; while(!(entry = entries.next()).done) { const file = app.vault.getAbstractFileByPath(entry.value[1]); if(file && file instanceof TFile) { - const data = await getObsidianImage(app,file); + const data = await getObsidianImage(plugin,file); files.push({ mimeType : data.mimeType, id: entry.value[0], @@ -382,6 +383,24 @@ export const loadSceneFiles = async (app:App, filesMap: Map,addF } } + entries = equationsMap.entries(); + while(!(entry = entries.next()).done) { + const tex = entry.value[1]; + const data = await tex2dataURL(tex); + if(data) { + files.push({ + mimeType : data.mimeType, + id: entry.value[0], + dataURL: data.dataURL, + created: data.created, + //@ts-ignore + size: data.size, + }); + } + } + + + try { //in try block because by the time files are loaded the user may have closed the view addFiles(files); } catch(e) { @@ -414,4 +433,28 @@ export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => { } } -export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark"); \ No newline at end of file +export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark"); + +export async function tex2dataURL(tex:string, color:string="black"):Promise<{ + mimeType: MimeType, + fileId: FileId, + dataURL: DataURL, + created: number, + size: {height: number, width: number}, +}> { + const div = document.body.createDiv(); + div.style.display = "table"; //this will ensure div fits width of formula exactly + //@ts-ignore + const eq = window.MathJax.tex2chtml("\\sum_{a}^{b}\\frac{x}{2}",{display: true, scale: 4}); //scale to ensure good resolution + eq.style.margin = "3px"; + eq.style.color = color; + div.appendChild(eq); + const canvas = await html2canvas(div, {backgroundColor:null}); //transparent + return { + mimeType: "image/png", + fileId: fileid() as FileId, + dataURL: canvas.toDataURL() as DataURL, + created: Date.now(), + size: {height: canvas.height, width: canvas.width} + } +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 065ac35..a073955 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,8 +1,10 @@ //This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("[","["));} +import { FileId } from "@zsviczian/excalidraw/types/element/types"; import {customAlphabet} from "nanoid"; export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8); +export const fileid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',40); export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg']; export const MAX_IMAGE_SIZE = 500; export const FRONTMATTER_KEY = "excalidraw-plugin"; diff --git a/src/main.ts b/src/main.ts index 23b6576..d3f2d76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,9 +12,9 @@ import { MenuItem, TAbstractFile, Tasks, - MarkdownRenderer, ViewState, Notice, + loadMathJax, } from "obsidian"; import { BLANK_DRAWING, @@ -53,7 +53,8 @@ import { } from "./InsertImageDialog"; import { initExcalidrawAutomate, - destroyExcalidrawAutomate + destroyExcalidrawAutomate, + ExcalidrawAutomate } from "./ExcalidrawAutomate"; import { Prompt } from "./Prompt"; import { around } from "monkey-around"; @@ -83,6 +84,7 @@ export default class ExcalidrawPlugin extends Plugin { private observer: MutationObserver; private fileExplorerObserver: MutationObserver; public opencount:number = 0; + public ea:ExcalidrawAutomate; constructor(app: App, manifest: PluginManifest) { super(app, manifest); @@ -96,8 +98,8 @@ export default class ExcalidrawPlugin extends Plugin { await this.loadSettings(); this.addSettingTab(new ExcalidrawSettingTab(this.app, this)); - await initExcalidrawAutomate(this); - + this.ea = await initExcalidrawAutomate(this); + this.registerView( VIEW_TYPE_EXCALIDRAW, (leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this) @@ -128,6 +130,8 @@ export default class ExcalidrawPlugin extends Plugin { patches.imageElementLaunchNotice(); this.switchToExcalidarwAfterLoad() + + this.app.workspace.onLayoutReady(()=>loadMathJax()); } private switchToExcalidarwAfterLoad() { @@ -415,7 +419,7 @@ export default class ExcalidrawPlugin extends Plugin { private registerCommands() { this.openDialog = new OpenFileDialog(this.app, this); this.insertLinkDialog = new InsertLinkDialog(this.app); - this.insertImageDialog = new InsertImageDialog(this.app); + this.insertImageDialog = new InsertImageDialog(this); this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => { this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey); @@ -688,10 +692,11 @@ export default class ExcalidrawPlugin extends Plugin { const prompt = new Prompt(this.app, t("ENTER_LATEX"),'','$\\theta$'); prompt.openAndGetValue( async (formula:string)=> { if(!formula) return; - const el = createEl('p'); - await MarkdownRenderer.renderMarkdown(formula,el,'',this) - view.addText(el.getText()); - el.empty(); + const ea = this.ea; + ea.reset(); + await ea.addLaTex(0,0,formula); + ea.setView(view); + ea.addElementsToView(true,true); }); return true; } diff --git a/yarn.lock b/yarn.lock index f150013..8cac6c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2275,6 +2275,16 @@ "mixin-deep" "^1.2.0" "pascalcase" "^0.1.1" +"base64-arraybuffer@^0.2.0": + "integrity" "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==" + "resolved" "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz" + "version" "0.2.0" + +"base64-arraybuffer@^1.0.1": + "integrity" "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==" + "resolved" "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz" + "version" "1.0.1" + "base64-js@^1.0.2": "integrity" "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" "resolved" "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" @@ -3278,6 +3288,13 @@ "resolved" "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz" "version" "0.0.4" +"css-line-break@2.0.1": + "integrity" "sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w==" + "resolved" "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "base64-arraybuffer" "^0.2.0" + "css-loader@0.28.7": "integrity" "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==" "resolved" "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz" @@ -5079,6 +5096,14 @@ "pretty-error" "^2.0.2" "toposort" "^1.0.0" +"html2canvas@^1.3.2": + "integrity" "sha512-4+zqv87/a1LsaCrINV69wVLGG8GBZcYBboz1JPWEgiXcWoD9kroLzccsBRU/L9UlfV2MAZ+3J92U9IQPVMDeSQ==" + "resolved" "https://registry.npmjs.org/html2canvas/-/html2canvas-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "css-line-break" "2.0.1" + "text-segmentation" "^1.0.2" + "htmlparser2@^6.1.0": "integrity" "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==" "resolved" "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" @@ -9315,6 +9340,13 @@ "read-pkg-up" "^1.0.1" "require-main-filename" "^1.0.1" +"text-segmentation@^1.0.2": + "integrity" "sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw==" + "resolved" "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "utrie" "^1.0.1" + "text-table@~0.2.0", "text-table@0.2.0": "integrity" "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -9735,6 +9767,13 @@ "resolved" "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" "version" "1.0.1" +"utrie@^1.0.1": + "integrity" "sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg==" + "resolved" "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "base64-arraybuffer" "^1.0.1" + "uuid@^3.0.1", "uuid@^3.3.2": "integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" "resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"