Merge pull request #209 from zsviczian/Image-Element

Image element
This commit is contained in:
zsviczian
2021-10-24 06:56:12 +02:00
committed by GitHub
11 changed files with 970 additions and 337 deletions

14
TODO.md Normal file
View File

@@ -0,0 +1,14 @@
[x] do not embed font into SVG when embedding Excalidraw into other Excalidraw
[x] add ```html <SVG>...</SVG> ``` codeblock to excalidraw markdown
[x] read pre-saved `<SVG>` 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

View File

@@ -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",

View File

@@ -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<string>;
createSVG (templatePath?:string):Promise<SVGSVGElement>;
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
createPNG (templatePath?:string):Promise<any>;
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<string>;
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<string> {
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<SVGSVGElement> {
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<SVGSVGElement> {
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<string> {
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,
}
}

View File

@@ -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<string,{raw:string, parsed:string}> = 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<FileId,string> = null; //fileId, path
private compatibilityMode:boolean = false;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
this.files = new Map<FileId,string>();
}
/**
@@ -96,6 +116,8 @@ export class ExcalidrawData {
this.loaded = false;
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
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<boolean> {
this.compatibilityMode = true;
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
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<boolean> {
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<boolean> {
//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;
}
}
}

View File

@@ -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<any> = null;
public excalidrawAPI: any = null;
private excalidrawWrapperRef: React.MutableRefObject<any> = 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 = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
}
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<boolean> => {
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
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<HTMLDivElement>):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<SVGSVGElement> {
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;
}
}
}
}

View File

@@ -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<DataURL> => {
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("&nbsp;"," "))));
}
const getDataURL = async (file: ArrayBuffer): Promise<DataURL> => {
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<FileId> => {
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<SVGSVGElement> => {
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<string> => {
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 = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
}
return svg;
}
export const loadSceneFiles = async (app:App, filesMap: Map<FileId, string>,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];
}
}

View File

@@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("&#91;"
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";

View File

@@ -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.",

View File

@@ -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("&nbsp;"," ")))));
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,'<SVG></SVG>');
}
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<string> {
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<string> {
@@ -1182,3 +1191,4 @@ export default class ExcalidrawPlugin extends Plugin {
}
}

View File

@@ -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"))

214
yarn.lock
View File

@@ -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"