diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..b666370 --- /dev/null +++ b/TODO.md @@ -0,0 +1,14 @@ +[x] do not embed font into SVG when embedding Excalidraw into other Excalidraw +[x] add ```html ... ``` codeblock to excalidraw markdown +[x] read pre-saved `` when generating image preview +[x] update code to adopt change files moving from AppState to App +- Add "files" to legacy excalidraw export + +[x] PNG preview +[x] markdown embed SVG 190 +[x] markdown embed PNG +[x] embed Excalidraw into other Excalidraw + + + + diff --git a/package.json b/package.json index a08b0c6..9b0f21d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-excalidraw-plugin", - "version": "1.1.10", + "version": "1.3.21", "description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings", "main": "main.js", "scripts": { @@ -11,7 +11,7 @@ "author": "", "license": "MIT", "dependencies": { - "@zsviczian/excalidraw": "0.10.0-obsidian-1", + "@zsviczian/excalidraw": "0.10.0-obsidian-2", "monkey-around": "^2.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -27,9 +27,11 @@ "@rollup/plugin-node-resolve": "^13.0.5", "@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-typescript": "^8.2.5", + "@types/js-beautify": "^1.13.3", "@types/node": "^15.12.4", "@types/react-dom": "^17.0.9", "cross-env": "^7.0.3", + "js-beautify": "1.13.3", "nanoid": "^3.1.23", "obsidian": "^0.12.16", "rollup": "^2.52.3", diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 12ba348..6b17bc5 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -9,15 +9,16 @@ import { normalizePath, TFile } from "obsidian" -import ExcalidrawView from "./ExcalidrawView"; -import { getJSON } from "./ExcalidrawData"; +import ExcalidrawView, { TextMode } from "./ExcalidrawView"; +import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData"; import { FRONTMATTER, nanoid, JSON_parse, - VIEW_TYPE_EXCALIDRAW + VIEW_TYPE_EXCALIDRAW, + MAX_IMAGE_SIZE } from "./constants"; -import { wrapText } from "./Utils"; +import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils"; import { AppState } from "@zsviczian/excalidraw/types/types"; declare type ConnectionPoint = "top"|"bottom"|"left"|"right"; @@ -26,6 +27,7 @@ export interface ExcalidrawAutomate extends Window { ExcalidrawAutomate: { plugin: ExcalidrawPlugin; elementsDict: {}; + imagesDict: {}; style: { strokeColor: string; backgroundColor: string; @@ -71,7 +73,7 @@ export interface ExcalidrawAutomate extends Window { } } ):Promise; - createSVG (templatePath?: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; @@ -102,6 +104,7 @@ export interface ExcalidrawAutomate extends Window { endObjectId?:string } ):string ; + addImage(topX:number, topY:number, imageFile: TFile):Promise; connectObjects ( objectA: string, connectionA: ConnectionPoint, @@ -160,6 +163,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { window.ExcalidrawAutomate = { plugin: plugin, elementsDict: {}, + imagesDict: {}, style: { strokeColor: "#000000", backgroundColor: "transparent", @@ -279,7 +283,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { } } ):Promise { - const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null; + const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null; let elements = template ? template.elements : []; elements = elements.concat(this.getElements()); let frontmatter:string; @@ -297,73 +301,83 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { } else { frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER; } + + const scene = { + type: "excalidraw", + version: 2, + source: "https://excalidraw.com", + elements: elements, + appState: { + theme: template?.appState?.theme ?? this.canvas.theme, + viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor, + currentItemStrokeColor: template?.appState?.currentItemStrokeColor ?? this.style.strokeColor, + currentItemBackgroundColor: template?.appState?.currentItemBackgroundColor ?? this.style.backgroundColor, + currentItemFillStyle: template?.appState?.currentItemFillStyle ?? this.style.fillStyle, + currentItemStrokeWidth: template?.appState?.currentItemStrokeWidth ?? this.style.strokeWidth, + currentItemStrokeStyle: template?.appState?.currentItemStrokeStyle ?? this.style.strokeStyle, + currentItemRoughness: template?.appState?.currentItemRoughness ?? this.style.roughness, + currentItemOpacity: template?.appState?.currentItemOpacity ?? this.style.opacity, + currentItemFontFamily: template?.appState?.currentItemFontFamily ?? this.style.fontFamily, + currentItemFontSize: template?.appState?.currentItemFontSize ?? this.style.fontSize, + currentItemTextAlign: template?.appState?.currentItemTextAlign ?? this.style.textAlign, + currentItemStrokeSharpness: template?.appState?.currentItemStrokeSharpness ?? this.style.strokeSharpness, + currentItemStartArrowhead: template?.appState?.currentItemStartArrowhead ?? this.style.startArrowHead, + currentItemEndArrowhead: template?.appState?.currentItemEndArrowhead ?? this.style.endArrowHead, + currentItemLinearStrokeSharpness: template?.appState?.currentItemLinearStrokeSharpness ?? this.style.strokeSharpness, + gridSize: template?.appState?.gridSize ?? this.canvas.gridSize, + }, + files: template?.files ?? {}, + }; + return plugin.createDrawing( params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(), params?.onNewPane ? params.onNewPane : false, params?.foldername ? params.foldername : this.plugin.settings.folder, - frontmatter + plugin.exportSceneToMD( - JSON.stringify({ + this.plugin.settings.compatibilityMode + ? JSON.stringify(scene,null,"\t") + : frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t")) + ); + }, + async createSVG(templatePath?:string,embedFont:boolean = false):Promise { + const automateElements = this.getElements(); + const template = templatePath ? (await getTemplate(templatePath,true)) : null; + let elements = template ? template.elements : []; + elements = elements.concat(automateElements); + const svg = await getSVG( + {//createDrawing type: "excalidraw", version: 2, source: "https://excalidraw.com", elements: elements, appState: { - theme: template ? template.appState.theme : this.canvas.theme, - viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor, - currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor, - currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor, - currentItemFillStyle: template? template.appState.currentItemFillStyle : this.style.fillStyle, - currentItemStrokeWidth: template? template.appState.currentItemStrokeWidth : this.style.strokeWidth, - currentItemStrokeStyle: template? template.appState.currentItemStrokeStyle : this.style.strokeStyle, - currentItemRoughness: template? template.appState.currentItemRoughness : this.style.roughness, - currentItemOpacity: template? template.appState.currentItemOpacity : this.style.opacity, - currentItemFontFamily: template? template.appState.currentItemFontFamily : this.style.fontFamily, - currentItemFontSize: template? template.appState.currentItemFontSize : this.style.fontSize, - currentItemTextAlign: template? template.appState.currentItemTextAlign : this.style.textAlign, - currentItemStrokeSharpness: template? template.appState.currentItemStrokeSharpness : this.style.strokeSharpness, - currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead, - currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead, - currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness, - gridSize: template ? template.appState.gridSize : this.canvas.gridSize - } - },null,"\t")) - ); - }, - async createSVG(templatePath?:string):Promise { - const template = templatePath ? (await getTemplate(templatePath)) : null; - let elements = template ? template.elements : []; - elements = elements.concat(this.getElements()); - return await ExcalidrawView.getSVG( - {//createDrawing - "type": "excalidraw", - "version": 2, - "source": "https://excalidraw.com", - "elements": elements, - "appState": { - "theme": template ? template.appState.theme : this.canvas.theme, - "viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor - } - },//), + theme: template?.appState?.theme ?? this.canvas.theme, + viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor, + }, + files: template?.files ?? {} + }, { withBackground: plugin.settings.exportWithBackground, withTheme: plugin.settings.exportWithTheme } - ) + ) + return embedFont ? embedFontsInSVG(svg) : svg; }, async createPNG(templatePath?:string, scale:number=1) { - const template = templatePath ? (await getTemplate(templatePath)) : null; + const automateElements = this.getElements(); + const template = templatePath ? (await getTemplate(templatePath,true)) : null; let elements = template ? template.elements : []; - elements = elements.concat(this.getElements()); - return ExcalidrawView.getPNG( + elements = elements.concat(automateElements); + return getPNG( { - "type": "excalidraw", - "version": 2, - "source": "https://excalidraw.com", - "elements": elements, - "appState": { - "theme": template ? template.appState.theme : this.canvas.theme, - "viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor - } + type: "excalidraw", + version: 2, + source: "https://excalidraw.com", + elements: elements, + appState: { + theme: template?.appState?.theme ?? this.canvas.theme, + viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor, + }, + files: template?.files ?? {} }, { withBackground: plugin.settings.exportWithBackground, @@ -513,6 +527,27 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { } return id; }, + async addImage(topX:number, topY:number, imageFile: TFile):Promise { + const id = nanoid(); + const image = await getObsidianImage(this.plugin.app,imageFile); + if(!image) return null; + this.imagesDict[image.fileId] = { + mimeType: image.mimeType, + id: image.fileId, + dataURL: image.dataURL, + created: image.created, + file: imageFile.path + } + 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); + image.size.width = scale*image.size.width; + image.size.height = scale*image.size.height; + } + 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; @@ -552,6 +587,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { }, clear() { this.elementsDict = {}; + this.imagesDict = {}; }, reset() { this.clear(); @@ -597,7 +633,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { errorMessage("targetView not set", "getExcalidrawAPI()"); return null; } - return (this.targetView as ExcalidrawView).excalidrawRef.current; + return (this.targetView as ExcalidrawView).excalidrawAPI; }, getViewElements ():ExcalidrawElement[] { if (!this.targetView || !this.targetView?._loaded) { @@ -682,7 +718,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { return false; } const elements = this.getElements(); - return await this.targetView.addElements(elements,repositionToCursor,save); + return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict); }, onDropHook:null, }; @@ -780,27 +816,61 @@ export function measureText (newText:string, fontSize:number, fontFamily:number) return {w: width, h: height, baseline: baseline }; }; -async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any, frontmatter: string}> { +async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{ + elements: any, + appState: any, + frontmatter: string, + files: any, + svgSnapshot: string +}> { const app = window.ExcalidrawAutomate.plugin.app; const vault = app.vault; const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),''); if(file && file instanceof TFile) { - const data = await vault.read(file); + const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n"); + let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin); + + if(file.extension === "excalidraw") { + await excalidrawData.loadLegacyData(data,file); + return { + elements: excalidrawData.scene.elements, + appState: excalidrawData.scene.appState, + frontmatter: "", + files: excalidrawData.scene.files, + svgSnapshot: null, + }; + } + + const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility + await excalidrawData.loadData(data,file,parsed ? TextMode.parsed : TextMode.raw) let trimLocation = data.search("# Text Elements\n"); if(trimLocation == -1) trimLocation = data.search("# Drawing\n"); - const excalidrawData = JSON_parse(getJSON(data)[0]); + if(loadFiles) { + await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{ + for(const f of fileArray) { + excalidrawData.scene.files[f.id] = f; + } + let foo; + [foo,excalidrawData] = scaleLoadedImage(excalidrawData,fileArray); + }); + } + return { - elements: excalidrawData.elements, - appState: excalidrawData.appState, - frontmatter: data.substring(0,trimLocation) + elements: excalidrawData.scene.elements, + appState: excalidrawData.scene.appState, + frontmatter: data.substring(0,trimLocation), + files: excalidrawData.scene.files, + svgSnapshot: excalidrawData.svgSnapshot }; }; return { elements: [], appState: {}, - frontmatter: null + frontmatter: null, + files: [], + svgSnapshot: null, } } diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 95d0b06..42cf1cf 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -11,8 +11,11 @@ import { JSON_parse } from "./constants"; import { TextMode } from "./ExcalidrawView"; -import { wrapText } from "./Utils"; +import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, wrapText } from "./Utils"; +import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types"; +import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types"; +type SceneDataWithFiles = SceneData & { files: BinaryFiles}; declare module "obsidian" { interface MetadataCache { @@ -67,10 +70,24 @@ export function getJSON(data:string):[string,number] { const result = parts.value[2]; return [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock } - return [data,parts.value.index]; + return [data,parts.value ? parts.value.index : 0]; +} + +//extracts SVG snapshot from Excalidraw Markdown string +const SVG_REG = /.*?```html\n([\s\S]*?)```/gm; +export function getSVGString(data:string):string { + let res = data.matchAll(SVG_REG); + + let parts; + parts = res.next(); + if(parts.value && parts.value.length>1) { + return parts.value[1]; + } + return null; } export class ExcalidrawData { + public svgSnapshot: string = null; private textElements:Map = null; public scene:any = null; private file:TFile = null; @@ -81,10 +98,13 @@ export class ExcalidrawData { private textMode: TextMode = TextMode.raw; private plugin: ExcalidrawPlugin; public loaded: boolean = false; + public files:Map = null; //fileId, path + private compatibilityMode:boolean = false; constructor(plugin: ExcalidrawPlugin) { this.plugin = plugin; this.app = plugin.app; + this.files = new Map(); } /** @@ -96,6 +116,8 @@ export class ExcalidrawData { this.loaded = false; this.file = file; this.textElements = new Map(); + this.files.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 //The drawing will use these values until next drawing is loaded or this drawing is re-loaded @@ -126,6 +148,13 @@ export class ExcalidrawData { if (!this.scene) { this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ``` } + + if(!this.scene.files) { + this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute. + } + + this.svgSnapshot = getSVGString(data.substr(pos+scene.length)); + data = data.substring(0,pos); //The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes @@ -145,7 +174,7 @@ export class ExcalidrawData { //iterating through all the text elements in .md //Text elements always contain the raw value const BLOCKREF_LEN:number = " ^12345678\n\n".length; - const res = data.matchAll(/\s\^(.{8})[\n]+/g); + let res = data.matchAll(/\s\^(.{8})[\n]+/g); let parts; while(!(parts = res.next()).done) { const text = data.substring(position,parts.value.index); @@ -158,6 +187,15 @@ export class ExcalidrawData { position = parts.value.index + BLOCKREF_LEN; } + + //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]); + } + //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(); @@ -167,12 +205,17 @@ export class ExcalidrawData { } public async loadLegacyData(data: string,file: TFile):Promise { + this.compatibilityMode = true; this.file = file; this.textElements = new Map(); this.setShowLinkBrackets(); this.setLinkPrefix(); this.setUrlPrefix(); this.scene = JSON.parse(data); + if(!this.scene.files) { + this.scene.files = {}; //loading legacy scenes without the files element + } + this.files.clear(); this.findNewTextElementsInScene(); await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode. return true; @@ -438,13 +481,58 @@ export class ExcalidrawData { for(const key of this.textElements.keys()){ outString += this.textElements.get(key).raw+' ^'+key+'\n\n'; } - return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t")); + 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'; + } + return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot); + } + + private async syncFiles(scene:SceneDataWithFiles):Promise { + let dirty = false; + + //remove files that no longer have a corresponding image element + const fileIds = (scene.elements.filter((e)=>e.type==="image") as ExcalidrawImageElement[]).map((e)=>e.fileId); + this.files.forEach((value,key)=>{ + if(!fileIds.contains(key)) { + this.files.delete(key); + dirty = true; + } + }); + + //check if there are any images that need to be processed in the new scene + if(!scene.files || scene.files == {}) return false; + + for(const key of Object.keys(scene.files)) { + if(!this.files.has(key as FileId)) { + dirty = true; + let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS"); + switch(scene.files[key].mimeType) { + case "image/png": fname += ".png"; break; + case "image/jpeg": fname += ".jpg"; break; + case "image/svg+xml": fname += ".svg"; break; + case "image/gif": fname += ".gif"; break; + default: fname += ".png"; + } + const [folder,filepath] = await getAttachmentsFolderAndFilePath(this.app,this.file.path,fname); + await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(scene.files[key].dataURL)); + this.files.set(key as FileId,filepath); + } + } + return dirty; } public async syncElements(newScene:any):Promise { - //console.log("Excalidraw.Data.syncElements()"); - this.scene = newScene;//JSON_parse(newScene); - const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets(); + this.scene = newScene; + let result = false; + if(!this.compatibilityMode) { + result = await this.syncFiles(newScene); + this.scene.files = {}; + } + result = result || this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets(); await this.updateTextElementsFromScene(); return result || this.findNewTextElementsInScene(); } @@ -526,6 +614,4 @@ export class ExcalidrawData { return showLinkBrackets != this.showLinkBrackets; } -} - - +} \ No newline at end of file diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 2209d54..ce1ce75 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -10,9 +10,10 @@ import { import * as React from "react"; import * as ReactDOM from "react-dom"; import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw"; -import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types"; +import { ExcalidrawElement,ExcalidrawImageElement,ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types"; import { AppState, + BinaryFileData, LibraryItems } from "@zsviczian/excalidraw/types/types"; import { @@ -28,15 +29,17 @@ import { TEXT_DISPLAY_RAW_ICON_NAME, TEXT_DISPLAY_PARSED_ICON_NAME, FULLSCREEN_ICON_NAME, - JSON_parse + JSON_parse, + IMAGE_TYPES } from './constants'; import ExcalidrawPlugin from './main'; -import {estimateBounds, ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate'; +import {ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate'; import { t } from "./lang/helpers"; import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData"; -import { checkAndCreateFolder, download, getNewOrAdjacentLeaf, getNewUniqueFilepath, rotatedDimensions, splitFolderAndFilename, viewportCoordsToSceneCoords } from "./Utils"; +import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, loadSceneFiles, rotatedDimensions, scaleLoadedImage, splitFolderAndFilename, svgToBase64, viewportCoordsToSceneCoords } from "./Utils"; import { Prompt } from "./Prompt"; import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard"; +import { ifStatement } from "@babel/types"; declare let window: ExcalidrawAutomate; @@ -61,9 +64,11 @@ export default class ExcalidrawView extends TextFileView { private getScene: Function = null; public addElements: Function = null; //add elements to the active Excalidraw drawing private getSelectedTextElement: Function = null; + private getSelectedImageElement: Function = null; public addText:Function = null; private refresh: Function = null; public excalidrawRef: React.MutableRefObject = null; + public excalidrawAPI: any = null; private excalidrawWrapperRef: React.MutableRefObject = null; private justLoaded: boolean = false; private plugin: ExcalidrawPlugin; @@ -111,26 +116,15 @@ export default class ExcalidrawView extends TextFileView { withBackground: this.plugin.settings.exportWithBackground, withTheme: this.plugin.settings.exportWithTheme } - const svg = await ExcalidrawView.getSVG(scene,exportSettings); + const svg = await getSVG(scene,exportSettings); if(!svg) return; let serializer =new XMLSerializer(); - const svgString = serializer.serializeToString(ExcalidrawView.embedFontsInSVG(svg)); + const svgString = serializer.serializeToString(embedFontsInSVG(svg)); if(file && file instanceof TFile) await this.app.vault.modify(file,svgString); else await this.app.vault.create(filepath,svgString); })(); } - public static embedFontsInSVG(svg:SVGSVGElement):SVGSVGElement { - //replace font references with base64 fonts - const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null; - const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null; - const defs = svg.querySelector("defs"); - if (defs && (includesCascadia || includesVirgil)) { - defs.innerHTML = ""; - } - return svg; - } - public savePNG(scene?: any) { if(!scene) { if (!this.getScene) return false; @@ -145,7 +139,7 @@ export default class ExcalidrawView extends TextFileView { withBackground: this.plugin.settings.exportWithBackground, withTheme: this.plugin.settings.exportWithTheme } - const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale); + const png = await getPNG(scene,exportSettings,this.plugin.settings.pngExportScale); if(!png) return; if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer()); else await this.app.vault.createBinary(filepath,await png.arrayBuffer()); @@ -156,13 +150,16 @@ export default class ExcalidrawView extends TextFileView { if(!this.getScene) return; this.preventReload = preventReload; this.dirty = null; - + const scene = this.getScene(); + if(this.compatibilityMode) { - await this.excalidrawData.syncElements(this.getScene()); + await this.excalidrawData.syncElements(scene); } else { - if(await this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) { + if(await this.excalidrawData.syncElements(scene) && !this.autosaving) { await this.loadDrawing(false); } + //generate SVG preview snapshot + this.excalidrawData.svgSnapshot = await generateSVGString(this.getScene(),this.plugin.settings); } await super.save(); } @@ -174,12 +171,12 @@ export default class ExcalidrawView extends TextFileView { //console.log("ExcalidrawView.getViewData()"); if(!this.getScene) return this.data; if(!this.excalidrawData.loaded) return this.data; + const scene = this.getScene(); if(!this.compatibilityMode) { let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m); if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/); if(trimLocation == -1) return this.data; - const scene = this.excalidrawData.scene; if(!this.autosaving) { if(this.plugin.settings.autoexportSVG) this.saveSVG(scene); if(this.plugin.settings.autoexportPNG) this.savePNG(scene); @@ -191,7 +188,6 @@ export default class ExcalidrawView extends TextFileView { return header + this.excalidrawData.generateMD(); } if(this.compatibilityMode) { - const scene = this.excalidrawData.scene; if(!this.autosaving) { if(this.plugin.settings.autoexportSVG) this.saveSVG(scene); if(this.plugin.settings.autoexportPNG) this.savePNG(scene); @@ -202,61 +198,79 @@ export default class ExcalidrawView extends TextFileView { } async handleLinkClick(view: ExcalidrawView, ev:MouseEvent) { - let text:string = (this.textMode == TextMode.parsed) - ? this.excalidrawData.getRawText(this.getSelectedTextElement().id) - : this.getSelectedTextElement().text; - if(!text) { + const selectedText = this.getSelectedTextElement(); + let file = null; + let lineNum = 0; + let linkText:string = null; + + if(selectedText?.id) { + linkText = (this.textMode == TextMode.parsed) + ? this.excalidrawData.getRawText(selectedText.id) + : selectedText.text; + + linkText = linkText.replaceAll("\n",""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187 + if(linkText.match(REG_LINKINDEX_HYPERLINK)) { + window.open(linkText,"_blank"); + return; + } + + const parts = REGEX_LINK.getRes(linkText).next(); + if(!parts.value) { + const tags = linkText.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next(); + if(!tags.value || tags.value.length<2) { + new Notice(t("TEXT_ELEMENT_EMPTY"),4000); + return; + } + const search=this.app.workspace.getLeavesOfType("search"); + if(search.length==0) return; + //@ts-ignore + search[0].view.setQuery("tag:"+tags.value[1]); + this.app.workspace.revealLeaf(search[0]); + + if(document.fullscreenElement === this.contentEl) { + document.exitFullscreen(); + this.zoomToFit(); + } + return; + } + + linkText = REGEX_LINK.getLink(parts); + + if(linkText.match(REG_LINKINDEX_HYPERLINK)) { + window.open(linkText,"_blank"); + return; + } + + if(linkText.search("#")>-1) { + let t; + [t,lineNum] = await this.excalidrawData.getTransclusion(linkText); + linkText = linkText.substring(0,linkText.search("#")); + } + if(linkText.match(REG_LINKINDEX_INVALIDCHARS)) { + new Notice(t("FILENAME_INVALID_CHARS"),4000); + return; + } + file = view.app.metadataCache.getFirstLinkpathDest(linkText,view.file.path); + if (!ev.altKey && !file) { + new Notice(t("FILE_DOES_NOT_EXIST"), 4000); + return; + } + } else { + const selectedImage = this.getSelectedImageElement(); + if(selectedImage?.id) { + await this.save(true); //in case pasted images haven't been saved yet + if(this.excalidrawData.files.has(selectedImage.fileId)) { + linkText = this.excalidrawData.files.get(selectedImage.fileId); + } + } + } + + if(!linkText) { new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000); return; } - text = text.replaceAll("\n",""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187 - if(text.match(REG_LINKINDEX_HYPERLINK)) { - window.open(text,"_blank"); - return; - } - const parts = REGEX_LINK.getRes(text).next(); - if(!parts.value) { - const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next(); - if(!tags.value || tags.value.length<2) { - new Notice(t("TEXT_ELEMENT_EMPTY"),4000); - return; - } - const search=this.app.workspace.getLeavesOfType("search"); - if(search.length==0) return; - //@ts-ignore - search[0].view.setQuery("tag:"+tags.value[1]); - this.app.workspace.revealLeaf(search[0]); - if(document.fullscreenElement === this.contentEl) { - document.exitFullscreen(); - this.zoomToFit(); - } - return; - } - text = REGEX_LINK.getLink(parts); - - if(text.match(REG_LINKINDEX_HYPERLINK)) { - window.open(text,"_blank"); - return; - } - - let lineNum = null; - if(text.search("#")>-1) { - let t; - [t,lineNum] = await this.excalidrawData.getTransclusion(text); - text = text.substring(0,text.search("#")); - } - if(text.match(REG_LINKINDEX_INVALIDCHARS)) { - new Notice(t("FILENAME_INVALID_CHARS"),4000); - return; - } - const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path); - if (!ev.altKey && !file) { - new Notice(t("FILE_DOES_NOT_EXIST"), 4000); - return; - } - try { const f = view.file; if(ev.shiftKey && document.fullscreenElement === this.contentEl) { @@ -265,7 +279,11 @@ export default class ExcalidrawView extends TextFileView { } const leaf = ev.shiftKey ? getNewOrAdjacentLeaf(this.plugin,view.leaf) : view.leaf; view.app.workspace.setActiveLeaf(leaf); - leaf.view.app.workspace.openLinkText(text,view.file.path); + if(file) { + leaf.openFile(file,{eState: {line: lineNum-1}}); //if file exists open file and jump to reference + } else { + leaf.view.app.workspace.openLinkText(linkText,view.file.path); + } } catch (e) { new Notice(e,4000); } @@ -321,7 +339,7 @@ export default class ExcalidrawView extends TextFileView { } if(reload) { await this.save(false); - this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text + this.excalidrawAPI.history.clear(); //to avoid undo replacing links with parsed text } } @@ -351,6 +369,10 @@ export default class ExcalidrawView extends TextFileView { this.preventReload = false; return; } + if(this.compatibilityMode) { + this.dirty = null; + return; + } if(!this.excalidrawRef) return; if(!this.file) return; if(file) this.data = await this.app.vault.cachedRead(file); @@ -363,8 +385,8 @@ export default class ExcalidrawView extends TextFileView { // clear the view content clear() { if(!this.excalidrawRef) return; - this.excalidrawRef.current.resetScene(); - this.excalidrawRef.current.history.clear(); + this.excalidrawAPI.resetScene(); + this.excalidrawAPI.history.clear(); } async setViewData (data: string, clear: boolean = false) { @@ -372,7 +394,7 @@ export default class ExcalidrawView extends TextFileView { data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n"); this.app.workspace.onLayoutReady(async ()=>{ this.dirty = null; - this.compatibilityMode = this.file.extension == "excalidraw"; + this.compatibilityMode = this.file.extension === "excalidraw"; await this.plugin.loadSettings(); this.plugin.opencount++; if(this.compatibilityMode) { @@ -410,27 +432,46 @@ export default class ExcalidrawView extends TextFileView { const excalidrawData = this.excalidrawData.scene; this.justLoaded = justloaded; if(this.excalidrawRef) { - const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled; - const zenModeEnabled = this.excalidrawRef.current.getAppState().zenModeEnabled; - this.excalidrawRef.current.updateScene({ + const viewModeEnabled = this.excalidrawAPI.getAppState().viewModeEnabled; + const zenModeEnabled = this.excalidrawAPI.getAppState().zenModeEnabled; + this.excalidrawAPI.updateScene({ elements: excalidrawData.elements, appState: { zenModeEnabled: zenModeEnabled, viewModeEnabled: viewModeEnabled, ... excalidrawData.appState, }, + files: excalidrawData.files, commitToHistory: true, }); if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) { this.excalidrawWrapperRef.current.focus(); } + loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files)); } else { this.instantiateExcalidraw({ elements: excalidrawData.elements, appState: excalidrawData.appState, + files: excalidrawData.files, libraryItems: await this.getLibrary(), }); + //files are loaded on excalidrawRef readyPromise + } + } + + private addFiles(files:any) { + if(files.length === 0) return; + const [dirty, scene] = scaleLoadedImage(this.getScene(),files); + + if(dirty) { + this.excalidrawAPI.updateScene({ + elements: scene.elements, + appState: scene.appState, + commitToHistory: false, + }); } + + this.excalidrawAPI.addFiles(files); } //Compatibility mode with .excalidraw files @@ -455,6 +496,12 @@ export default class ExcalidrawView extends TextFileView { } setMarkdownView() { + if(this.excalidrawRef) { + const el = this.excalidrawAPI.getSceneElements(); + if(el.filter((e:any)=>e.type==="image").length>0) { + new Notice(t("DRAWING_CONTAINS_IMAGE"),6000); + } + } this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown"; this.plugin.setMarkdownView(this.leaf); } @@ -517,7 +564,7 @@ export default class ExcalidrawView extends TextFileView { withBackground: this.plugin.settings.exportWithBackground, withTheme: this.plugin.settings.exportWithTheme } - const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale); + const png = await getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale); if(!png) return; let reader = new FileReader(); reader.readAsDataURL(png); @@ -542,10 +589,10 @@ export default class ExcalidrawView extends TextFileView { withBackground: this.plugin.settings.exportWithBackground, withTheme: this.plugin.settings.exportWithTheme } - let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings); + let svg = await getSVG(this.getScene(),exportSettings); if(!svg) return null; - svg = ExcalidrawView.embedFontsInSVG(svg); - download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg'); + svg = embedFontsInSVG(svg); + download(null,svgToBase64(svg.outerHTML),this.file.basename+'.svg'); return; } this.saveSVG() @@ -567,13 +614,45 @@ export default class ExcalidrawView extends TextFileView { const reactElement = React.createElement(() => { let previousSceneVersion = 0; let currentPosition = {x:0, y:0}; - const excalidrawRef = React.useRef(null); const excalidrawWrapperRef = React.useRef(null); const [dimensions, setDimensions] = React.useState({ width: undefined, height: undefined }); + //excalidrawRef readypromise based on + //https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760 + const resolvablePromise = () => { + let resolve; + let reject; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + //@ts-ignore + promise.resolve = resolve; + //@ts-ignore + promise.reject = reject; + return promise; + }; + + // To memoize value between rerenders + const excalidrawRef = React.useMemo( + () => ({ + current: { + readyPromise: resolvablePromise() + } + }), + [] + ); + + React.useEffect(() => { + excalidrawRef.current.readyPromise.then((api) => { + this.excalidrawAPI = api; + loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files)); + }); + }, [excalidrawRef]); + this.excalidrawRef = excalidrawRef; this.excalidrawWrapperRef = excalidrawWrapperRef; @@ -595,24 +674,23 @@ export default class ExcalidrawView extends TextFileView { return () => window.removeEventListener("resize", onResize); }, [excalidrawWrapperRef]); - this.getSelectedTextElement = ():{id: string, text:string} => { if(!excalidrawRef?.current) return {id:null,text:null}; - if(this.excalidrawRef.current.getAppState().viewModeEnabled) { + if(this.excalidrawAPI.getAppState().viewModeEnabled) { if(selectedTextElement) { const retval = selectedTextElement; - selectedTextElement == null; + selectedTextElement = null; return retval; } return {id:null,text:null}; } - const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]); + const selectedElement = this.excalidrawAPI.getSceneElements().filter((el:any)=>el.id==Object.keys(this.excalidrawAPI.getAppState().selectedElementIds)[0]); if(selectedElement.length==0) return {id:null,text:null}; if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Return text if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group? const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of - const textElement = excalidrawRef - .current + const textElement = this + .excalidrawAPI .getSceneElements() .filter((el:any)=>el.groupIds?.includes(group)) .filter((el:any)=>el.type=="text"); //filter for text elements of the group @@ -620,12 +698,36 @@ export default class ExcalidrawView extends TextFileView { return {id:selectedElement[0].id, text:selectedElement[0].text}; //return text element text }; + this.getSelectedImageElement = ():{id: string, fileId:string} => { + if(!excalidrawRef?.current) return {id:null,fileId:null}; + if(this.excalidrawAPI.getAppState().viewModeEnabled) { + if(selectedImageElement) { + const retval = selectedImageElement; + selectedImageElement = null; + return retval; + } + return {id:null,fileId:null}; + } + const selectedElement = this.excalidrawAPI.getSceneElements().filter((el:any)=>el.id==Object.keys(this.excalidrawAPI.getAppState().selectedElementIds)[0]); + if(selectedElement.length===0) return {id:null,fileId:null}; + if(selectedElement[0].type == "image") return {id:selectedElement[0].id, fileId:selectedElement[0].fileId}; //an image element was selected. Return fileId + if(selectedElement[0].groupIds.length === 0) return {id:null,fileId:null}; //is the selected element part of a group? + const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of + const imageElement = this + .excalidrawAPI + .getSceneElements() + .filter((el:any)=>el.groupIds?.includes(group)) + .filter((el:any)=>el.type=="image"); //filter for Image elements of the group + if(imageElement.length===0) return {id:null,fileId:null}; //the group had no image element member + return {id:selectedElement[0].id, fileId:selectedElement[0].fileId}; //return image element fileId + }; + this.addText = (text:string, fontFamily?:1|2|3) => { if(!excalidrawRef?.current) { return; } - const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements(); - const st: AppState = excalidrawRef.current.getAppState(); + 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; @@ -635,8 +737,8 @@ export default class ExcalidrawView extends TextFileView { const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text); this.addElements(window.ExcalidrawAutomate.getElements(),false,true); } - - this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false):Promise => { + + this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise => { if(!excalidrawRef?.current) return false; const textElements = newElements.filter((el)=>el.type=="text"); @@ -648,14 +750,28 @@ export default class ExcalidrawView extends TextFileView { } }; - const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements(); - const st: AppState = excalidrawRef.current.getAppState(); + const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements(); + let st: AppState = this.excalidrawAPI.getAppState(); + if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true); - this.excalidrawRef.current.updateScene({ + this.excalidrawAPI.updateScene({ elements: el.concat(newElements), appState: st, commitToHistory: true, }); + if(images) { + let files:BinaryFileData[] = []; + Object.keys(images).forEach((k)=>{ + files.push({ + mimeType :images[k].mimeType, + id: images[k].id, + dataURL: images[k].dataURL, + created: images[k].created + }); + this.excalidrawData.files.set(images[k].id,images[k].file); + }); + this.excalidrawAPI.addFiles(files); + } if(save) this.save(); else this.dirty = this.file?.path; return true; }; @@ -664,8 +780,16 @@ export default class ExcalidrawView extends TextFileView { if(!excalidrawRef?.current) { return null; } - const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements(); - const st: AppState = excalidrawRef.current.getAppState(); + const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements(); + const st: AppState = this.excalidrawAPI.getAppState(); + const files = this.excalidrawAPI.getFiles(); + + if(files) { + const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.fileId); + const toDelete = Object.keys(files).filter((k)=>!imgIds.contains(k)); + toDelete.forEach((k)=>delete files[k]); + } + return { type: "excalidraw", version: 2, @@ -689,29 +813,31 @@ export default class ExcalidrawView extends TextFileView { currentItemEndArrowhead: st.currentItemEndArrowhead, currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness, gridSize: st.gridSize, - } + }, + files: files, }; }; this.refresh = () => { if(!excalidrawRef?.current) return; - excalidrawRef.current.refresh(); + this.excalidrawAPI.refresh(); }; //variables used to handle click events in view mode let selectedTextElement:{id:string,text:string} = null; + let selectedImageElement:{id:string,fileId:string} = null; let timestamp = 0; let blockOnMouseButtonDown = false; const getTextElementAtPointer = (pointer:any) => { - const elements = this.excalidrawRef.current.getSceneElements() + const elements = this.excalidrawAPI.getSceneElements() .filter((e:ExcalidrawElement)=>{ if (e.type !== "text") return false; const [x,y,w,h] = rotatedDimensions(e); return x<=pointer.x && x+w>=pointer.x && y<=pointer.y && y+h>=pointer.y; }); - if(elements.length==0) return null; + if(elements.length==0) return {id:null, text:null}; if(elements.length===1) return {id:elements[0].id,text:elements[0].text}; //if more than 1 text elements are at the location, look for one that has a link const elementsWithLinks = elements.filter((e:ExcalidrawTextElement)=> { @@ -729,6 +855,19 @@ export default class ExcalidrawView extends TextFileView { //if there are still multiple text elements with links on top of each other, return the first return {id:elementsWithLinks[0].id,text:elementsWithLinks[0].text}; } + + const getImageElementAtPointer = (pointer:any) => { + const elements = this.excalidrawAPI.getSceneElements() + .filter((e:ExcalidrawElement)=>{ + if (e.type !== "image") return false; + const [x,y,w,h] = rotatedDimensions(e); + return x<=pointer.x && x+w>=pointer.x + && y<=pointer.y && y+h>=pointer.y; + }); + if(elements.length===0) return {id:null, fileId:null}; + if(elements.length>=1) return {id:elements[0].id,fileId:elements[0].fileId}; + //if more than 1 image elements are at the location, return the first + } let hoverPoint = {x:0,y:0}; let hoverPreviewTarget:EventTarget = null; @@ -762,11 +901,17 @@ export default class ExcalidrawView extends TextFileView { let viewModeEnabled = false; const handleLinkClick = () => { selectedTextElement = getTextElementAtPointer(currentPosition); - if(selectedTextElement) { + if(selectedTextElement && selectedTextElement.id) { const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown}); this.handleLinkClick(this,event); selectedTextElement = null; - } + } + selectedImageElement = getImageElementAtPointer(currentPosition); + if(selectedImageElement && selectedImageElement.id) { + const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown}); + this.handleLinkClick(this,event); + selectedImageElement = null; + } } let mouseEvent:any = null; @@ -780,11 +925,12 @@ export default class ExcalidrawView extends TextFileView { tabIndex: 0, onKeyDown: (e:any) => { //@ts-ignore - if(e.target === excalidrawDiv.ref.current) return; //event should originate from the canvas + if(e.target === excalidrawDiv.ref.current) return; //event should originate from the canvas if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) { document.exitFullscreen(); this.zoomToFit(); } + this.ctrlKeyDown = e.ctrlKey || e.metaKey; this.shiftKeyDown = e.shiftKey; this.altKeyDown = e.altKey; @@ -800,7 +946,7 @@ export default class ExcalidrawView extends TextFileView { if(!text) return; if(text.match(REG_LINKINDEX_HYPERLINK)) return; - const parts = REGEX_LINK.getRes(text).next(); + const parts = REGEX_LINK.getRes(text).next(); if(!parts.value) return; let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6]; @@ -836,7 +982,7 @@ export default class ExcalidrawView extends TextFileView { //@ts-ignore if(!(e.ctrlKey||e.metaKey)) return; if(!(this.plugin.settings.allowCtrlClick)) return; - if(!this.getSelectedTextElement().id) return; + if(!(this.getSelectedTextElement().id || this.getSelectedImageElement().id)) return; this.handleLinkClick(this,e); }, onMouseMove: (e:MouseEvent) => { @@ -931,7 +1077,7 @@ export default class ExcalidrawView extends TextFileView { return true; }, onDrop: (event: React.DragEvent):boolean => { - const st: AppState = excalidrawRef.current.getAppState(); + const st: AppState = this.excalidrawAPI.getAppState(); currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st); const draggable = (this.app as any).dragManager.draggable; @@ -966,6 +1112,21 @@ export default class ExcalidrawView extends TextFileView { switch(draggable?.type) { case "file": if (!onDropHook("file",[draggable.file],null)) { + if((event.ctrlKey || event.metaKey) + && (IMAGE_TYPES.contains(draggable.file.extension) + || this.plugin.isExcalidrawFile(draggable.file))) { + const f = draggable.file; + const topX = currentPosition.x; + const topY = currentPosition.y; + const ea = window.ExcalidrawAutomate; + ea.reset(); + ea.setView(this); + (async () => { + await ea.addImage(currentPosition.x,currentPosition.y,draggable.file); + ea.addElementsToView(false,false); + })(); + return false; + } this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`); } return false; @@ -1016,14 +1177,14 @@ export default class ExcalidrawView extends TextFileView { await this.save(false); //this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text //thus I only check if TextMode.parsed, text is always != with parseResult - if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear(); + if(this.textMode == TextMode.parsed) this.excalidrawAPI.history.clear(); this.setupAutosaveTimer(); }); if(parseResult) { //there were no transclusions in the raw text, quick parse was successful this.setupAutosaveTimer(); if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent - this.excalidrawRef.current.history.clear(); + this.excalidrawAPI.history.clear(); return parseResult; } return; @@ -1041,12 +1202,15 @@ export default class ExcalidrawView extends TextFileView { ); }); - ReactDOM.render(reactElement,this.contentEl,()=>this.excalidrawWrapperRef.current.focus()); + + ReactDOM.render(reactElement,this.contentEl,()=>{ + this.excalidrawWrapperRef.current.focus(); + }); } public zoomToFit(delay:boolean = true) { if(!this.excalidrawRef) return; - const current = this.excalidrawRef.current; + const current = this.excalidrawAPI; const fullscreen = (document.fullscreenElement==this.contentEl); const elements = current.getSceneElements(); if(delay) { //time for the DOM to render, I am sure there is a more elegant solution @@ -1055,37 +1219,4 @@ export default class ExcalidrawView extends TextFileView { current.zoomToFit(elements,2,fullscreen?0:0.05); } } - - public static async getSVG(scene:any, exportSettings:ExportSettings):Promise { - try { - return exportToSvg({ - elements: scene.elements, - appState: { - exportBackground: exportSettings.withBackground, - exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false, - ... scene.appState,}, - exportPadding:10, - }); - } catch (error) { - return null; - } - } - - public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) { - try { - return await Excalidraw.exportToBlob({ - elements: scene.elements, - appState: { - exportBackground: exportSettings.withBackground, - exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false, - ... scene.appState,}, - mimeType: "image/png", - exportWithDarkMode: "true", - metadata: "Generated by Excalidraw-Obsidian plugin", - getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale }) - }); - } catch (error) { - return null; - } - } -} +} \ No newline at end of file diff --git a/src/Utils.ts b/src/Utils.ts index 1047e8d..8bd4865 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,15 +1,29 @@ -import { normalizePath, TAbstractFile, TFolder, Vault, WorkspaceLeaf } from "obsidian"; +import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw"; +import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian"; import { Random } from "roughjs/bin/math"; -import { Zoom } from "@zsviczian/excalidraw/types/types"; +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 ExcalidrawPlugin from "./main"; -import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types"; +import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types"; +import { ExportSettings } from "./ExcalidrawView"; +import { ExcalidrawSettings } from "./settings"; +import { html_beautify } from "js-beautify" declare module "obsidian" { interface Workspace { getAdjacentLeafInDirection(leaf: WorkspaceLeaf, direction: string): WorkspaceLeaf; } + interface Vault { + getConfig(option:"attachmentFolderPath"): string; + } } +declare let window: ExcalidrawAutomate; + +export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream"; + /** * Splits a full path including a folderpath and a filename into separate folderpath and filename components * @param filepath @@ -174,4 +188,229 @@ export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLe return leafToUse; } return plugin.app.workspace.createLeafBySplit(leaf); +} + +export const getObsidianImage = async (app: App, file: TFile) + :Promise<{ + mimeType: MimeType, + fileId: FileId, + dataURL: DataURL, + created: number, + size: {height: number, width: number}, + }> => { + if(!app || !file) return null; + const isExcalidrawFile = window.ExcalidrawAutomate.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 + : null; + let mimeType:MimeType = "image/svg+xml"; + if (!isExcalidrawFile) { + switch (file.extension) { + case "png": mimeType = "image/png";break; + case "jpeg":mimeType = "image/jpeg";break; + case "jpg": mimeType = "image/jpeg";break; + case "gif": mimeType = "image/gif";break; + case "svg": mimeType = "image/svg+xml";break; + default: mimeType = "application/octet-stream"; + } + } + return { + mimeType: mimeType, + fileId: await generateIdFromFile(ab), + dataURL: excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab)), + created: file.stat.mtime, + size: await getImageSize(app,excalidrawSVG??app.vault.getResourcePath(file)) + } +} + + +const getSVGData = async (app: App, file: TFile): Promise => { + const svg = await app.vault.read(file); + return svgToBase64(svg) as DataURL; +} + +export const svgToBase64 = (svg:string):string => { + return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," ")))); +} +const getDataURL = async (file: ArrayBuffer): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const dataURL = reader.result as DataURL; + resolve(dataURL); + }; + reader.onerror = (error) => reject(error); + reader.readAsDataURL(new Blob([new Uint8Array(file)])); + }); +}; + +const generateIdFromFile = async (file: ArrayBuffer):Promise => { + let id: FileId; + try { + const hashBuffer = await window.crypto.subtle.digest( + "SHA-1", + file, + ); + id = + // convert buffer to byte array + Array.from(new Uint8Array(hashBuffer)) + // convert to hex string + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") as FileId; + } catch (error) { + console.error(error); + id = nanoid(40) as FileId; + } + return id; +}; + +const getImageSize = async (app: App, src:string):Promise<{height:number, width:number}> => { + return new Promise((resolve, reject) => { + let img = new Image() + img.onload = () => resolve({height: img.height, width:img.width}); + img.onerror = reject; + img.src = src; + }) +} + +export const getBinaryFileFromDataURL = (dataURL:string):ArrayBuffer => { + if(!dataURL) return null; + const parts = dataURL.matchAll(/base64,(.*)/g).next(); + const binary_string = window.atob(parts.value[1]); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; +} + +export const getAttachmentsFolderAndFilePath = async (app:App, activeViewFilePath:string, newFileName:string):Promise<[string,string]> => { + let folder = app.vault.getConfig("attachmentFolderPath"); + // folder == null: save to vault root + // folder == "./" save to same folder as current file + // folder == "folder" save to specific folder in vault + // folder == "./folder" save to specific subfolder of current active folder + if(folder && folder.startsWith("./")) { // folder relative to current file + const activeFileFolder = splitFolderAndFilename(activeViewFilePath).folderpath + "/"; + folder = normalizePath(activeFileFolder + folder.substring(2)); + } + if(!folder) folder = ""; + await checkAndCreateFolder(app.vault,folder); + return [folder,normalizePath(folder + "/" + newFileName)]; +} + +export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise => { + try { + return exportToSvg({ + elements: scene.elements, + appState: { + exportBackground: exportSettings.withBackground, + exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false, + ... scene.appState,}, + files: scene.files, + exportPadding:10, + }); + } catch (error) { + return null; + } +} + +export const generateSVGString = async (scene:any, settings: ExcalidrawSettings):Promise => { + const exportSettings: ExportSettings = { + withBackground: settings.exportWithBackground, + withTheme: settings.exportWithTheme + } + const svg = await getSVG(scene,exportSettings); + if(svg) { + + return html_beautify(svg.outerHTML,{"indent_with_tabs": true}); + } + return null; +} + +export const getPNG = async (scene:any, exportSettings:ExportSettings, scale:number = 1) => { + try { + return await Excalidraw.exportToBlob({ + elements: scene.elements, + appState: { + exportBackground: exportSettings.withBackground, + exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false, + ... scene.appState,}, + files: scene.files, + mimeType: "image/png", + exportWithDarkMode: "true", + metadata: "Generated by Excalidraw-Obsidian plugin", + getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale }) + }); + } catch (error) { + return null; + } +} + +export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => { + //replace font references with base64 fonts + const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null; + const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null; + const defs = svg.querySelector("defs"); + if (defs && (includesCascadia || includesVirgil)) { + defs.innerHTML = ""; + } + return svg; +} + + +export const loadSceneFiles = async (app:App, filesMap: Map,addFiles:Function) => { + const 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); + 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) { + + } +} + +export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => { + let dirty = false; + for(const f of files) { + const [w_image,h_image] = [f.size.width,f.size.height]; + const imageAspectRatio = f.size.width/f.size.height; + scene + .elements + .filter((e:any)=>(e.type === "image" && e.fileId === f.id)) + .forEach((el:any)=>{ + const [w_old,h_old] = [el.width,el.height]; + const elementAspectRatio = w_old/h_old; + if(imageAspectRatio != elementAspectRatio) { + dirty = true; + const h_new = Math.sqrt(w_old*h_old*h_image/w_image); + const w_new = Math.sqrt(w_old*h_old*w_image/h_image); + el.height = h_new; + el.width = w_new; + el.y += (h_old-h_new)/2; + el.x += (w_old-w_new)/2; + } + }); + return [dirty,scene]; + } } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 8eb0344..38ca152 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("[" import {customAlphabet} from "nanoid"; export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8); +export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp']; +export const MAX_IMAGE_SIZE = 600; export const FRONTMATTER_KEY = "excalidraw-plugin"; export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix"; export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix"; diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index f35012c..3958de5 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -32,10 +32,10 @@ export default { SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)", OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)", EXPORT_EXCALIDRAW: "Export to an .Excalidraw file", - LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+ + LINK_BUTTON_CLICK_NO_TEXT: 'Select a an ImageElement, or select a TextElement that contains an internal or external link.\n'+ 'SHIFT CLICK this button to open the link in a new pane.\n'+ - 'CTRL/META CLICK the Text Element on the canvas has the same effect!', - TEXT_ELEMENT_EMPTY: "Text Element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found", + 'CTRL/META CLICK the Image or TextElement on the canvas has the same effect!', + TEXT_ELEMENT_EMPTY: "No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found", FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\  < > : | ?', FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.", FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)", @@ -44,6 +44,8 @@ export default { NOFILE: "Excalidraw (no file)", COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.", CONVERT_FILE: "Convert to new format", + DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " + + "loading Markdown View may take a while. Please be patient. ", //settings.ts FOLDER_NAME: "Excalidraw folder", @@ -105,7 +107,7 @@ export default { TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text", TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " + "Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " + - "text (i.e. no overflow), or OFF to soft-warp text (at the nearest whitespace).", + "text (i.e. no overflow), or OFF to soft-wrap text (at the nearest whitespace).", PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count", PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+ "![[markdown page]] format.", diff --git a/src/main.ts b/src/main.ts index 86014e8..e8d5c37 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,7 +35,7 @@ import { DARK_BLANK_DRAWING } from "./constants"; import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView"; -import {getJSON} from "./ExcalidrawData"; +import {getJSON, getSVGString} from "./ExcalidrawData"; import { ExcalidrawSettings, DEFAULT_SETTINGS, @@ -56,15 +56,12 @@ import { Prompt } from "./Prompt"; import { around } from "monkey-around"; import { t } from "./lang/helpers"; import { MigrationPrompt } from "./MigrationPrompt"; -import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils"; +import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, splitFolderAndFilename, svgToBase64 } from "./Utils"; declare module "obsidian" { interface App { isMobile():boolean; } - interface Vault { - getConfig(option:"attachmentFolderPath"): string; - } interface Workspace { on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef; } @@ -113,7 +110,6 @@ export default class ExcalidrawPlugin extends Plugin { //inspiration taken from kanban: //https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267 this.registerMonkeyPatches(); - new Notice("Excalidraw was updated. Files opened with this version will not open with the older version. Please update plugin on all your devices.\n\nI will remove this message with next update.",8000); if(this.settings.loadCount<1) this.migrationNotice(); const electron:string = process.versions.electron; if(electron.startsWith("8.")) { @@ -224,24 +220,39 @@ export default class ExcalidrawPlugin extends Plugin { if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight); img.addClass(imgAttributes.style); + const [scene,pos] = getJSON(content); + const svgSnapshot = getSVGString(content.substr(pos+scene.length)); - if(!this.settings.displaySVGInPreview) { + //Removed in 1.4.0 when implementing ImageElement. Key reason for removing this + //is to use SVG snapshot in file, to avoid resource intensive process to generating PNG + //due to the need to load excalidraw plus all linked images +/* if(!this.settings.displaySVGInPreview) { const width = parseInt(imgAttributes.fwidth); let scale = 1; if(width>=800) scale = 2; if(width>=1600) scale = 3; if(width>=2400) scale = 4; - const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)[0]),exportSettings, scale); + const png = await getPNG(JSON_parse(scene),exportSettings, scale); if(!png) return null; img.src = URL.createObjectURL(png); return img; + }*/ + let svg:SVGSVGElement = null; + if(svgSnapshot) { + const el = document.createElement('div'); + el.innerHTML = svgSnapshot; + const firstChild = el.firstChild; + if(firstChild instanceof SVGSVGElement) { + svg=firstChild; + } + } else { + svg = await getSVG(JSON_parse(scene),exportSettings); } - let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)[0]),exportSettings); if(!svg) return null; - svg = ExcalidrawView.embedFontsInSVG(svg); + svg = embedFontsInSVG(svg); svg.removeAttribute('width'); svg.removeAttribute('height'); - img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," "))))); + img.setAttribute("src",svgToBase64(svg.outerHTML)); return img; } @@ -577,21 +588,11 @@ export default class ExcalidrawPlugin extends Plugin { const insertDrawingToDoc = async (inNewPane:boolean) => { const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); if(!activeView) return; - let folder = this.app.vault.getConfig("attachmentFolderPath"); - // folder == null: save to vault root - // folder == "./" save to same folder as current file - // folder == "folder" save to specific folder in vault - // folder == "./folder" save to specific subfolder of current active folder - if(folder && folder.startsWith("./")) { // folder relative to current file - const activeFileFolder = splitFolderAndFilename(activeView.file.path).folderpath + "/"; - folder = normalizePath(activeFileFolder + folder.substring(2)); - } - if(!folder) folder = ""; - await checkAndCreateFolder(this.app.vault,folder); const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime) + (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md'); - this.embedDrawing(normalizePath(folder + "/" + filename)); - this.createDrawing(filename, inNewPane,folder==""?null:folder); + const [folder, filepath] = await getAttachmentsFolderAndFilePath(this.app,activeView.file.path,filename); + this.embedDrawing(filepath); + this.createDrawing(filename, inNewPane, folder===""?null:folder); } this.addCommand({ @@ -787,7 +788,7 @@ export default class ExcalidrawPlugin extends Plugin { const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md"); const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name)))); console.log(fname); - const result = await this.app.vault.create(fname,FRONTMATTER + this.exportSceneToMD(data)); + const result = await this.app.vault.create(fname,FRONTMATTER + await this.exportSceneToMD(data)); if (this.settings.keepInSync) { ['.svg','.png'].forEach( (ext:string)=>{ const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext; @@ -1106,14 +1107,21 @@ export default class ExcalidrawPlugin extends Plugin { return this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING; } const blank = this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING; - return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank); + return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank,''); } - public getMarkdownDrawingSection(jsonString: string) { + public getMarkdownDrawingSection(jsonString: string,svgString: string) { return '%%\n# Drawing\n' + String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n' + jsonString + '\n' - + String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + '\n%%'; + + String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + + (svgString ? + '\n\n# SVG snapshot\n' + + String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'html\n' + + svgString + '\n' + + String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + : '') + + '\n%%'; } /** @@ -1121,9 +1129,10 @@ export default class ExcalidrawPlugin extends Plugin { * @param {string} data - Excalidraw scene JSON string * @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text */ - public exportSceneToMD(data:string): string { + public async exportSceneToMD(data:string): Promise { if(!data) return ""; const excalidrawData = JSON_parse(data); + const svgString = await generateSVGString(excalidrawData,this.settings); const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text") let outString = '# Text Elements\n'; let id:string; @@ -1138,7 +1147,7 @@ export default class ExcalidrawPlugin extends Plugin { } outString += te.text+' ^'+id+'\n\n'; } - return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t")); + return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t"),svgString); } public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise { @@ -1182,3 +1191,4 @@ export default class ExcalidrawPlugin extends Plugin { } } + diff --git a/src/settings.ts b/src/settings.ts index c01c5eb..31365bd 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -15,7 +15,7 @@ export interface ExcalidrawSettings { templateFilePath: string, drawingFilenamePrefix: string, drawingFilenameDateTime: string, - displaySVGInPreview: boolean, + //displaySVGInPreview: boolean, width: string, matchTheme: boolean, zoomToFitOnResize: boolean, @@ -49,7 +49,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = { templateFilePath: 'Excalidraw/Template.excalidraw', drawingFilenamePrefix: 'Drawing ', drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss', - displaySVGInPreview: true, + //displaySVGInPreview: true, width: '400', matchTheme: false, zoomToFitOnResize: true, @@ -307,8 +307,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.containerEl.createEl('h1', {text: t("EMBED_HEAD")}); - - new Setting(containerEl) +//Removed in 1.4.0 when implementing ImageElement. +/* new Setting(containerEl) .setName(t("EMBED_PREVIEW_SVG_NAME")) .setDesc(t("EMBED_PREVIEW_SVG_DESC")) .addToggle(toggle => toggle @@ -316,8 +316,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.displaySVGInPreview = value; this.applySettingsUpdate(); - })); - + }));*/ new Setting(containerEl) .setName(t("EMBED_WIDTH_NAME")) diff --git a/yarn.lock b/yarn.lock index 9786af3..da5da51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1023,6 +1023,11 @@ "resolved" "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" "version" "0.0.39" +"@types/js-beautify@^1.13.3": + "integrity" "sha512-ucIPw5gmNyvRKi6mpeojlqp+T+6ZBJeU+kqMDnIEDlijEU4QhLTon90sZ3cz9HZr+QTwXILjNsMZImzA7+zuJA==" + "resolved" "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.13.3.tgz" + "version" "1.13.3" + "@types/node@*", "@types/node@^15.12.4": "integrity" "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==" "resolved" "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz" @@ -1062,16 +1067,21 @@ dependencies: "@types/estree" "*" -"@zsviczian/excalidraw@0.10.0-obsidian-1": - "integrity" "sha512-k9xPYTp8wJlWwcJwVBLjZcbccthEYqiFkIAZRRIGPVAxGUOpyxZdJ5X4/QsmOfiRqErtiq3JboAPnYEHGtLjIg==" - "resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-1.tgz" - "version" "0.10.0-obsidian-1" +"@zsviczian/excalidraw@0.10.0-obsidian-2": + "integrity" "sha512-H9w7cB0ZgQIHujMB7Zwz82zoZl85ZGDtmxkX9swrPJXYcrJjx5j4oNqEK+dUSYwUlnN2iycdX9l93MjXjAK0+A==" + "resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-2.tgz" + "version" "0.10.0-obsidian-2" "abab@^1.0.3": "integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" "resolved" "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz" "version" "1.0.4" +"abbrev@1": + "integrity" "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "resolved" "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + "version" "1.1.1" + "accepts@~1.3.4", "accepts@~1.3.5", "accepts@~1.3.7": "integrity" "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==" "resolved" "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" @@ -2740,34 +2750,7 @@ "strip-ansi" "^3.0.0" "supports-color" "^2.0.0" -"chalk@^2.0.0": - "integrity" "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==" - "resolved" "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - "version" "2.4.2" - dependencies: - "ansi-styles" "^3.2.1" - "escape-string-regexp" "^1.0.5" - "supports-color" "^5.3.0" - -"chalk@^2.0.1": - "integrity" "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==" - "resolved" "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - "version" "2.4.2" - dependencies: - "ansi-styles" "^3.2.1" - "escape-string-regexp" "^1.0.5" - "supports-color" "^5.3.0" - -"chalk@^2.1.0": - "integrity" "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==" - "resolved" "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - "version" "2.4.2" - dependencies: - "ansi-styles" "^3.2.1" - "escape-string-regexp" "^1.0.5" - "supports-color" "^5.3.0" - -"chalk@^2.4.1": +"chalk@^2.0.0", "chalk@^2.0.1", "chalk@^2.1.0", "chalk@^2.4.1": "integrity" "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==" "resolved" "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" "version" "2.4.2" @@ -3010,7 +2993,7 @@ dependencies: "delayed-stream" "~1.0.0" -"commander@^2.11.0": +"commander@^2.11.0", "commander@^2.19.0": "integrity" "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" "resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" "version" "2.20.3" @@ -3070,6 +3053,14 @@ "readable-stream" "^2.2.2" "typedarray" "^0.0.6" +"config-chain@^1.1.12": + "integrity" "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==" + "resolved" "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz" + "version" "1.1.13" + dependencies: + "ini" "^1.3.4" + "proto-list" "~1.2.1" + "configstore@^3.0.0": "integrity" "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==" "resolved" "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz" @@ -3782,6 +3773,16 @@ "jsbn" "~0.1.0" "safer-buffer" "^2.1.0" +"editorconfig@^0.15.3": + "integrity" "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==" + "resolved" "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz" + "version" "0.15.3" + dependencies: + "commander" "^2.19.0" + "lru-cache" "^4.1.5" + "semver" "^5.6.0" + "sigmund" "^1.0.1" + "ee-first@1.1.1": "integrity" "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" "resolved" "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -3860,21 +3861,25 @@ "is-arrayish" "^0.2.1" "es-abstract@^1.18.0-next.2": - "integrity" "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==" - "resolved" "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz" - "version" "1.18.3" + "integrity" "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==" + "resolved" "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz" + "version" "1.19.1" dependencies: "call-bind" "^1.0.2" "es-to-primitive" "^1.2.1" "function-bind" "^1.1.1" "get-intrinsic" "^1.1.1" + "get-symbol-description" "^1.0.0" "has" "^1.0.3" "has-symbols" "^1.0.2" - "is-callable" "^1.2.3" + "internal-slot" "^1.0.3" + "is-callable" "^1.2.4" "is-negative-zero" "^2.0.1" - "is-regex" "^1.1.3" - "is-string" "^1.0.6" - "object-inspect" "^1.10.3" + "is-regex" "^1.1.4" + "is-shared-array-buffer" "^1.0.1" + "is-string" "^1.0.7" + "is-weakref" "^1.0.1" + "object-inspect" "^1.11.0" "object-keys" "^1.1.1" "object.assign" "^4.1.2" "string.prototype.trimend" "^1.0.4" @@ -4684,7 +4689,7 @@ "resolved" "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" "version" "2.0.5" -"get-intrinsic@^1.0.2", "get-intrinsic@^1.1.1": +"get-intrinsic@^1.0.2", "get-intrinsic@^1.1.0", "get-intrinsic@^1.1.1": "integrity" "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==" "resolved" "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" "version" "1.1.1" @@ -4703,6 +4708,14 @@ "resolved" "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz" "version" "3.0.0" +"get-symbol-description@^1.0.0": + "integrity" "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==" + "resolved" "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "call-bind" "^1.0.2" + "get-intrinsic" "^1.1.1" + "get-value@^2.0.3", "get-value@^2.0.6": "integrity" "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" "resolved" "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" @@ -4918,6 +4931,13 @@ "resolved" "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" "version" "1.0.2" +"has-tostringtag@^1.0.0": + "integrity" "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==" + "resolved" "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "has-symbols" "^1.0.2" + "has-value@^0.3.1": "integrity" "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=" "resolved" "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz" @@ -5247,6 +5267,15 @@ dependencies: "meow" "^3.3.0" +"internal-slot@^1.0.3": + "integrity" "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==" + "resolved" "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "get-intrinsic" "^1.1.0" + "has" "^1.0.3" + "side-channel" "^1.0.4" + "interpret@^1.0.0": "integrity" "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" "resolved" "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" @@ -5343,10 +5372,10 @@ dependencies: "builtin-modules" "^1.0.0" -"is-callable@^1.1.4", "is-callable@^1.2.3": - "integrity" "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" - "resolved" "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz" - "version" "1.2.3" +"is-callable@^1.1.4", "is-callable@^1.2.4": + "integrity" "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + "resolved" "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" + "version" "1.2.4" "is-ci@^1.0.10": "integrity" "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==" @@ -5603,13 +5632,13 @@ dependencies: "@types/estree" "*" -"is-regex@^1.0.4", "is-regex@^1.1.3": - "integrity" "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==" - "resolved" "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz" - "version" "1.1.3" +"is-regex@^1.0.4", "is-regex@^1.1.4": + "integrity" "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==" + "resolved" "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" + "version" "1.1.4" dependencies: "call-bind" "^1.0.2" - "has-symbols" "^1.0.2" + "has-tostringtag" "^1.0.0" "is-resolvable@^1.0.0": "integrity" "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" @@ -5626,15 +5655,22 @@ "resolved" "https://registry.npmjs.org/is-root/-/is-root-1.0.0.tgz" "version" "1.0.0" +"is-shared-array-buffer@^1.0.1": + "integrity" "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + "resolved" "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz" + "version" "1.0.1" + "is-stream@^1.0.0", "is-stream@^1.1.0": "integrity" "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" "resolved" "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" "version" "1.1.0" -"is-string@^1.0.5", "is-string@^1.0.6": - "integrity" "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" - "resolved" "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz" - "version" "1.0.6" +"is-string@^1.0.5", "is-string@^1.0.7": + "integrity" "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==" + "resolved" "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" + "version" "1.0.7" + dependencies: + "has-tostringtag" "^1.0.0" "is-svg@^2.0.0": "integrity" "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=" @@ -5660,6 +5696,13 @@ "resolved" "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz" "version" "0.2.1" +"is-weakref@^1.0.1": + "integrity" "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==" + "resolved" "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "call-bind" "^1.0.0" + "is-windows@^1.0.1", "is-windows@^1.0.2": "integrity" "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" "resolved" "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" @@ -6021,6 +6064,17 @@ "resolved" "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz" "version" "2.6.4" +"js-beautify@1.13.3": + "integrity" "sha512-mi4/bWIsWFqE2/Yr8cr7EtHbbGKCBkUgPotkyTFphpsRUuyRG8gxBqH9QbonJTV8Gw8RtjPquoYFxuWEjz2HLg==" + "resolved" "https://registry.npmjs.org/js-beautify/-/js-beautify-1.13.3.tgz" + "version" "1.13.3" + dependencies: + "config-chain" "^1.1.12" + "editorconfig" "^0.15.3" + "glob" "^7.1.3" + "mkdirp" "^1.0.4" + "nopt" "^5.0.0" + "js-tokens@^3.0.0 || ^4.0.0", "js-tokens@^4.0.0": "integrity" "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" "resolved" "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -6409,7 +6463,7 @@ "resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" "version" "1.0.1" -"lru-cache@^4.0.1": +"lru-cache@^4.0.1", "lru-cache@^4.1.5": "integrity" "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==" "resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz" "version" "4.1.5" @@ -6637,6 +6691,11 @@ dependencies: "minimist" "^1.2.5" +"mkdirp@^1.0.4": + "integrity" "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "resolved" "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + "version" "1.0.4" + "moment@2.29.1": "integrity" "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" "resolved" "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz" @@ -6789,6 +6848,13 @@ "resolved" "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz" "version" "1.1.77" +"nopt@^5.0.0": + "integrity" "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==" + "resolved" "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "abbrev" "1" + "normalize-package-data@^2.3.2", "normalize-package-data@^2.3.4": "integrity" "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==" "resolved" "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" @@ -6879,10 +6945,10 @@ "resolved" "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz" "version" "1.3.1" -"object-inspect@^1.10.3": - "integrity" "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" - "resolved" "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz" - "version" "1.10.3" +"object-inspect@^1.11.0", "object-inspect@^1.9.0": + "integrity" "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" + "resolved" "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz" + "version" "1.11.0" "object-is@^1.0.1": "integrity" "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==" @@ -7795,6 +7861,11 @@ "object-assign" "^4.1.1" "react-is" "^16.8.1" +"proto-list@~1.2.1": + "integrity" "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + "resolved" "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz" + "version" "1.2.4" + "proxy-addr@~2.0.5": "integrity" "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==" "resolved" "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" @@ -8561,7 +8632,7 @@ dependencies: "semver" "^5.0.3" -"semver@^5.0.3", "semver@^5.1.0", "semver@^5.3.0", "semver@^5.5.0", "semver@2 || 3 || 4 || 5": +"semver@^5.0.3", "semver@^5.1.0", "semver@^5.3.0", "semver@^5.5.0", "semver@^5.6.0", "semver@2 || 3 || 4 || 5": "integrity" "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" "resolved" "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" "version" "5.7.1" @@ -8710,6 +8781,20 @@ "resolved" "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz" "version" "0.1.1" +"side-channel@^1.0.4": + "integrity" "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==" + "resolved" "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + "version" "1.0.4" + dependencies: + "call-bind" "^1.0.0" + "get-intrinsic" "^1.0.2" + "object-inspect" "^1.9.0" + +"sigmund@^1.0.1": + "integrity" "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + "resolved" "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "version" "1.0.1" + "signal-exit@^3.0.0", "signal-exit@^3.0.2": "integrity" "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" "resolved" "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz" @@ -9137,14 +9222,7 @@ dependencies: "has-flag" "^2.0.0" -"supports-color@^5.1.0": - "integrity" "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==" - "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - "version" "5.5.0" - dependencies: - "has-flag" "^3.0.0" - -"supports-color@^5.3.0", "supports-color@^5.4.0": +"supports-color@^5.1.0", "supports-color@^5.3.0", "supports-color@^5.4.0": "integrity" "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==" "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" "version" "5.5.0"