Compare commits

...

18 Commits

Author SHA1 Message Date
Zsolt Viczian
08528d9a88 1.4.8-beta-2 2021-11-14 23:13:06 +01:00
Zsolt Viczian
3e9ef99226 Reworked recursive image loading 2021-11-14 20:12:11 +01:00
Zsolt Viczian
f50ecd95c3 removed SVG snapshot 2021-11-14 12:08:19 +01:00
Zsolt Viczian
8b477a0e16 Added missing scale to createPNG interface 2021-11-14 09:41:53 +01:00
Zsolt Viczian
78891a1065 1.4.8-beta 2021-11-10 23:05:13 +01:00
Zsolt Viczian
b428cb7eed 1.4.7 (embed Excalidraw into Excalidraw fixed) 2021-11-08 19:44:45 +01:00
Zsolt Viczian
b20c1bed5a 1.4.6 2021-11-02 21:51:04 +01:00
Zsolt Viczian
f24c41eace 1.4.5 2021-11-02 21:33:11 +01:00
Zsolt Viczian
d33cf5ddd5 1.4.4 - basic copy/paste for equations & images 2021-11-01 17:41:12 +01:00
Zsolt Viczian
41491079be minapp version 12.16 2021-11-01 14:17:37 +01:00
Zsolt Viczian
5345c63672 updated versions 2021-11-01 14:17:05 +01:00
Zsolt Viczian
06acf09a85 1.4.3 2021-11-01 14:12:44 +01:00
Zsolt Viczian
ce6d983b38 LaTex MVP ready 2021-11-01 10:27:58 +01:00
zsviczian
bb6c0b54ff added Excalidraw files to filter 2021-10-30 21:54:51 +02:00
zsviczian
571dae52d3 Update Utils.ts 2021-10-29 19:51:00 +02:00
Zsolt Viczian
e6b5b0d125 latex WIP 2021-10-29 11:01:08 +02:00
Zsolt Viczian
8a1cf72095 1.4.2 2021-10-28 23:55:03 +02:00
Zsolt Viczian
f02425dcac 1.4.1 2021-10-26 23:06:00 +02:00
21 changed files with 1157 additions and 662 deletions

View File

@@ -1,5 +1,7 @@
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
![image](https://user-images.githubusercontent.com/14358394/125159831-336d6880-e17a-11eb-8a3d-ceabc2555a08.png)
# Video walkthrough
@@ -36,7 +38,7 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
- CTRL/CMD + SHIFT + CLICK to open the file in a new pane
- CTRL/CMD + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
- Insert LaTex formulas using the Command Palette action "Insert LaTeX formula". You can edit formulas either in Markdown view, or by CTRL/CMD + Click on the formula.
- Drag & Drop support
- You can drag files from the Obsidian file explorer and they will become links to those files in Excalidraw.
- Dragging image files (PNG, SVG, JPG, Excalidraw) from obsidian files explorer while pressing the CTRL/CMD button will embed the image into your drawing.

View File

@@ -3,136 +3,137 @@
Here's the interface implemented by ExcalidrawAutomate:
```typescript
export interface ExcalidrawAutomate extends Window {
ExcalidrawAutomate: {
plugin: ExcalidrawPlugin;
elementsDict: {};
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
storkeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
}
canvas: {
theme: string,
viewBackgroundColor: string,
gridSize: number
};
setFillStyle (val:number): void;
setStrokeStyle (val:number): void;
setStrokeSharpness (val:number): void;
setFontFamily (val:number): void;
setTheme (val:number): void;
addToGroup (objectIds:[]):string;
toClipboard (templatePath?:string): void;
getElements ():ExcalidrawElement[];
getElement (id:string):ExcalidrawElement;
create (
params?: {
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string>;
createSVG (templatePath?:string):Promise<SVGSVGElement>;
createPNG (templatePath?:string):Promise<any>;
wrapText (text:string, lineLen:number):string;
addRect (topX:number, topY:number, width:number, height:number):string;
addDiamond (topX:number, topY:number, width:number, height:number):string;
addEllipse (topX:number, topY:number, width:number, height:number):string;
addBlob (topX:number, topY:number, width:number, height:number):string;
addText (
topX:number,
topY:number,
text:string,
formatting?: {
wrapAt?:number,
width?:number,
height?:number,
textAlign?: string,
box?: "box"|"blob"|"ellipse"|"diamond",
boxPadding?: number
},
id?:string
):string;
addLine(points: [[x:number,y:number]]):string;
addArrow (
points: [[x:number,y:number]],
formatting?: {
startArrowHead?:string,
endArrowHead?:string,
startObjectId?:string,
endObjectId?:string
}
):string ;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
objectB: string,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):void;
clear (): void;
reset (): void;
isExcalidrawFile (f:TFile): boolean;
//view manipulation
targetView: ExcalidrawView;
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
getExcalidrawAPI ():any;
getViewElements ():ExcalidrawElement[];
deleteViewElements (el: ExcalidrawElement[]):boolean;
getViewSelectedElement( ):ExcalidrawElement;
getViewSelectedElements ():ExcalidrawElement[];
viewToggleFullScreen (forceViewMode?:boolean):void;
connectObjectWithViewSelectedElement (
objectA:string,
connectionA: ConnectionPoint,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):boolean;
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
onDropHook (data: {
ea: ExcalidrawAutomate,
event: React.DragEvent<HTMLDivElement>,
draggable: any, //Obsidian draggable object
type: "file"|"text"|"unknown",
payload: {
files: TFile[], //TFile[] array of dropped files
text: string, //string
},
excalidrawFile: TFile, //the file receiving the drop event
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
export interface ExcalidrawAutomate {
plugin: ExcalidrawPlugin;
elementsDict: {};
imagesDict: {};
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
storkeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
}
canvas: {
theme: string,
viewBackgroundColor: string,
gridSize: number
};
setFillStyle (val:number): void;
setStrokeStyle (val:number): void;
setStrokeSharpness (val:number): void;
setFontFamily (val:number): void;
setTheme (val:number): void;
addToGroup (objectIds:[]):string;
toClipboard (templatePath?:string): void;
getElements ():ExcalidrawElement[];
getElement (id:string):ExcalidrawElement;
create (
params?: {
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string>;
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;
addDiamond (topX:number, topY:number, width:number, height:number):string;
addEllipse (topX:number, topY:number, width:number, height:number):string;
addBlob (topX:number, topY:number, width:number, height:number):string;
addText (
topX:number,
topY:number,
text:string,
formatting?: {
wrapAt?:number,
width?:number,
height?:number,
textAlign?: string,
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
boxPadding?: number
},
id?:string
):string;
addLine(points: [[x:number,y:number]]):string;
addArrow (
points: [[x:number,y:number]],
formatting?: {
startArrowHead?:string,
endArrowHead?:string,
startObjectId?:string,
endObjectId?:string
}
):string ;
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
addLaTex(topX:number, topY:number, tex: string, color?:string):Promise<string>;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
objectB: string,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):void;
clear (): void;
reset (): void;
isExcalidrawFile (f:TFile): boolean;
//view manipulation
targetView: ExcalidrawView;
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
getExcalidrawAPI ():any;
getViewElements ():ExcalidrawElement[];
deleteViewElements (el: ExcalidrawElement[]):boolean;
getViewSelectedElement ():ExcalidrawElement;
getViewSelectedElements ():ExcalidrawElement[];
viewToggleFullScreen (forceViewMode?:boolean):void;
connectObjectWithViewSelectedElement (
objectA:string,
connectionA: ConnectionPoint,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):boolean;
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
onDropHook (data: {
ea: ExcalidrawAutomate,
event: React.DragEvent<HTMLDivElement>,
draggable: any, //Obsidian draggable object
type: "file"|"text"|"unknown",
payload: {
files: TFile[], //TFile[] array of dropped files
text: string, //string
},
excalidrawFile: TFile, //the file receiving the drop event
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
}
```

View File

@@ -1,8 +1,8 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.4.0",
"minAppVersion": "0.12.0",
"version": "1.4.7",
"minAppVersion": "0.12.16",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",

View File

@@ -11,7 +11,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"@zsviczian/excalidraw": "0.10.0-obsidian-3",
"@zsviczian/excalidraw": "0.10.0-obsidian-8",
"monkey-around": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -31,7 +31,7 @@
"@types/node": "^15.12.4",
"@types/react-dom": "^17.0.9",
"cross-env": "^7.0.3",
"js-beautify": "1.13.3",
"html2canvas": "^1.3.2",
"nanoid": "^3.1.23",
"obsidian": "^0.12.16",
"rollup": "^2.52.3",

165
src/EmbeddedFileLoader.ts Normal file
View File

@@ -0,0 +1,165 @@
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { App, Notice, TFile } from "obsidian";
import { fileid, IMAGE_TYPES } from "./constants";
import { ExcalidrawData } from "./ExcalidrawData";
import ExcalidrawView, { ExportSettings } from "./ExcalidrawView";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import {getImageSize, svgToBase64 } from "./Utils";
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
export class EmbeddedFilesLoader {
private plugin:ExcalidrawPlugin;
private processedFiles: Set<string> = new Set<string>();
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
}
public async getObsidianImage (file: TFile)
:Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
size: {height: number, width: number},
}> {
if(!this.plugin || !file) return null;
//to block infinite loop of recursive loading of images
if((file.extension==="md" || file.extension === "excalidraw") && this.processedFiles.has(file.path)) {
new Notice("Stopped loading infinite image embed loop at repeated instance of " + file.path,6000);
return null;
}
this.processedFiles.add(file.path);
const app = this.plugin.app;
const isExcalidrawFile = this.plugin.ea.isExcalidrawFile(file);
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
return null;
}
const ab = await app.vault.readBinary(file);
const getExcalidrawSVG = async () => {
const exportSettings:ExportSettings = {
withBackground: false,
withTheme: false
};
this.plugin.ea.reset();
const svg = await this.plugin.ea.createSVG(file.path,true,exportSettings,this);
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return dURL as DataURL;
}
const excalidrawSVG = isExcalidrawFile
? await getExcalidrawSVG()
: 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";
}
}
const dataURL = excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab,mimeType));
const size = await getImageSize(excalidrawSVG??app.vault.getResourcePath(file));
return {
mimeType: mimeType,
fileId: await generateIdFromFile(ab),
dataURL: dataURL,
created: file.stat.mtime,
size: size
}
}
public async loadSceneFiles (
excalidrawData: ExcalidrawData,
view: ExcalidrawView,
addFiles:Function,
sourcePath:string
) {
const app = this.plugin.app;
let entries = excalidrawData.getFileEntries();
let entry;
let files:BinaryFileData[] = [];
while(!(entry = entries.next()).done) {
const file = app.metadataCache.getFirstLinkpathDest(entry.value[1],sourcePath);
if(file && file instanceof TFile) {
const data = await this.getObsidianImage(file);
if(data) {
files.push({
mimeType : data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
created: data.created,
//@ts-ignore
size: data.size,
});
}
}
}
entries = excalidrawData.getEquationEntries();
while(!(entry = entries.next()).done) {
const tex = entry.value[1];
const data = await tex2dataURL(tex, this.plugin);
if(data) {
files.push({
mimeType : data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
created: data.created,
//@ts-ignore
size: data.size,
});
}
}
try { //in try block because by the time files are loaded the user may have closed the view
addFiles(files,view);
} catch(e) {
}
}
}
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
const svg = await app.vault.read(file);
return svgToBase64(svg) as DataURL;
}
const getDataURL = async (file: ArrayBuffer,mimeType: string): 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)],{type:'mimeType'}));
});
};
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 = fileid() as FileId;
}
return id;
};

View File

@@ -9,157 +9,157 @@ import {
normalizePath,
TFile
} from "obsidian"
import ExcalidrawView, { TextMode } from "./ExcalidrawView";
import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData";
import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
import { ExcalidrawData} from "./ExcalidrawData";
import {
FRONTMATTER,
nanoid,
JSON_parse,
VIEW_TYPE_EXCALIDRAW,
MAX_IMAGE_SIZE
MAX_IMAGE_SIZE,
} from "./constants";
import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils";
import { embedFontsInSVG, getPNG, getSVG, scaleLoadedImage, wrapText } from "./Utils";
import { AppState } from "@zsviczian/excalidraw/types/types";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { tex2dataURL } from "./LaTeX";
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
export interface ExcalidrawAutomate extends Window {
ExcalidrawAutomate: {
plugin: ExcalidrawPlugin;
elementsDict: {};
imagesDict: {};
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
storkeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
}
canvas: {
theme: string,
viewBackgroundColor: string,
gridSize: number
};
setFillStyle (val:number): void;
setStrokeStyle (val:number): void;
setStrokeSharpness (val:number): void;
setFontFamily (val:number): void;
setTheme (val:number): void;
addToGroup (objectIds:[]):string;
toClipboard (templatePath?:string): void;
getElements ():ExcalidrawElement[];
getElement (id:string):ExcalidrawElement;
create (
params?: {
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string>;
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;
addDiamond (topX:number, topY:number, width:number, height:number):string;
addEllipse (topX:number, topY:number, width:number, height:number):string;
addBlob (topX:number, topY:number, width:number, height:number):string;
addText (
topX:number,
topY:number,
text:string,
formatting?: {
wrapAt?:number,
width?:number,
height?:number,
textAlign?: string,
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
boxPadding?: number
},
id?:string
):string;
addLine(points: [[x:number,y:number]]):string;
addArrow (
points: [[x:number,y:number]],
formatting?: {
startArrowHead?:string,
endArrowHead?:string,
startObjectId?:string,
endObjectId?:string
}
):string ;
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
objectB: string,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):void;
clear (): void;
reset (): void;
isExcalidrawFile (f:TFile): boolean;
//view manipulation
targetView: ExcalidrawView;
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
getExcalidrawAPI ():any;
getViewElements ():ExcalidrawElement[];
deleteViewElements (el: ExcalidrawElement[]):boolean;
getViewSelectedElement ():ExcalidrawElement;
getViewSelectedElements ():ExcalidrawElement[];
viewToggleFullScreen (forceViewMode?:boolean):void;
connectObjectWithViewSelectedElement (
objectA:string,
connectionA: ConnectionPoint,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):boolean;
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
onDropHook (data: {
ea: ExcalidrawAutomate,
event: React.DragEvent<HTMLDivElement>,
draggable: any, //Obsidian draggable object
type: "file"|"text"|"unknown",
payload: {
files: TFile[], //TFile[] array of dropped files
text: string, //string
},
excalidrawFile: TFile, //the file receiving the drop event
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
export interface ExcalidrawAutomate {
plugin: ExcalidrawPlugin;
elementsDict: {};
imagesDict: {};
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
storkeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
}
canvas: {
theme: string,
viewBackgroundColor: string,
gridSize: number
};
setFillStyle (val:number): void;
setStrokeStyle (val:number): void;
setStrokeSharpness (val:number): void;
setFontFamily (val:number): void;
setTheme (val:number): void;
addToGroup (objectIds:[]):string;
toClipboard (templatePath?:string): void;
getElements ():ExcalidrawElement[];
getElement (id:string):ExcalidrawElement;
create (
params?: {
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string>;
createSVG (templatePath?:string, embedFont?:boolean, exportSettings?:ExportSettings, loader?:EmbeddedFilesLoader):Promise<SVGSVGElement>;
createPNG (templatePath?:string, scale?:number, loader?:EmbeddedFilesLoader):Promise<any>;
wrapText (text:string, lineLen:number):string;
addRect (topX:number, topY:number, width:number, height:number):string;
addDiamond (topX:number, topY:number, width:number, height:number):string;
addEllipse (topX:number, topY:number, width:number, height:number):string;
addBlob (topX:number, topY:number, width:number, height:number):string;
addText (
topX:number,
topY:number,
text:string,
formatting?: {
wrapAt?:number,
width?:number,
height?:number,
textAlign?: string,
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
boxPadding?: number
},
id?:string
):string;
addLine(points: [[x:number,y:number]]):string;
addArrow (
points: [[x:number,y:number]],
formatting?: {
startArrowHead?:string,
endArrowHead?:string,
startObjectId?:string,
endObjectId?:string
}
):string ;
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
addLaTex(topX:number, topY:number, tex: string):Promise<string>;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
objectB: string,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):void;
clear (): void;
reset (): void;
isExcalidrawFile (f:TFile): boolean;
//view manipulation
targetView: ExcalidrawView;
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
getExcalidrawAPI ():any;
getViewElements ():ExcalidrawElement[];
deleteViewElements (el: ExcalidrawElement[]):boolean;
getViewSelectedElement ():ExcalidrawElement;
getViewSelectedElements ():ExcalidrawElement[];
viewToggleFullScreen (forceViewMode?:boolean):void;
connectObjectWithViewSelectedElement (
objectA:string,
connectionA: ConnectionPoint,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):boolean;
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
onDropHook (data: {
ea: ExcalidrawAutomate,
event: React.DragEvent<HTMLDivElement>,
draggable: any, //Obsidian draggable object
type: "file"|"text"|"unknown",
payload: {
files: TFile[], //TFile[] array of dropped files
text: string, //string
},
excalidrawFile: TFile, //the file receiving the drop event
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
}
declare let window: ExcalidrawAutomate;
declare let window: any;
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<ExcalidrawAutomate> {
window.ExcalidrawAutomate = {
plugin: plugin,
elementsDict: {},
@@ -249,7 +249,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
return id;
},
async toClipboard(templatePath?:string) {
const template = templatePath ? (await getTemplate(templatePath)) : null;
const template = templatePath ? (await getTemplate(this.plugin,templatePath, false, new EmbeddedFilesLoader(this.plugin) )) : null;
let elements = template ? template.elements : [];
elements = elements.concat(this.getElements());
navigator.clipboard.writeText(
@@ -283,7 +283,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
}
}
):Promise<string> {
const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null;
const template = params?.templatePath
? (await getTemplate(this.plugin,params.templatePath,true, new EmbeddedFilesLoader(this.plugin)))
: null;
let elements = template ? template.elements : [];
elements = elements.concat(this.getElements());
let frontmatter:string;
@@ -338,9 +340,14 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
: frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t"))
);
},
async createSVG(templatePath?:string,embedFont:boolean = false):Promise<SVGSVGElement> {
async createSVG(
templatePath?:string,
embedFont:boolean = false,
exportSettings?:ExportSettings,
loader:EmbeddedFilesLoader = new EmbeddedFilesLoader(this.plugin)
):Promise<SVGSVGElement> {
const automateElements = this.getElements();
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
const template = templatePath ? (await getTemplate(this.plugin,templatePath,true,loader)) : null;
let elements = template ? template.elements : [];
elements = elements.concat(automateElements);
const svg = await getSVG(
@@ -350,24 +357,28 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: template?.appState?.theme ?? this.canvas.theme,
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
withBackground: (exportSettings === undefined) ? plugin.settings.exportWithBackground : exportSettings.withBackground,
withTheme: (exportSettings === undefined) ? plugin.settings.exportWithTheme : exportSettings.withTheme
}
)
return embedFont ? embedFontsInSVG(svg) : svg;
},
async createPNG(templatePath?:string, scale:number=1) {
async createPNG(
templatePath?:string,
scale:number=1,
loader:EmbeddedFilesLoader = new EmbeddedFilesLoader(this.plugin)
) {
const automateElements = this.getElements();
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
const template = templatePath ? (await getTemplate(this.plugin,templatePath,true,loader)) : null;
let elements = template ? template.elements : [];
elements = elements.concat(automateElements);
return getPNG(
return await getPNG(
{
type: "excalidraw",
version: 2,
@@ -477,12 +488,13 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
}
}
const ea = window.ExcalidrawAutomate;
this.elementsDict[id] = {
text: text,
fontSize: window.ExcalidrawAutomate.style.fontSize,
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
verticalAlign: window.ExcalidrawAutomate.style.verticalAlign,
fontSize: ea.style.fontSize,
fontFamily: ea.style.fontFamily,
textAlign: formatting?.textAlign ? formatting.textAlign : ea.style.textAlign,
verticalAlign: ea.style.verticalAlign,
baseline: baseline,
... boxedElement(id,"text",topX,topY,width,height)
};
@@ -529,14 +541,16 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
},
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
const id = nanoid();
const image = await getObsidianImage(this.plugin.app,imageFile);
const loader = new EmbeddedFilesLoader(this.plugin)
const image = await loader.getObsidianImage(imageFile);
if(!image) return null;
this.imagesDict[image.fileId] = {
mimeType: image.mimeType,
id: image.fileId,
dataURL: image.dataURL,
created: image.created,
file: imageFile.path
file: imageFile.path,
tex: null
}
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
@@ -548,6 +562,23 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
this.elementsDict[id].scale = [1,1];
return id;
},
async addLaTex(topX:number, topY:number, tex:string):Promise<string> {
const id = nanoid();
const image = await tex2dataURL(tex, this.plugin);
if(!image) return null;
this.imagesDict[image.fileId] = {
mimeType: image.mimeType,
id: image.fileId,
dataURL: image.dataURL,
created: image.created,
file: null,
tex: tex
}
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
this.elementsDict[id].fileId = image.fileId;
this.elementsDict[id].scale = [1,1];
return id;
},
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
return;
@@ -723,6 +754,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
onDropHook:null,
};
await initFonts();
return window.ExcalidrawAutomate;
}
export function destroyExcalidrawAutomate() {
@@ -738,6 +770,7 @@ function normalizeLinePoints(points:[[x:number,y:number]],box:{x:number,y:number
}
function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number) {
const ea = window.ExcalidrawAutomate;
return {
id: id,
type: eltype,
@@ -745,15 +778,15 @@ function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number)
y: y,
width: w,
height: h,
angle: window.ExcalidrawAutomate.style.angle,
strokeColor: window.ExcalidrawAutomate.style.strokeColor,
backgroundColor: window.ExcalidrawAutomate.style.backgroundColor,
fillStyle: window.ExcalidrawAutomate.style.fillStyle,
strokeWidth: window.ExcalidrawAutomate.style.strokeWidth,
storkeStyle: window.ExcalidrawAutomate.style.storkeStyle,
roughness: window.ExcalidrawAutomate.style.roughness,
opacity: window.ExcalidrawAutomate.style.opacity,
strokeSharpness: window.ExcalidrawAutomate.style.strokeSharpness,
angle: ea.style.angle,
strokeColor: ea.style.strokeColor,
backgroundColor: ea.style.backgroundColor,
fillStyle: ea.style.fillStyle,
strokeWidth: ea.style.strokeWidth,
storkeStyle: ea.style.storkeStyle,
roughness: ea.style.roughness,
opacity: ea.style.opacity,
strokeSharpness: ea.style.strokeSharpness,
seed: Math.floor(Math.random() * 100000),
version: 1,
versionNounce: 1,
@@ -816,19 +849,24 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
return {w: width, h: height, baseline: baseline };
};
async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{
async function getTemplate(
plugin: ExcalidrawPlugin,
fileWithPath:string,
loadFiles:boolean = false,
loader:EmbeddedFilesLoader
):Promise<{
elements: any,
appState: any,
frontmatter: string,
files: any,
svgSnapshot: string
}> {
const app = window.ExcalidrawAutomate.plugin.app;
const app = plugin.app;
const vault = app.vault;
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
const templatePath = normalizePath(fileWithPath);
const file = app.metadataCache.getFirstLinkpathDest(templatePath,'');
if(file && file instanceof TFile) {
const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n");
let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin);
let excalidrawData:ExcalidrawData = new ExcalidrawData(plugin);
if(file.extension === "excalidraw") {
await excalidrawData.loadLegacyData(data,file);
@@ -837,7 +875,6 @@ async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promi
appState: excalidrawData.scene.appState,
frontmatter: "",
files: excalidrawData.scene.files,
svgSnapshot: null,
};
}
@@ -847,22 +884,23 @@ async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promi
let trimLocation = data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
let scene = excalidrawData.scene;
if(loadFiles) {
await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{
await loader.loadSceneFiles(excalidrawData, null, (fileArray:any, view:any)=>{
if(!fileArray) return;
for(const f of fileArray) {
excalidrawData.scene.files[f.id] = f;
}
let foo;
[foo,excalidrawData] = scaleLoadedImage(excalidrawData,fileArray);
});
[foo,scene] = scaleLoadedImage(excalidrawData.scene,fileArray);
},templatePath);
}
return {
elements: excalidrawData.scene.elements,
appState: excalidrawData.scene.appState,
elements: scene.elements,
appState: scene.appState,
frontmatter: data.substring(0,trimLocation),
files: excalidrawData.scene.files,
svgSnapshot: excalidrawData.svgSnapshot
files: scene.files,
};
};
return {
@@ -870,7 +908,6 @@ async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promi
appState: {},
frontmatter: null,
files: [],
svgSnapshot: null,
}
}

View File

@@ -1,4 +1,4 @@
import { App, normalizePath, TFile } from "obsidian";
import { App, TFile } from "obsidian";
import {
nanoid,
FRONTMATTER_KEY_CUSTOM_PREFIX,
@@ -11,8 +11,8 @@ import {
JSON_parse
} from "./constants";
import { TextMode } from "./ExcalidrawView";
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, isObsidianThemeDark, wrapText } from "./Utils";
import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, getIMGFilename, isObsidianThemeDark, wrapText } from "./Utils";
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types";
type SceneDataWithFiles = SceneData & { files: BinaryFiles};
@@ -54,7 +54,7 @@ export const REGEX_LINK = {
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const DRAWING_REG = /\n%%\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG = /\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
export function getJSON(data:string):[string,number] {
let res = data.matchAll(DRAWING_REG);
@@ -73,21 +73,14 @@ export function getJSON(data:string):[string,number] {
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 function getMarkdownDrawingSection(jsonString: 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%%';
}
export class ExcalidrawData {
public svgSnapshot: string = null;
private textElements:Map<string,{raw:string, parsed:string}> = null;
public scene:any = null;
private file:TFile = null;
@@ -98,13 +91,15 @@ export class ExcalidrawData {
private textMode: TextMode = TextMode.raw;
private plugin: ExcalidrawPlugin;
public loaded: boolean = false;
public files:Map<FileId,string> = null; //fileId, path
private files:Map<FileId,string> = null; //fileId, path
private equations: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>();
this.equations = new Map<FileId,string>();
}
/**
@@ -117,6 +112,7 @@ export class ExcalidrawData {
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
this.files.clear();
this.equations.clear();
this.compatibilityMode = false;
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
@@ -157,8 +153,6 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
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
@@ -192,12 +186,19 @@ export class ExcalidrawData {
}
data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length);
//Load Embedded files
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length);
res = data.matchAll(REG_FILEID_FILEPATH);
while(!(parts = res.next()).done) {
this.files.set(parts.value[1] as FileId,parts.value[2]);
this.setFile(parts.value[1] as FileId,parts.value[2]);
}
//Load Equations
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$(.*)(\$\$\s*\n)/gm;
res = data.matchAll(REG_FILEID_EQUATION);
while(!(parts = res.next()).done) {
this.setEquation(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
@@ -223,6 +224,7 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
this.files.clear();
this.equations.clear();
this.findNewTextElementsInScene();
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
return true;
@@ -488,20 +490,29 @@ export class ExcalidrawData {
for(const key of this.textElements.keys()){
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
}
outString += (this.equations.size>0 || this.files.size>0) ? '\n# Embedded files\n' : '';
if(this.equations.size>0) {
for(const key of this.equations.keys()) {
outString += key +': $$'+this.equations.get(key) + '$$\n';
}
}
if(this.files.size>0) {
outString += '\n# Embedded files\n';
for(const key of this.files.keys()) {
outString += key +': [['+this.files.get(key) + ']]\n';
}
outString += '\n';
}
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot);
outString += (this.equations.size>0 || this.files.size>0) ? '\n' : '';
const sceneJSONstring = JSON.stringify(this.scene,null,"\t");
return outString + getMarkdownDrawingSection(sceneJSONstring);
}
private async syncFiles(scene:SceneDataWithFiles):Promise<boolean> {
let dirty = false;
//remove files that no longer have a corresponding image element
//remove files and equations 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)) {
@@ -510,11 +521,18 @@ export class ExcalidrawData {
}
});
this.equations.forEach((value,key)=>{
if(!fileIds.contains(key)) {
this.equations.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)) {
if(!(this.hasFile(key as FileId) || this.hasEquation(key as FileId))) {
dirty = true;
let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS");
switch(scene.files[key].mimeType) {
@@ -526,7 +544,7 @@ export class ExcalidrawData {
}
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);
this.setFile(key as FileId,filepath);
}
}
return dirty;
@@ -621,4 +639,74 @@ export class ExcalidrawData {
return showLinkBrackets != this.showLinkBrackets;
}
/*
// Files and equations copy/paste support
// This is not a complete solution, it assumes the source document is opened first
// at that time the fileId is stored in the master files/equations map
// when pasted the map is checked if the file already exists
// This will not work if pasting from one vault to another, but for the most common usecase
// of copying an image or equation from one drawing to another within the same vault
// this is going to do the job
*/
public setFile(fileId:FileId, path:string) {
//always store absolute path because in case of paste, relative path may not resolve ok
const file = this.app.metadataCache.getFirstLinkpathDest(path,this.file.path);
const p = file?.path ?? path;
this.files.set(fileId,p);
this.plugin.filesMaster.set(fileId,p);
}
public getFile(fileId:FileId) {
return this.files.get(fileId);
}
public getFileEntries() {
return this.files.entries();
}
public deleteFile(fileId:FileId) {
this.files.delete(fileId);
//deliberately not deleting from plugin.filesMaster
//could be present in other drawings as well
}
//Image copy/paste support
public hasFile(fileId:FileId):boolean {
if(this.files.has(fileId)) return true;
if(this.plugin.filesMaster.has(fileId)) {
this.files.set(fileId,this.plugin.filesMaster.get(fileId));
return true;
}
return false;
}
public setEquation(fileId:FileId, equation:string) {
this.equations.set(fileId,equation);
this.plugin.equationsMaster.set(fileId,equation);
}
public getEquation(fileId: FileId) {
return this.equations.get(fileId);
}
public getEquationEntries() {
return this.equations.entries();
}
public deleteEquation(fileId:FileId) {
this.equations.delete(fileId);
//deliberately not deleting from plugin.equationsMaster
//could be present in other drawings as well
}
//Image copy/paste support
public hasEquation(fileId:FileId):boolean {
if(this.equations.has(fileId)) return true;
if(this.plugin.equationsMaster.has(fileId)) {
this.equations.set(fileId,this.plugin.equationsMaster.get(fileId));
return true;
}
return false;
}
}

View File

@@ -20,8 +20,6 @@ import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
EXCALIDRAW_LIB_HEADER,
VIRGIL_FONT,
CASCADIA_FONT,
DISK_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME,
@@ -33,15 +31,14 @@ import {
IMAGE_TYPES
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
import { repositionElementsToCursor} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, loadSceneFiles, rotatedDimensions, scaleLoadedImage, splitFolderAndFilename, svgToBase64, viewportCoordsToSceneCoords } from "./Utils";
import { checkAndCreateFolder, download, embedFontsInSVG, getIMGFilename, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, 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;
import { updateEquation } from "./LaTeX";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
export enum TextMode {
parsed,
@@ -59,9 +56,25 @@ export interface ExportSettings {
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export const addFiles = (files:any, view: ExcalidrawView) => {
if(files.length === 0) return;
const [dirty, scene] = scaleLoadedImage(view.getScene(),files);
if(dirty) {
view.excalidrawAPI.updateScene({
elements: scene.elements,
appState: scene.appState,
commitToHistory: false,
});
}
view.excalidrawAPI.addFiles(files);
}
export default class ExcalidrawView extends TextFileView {
private excalidrawData: ExcalidrawData;
private getScene: Function = null;
public getScene: Function = null;
public addElements: Function = null; //add elements to the active Excalidraw drawing
private getSelectedTextElement: Function = null;
private getSelectedImageElement: Function = null;
@@ -109,7 +122,7 @@ export default class ExcalidrawView extends TextFileView {
if (!this.getScene) return false;
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
const filepath = getIMGFilename(this.file.path,'svg'); //.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
(async () => {
const exportSettings: ExportSettings = {
@@ -131,7 +144,7 @@ export default class ExcalidrawView extends TextFileView {
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
const filepath = getIMGFilename(this.file.path,'png'); //this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
(async () => {
@@ -158,8 +171,6 @@ export default class ExcalidrawView extends TextFileView {
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();
}
@@ -183,8 +194,27 @@ export default class ExcalidrawView extends TextFileView {
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
}
const header = this.data.substring(0,trimLocation)
let header = this.data.substring(0,trimLocation)
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (this.textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
if (header.search(/cssclass:[\s]*excalidraw-hide-preview-text/) === -1) {
header = header.replace(/(excalidraw-plugin:\s.*\n)/,"$1cssclass: excalidraw-hide-preview-text\n");
}
const ext = this.plugin.settings.autoexportSVG
? "svg"
: ( this.plugin.settings.autoexportPNG
? "png"
: null);
if(ext) {
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
if(header.match(REG_IMG)) {
header = header.replace(REG_IMG,"$1![["+getIMGFilename(this.file.path,ext)+"]]\n");
} else {
header = header.replace(/(^---[\w\W]*?---\n)/m, "$1![["+getIMGFilename(this.file.path,ext)+"]]\n");
}
}
return header + this.excalidrawData.generateMD();
}
if(this.compatibilityMode) {
@@ -258,9 +288,20 @@ export default class ExcalidrawView extends TextFileView {
} else {
const selectedImage = this.getSelectedImageElement();
if(selectedImage?.id) {
if(this.excalidrawData.hasEquation(selectedImage.fileId)) {
const equation = this.excalidrawData.getEquation(selectedImage.fileId);
const prompt = new Prompt(this.app, t("ENTER_LATEX"),equation,'');
prompt.openAndGetValue( async (formula:string)=> {
if(!formula) return;
this.excalidrawData.setEquation(selectedImage.fileId,formula);
await this.save(true);
await updateEquation(formula,selectedImage.fileId,this,addFiles,this.plugin);
});
return;
}
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(this.excalidrawData.hasFile(selectedImage.fileId)) {
linkText = this.excalidrawData.getFile(selectedImage.fileId);
}
}
}
@@ -428,7 +469,7 @@ export default class ExcalidrawView extends TextFileView {
*
* @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
*/
private async loadDrawing(justloaded:boolean) {
private async loadDrawing(justloaded:boolean) {
const excalidrawData = this.excalidrawData.scene;
this.justLoaded = justloaded;
if(this.excalidrawRef) {
@@ -447,7 +488,13 @@ export default class ExcalidrawView extends TextFileView {
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
this.excalidrawWrapperRef.current.focus();
}
loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files));
const loader = new EmbeddedFilesLoader(this.plugin);
loader.loadSceneFiles(
this.excalidrawData,
this,
(files:any, view:ExcalidrawView) => addFiles(files,view),
this.file?.path
);
} else {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
@@ -459,21 +506,6 @@ export default class ExcalidrawView extends TextFileView {
}
}
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
canAcceptExtension(extension: string) {
return extension == "excalidraw";
@@ -496,12 +528,6 @@ 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);
}
@@ -649,7 +675,13 @@ export default class ExcalidrawView extends TextFileView {
React.useEffect(() => {
excalidrawRef.current.readyPromise.then((api) => {
this.excalidrawAPI = api;
loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files));
const loader = new EmbeddedFilesLoader(this.plugin);
loader.loadSceneFiles(
this.excalidrawData,
this,
(files:any, view:ExcalidrawView)=>addFiles(files,view),
this.file?.path
);
});
}, [excalidrawRef]);
@@ -728,14 +760,15 @@ export default class ExcalidrawView extends TextFileView {
}
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
const st: AppState = this.excalidrawAPI.getAppState();
window.ExcalidrawAutomate.reset();
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
const ea = this.plugin.ea;
ea.reset();
ea.style.strokeColor = st.currentItemStrokeColor;
ea.style.opacity = st.currentItemOpacity;
ea.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
ea.style.fontSize = st.currentItemFontSize;
ea.style.textAlign = st.currentItemTextAlign;
const id:string = ea.addText(currentPosition.x, currentPosition.y, text);
this.addElements(ea.getElements(),false,true);
}
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
@@ -768,7 +801,12 @@ export default class ExcalidrawView extends TextFileView {
dataURL: images[k].dataURL,
created: images[k].created
});
this.excalidrawData.files.set(images[k].id,images[k].file);
if(images[k].file) {
this.excalidrawData.setFile(images[k].id,images[k].file);
}
if(images[k].tex) {
this.excalidrawData.setEquation(images[k].id,images[k].tex);
}
});
this.excalidrawAPI.addFiles(files);
}
@@ -1079,11 +1117,11 @@ export default class ExcalidrawView extends TextFileView {
const draggable = (this.app as any).dragManager.draggable;
const onDropHook = (type:"file"|"text"|"unknown", files:TFile[], text:string):boolean => {
if (window.ExcalidrawAutomate.onDropHook) {
if (this.plugin.ea.onDropHook) {
try {
return window.ExcalidrawAutomate.onDropHook({
return this.plugin.ea.onDropHook({
//@ts-ignore
ea: window.ExcalidrawAutomate, //the Excalidraw Automate object
ea: this.plugin.ea, //the Excalidraw Automate object
event: event, //React.DragEvent<HTMLDivElement>
draggable: draggable, //Obsidian draggable object
type: type, //"file"|"text"
@@ -1115,7 +1153,7 @@ export default class ExcalidrawView extends TextFileView {
const f = draggable.file;
const topX = currentPosition.x;
const topY = currentPosition.y;
const ea = window.ExcalidrawAutomate;
const ea = this.plugin.ea;
ea.reset();
ea.setView(this);
(async () => {
@@ -1140,6 +1178,33 @@ export default class ExcalidrawView extends TextFileView {
const text:string = event.dataTransfer.getData("text");
if(!text) return true;
if (!onDropHook("text",null,text)) {
if(this.plugin.settings.iframelyAllowed && text.match(/^https?:\/\/\S*$/)) {
let linkAdded = false;
const self = this;
ajaxPromise({
url: `http://iframely.server.crestify.com/iframely?url=${text}`
}).then((res) => {
if(!res || linkAdded) return false;
linkAdded = true;
const data = JSON.parse(res);
if(!data || !(data.meta?.title)) {
this.addText(text);
return false;
}
this.addText(`[${data.meta.title}](${text})`);
return false;
},()=>{
if(linkAdded) return false;
linkAdded = true;
self.addText(text)
});
setTimeout(()=>{
if(linkAdded) return;
linkAdded = true;
self.addText(text)
},600);
return false;
}
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g,"$1{40}"));
}
return false;

55
src/InsertImageDialog.ts Normal file
View File

@@ -0,0 +1,55 @@
import {
App,
FuzzySuggestModal,
TFile
} from "obsidian";
import { IMAGE_TYPES } from "./constants";
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
import ExcalidrawView from "./ExcalidrawView";
import {t} from './lang/helpers'
import ExcalidrawPlugin from "./main";
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
this.app = plugin.app;
this.limit = 20;
this.setInstructions([{
command: t("SELECT_FILE"),
purpose: "",
}]);
this.setPlaceholder(t("SELECT_DRAWING"));
this.emptyStateText = t("NO_MATCH");
}
getItems(): TFile[] {
return (this.app.vault.getFiles() || []).filter((f:TFile) => IMAGE_TYPES.contains(f.extension) || this.plugin.isExcalidrawFile(f));
}
getItemText(item: TFile): string {
return item.path;
}
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
const ea = this.plugin.ea;
ea.reset();
ea.setView(this.view);
(async () => {
await ea.addImage(0,0,item);
ea.addElementsToView(true,false);
})();
}
public start(view: ExcalidrawView) {
this.view = view;
this.open();
}
}

103
src/LaTeX.ts Normal file
View File

@@ -0,0 +1,103 @@
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {MimeType} from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { getImageSize, svgToBase64 } from "./Utils";
import { fileid } from "./constants";
import html2canvas from "html2canvas";
declare let window: any;
export const updateEquation = async (
equation: string,
fileId: string,
view: ExcalidrawView,
addFiles:Function,
plugin: ExcalidrawPlugin
) => {
const data = await tex2dataURL(equation, plugin);
if(data) {
let files:BinaryFileData[] = [];
files.push({
mimeType : data.mimeType,
id: fileId as FileId,
dataURL: data.dataURL,
created: data.created,
//@ts-ignore
size: data.size,
});
addFiles(files,view);
}
}
export async function tex2dataURL(tex:string, plugin:ExcalidrawPlugin):Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
size: {height: number, width: number},
}> {
//if network is slow, or not available, or mathjax has not yet fully loaded
try {
return await mathjaxSVG(tex, plugin);
} catch(e) {
//fallback
return await mathjaxImage2html(tex);
}
}
async function mathjaxSVG (tex:string, plugin:ExcalidrawPlugin):Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
size: {height: number, width: number},
}> {
const eq = plugin.mathjax.tex2svg(tex,{display: true, scale: 4});
const svg = eq.querySelector("svg");
if(svg) {
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(dataURL)
}
}
return null;
}
async function mathjaxImage2html(tex:string):Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
size: {height: number, width: number},
}> {
const div = document.body.createDiv();
div.style.display = "table"; //this will ensure div fits width of formula exactly
//@ts-ignore
const eq = window.MathJax.tex2chtml(tex,{display: true, scale: 4}); //scale to ensure good resolution
eq.style.margin = "3px";
eq.style.color = "black";
//ipad support - removing mml as that was causing phantom double-image blur.
const el = eq.querySelector("mjx-assistive-mml");
if(el) {
el.parentElement.removeChild(el);
}
div.appendChild(eq);
window.MathJax.typeset();
const canvas = await html2canvas(div, {backgroundColor:null}); //transparent
document.body.removeChild(div);
return {
mimeType: "image/png",
fileId: fileid() as FileId,
dataURL: canvas.toDataURL() as DataURL,
created: Date.now(),
size: {height: canvas.height, width: canvas.width}
}
}

View File

@@ -137,9 +137,10 @@ class ImageElementNotice extends Modal {
this.createForm();
}
onClose(): void {
async onClose() {
this.contentEl.empty();
if(!this.saveChanges) return;
await this.plugin.loadSettings();
this.plugin.settings.imageElementNotice = false;
this.plugin.saveSettings();
}

View File

@@ -19,7 +19,7 @@ export class Prompt extends Modal {
createForm(): void {
const div = this.contentEl.createDiv();
div.addClass("excalidarw-prompt-div");
div.addClass("excalidraw-prompt-div");
const form = div.createEl("form");
form.addClass("excalidraw-prompt-form");

View File

@@ -1,15 +1,12 @@
import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw";
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian";
import { Random } from "roughjs/bin/math";
import { BinaryFileData, DataURL, Zoom } from "@zsviczian/excalidraw/types/types";
import { nanoid } from "nanoid";
import { CASCADIA_FONT, IMAGE_TYPES, VIRGIL_FONT } from "./constants";
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import { Zoom } from "@zsviczian/excalidraw/types/types";
import { CASCADIA_FONT, VIRGIL_FONT } from "./constants";
import ExcalidrawPlugin from "./main";
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { ExportSettings } from "./ExcalidrawView";
import { ExcalidrawSettings } from "./settings";
import { html_beautify } from "js-beautify"
declare module "obsidian" {
interface Workspace {
@@ -20,16 +17,11 @@ declare module "obsidian" {
}
}
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
*/
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
let folderpath: string, filename:string;
const lastIndex = filepath.lastIndexOf("/");
return {
folderpath: normalizePath(filepath.substr(0,lastIndex)),
@@ -155,7 +147,6 @@ export const rotatedDimensions = (
];
}
export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number },
{
@@ -190,92 +181,9 @@ export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLe
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;
@@ -306,7 +214,7 @@ export const getAttachmentsFolderAndFilePath = async (app:App, activeViewFilePat
export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> => {
try {
return exportToSvg({
return await exportToSvg({
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
@@ -320,19 +228,6 @@ export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise<S
}
}
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({
@@ -363,31 +258,13 @@ export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => {
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 getImageSize = async (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 scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
@@ -415,4 +292,11 @@ export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
}
}
export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark");
export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark");
export function getIMGFilename(path:string,extension:string):string {
return path.substring(0,path.lastIndexOf('.')) + '.' + extension;
}
//export const debug = console.log.bind(window.console);
//export const debug = function(){};

View File

@@ -1,10 +1,12 @@
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("&#91;","["));}
import { FileId } from "@zsviczian/excalidraw/types/element/types";
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 fileid = customAlphabet('1234567890abcdef',40);
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg'];
export const MAX_IMAGE_SIZE = 500;
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

@@ -23,7 +23,8 @@ export default {
EXPORT_PNG: "Save as PNG next to the current file",
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
INSERT_LINK: "Insert link to file",
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
INSERT_IMAGE: "Insert image from vault",
INSERT_LATEX: "Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
ENTER_LATEX: "Enter a valid LaTeX expression",
//ExcalidrawView.ts
@@ -44,8 +45,6 @@ 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",
@@ -71,6 +70,11 @@ export default {
FILENAME_PREFIX_DESC: "The first part of the filename",
FILENAME_DATE_NAME: "Filename date",
FILENAME_DATE_DESC: "The second part of the filename",
/*SVG_IN_MD_NAME: "SVG Snapshot to markdown file",
SVG_IN_MD_DESC: "If the switch is 'on' Excalidraw will include an SVG snapshot in the markdown file. "+
"When SVG snapshots are saved to the Excalidraw.md file, drawings that include large png, jpg, gif images may take extreme long time to open in markdown view. " +
"On the other hand, SVG snapshots provide some level of platform independence and longevity to your drawings. Even if Excalidraw will no longer exist, the snapshot " +
"can be opened with an app that reads SVGs. In addition hover previews will be less resource intensive if SVG snapshots are enabled.",*/
DISPLAY_HEAD: "Display",
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
MATCH_THEME_DESC: "If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
@@ -115,6 +119,8 @@ export default {
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.",
GET_URL_TITLE_NAME: "Use iframely to resolve page title",
GET_URL_TITLE_DESC: "Use the http://iframely.server.crestify.com/iframely?url= to get title of page when dropping a link into Excalidraw",
EMBED_HEAD: "Embed & Export",
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
@@ -167,7 +173,8 @@ export default {
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",
NO_MATCH: "No file matches your query.",
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
SELECT_DRAWING: "Select the drawing you want to insert",
TYPE_FILENAME: "Type name of drawing to select.",
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
SELECT_TO_EMBED: "Select the drawing to insert into active document.",

View File

@@ -12,9 +12,10 @@ import {
MenuItem,
TAbstractFile,
Tasks,
MarkdownRenderer,
ViewState,
Notice,
loadMathJax,
MarkdownRenderer,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -35,7 +36,7 @@ import {
DARK_BLANK_DRAWING
} from "./constants";
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
import {getJSON, getSVGString} from "./ExcalidrawData";
import {getJSON, getMarkdownDrawingSection} from "./ExcalidrawData";
import {
ExcalidrawSettings,
DEFAULT_SETTINGS,
@@ -48,15 +49,22 @@ import {
import {
InsertLinkDialog
} from "./InsertLinkDialog";
import {
InsertImageDialog
} from "./InsertImageDialog";
import {
initExcalidrawAutomate,
destroyExcalidrawAutomate
destroyExcalidrawAutomate,
ExcalidrawAutomate
} from "./ExcalidrawAutomate";
import { Prompt } from "./Prompt";
import { around } from "monkey-around";
import { t } from "./lang/helpers";
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, splitFolderAndFilename, svgToBase64 } from "./Utils";
import { checkAndCreateFolder, download, embedFontsInSVG, getAttachmentsFolderAndFilePath, getIMGFilename, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, splitFolderAndFilename, svgToBase64 } from "./Utils";
import { OneOffs } from "./OneOffs";
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { MATHJAX_DATAURL } from "./mathjax";
import { config, disconnect } from "process";
declare module "obsidian" {
interface App {
@@ -73,15 +81,24 @@ export default class ExcalidrawPlugin extends Plugin {
public settings: ExcalidrawSettings;
private openDialog: OpenFileDialog;
private insertLinkDialog: InsertLinkDialog;
private insertImageDialog: InsertImageDialog;
private activeExcalidrawView: ExcalidrawView = null;
public lastActiveExcalidrawFilePath: string = null;
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
private observer: MutationObserver;
private fileExplorerObserver: MutationObserver;
public opencount:number = 0;
public ea:ExcalidrawAutomate;
//A master list of fileIds to facilitate copy / paste
public filesMaster:Map<FileId,string> = null; //fileId, path
public equationsMaster:Map<FileId,string> = null; //fileId, formula
public mathjax: any = null;
private mathjaxDiv: HTMLDivElement = null;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
this.filesMaster = new Map<FileId,string>();
this.equationsMaster = new Map<FileId,string>();
}
async onload() {
@@ -92,8 +109,8 @@ export default class ExcalidrawPlugin extends Plugin {
await this.loadSettings();
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
await initExcalidrawAutomate(this);
this.ea = await initExcalidrawAutomate(this);
this.registerView(
VIEW_TYPE_EXCALIDRAW,
(leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this)
@@ -124,6 +141,37 @@ export default class ExcalidrawPlugin extends Plugin {
patches.imageElementLaunchNotice();
this.switchToExcalidarwAfterLoad()
const self = this;
this.loadMathJax();
}
private loadMathJax() {
//loading Obsidian MathJax as fallback
this.app.workspace.onLayoutReady(()=>{
loadMathJax();
});
this.mathjaxDiv = document.body.createDiv();
this.mathjaxDiv.title = "Excalidraw MathJax Support";
this.mathjaxDiv.style.display = "none";
const iframe = this.mathjaxDiv.createEl("iframe");
const doc = iframe.contentWindow.document;
const script = doc.createElement("script");
script.type = "text/javascript";
const self = this;
script.onload = () => {
const win = iframe.contentWindow;
//@ts-ignore
win.MathJax.startup.pagePromise.then(() => {
//@ts-ignore
this.mathjax = win.MathJax;
});
};
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';
//script.src = MATHJAX_DATAURL;
doc.head.appendChild(script);
}
private switchToExcalidarwAfterLoad() {
@@ -179,32 +227,28 @@ export default class ExcalidrawPlugin extends Plugin {
img.addClass(imgAttributes.style);
const [scene,pos] = getJSON(content);
const svgSnapshot = getSVGString(content.substr(pos+scene.length));
this.ea.reset();
//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) {
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 getPNG(JSON_parse(scene),exportSettings, scale);
const png = await this.ea.createPNG(file.path,scale);
//const png = await getPNG(JSON_parse(scene),exportSettings, scale);
if(!png) return null;
img.src = URL.createObjectURL(png);
return img;
}*/
}
const svgSnapshot = (await this.ea.createSVG(file.path,true,exportSettings)).outerHTML;
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);
const el = document.createElement('div');
el.innerHTML = svgSnapshot;
const firstChild = el.firstChild;
if(firstChild instanceof SVGSVGElement) {
svg=firstChild;
}
if(!svg) return null;
svg = embedFontsInSVG(svg);
@@ -299,9 +343,17 @@ export default class ExcalidrawPlugin extends Plugin {
if(m.length == 0) return;
if(!this.hover.linkText) return;
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
if(!file || !(file instanceof TFile) || !this.isExcalidrawFile(file)) return
if(!file || !(file instanceof TFile) || !this.isExcalidrawFile(file)) return;
if(file.extension == "excalidraw") {
const svgFileName = getIMGFilename(file.path,"svg");
const svgFile = this.app.vault.getAbstractFileByPath(svgFileName);
if(svgFile && svgFile instanceof TFile) return; //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview
const pngFileName = getIMGFilename(file.path,"png");
const pngFile = this.app.vault.getAbstractFileByPath(pngFileName);
if(pngFile && pngFile instanceof TFile) return; //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview
if(file.extension === "excalidraw") {
observerForLegacyFileFormat(m,file);
return;
}
@@ -411,6 +463,7 @@ export default class ExcalidrawPlugin extends Plugin {
private registerCommands() {
this.openDialog = new OpenFileDialog(this.app, this);
this.insertLinkDialog = new InsertLinkDialog(this.app);
this.insertImageDialog = new InsertImageDialog(this);
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
@@ -653,6 +706,24 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "insert-image",
name: t("INSERT_IMAGE"),
checkCallback: (checking: boolean) => {
if (checking) {
const view = this.app.workspace.activeLeaf.view;
return (view instanceof ExcalidrawView);
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
this.insertImageDialog.start(view);
return true;
}
else return false;
}
},
});
this.addCommand({
id: "insert-LaTeX-symbol",
name: t("INSERT_LATEX"),
@@ -662,13 +733,14 @@ export default class ExcalidrawPlugin extends Plugin {
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'','$\\theta$');
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'','\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}');
prompt.openAndGetValue( async (formula:string)=> {
if(!formula) return;
const el = createEl('p');
await MarkdownRenderer.renderMarkdown(formula,el,'',this)
view.addText(el.getText());
el.empty();
const ea = this.ea;
ea.reset();
await ea.addLaTex(0,0,formula);
ea.setView(view);
ea.addElementsToView(true,true);
});
return true;
}
@@ -945,17 +1017,22 @@ export default class ExcalidrawPlugin extends Plugin {
//save Excalidraw leaf and update embeds when switching to another leaf
const activeLeafChangeEventHandler = async (leaf:WorkspaceLeaf) => {
const activeExcalidrawView = self.activeExcalidrawView;
const newActiveview:ExcalidrawView = (leaf.view instanceof ExcalidrawView) ? leaf.view as ExcalidrawView : null;
if(activeExcalidrawView && activeExcalidrawView != newActiveview) {
await activeExcalidrawView.save(false);
if(activeExcalidrawView.file) {
self.triggerEmbedUpdates(activeExcalidrawView.file.path);
}
}
const newActiveview:ExcalidrawView = (leaf.view instanceof ExcalidrawView) ? leaf.view : null;
self.activeExcalidrawView = newActiveview;
if(newActiveview) {
self.lastActiveExcalidrawFilePath = newActiveview.file?.path;
}
if(activeExcalidrawView && activeExcalidrawView != newActiveview) {
if(activeExcalidrawView.leaf != leaf) {
//if loading new view to same leaf then don't save. Excalidarw view will take care of saving anyway.
//avoid double saving
await activeExcalidrawView.save(false);
}
if(activeExcalidrawView.file) {
self.triggerEmbedUpdates(activeExcalidrawView.file.path);
}
}
};
self.registerEvent(
self.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler)
@@ -971,8 +1048,9 @@ export default class ExcalidrawPlugin extends Plugin {
excalidrawLeaves.forEach((leaf) => {
this.setMarkdownView(leaf);
});
this.settings.drawingOpenCount += this.opencount;
this.settings.loadCount++;
if(this.mathjaxDiv) document.body.removeChild(this.mathjaxDiv);
//this.settings.drawingOpenCount += this.opencount;
//this.settings.loadCount++;
//this.saveSettings();
}
@@ -1065,21 +1143,7 @@ export default class ExcalidrawPlugin extends Plugin {
return this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
}
const blank = this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank,'<SVG></SVG>');
}
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)
+ (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%%';
return FRONTMATTER + '\n' + getMarkdownDrawingSection(blank);
}
/**
@@ -1090,7 +1154,6 @@ export default class ExcalidrawPlugin extends Plugin {
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;
@@ -1105,7 +1168,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t"),svgString);
return outString + getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t"));
}
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
@@ -1124,10 +1187,17 @@ export default class ExcalidrawPlugin extends Plugin {
}
public async setMarkdownView(leaf: WorkspaceLeaf) {
const state=leaf.view.getState();
await leaf.setViewState({
type:VIEW_TYPE_EXCALIDRAW,
state: {file:null}
});
await leaf.setViewState(
{
type: "markdown",
state: leaf.view.getState(),
state: state,
popstate: true,
} as ViewState,
{ focus: true }

5
src/mathjax.ts Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,6 @@ import {
DropdownComponent,
PluginSettingTab,
Setting,
TFile
} from 'obsidian';
import { VIEW_TYPE_EXCALIDRAW } from './constants';
import ExcalidrawView from './ExcalidrawView';
@@ -15,7 +14,7 @@ export interface ExcalidrawSettings {
templateFilePath: string,
drawingFilenamePrefix: string,
drawingFilenameDateTime: string,
//displaySVGInPreview: boolean,
displaySVGInPreview: boolean,
width: string,
matchTheme: boolean,
matchThemeAlways: boolean,
@@ -28,6 +27,7 @@ export interface ExcalidrawSettings {
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
forceWrap: boolean,
pageTransclusionCharLimit: number,
iframelyAllowed: boolean,
pngExportScale: number,
exportWithTheme: boolean,
exportWithBackground: boolean,
@@ -52,7 +52,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,
matchThemeAlways: false,
@@ -65,6 +65,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
allowCtrlClick: true,
forceWrap: false,
pageTransclusionCharLimit: 200,
iframelyAllowed: true,
pngExportScale: 1,
exportWithTheme: true,
exportWithBackground: true,
@@ -196,7 +197,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text.setValue(this.plugin.settings.drawingFilenameDateTime);
filenameEl.innerHTML = getFilenameSample();
this.applySettingsUpdate();
}));
}));
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
@@ -282,7 +283,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setPlaceholder(t("INSERT_EMOJI"))
.setValue(this.plugin.settings.linkPrefix)
.onChange((value) => {
console.log(value);
this.plugin.settings.linkPrefix = value;
this.applySettingsUpdate(true);
}));
@@ -342,10 +342,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
.setName(t("GET_URL_TITLE_NAME"))
.setDesc(t("GET_URL_TITLE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.iframelyAllowed)
.onChange(async (value) => {
this.plugin.settings.iframelyAllowed = value;
this.applySettingsUpdate();
}));
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
//Removed in 1.4.0 when implementing ImageElement.
/* new Setting(containerEl)
new Setting(containerEl)
.setName(t("EMBED_PREVIEW_SVG_NAME"))
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
.addToggle(toggle => toggle
@@ -353,7 +363,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.displaySVGInPreview = value;
this.applySettingsUpdate();
}));*/
}));
new Setting(containerEl)
.setName(t("EMBED_WIDTH_NAME"))

View File

@@ -51,7 +51,7 @@ button.ToolIcon_type_button[title="Export"] {
.excalidraw-prompt-div {
display: flex;
max-width: 600px;
max-width: 800px;
}
.excalidraw-prompt-form {
@@ -95,3 +95,19 @@ li[data-testid] {
margin-bottom: 20px;
}
/*hide text elements in markdown preview*/
.markdown-preview-view.excalidraw-hide-preview-text {
font-size: 0px;
}
.markdown-preview-view mark,
.markdown-preview-view.excalidraw-hide-preview-text span:not(.image-embed),
.markdown-preview-view.excalidraw-hide-preview-text h1,
.markdown-preview-view.excalidraw-hide-preview-text h2,
.markdown-preview-view.excalidraw-hide-preview-text h3,
.markdown-preview-view.excalidraw-hide-preview-text h4,
.markdown-preview-view.excalidraw-hide-preview-text h5 {
display: none;
}

View File

@@ -1,3 +1,4 @@
{
"1.4.0": "0.11.13"
"1.4.7": "0.12.16",
"1.4.2": "0.11.13"
}

109
yarn.lock
View File

@@ -1067,21 +1067,16 @@
dependencies:
"@types/estree" "*"
"@zsviczian/excalidraw@0.10.0-obsidian-3":
"integrity" "sha512-mLpWNJ9whVpsREqbg4D9Kt6ftlgSzoxHdukFbbMlhLwtQ2pRh+pgmkssX2hmeWkGDvkAtPW30+qh4LqZBA3oFQ=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-3.tgz"
"version" "0.10.0-obsidian-3"
"@zsviczian/excalidraw@0.10.0-obsidian-8":
"integrity" "sha512-GLyOdnA2yH+acXjTaIuy6oeUz9JL8Wj8n/qzdTfqGhAWz6wPoXXEewTvZIsW4pij/0MC0A+unvHNK/k+YDbpBA=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-8.tgz"
"version" "0.10.0-obsidian-8"
"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"
@@ -2275,6 +2270,16 @@
"mixin-deep" "^1.2.0"
"pascalcase" "^0.1.1"
"base64-arraybuffer@^0.2.0":
"integrity" "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
"resolved" "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz"
"version" "0.2.0"
"base64-arraybuffer@^1.0.1":
"integrity" "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA=="
"resolved" "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz"
"version" "1.0.1"
"base64-js@^1.0.2":
"integrity" "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
"resolved" "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
@@ -2993,7 +2998,7 @@
dependencies:
"delayed-stream" "~1.0.0"
"commander@^2.11.0", "commander@^2.19.0":
"commander@^2.11.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"
@@ -3053,14 +3058,6 @@
"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"
@@ -3278,6 +3275,13 @@
"resolved" "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz"
"version" "0.0.4"
"css-line-break@2.0.1":
"integrity" "sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w=="
"resolved" "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz"
"version" "2.0.1"
dependencies:
"base64-arraybuffer" "^0.2.0"
"css-loader@0.28.7":
"integrity" "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg=="
"resolved" "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz"
@@ -3773,16 +3777,6 @@
"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"
@@ -5079,6 +5073,14 @@
"pretty-error" "^2.0.2"
"toposort" "^1.0.0"
"html2canvas@^1.3.2":
"integrity" "sha512-4+zqv87/a1LsaCrINV69wVLGG8GBZcYBboz1JPWEgiXcWoD9kroLzccsBRU/L9UlfV2MAZ+3J92U9IQPVMDeSQ=="
"resolved" "https://registry.npmjs.org/html2canvas/-/html2canvas-1.3.2.tgz"
"version" "1.3.2"
dependencies:
"css-line-break" "2.0.1"
"text-segmentation" "^1.0.2"
"htmlparser2@^6.1.0":
"integrity" "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A=="
"resolved" "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz"
@@ -6064,17 +6066,6 @@
"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"
@@ -6463,7 +6454,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.1.5":
"lru-cache@^4.0.1":
"integrity" "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="
"resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz"
"version" "4.1.5"
@@ -6691,11 +6682,6 @@
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"
@@ -6848,13 +6834,6 @@
"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"
@@ -7861,11 +7840,6 @@
"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"
@@ -8632,7 +8606,7 @@
dependencies:
"semver" "^5.0.3"
"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":
"semver@^5.0.3", "semver@^5.1.0", "semver@^5.3.0", "semver@^5.5.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"
@@ -8790,11 +8764,6 @@
"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"
@@ -9315,6 +9284,13 @@
"read-pkg-up" "^1.0.1"
"require-main-filename" "^1.0.1"
"text-segmentation@^1.0.2":
"integrity" "sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw=="
"resolved" "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz"
"version" "1.0.2"
dependencies:
"utrie" "^1.0.1"
"text-table@~0.2.0", "text-table@0.2.0":
"integrity" "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ="
"resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
@@ -9735,6 +9711,13 @@
"resolved" "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
"version" "1.0.1"
"utrie@^1.0.1":
"integrity" "sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg=="
"resolved" "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz"
"version" "1.0.1"
dependencies:
"base64-arraybuffer" "^1.0.1"
"uuid@^3.0.1", "uuid@^3.3.2":
"integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
"resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"