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"