Compare commits

...

33 Commits

Author SHA1 Message Date
Zsolt Viczian
e0895d00ae 1.4.11 2021-11-21 19:18:16 +01:00
Zsolt Viczian
9e5cce2f6f Image inversion solved 2021-11-21 15:01:24 +01:00
Zsolt Viczian
272204263d svg improvements WIP 2021-11-20 15:29:56 +01:00
Zsolt Viczian
16b5472024 cleanup exportToBlob, CTRL_OR_CMD 2021-11-20 10:43:54 +01:00
zsviczian
d6a4350e1e Merge pull request #257 from JaspalSuri/patch-1
fix: correct some spelling mistakes
2021-11-19 11:18:03 +01:00
Jaspal Suri
3602fbb807 fix: correct some spelling mistakes
This is not a comprehensive review.
2021-11-18 23:14:13 -08:00
Zsolt Viczian
ecce315924 1.4.10 2021-11-16 21:44:23 +01:00
Zsolt Viczian
eee470dad5 corrected message 2021-11-15 22:08:00 +01:00
Zsolt Viczian
636c4bcafe 1.4.9 2021-11-15 22:05:44 +01:00
zsviczian
4eb2b293e2 embed excalidraw.md instead of svg or png 2021-11-15 14:12:38 +01:00
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
Zsolt Viczian
6e3cf60eab 1.4.0 2021-10-24 20:40:36 +02:00
zsviczian
32fdbf9dc2 Update README.md 2021-10-24 20:29:43 +02:00
Zsolt Viczian
1aed684ebe Fixed Mac CTRL vs. CMD button issue 2021-10-24 20:07:22 +02:00
Zsolt Viczian
1ad791d9bc 1.4.0 pre-release 2 2021-10-24 13:40:11 +02:00
zsviczian
bb9925024d Merge pull request #209 from zsviczian/Image-Element
Image element
2021-10-24 06:56:12 +02:00
23 changed files with 1940 additions and 981 deletions

View File

@@ -1,58 +1,54 @@
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)
# Important notice to the 1.2.0 update
This version comes with tons of new features and possibilities.
Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. If you want, you can also continue to use your exisiting drawings in compatibility mode (e.g. if you use Logseq and Obsidian in parallel). During conversion your existing `*.excalidraw` files will be replaced with new `*.excalidraw.md` files.
## Conversion and compatibility
To convert files you have the following options:
- Click `CONVERT FILES` in the migration dialog when installing 1.2.0
- In the Command Palette select `Excalidraw: Convert *.excalidraw files to *.excalidraw.md files` to convert all `*.excalidraw` files to `*.excalidraw.md` files.
- To convert files individually:
- Right click an `*.excalidraw` file in File Explorer and select one of the following options:
- `*.excalidraw => *.excalidraw.md`
- `*.excalidraw => *.md (Logseq compatibility)`: This option will retain the original *.excalidraw file next to the new Obsidian format. Make sure you also enable additional `Compatibility features` in `Settings` for a full solution.
- Open a legacy `*.excalidraw` file and select `Convert to new format` from the `Options Menu` in the Excalidraw view.
# Video walkthrough
| | | |
|----|----|----|
|[![Obsidian-Excalidraw 1.2.0 update - Major IMPROVEMENTS](https://user-images.githubusercontent.com/14358394/124356817-7b3f3d80-dc18-11eb-932d-363bb373c5ab.jpg)](https://youtu.be/UxJLLYtgDKE)|[![1 Getting Started](https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg)](https://youtu.be/sY4FoflGaiM)|[![2 Basic shapes and features](https://user-images.githubusercontent.com/14358394/125160312-8a743d00-e17c-11eb-9fa2-490ef4cbd59e.jpg)](https://youtu.be/Iy_oVTq12Gw)|
|[![3 Groups](https://user-images.githubusercontent.com/14358394/125160323-96f89580-e17c-11eb-9bce-8eb1067a51bb.jpg)](https://youtu.be/QOL1KF7-kdc)|[![4 Stencil](https://user-images.githubusercontent.com/14358394/125160332-9f50d080-e17c-11eb-98e9-fec60fe147d9.jpg)](https://youtu.be/aSgcbfspvfo)|[![5 embedding](https://user-images.githubusercontent.com/14358394/125160341-a546b180-e17c-11eb-9de8-d87fdc844c9c.jpg)](https://youtu.be/MaJ5jJwBRWs)|
|[![6 Links](https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg)](https://youtu.be/MXzeCOEExNo)|[![7 Markdown](https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg)](https://youtu.be/R0IAg0s-wQE)|[![8 Templates](https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg)](https://youtu.be/ibdS7ykwpW4)|
|[![9 Excalidraw Automate](https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg)](https://youtu.be/VRZVujfVab0)|[![10 Miscellaneous](https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg)](https://youtu.be/D1iBYo1_jjc)||
|[![9 Excalidraw Automate](https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg)](https://youtu.be/VRZVujfVab0)|[![10 Miscellaneous](https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg)](https://youtu.be/D1iBYo1_jjc)|[![Image Elements](https://user-images.githubusercontent.com/14358394/138607067-ccb62f92-48a4-4880-ac6e-68c1bf86ac2c.png)](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
# Key features
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
- CTRL+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
- Settings will allow you to customzie Excalidraw to your needs:
- The plugin aims to integrate Excalidraw seamlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
- CTRL/CMD+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
- Settings will allow you to customize Excalidraw to your needs:
- Default folder for new drawings and define custom filename pattern for new drawings.
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- If portability is important to you: Auto-export SVG and/or PNG files including keep-in-sync feature so you can embed svg/png into your documents instead of embedding excalidraw files.
- If portability is important to you: Auto-export SVG and/or PNG files including keep-in-sync feature so you can embed SVG/PNG into your documents instead of embedding excalidraw files.
- Specify the default width of embedded drawings.
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy .excalidraw files.
- Experimental feature to add custom TAG to file expolorer to mark drawing files.
- Experimental feature to add custom TAG to file explorer to mark drawing files.
- Enable / disable autosave.
- You can customize the size and position of the embedded images using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element. Check below and styles.css for more insight.
- You can customize the size and position of the embedded images using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via CSS. Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element. Check below and styles.css for more insight.
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enalbled.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enabled.
- Links in drawings will show up in backlinks of documents
- Transclusions are supported
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
- `![[myfile#section]]` also works, this will transclude the section
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience you can also use the command palette to insert links into drawings
- CTRL/META + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
- CTRL/META + CLICK a text element to open it as a link.
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
- CTRL/META + SHIFT + CLICK to open the file in a new pane
- CTRL/META + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
- CTRL/CMD + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
- CTRL/CMD + CLICK a text element to open it as a link.
- CTRL/CMD + ALT + CLICK to create the file (if it does not yet exist) and open it
- 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.
- You can drag and drop images from outside obsidian onto Excalidraw. These images will be embedded into your drawing and saved to Obsidian.
- You can drag and drop text from Markdown views onto Excalidraw.
- You can drag and drop web addresses from your browser and they will become links.
- Image support
- On iOS and Android you can add images from your camera by pressing the add image button in Excalidraw.
- You can copy/paste images into your drawing. Images will be saved in your vault.
- You can drag and drop images as explained above.
- Since 1.2.0 Drawing files are stored in Markdown files
- You can add tags to drawings
- You can add metadata to the YAML front matter of drawings
@@ -62,18 +58,17 @@ To convert files you have the following options:
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- Includes full [QuickAdd](https://github.com/chhoumann/quickadd), [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/). I also have a [YouTube ExcalidrawAutomate Playlist](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1IR4nXxSlMA4PA5E-qpyHZ) with lots of examples.
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
# Known issues
- Mobile support
- Positioning of the pen gets misaligned after you open the command palette.
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
- Text elements "jumps off screen" when editing, if drawing is zoomed in and text element does not fit the visible screen area. I am working on a resolution.
# Tips and tricks
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode. Note that Ozan's plugin will only display Excalidraw drawings if the link ends with `.md` or `.excalidraw`. i.e. the following drawing will show in Edit Mode `![[My Drawing.md]]`, but wiki links such as `[[My Drawing]]` will not.
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode.
# Feedback, questions, ideas, problems
Join the conversation about the Excalidraw plugin on [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian)

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.3.20",
"minAppVersion": "0.12.0",
"version": "1.4.11",
"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-2",
"@zsviczian/excalidraw": "0.10.0-obsidian-12",
"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",

204
src/EmbeddedFileLoader.ts Normal file
View File

@@ -0,0 +1,204 @@
import { exportToBlob } from "@zsviczian/excalidraw";
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 { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import {errorlog, getImageSize, svgToBase64 } from "./Utils";
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
export type FileData = BinaryFileData & {
size: {
height: number;
width: number;
},
hasSVGwithBitmap: boolean
};
export class EmbeddedFilesLoader {
private plugin:ExcalidrawPlugin;
private processedFiles: Map<string,number> = new Map<string,number>();
private isDark:boolean;
public terminate=false;
constructor(plugin: ExcalidrawPlugin, isDark?:boolean) {
this.plugin = plugin;
this.isDark = isDark;
}
public async getObsidianImage (
file: TFile
):Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
hasSVGwithBitmap: boolean,
size: {height: number, width: number},
}> {
if(!this.plugin || !file) return null;
//to block infinite loop of recursive loading of images
let count=this.processedFiles.has(file.path) ? this.processedFiles.get(file.path):0;
if(file.extension==="md" && count>2) {
new Notice(t("INFINITE_LOOP_WARNING") + file.path,6000);
return null;
}
this.processedFiles.set(file.path,count+1);
let hasSVGwithBitmap = false;
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 (isDark:boolean) => {
const exportSettings:ExportSettings = {
withBackground: false,
withTheme: false,
};
this.plugin.ea.reset();
const svg = await this.plugin.ea.createSVG(file.path,true,exportSettings,this,null);
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll("image:not([href^='data:image/svg'])");
if(imageList.length>0) hasSVGwithBitmap = true;
if(hasSVGwithBitmap && isDark) {
const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
u.setAttribute("filter", THEME_FILTER);
});
});
}
if(!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) hasSVGwithBitmap=true;
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return dURL as DataURL;
}
const excalidrawSVG = isExcalidrawFile
? await getExcalidrawSVG(this.isDark)
: 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,
fileId: await generateIdFromFile(ab),
dataURL,
created: file.stat.mtime,
hasSVGwithBitmap,
size
}
}
public async loadSceneFiles (
excalidrawData: ExcalidrawData,
view: ExcalidrawView,
addFiles:Function,
sourcePath:string,
) {
const app = this.plugin.app;
const entries = excalidrawData.getFileEntries();
if(this.isDark===undefined) {
this.isDark = excalidrawData.scene.appState.theme==="dark";
}
let entry;
let files:FileData[] = [];
while(!this.terminate && !(entry = entries.next()).done) {
if(!entry.value[1].isLoaded || entry.value[1].hasSVGwithBitmap) {
const file = app.metadataCache.getFirstLinkpathDest(entry.value[1].path,sourcePath);
if(file && file instanceof TFile) {
const data = await this.getObsidianImage(file);//,theme);
if(data) {
files.push({
mimeType : data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
created: data.created,
size: data.size,
hasSVGwithBitmap: data.hasSVGwithBitmap
});
}
}
}
}
let equation;
const equations = excalidrawData.getEquationEntries();
while(!this.terminate && !(equation = equations.next()).done) {
if(!excalidrawData.getEquation(equation.value[0]).isLoaded) {
const latex = equation.value[1].latex;
const data = await tex2dataURL(latex, this.plugin);
if(data) {
files.push({
mimeType : data.mimeType,
id: equation.value[0],
dataURL: data.dataURL,
created: data.created,
size: data.size,
hasSVGwithBitmap: false
});
}
}
}
if(this.terminate) return;
try { //in try block because by the time files are loaded the user may have closed the view
addFiles(files,view);
} catch(e) {
errorlog({where:"EmbeddedFileLoader.loadSceneFiles", error: 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

@@ -4,162 +4,175 @@ import {
StrokeStyle,
StrokeSharpness,
ExcalidrawElement,
FileId,
} from "@zsviczian/excalidraw/types/element/types";
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 { AppState } from "@zsviczian/excalidraw/types/types";
import { embedFontsInSVG, getPNG, getSVG, scaleLoadedImage, wrapText } from "./Utils";
import { AppState, DataURL } from "@zsviczian/excalidraw/types/types";
import { EmbeddedFilesLoader, FileData, MimeType } 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,
theme?:string
):Promise<SVGSVGElement>;
createPNG (
templatePath?:string,
scale?:number,
exportSettings?:ExportSettings,
loader?:EmbeddedFilesLoader,
theme?: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):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 +262,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 +296,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,10 +353,16 @@ 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),
theme?:string,
):Promise<SVGSVGElement> {
const automateElements = this.getElements();
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
let elements = template ? template.elements : [];
const template = templatePath ? (await getTemplate(this.plugin,templatePath,true,loader)) : null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
const svg = await getSVG(
{//createDrawing
@@ -350,38 +371,45 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: template?.appState?.theme ?? this.canvas.theme,
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?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme
}
)
return embedFont ? embedFontsInSVG(svg) : svg;
if(template?.hasSVGwithBitmap) svg.setAttribute("hasbitmap","true");
return embedFont ? embedFontsInSVG(svg) : svg;
},
async createPNG(templatePath?:string, scale:number=1) {
async createPNG(
templatePath?:string,
scale:number=1,
exportSettings?:ExportSettings,
loader:EmbeddedFilesLoader = new EmbeddedFilesLoader(this.plugin),
theme?:string
) {
const automateElements = this.getElements();
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
let elements = template ? template.elements : [];
const template = templatePath ? (await getTemplate(this.plugin,templatePath,true,loader)) : null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
return getPNG(
return await getPNG(
{
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: template?.appState?.theme ?? this.canvas.theme,
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?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme
},
scale
)
@@ -477,12 +505,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 +558,17 @@ 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,this.canvas.theme==="dark")
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,
hasSVGwithBitmap: image.hasSVGwithBitmap,
latex: 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 +580,24 @@ 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,
hasSVGwithBitmap: false,
latex: 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 +773,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
onDropHook:null,
};
await initFonts();
return window.ExcalidrawAutomate;
}
export function destroyExcalidrawAutomate() {
@@ -738,6 +789,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 +797,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 +868,26 @@ 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
hasSVGwithBitmap:boolean
}> {
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,'');
let hasSVGwithBitmap = false;
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 +896,7 @@ async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promi
appState: excalidrawData.scene.appState,
frontmatter: "",
files: excalidrawData.scene.files,
svgSnapshot: null,
hasSVGwithBitmap
};
}
@@ -847,22 +906,30 @@ 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:FileData[], view:any)=>{
if(!fileArray || fileArray.length===0) return;
for(const f of fileArray) {
excalidrawData.scene.files[f.id] = f;
if(f.hasSVGwithBitmap) hasSVGwithBitmap = true;
excalidrawData.scene.files[f.id] = {
mimeType : f.mimeType,
id: f.id,
dataURL: f.dataURL,
created: f.created
}
}
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,
hasSVGwithBitmap
};
};
return {
@@ -870,7 +937,7 @@ async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promi
appState: {},
frontmatter: null,
files: [],
svgSnapshot: null,
hasSVGwithBitmap
}
}

View File

@@ -1,9 +1,10 @@
import { App, normalizePath, TFile } from "obsidian";
import { App, TFile } from "obsidian";
import {
nanoid,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
FRONTMATTER_KEY_DEFAULT_MODE,
} from "./constants";
import { measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
@@ -11,8 +12,8 @@ import {
JSON_parse
} from "./constants";
import { TextMode } from "./ExcalidrawView";
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, 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 +55,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 +74,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 +92,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,{path:string,hasSVGwithBitmap:boolean,isLoaded:boolean}> = null; //fileId, path
private equations:Map<FileId,{latex:string,isLoaded:boolean}> = null; //fileId, path
private compatibilityMode:boolean = false;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
this.files = new Map<FileId,string>();
this.files = new Map<FileId,{path:string,hasSVGwithBitmap:boolean,isLoaded:boolean}>();
this.equations = new Map<FileId,{latex:string,isLoaded:boolean}>();
}
/**
@@ -117,6 +113,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
@@ -153,7 +150,9 @@ export class ExcalidrawData {
this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
}
this.svgSnapshot = getSVGString(data.substr(pos+scene.length));
if(this.plugin.settings.matchThemeAlways) {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
data = data.substring(0,pos);
@@ -188,12 +187,25 @@ 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,
{
path:parts.value[2],
hasSVGwithBitmap:undefined,
isLoaded:false
});
}
//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,{latex:parts.value[2],isLoaded:false});
}
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
@@ -215,7 +227,11 @@ export class ExcalidrawData {
if(!this.scene.files) {
this.scene.files = {}; //loading legacy scenes without the files element
}
if(this.plugin.settings.matchThemeAlways) {
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;
@@ -248,7 +264,7 @@ export class ExcalidrawData {
//first get scene text elements
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
for (const te of texts) {
this.updateTextElement(te,await this.getText(te.id),forceupdate);
this.updateTextElement(te,(await this.getText(te.id))??te.text,forceupdate); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
}
}
@@ -262,7 +278,7 @@ export class ExcalidrawData {
return this.textElements.get(id).parsed;
}
//console.log("raw",this.textElements.get(id).raw);
return this.textElements.get(id).raw;
return this.textElements.get(id)?.raw;
}
/**
@@ -481,20 +497,29 @@ export class ExcalidrawData {
for(const key of this.textElements.keys()){
outString += this.textElements.get(key).raw+' ^'+key+'\n\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 += (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).latex + '$$\n';
}
outString += '\n';
}
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot);
if(this.files.size>0) {
for(const key of this.files.keys()) {
outString += key +': [['+this.files.get(key).path + ']]\n';
}
}
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)) {
@@ -503,11 +528,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) {
@@ -519,7 +551,13 @@ 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,
{
path:filepath,
hasSVGwithBitmap:false,
isLoaded:true
});
}
}
return dirty;
@@ -581,6 +619,20 @@ export class ExcalidrawData {
this.textElements.delete(id);
}
public getOpenMode():{viewModeEnabled:boolean,zenModeEnabled:boolean} {
const fileCache = this.app.metadataCache.getFileCache(this.file);
let mode = this.plugin.settings.defaultMode;
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_DEFAULT_MODE]!=null) {
mode = fileCache.frontmatter[FRONTMATTER_KEY_DEFAULT_MODE];
}
switch(mode) {
case "zen": return {viewModeEnabled:false,zenModeEnabled:true};
case "view": return {viewModeEnabled:true,zenModeEnabled:false};
default: return {viewModeEnabled:false,zenModeEnabled:false};
}
}
private setLinkPrefix():boolean {
const linkPrefix = this.linkPrefix;
const fileCache = this.app.metadataCache.getFileCache(this.file);
@@ -614,4 +666,90 @@ 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, data:{path:string,hasSVGwithBitmap:boolean,isLoaded:boolean}) {
//always store absolute path because in case of paste, relative path may not resolve ok
const file = this.app.metadataCache.getFirstLinkpathDest(data.path,this.file.path);
const p = file?.path ?? data.path;
this.files.set(
fileId,
{
path:p,
hasSVGwithBitmap:data.hasSVGwithBitmap,
isLoaded:data.isLoaded
});
this.plugin.filesMaster.set(
fileId,
{
path:p,
hasSVGwithBitmap:data.hasSVGwithBitmap
});
}
public getFile(fileId:FileId):{path:string,hasSVGwithBitmap?:boolean,isLoaded:boolean} {
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,
{
isLoaded:false,
...this.plugin.filesMaster.get(fileId)
});
return true;
}
return false;
}
public setEquation(fileId:FileId, data:{latex:string,isLoaded:boolean}) {
this.equations.set(fileId,{latex:data.latex,isLoaded:data.isLoaded});
this.plugin.equationsMaster.set(fileId,data.latex);
}
public getEquation(fileId: FileId):{latex:string,isLoaded:boolean} {
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,{latex:this.plugin.equationsMaster.get(fileId),isLoaded:false});
return true;
}
return false;
}
}

View File

@@ -19,9 +19,6 @@ import {
import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
EXCALIDRAW_LIB_HEADER,
VIRGIL_FONT,
CASCADIA_FONT,
DISK_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME,
@@ -30,18 +27,18 @@ import {
TEXT_DISPLAY_PARSED_ICON_NAME,
FULLSCREEN_ICON_NAME,
JSON_parse,
IMAGE_TYPES
IMAGE_TYPES,
CTRL_OR_CMD,
} 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, errorlog, getIMGFilename, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, 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, FileData } from "./EmbeddedFileLoader";
export enum TextMode {
parsed,
@@ -59,9 +56,34 @@ export interface ExportSettings {
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export const addFiles = (files:FileData[], view: ExcalidrawView) => {
if(!files || files.length === 0 || !view) return;
const [dirty, scene] = scaleLoadedImage(view.getScene(),files);
if(dirty) {
view.excalidrawAPI.updateScene({
elements: scene.elements,
appState: scene.appState,
commitToHistory: false,
});
}
files.forEach((f:FileData)=>{
if(view.excalidrawData.hasFile(f.id)) {
const path = view.excalidrawData.getFile(f.id).path;
view.excalidrawData.setFile(f.id,{path,hasSVGwithBitmap:f.hasSVGwithBitmap,isLoaded:true});
}
if(view.excalidrawData.hasEquation(f.id)) {
const latex = view.excalidrawData.getEquation(f.id).latex;
view.excalidrawData.setEquation(f.id,{latex,isLoaded:true});
}
});
view.excalidrawAPI.addFiles(files);
}
export default class ExcalidrawView extends TextFileView {
private excalidrawData: ExcalidrawData;
private getScene: Function = null;
public excalidrawData: ExcalidrawData;
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 +131,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 +153,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 () => {
@@ -148,6 +170,7 @@ export default class ExcalidrawView extends TextFileView {
async save(preventReload:boolean=true) {
if(!this.getScene) return;
if(!this.isLoaded) return;
this.preventReload = preventReload;
this.dirty = null;
const scene = this.getScene();
@@ -158,8 +181,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 +204,16 @@ 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"));
//this should be removed at a later time. Left it here to remediate 1.4.9 mistake
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");
}
//end of remove
return header + this.excalidrawData.generateMD();
}
if(this.compatibilityMode) {
@@ -258,9 +287,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).latex;
const prompt = new Prompt(this.app, t("ENTER_LATEX"),equation,'');
prompt.openAndGetValue( async (formula:string)=> {
if(!formula) return;
this.excalidrawData.setEquation(selectedImage.fileId,{latex:formula,isLoaded:false});
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).path;
}
}
}
@@ -328,6 +368,18 @@ export default class ExcalidrawView extends TextFileView {
this.setupAutosaveTimer();
}
public setTheme(theme:string) {
if(!this.excalidrawRef) return;
const st:AppState = this.excalidrawAPI.getAppState();
this.excalidrawAPI.updateScene({
appState: {
...st,
theme: theme,
},
commitToHistory: false,
});
}
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
this.textMode = textMode;
if(textMode == TextMode.parsed) {
@@ -389,14 +441,15 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawAPI.history.clear();
}
private isLoaded:boolean = false;
async setViewData (data: string, clear: boolean = false) {
this.isLoaded = false;
if(clear) this.clear();
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
this.app.workspace.onLayoutReady(async ()=>{
this.dirty = null;
this.compatibilityMode = this.file.extension === "excalidraw";
await this.plugin.loadSettings();
this.plugin.opencount++;
if(this.compatibilityMode) {
this.textIsRaw_Element.hide();
this.textIsParsed_Element.hide();
@@ -405,11 +458,12 @@ export default class ExcalidrawView extends TextFileView {
new Notice(t("COMPATIBILITY_MODE"),4000);
}
} else {
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
const textMode = getTextMode(data);
this.changeTextMode(textMode,false);
try {
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
} catch(e) {
errorlog({where:"ExcalidrawView.setViewData",error:e});
new Notice( "Error loading drawing:\n"
+ e.message
+ ((e.message === "Cannot read property 'index' of undefined")
@@ -420,20 +474,41 @@ export default class ExcalidrawView extends TextFileView {
return;
}
}
await this.loadDrawing(true)
await this.loadDrawing(true);
this.isLoaded=true;
});
}
private activeLoader:EmbeddedFilesLoader = null;
private async loadSceneFiles(isDark?:boolean) {
if(this.activeLoader) this.activeLoader.terminate=true;
const loader = new EmbeddedFilesLoader(this.plugin,isDark);
this.activeLoader = loader;
loader.loadSceneFiles(
this.excalidrawData,
this,
(files:FileData[], view:ExcalidrawView) => {
this.activeLoader = null;
if(!files || !view) return;
addFiles(files,view);
},
this.file?.path
);
}
/**
*
* @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;
const om = this.excalidrawData.getOpenMode();
if(this.excalidrawRef) {
const viewModeEnabled = this.excalidrawAPI.getAppState().viewModeEnabled;
const zenModeEnabled = this.excalidrawAPI.getAppState().zenModeEnabled;
//isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes
const viewModeEnabled = !this.isLoaded ? om.viewModeEnabled : this.excalidrawAPI.getAppState().viewModeEnabled;
const zenModeEnabled = !this.isLoaded ? om.zenModeEnabled : this.excalidrawAPI.getAppState().zenModeEnabled;
this.excalidrawAPI.updateScene({
elements: excalidrawData.elements,
appState: {
@@ -447,11 +522,15 @@ 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));
this.loadSceneFiles();
} else {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
appState: {
zenModeEnabled: om.zenModeEnabled,
viewModeEnabled: om.viewModeEnabled,
... excalidrawData.appState
},
files: excalidrawData.files,
libraryItems: await this.getLibrary(),
});
@@ -459,21 +538,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 +560,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);
}
@@ -559,7 +617,7 @@ export default class ExcalidrawView extends TextFileView {
.setIcon(PNG_ICON_NAME)
.onClick( async (ev)=> {
if(!this.getScene || !this.file) return;
if(ev.ctrlKey || ev.metaKey) {
if(ev[CTRL_OR_CMD]) { //.ctrlKey||ev.metaKey) {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
@@ -584,7 +642,7 @@ export default class ExcalidrawView extends TextFileView {
.setIcon(SVG_ICON_NAME)
.onClick(async (ev)=> {
if(!this.getScene || !this.file) return;
if(ev.ctrlKey || ev.metaKey) {
if(ev[CTRL_OR_CMD]) { //.ctrlKey||ev.metaKey) {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
@@ -603,8 +661,10 @@ export default class ExcalidrawView extends TextFileView {
}
async getLibrary() {
const data = JSON_parse(this.plugin.getStencilLibrary());
return data?.library ? data.library : [];
const data:any = this.plugin.getStencilLibrary();
return data?.library
? data.library
: (data?.libraryItems??[]);
}
@@ -649,7 +709,8 @@ 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));
//console.log({where:"ExcalidrawView.React.ReadyPromise"});
this.loadSceneFiles();
});
}, [excalidrawRef]);
@@ -728,14 +789,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 +830,23 @@ 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,
{
path:images[k].file,
hasSVGwithBitmap:images[k].hasSVGwithBitmap,
isLoaded:true
});
}
if(images[k].tex) {
this.excalidrawData.setEquation(
images[k].id,
{
latex:images[k].latex,
isLoaded:true
});
}
});
this.excalidrawAPI.addFiles(files);
}
@@ -785,7 +863,7 @@ export default class ExcalidrawView extends TextFileView {
const files = this.excalidrawAPI.getFiles();
if(files) {
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.fileId);
const imgIds = el.filter((e)=>e.type==="image").map((e:any)=>e.fileId);
const toDelete = Object.keys(files).filter((k)=>!imgIds.contains(k));
toDelete.forEach((k)=>delete files[k]);
}
@@ -829,14 +907,17 @@ export default class ExcalidrawView extends TextFileView {
let timestamp = 0;
let blockOnMouseButtonDown = false;
const getElementsAtPointer = (pointer:any, elements:ExcalidrawElement[], type:string):ExcalidrawElement[] => {
return elements.filter((e:ExcalidrawElement)=>{
if (e.type !== type) return false;
const [x,y,w,h] = rotatedDimensions(e);
return x<=pointer.x && x+w>=pointer.x
&& y<=pointer.y && y+h>=pointer.y;
});
}
const getTextElementAtPointer = (pointer:any) => {
const elements = this.excalidrawAPI.getSceneElements()
.filter((e:ExcalidrawElement)=>{
if (e.type !== "text") return false;
const [x,y,w,h] = rotatedDimensions(e);
return x<=pointer.x && x+w>=pointer.x
&& y<=pointer.y && y+h>=pointer.y;
});
const elements = getElementsAtPointer(pointer,this.excalidrawAPI.getSceneElements(),'text') as ExcalidrawTextElement[];
if(elements.length==0) return {id:null, text:null};
if(elements.length===1) return {id:elements[0].id,text:elements[0].text};
//if more than 1 text elements are at the location, look for one that has a link
@@ -857,13 +938,7 @@ export default class ExcalidrawView extends TextFileView {
}
const getImageElementAtPointer = (pointer:any) => {
const elements = this.excalidrawAPI.getSceneElements()
.filter((e:ExcalidrawElement)=>{
if (e.type !== "image") return false;
const [x,y,w,h] = rotatedDimensions(e);
return x<=pointer.x && x+w>=pointer.x
&& y<=pointer.y && y+h>=pointer.y;
});
const elements = getElementsAtPointer(pointer,this.excalidrawAPI.getSceneElements(),'image') as ExcalidrawImageElement[];
if(elements.length===0) return {id:null, fileId:null};
if(elements.length>=1) return {id:elements[0].id,fileId:elements[0].fileId};
//if more than 1 image elements are at the location, return the first
@@ -885,8 +960,6 @@ export default class ExcalidrawView extends TextFileView {
const dropAction = (transfer: DataTransfer) => {
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
//if (transfer.types.includes('text/uri-list')) return 'link';
let files = (this.app as any).dragManager.draggable?.files;
if(files) {
if(files[0] == this.file) {
@@ -895,20 +968,23 @@ export default class ExcalidrawView extends TextFileView {
}
}
if (['file', 'files'].includes((this.app as any).dragManager.draggable?.type)) return 'link';
if (transfer.types?.includes('text/html') || transfer.types?.includes('text/plain')) return 'copy';
if ( transfer.types?.includes('text/html')
|| transfer.types?.includes('text/plain')
|| transfer.types?.includes('Files')
) return 'copy';
}
let viewModeEnabled = false;
const handleLinkClick = () => {
selectedTextElement = getTextElementAtPointer(currentPosition);
if(selectedTextElement && selectedTextElement.id) {
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
const event = new MouseEvent("click", {ctrlKey:true, metaKey:true, shiftKey:this.shiftKeyDown, altKey:this.altKeyDown});
this.handleLinkClick(this,event);
selectedTextElement = null;
}
selectedImageElement = getImageElementAtPointer(currentPosition);
if(selectedImageElement && selectedImageElement.id) {
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
const event = new MouseEvent("click", {ctrlKey:true, metaKey:true, shiftKey:this.shiftKeyDown, altKey:this.altKeyDown});
this.handleLinkClick(this,event);
selectedImageElement = null;
}
@@ -931,11 +1007,11 @@ export default class ExcalidrawView extends TextFileView {
this.zoomToFit();
}
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
this.ctrlKeyDown = e[CTRL_OR_CMD]; //.ctrlKey||e.metaKey;
this.shiftKeyDown = e.shiftKey;
this.altKeyDown = e.altKey;
if(e.ctrlKey && !e.shiftKey && !e.altKey) { // && !e.metaKey) {
if(e[CTRL_OR_CMD] && !e.shiftKey && !e.altKey) { //.ctrlKey||e.metaKey) && !e.shiftKey && !e.altKey) {
const selectedElement = getTextElementAtPointer(currentPosition);
if(!selectedElement) return;
@@ -974,13 +1050,12 @@ export default class ExcalidrawView extends TextFileView {
}
},
onKeyUp: (e:any) => {
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
this.ctrlKeyDown = e[CTRL_OR_CMD]; //.ctrlKey||e.metaKey;
this.shiftKeyDown = e.shiftKey;
this.altKeyDown = e.altKey;
},
onClick: (e:MouseEvent):any => {
//@ts-ignore
if(!(e.ctrlKey||e.metaKey)) return;
if(!e[CTRL_OR_CMD]) return; //.ctrlKey||e.metaKey)) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!(this.getSelectedTextElement().id || this.getSelectedImageElement().id)) return;
this.handleLinkClick(this,e);
@@ -1022,7 +1097,7 @@ export default class ExcalidrawView extends TextFileView {
currentPosition = p.pointer;
if(hoverPreviewTarget && (Math.abs(hoverPoint.x-p.pointer.x)>50 || Math.abs(hoverPoint.y-p.pointer.y)>50)) clearHoverPreview();
if(!viewModeEnabled) return;
const buttonDown = !blockOnMouseButtonDown && p.button === "down";
if(buttonDown) {
blockOnMouseButtonDown = true;
@@ -1065,7 +1140,13 @@ export default class ExcalidrawView extends TextFileView {
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
const lib = {
type: "excalidrawlib",
version: 2,
source: "https://excalidraw.com",
libraryItems: items
}
this.plugin.setStencilLibrary(lib);
await this.plugin.saveSettings();
})();
},
@@ -1076,17 +1157,20 @@ export default class ExcalidrawView extends TextFileView {
}
return true;
},
onThemeChange: async (newTheme:string) => {
this.loadSceneFiles(newTheme==="dark");
},
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
const st: AppState = this.excalidrawAPI.getAppState();
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
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"
@@ -1100,7 +1184,7 @@ export default class ExcalidrawView extends TextFileView {
});
} catch (e) {
new Notice("on drop hook error. See console log for details");
console.log(e);
errorlog({where:"ExcalidrawView.onDrop",error:e});
return false;
}
} else {
@@ -1112,16 +1196,17 @@ export default class ExcalidrawView extends TextFileView {
switch(draggable?.type) {
case "file":
if (!onDropHook("file",[draggable.file],null)) {
if((event.ctrlKey || event.metaKey)
if(event[CTRL_OR_CMD] //.ctrlKey||event.metaKey)
&& (IMAGE_TYPES.contains(draggable.file.extension)
|| this.plugin.isExcalidrawFile(draggable.file))) {
const f = draggable.file;
const topX = currentPosition.x;
const topY = currentPosition.y;
const ea = window.ExcalidrawAutomate;
const ea = this.plugin.ea;
ea.reset();
ea.setView(this);
(async () => {
ea.canvas.theme = this.excalidrawAPI.getAppState().theme;
await ea.addImage(currentPosition.x,currentPosition.y,draggable.file);
ea.addElementsToView(false,false);
})();
@@ -1143,6 +1228,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;
@@ -1210,13 +1322,19 @@ export default class ExcalidrawView extends TextFileView {
public zoomToFit(delay:boolean = true) {
if(!this.excalidrawRef) return;
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
const current = this.excalidrawAPI;
const fullscreen = (document.fullscreenElement==this.contentEl);
const elements = current.getSceneElements();
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
setTimeout(() => current.zoomToFit(elements,2,fullscreen?0:0.05),100);
setTimeout(() => current.zoomToFit(elements,maxZoom,fullscreen?0:0.05),100);
} else {
current.zoomToFit(elements,2,fullscreen?0:0.05);
current.zoomToFit(elements,maxZoom,fullscreen?0:0.05);
}
}
}
export function getTextMode(data:string):TextMode {
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
return parsed ? TextMode.parsed : TextMode.raw;
}

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();
}
}

109
src/LaTeX.ts Normal file
View File

@@ -0,0 +1,109 @@
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {FileData, MimeType} from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { getImageSize, sleep, 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:FileData[] = [];
files.push({
mimeType : data.mimeType,
id: fileId as FileId,
dataURL: data.dataURL,
created: data.created,
size: data.size,
hasSVGwithBitmap: false
});
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) {
await sleep(200); //grace period for mathjax to load, if not, then we go for the slower fallback
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

@@ -1,54 +0,0 @@
import { App, Modal } from "obsidian";
import { t } from "./lang/helpers";
import ExcalidrawPlugin from "./main";
export class MigrationPrompt extends Modal {
private plugin: ExcalidrawPlugin;
constructor(app: App, plugin:ExcalidrawPlugin) {
super(app);
this.plugin = plugin;
}
onOpen(): void {
this.titleEl.setText("Welcome to Excalidraw 1.2");
this.createForm();
}
onClose(): void {
this.contentEl.empty();
}
createForm(): void {
const div = this.contentEl.createDiv();
div.addClass("excalidraw-prompt-div");
div.style.maxWidth = "600px";
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
div.createEl('p',{text: ""} , (el) => {
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
});
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
});
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
const bConvert = div.createEl('button', {text: "CONVERT FILES"});
bConvert.onclick = (ev)=>{
this.plugin.convertExcalidrawToMD();
this.close();
};
const bCancel = div.createEl('button', {text: "CANCEL"});
bCancel.onclick = (ev)=>{
this.close();
};
}
}

265
src/OneOffs.ts Normal file
View File

@@ -0,0 +1,265 @@
import { App, Modal, Notice, TFile } from "obsidian";
import { FRONTMATTER_KEY } from "./constants";
import { ExcalidrawData, getJSON } from "./ExcalidrawData";
import { getTextMode, TextMode } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
export class OneOffs {
private plugin:ExcalidrawPlugin
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
}
public patchCommentBlock() {
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
if(!this.plugin.settings.patchCommentBlock) return;
const plugin = this.plugin;
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings in 5 minutes");
setTimeout(async ()=>{
await plugin.loadSettings();
if (!plugin.settings.patchCommentBlock) {
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
return;
}
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
let i = 0;
const excalidrawFiles = plugin.app.vault.getFiles();
for (const f of (excalidrawFiles || []).filter((f:TFile) => plugin.isExcalidrawFile(f))) {
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
&& (plugin.app.workspace.getActiveFile() !== f)) { //file is currently being edited
let drawing = await plugin.app.vault.read(f);
const orig_drawing = drawing;
drawing = drawing.replaceAll("\r\n","\n").replaceAll("\r","\n"); //Win, Mac, Linux compatibility
drawing = drawing.replace("\n%%\n# Text Elements\n","\n# Text Elements\n");
if (drawing.search("\n%%\n# Drawing\n") === -1) {
const [json,pos] = getJSON(drawing);
drawing = drawing.substr(0,pos)+"\n%%\n# Drawing\n```json\n"+json+"\n```%%";
};
if (drawing !== orig_drawing) {
i++;
console.log("Excalidraw patched: " + f.path);
await plugin.app.vault.modify(f,drawing);
}
}
}
plugin.settings.patchCommentBlock = false;
plugin.saveSettings();
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
},300000) //5 minutes
}
public migrationNotice () {
if(this.plugin.settings.loadCount>0) return;
const plugin = this.plugin;
plugin.app.workspace.onLayoutReady(async () => {
plugin.settings.loadCount++;
plugin.saveSettings();
const files = plugin.app.vault.getFiles().filter((f)=>f.extension==="excalidraw");
if(files.length>0) {
const prompt = new MigrationPrompt(plugin.app, plugin);
prompt.open();
}
});
}
public imageElementLaunchNotice () {
if(!this.plugin.settings.imageElementNotice) return;
const plugin = this.plugin;
plugin.app.workspace.onLayoutReady(async () => {
const prompt = new ImageElementNotice(plugin.app, plugin);
prompt.open();
});
}
public wysiwygPatch() {
if(this.plugin.settings.patchCommentBlock) return; //the comment block patch needs to happen first (unlikely that someone has waited this long with the update...)
//This is a once off process to patch excalidraw files remediate incorrectly placed comment %% before # Text Elements
if(!(this.plugin.settings.runWYSIWYGpatch||this.plugin.settings.fixInfinitePreviewLoop)) return;
const plugin = this.plugin;
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings to support WYSIWYG in 7 minutes");
setTimeout(async ()=>{
await plugin.loadSettings();
if (!(this.plugin.settings.runWYSIWYGpatch||this.plugin.settings.fixInfinitePreviewLoop)) {
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
return;
}
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
let i = 0;
const excalidrawFiles = plugin.app.vault.getFiles();
for (const f of (excalidrawFiles || []).filter((f:TFile) => plugin.isExcalidrawFile(f))) {
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
&& (plugin.app.workspace.getActiveFile() !== f)) { //file is currently being edited
try{
const excalidrawData = new ExcalidrawData(plugin);
const data = await plugin.app.vault.read(f);
const textMode = getTextMode(data);
await excalidrawData.loadData(data,f,textMode);
let trimLocation = data.search(/(^%%\n)?# Text Elements\n/m);
if(trimLocation == -1) trimLocation = data.search(/(%%\n)?# Drawing\n/);
if(trimLocation > -1) {
let header = data.substring(0,trimLocation)
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
header = header.replace(/cssclass:[\s]*excalidraw-hide-preview-text[\s]*\n/,"");
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");
}
const newData = header + excalidrawData.generateMD();
if (data !== newData) {
i++;
console.log("Excalidraw patched: " + f.path);
await plugin.app.vault.modify(f,newData);
}
}
} catch (e) {
console.log("Unable to process: "+f.path,{error:e});
}
}
}
plugin.settings.runWYSIWYGpatch = false;
plugin.settings.fixInfinitePreviewLoop = false;
plugin.saveSettings();
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
},420000) //7 minutes
}
}
class MigrationPrompt extends Modal {
private plugin: ExcalidrawPlugin;
constructor(app: App, plugin:ExcalidrawPlugin) {
super(app);
this.plugin = plugin;
}
onOpen(): void {
this.titleEl.setText("Welcome to Excalidraw 1.2");
this.createForm();
}
onClose(): void {
this.contentEl.empty();
}
createForm(): void {
const div = this.contentEl.createDiv();
// div.addClass("excalidraw-prompt-div");
// div.style.maxWidth = "600px";
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
div.createEl('p',{text: ""} , (el) => {
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
});
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
});
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
const bConvert = div.createEl('button', {text: "CONVERT FILES"});
bConvert.onclick = (ev)=>{
this.plugin.convertExcalidrawToMD();
this.close();
};
const bCancel = div.createEl('button', {text: "CANCEL"});
bCancel.onclick = (ev)=>{
this.close();
};
}
}
class ImageElementNotice extends Modal {
private plugin: ExcalidrawPlugin;
private saveChanges: boolean = false;
constructor(app: App, plugin:ExcalidrawPlugin) {
super(app);
this.plugin = plugin;
}
onOpen(): void {
this.titleEl.setText("Image Elements have arrived!");
this.createForm();
}
async onClose() {
this.contentEl.empty();
if(!this.saveChanges) return;
await this.plugin.loadSettings();
this.plugin.settings.imageElementNotice = false;
this.plugin.saveSettings();
}
createForm(): void {
const div = this.contentEl.createDiv();
//div.addClass("excalidraw-prompt-div");
//div.style.maxWidth = "600px";
div.createEl('p',{text: ""},(el) => {
el.innerHTML = "Welcome to Obsidian-Excalidraw 1.4! I've added Image Elements. "
+ "Please watch the video below to learn how to use this new feature.";
});
div.createEl('p',{text: ""}, (el) => {
el.innerHTML = "<u>⚠ WARNING:</u> Opening new drawings with an older version of the plugin will lead to loss of images. "
+ "Update the plugin on all your devices.";
});
div.createEl('p',{text: ""}, (el) => {
el.innerHTML = "Since March, I have spent most of my free time building this plugin. Close to 75 workdays worth of my time (assuming 8-hour days). "
+ "Some of you have already bought me a coffee. THANK YOU! Your support really means a lot to me! If you have not yet done so, please consider clicking the button below.";
});
const coffeeDiv = div.createDiv('coffee');
coffeeDiv.addClass('ex-coffee-div');
const coffeeLink = coffeeDiv.createEl('a', { href: 'https://ko-fi.com/zsolt' });
const coffeeImg = coffeeLink.createEl('img', {
attr: {
src: 'https://cdn.ko-fi.com/cdn/kofi3.png?v=3',
},
});
coffeeImg.height = 45;
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
el.style.textAlign = "center";
el.innerHTML = '<iframe width="560" height="315" src="https://www.youtube.com/embed/_c_0zpBJ4Xc?start=20" title="YouTube video player" '
+'frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" '
+ 'allowfullscreen></iframe>';
});
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
el.style.textAlign = "right";
const bOk = el.createEl('button', {text: "OK - Don't show this again"});
bOk.onclick = (ev)=>{
this.saveChanges = true;
this.close();
}
const bCancel = el.createEl('button', {text: "CANCEL - Read next time"});
bCancel.onclick = (ev)=>{
this.saveChanges = false;
this.close();
};
});
}
}

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,11 @@
import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw";
import {exportToSvg, exportToBlob} 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 +16,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 +146,6 @@ export const rotatedDimensions = (
];
}
export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number },
{
@@ -190,92 +180,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 +213,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,34 +227,20 @@ 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({
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
files: scene.files,
mimeType: "image/png",
exportWithDarkMode: "true",
metadata: "Generated by Excalidraw-Obsidian plugin",
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
try {
return await exportToBlob({
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
files: scene.files,
mimeType: "image/png",
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
});
} catch (error) {
console.log(error);
return null;
}
}
@@ -363,35 +256,18 @@ 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] => {
let dirty = false;
if(!files || !scene) return [dirty,scene];
for(const f of files) {
const [w_image,h_image] = [f.size.width,f.size.height];
const imageAspectRatio = f.size.width/f.size.height;
@@ -413,4 +289,21 @@ export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
});
return [dirty,scene];
}
}
}
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 errorlog = (data:{}) => {
console.log({plugin:"Excalidraw",...data});
}
export const sleep = async (ms:number) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
//export const debug = console.log.bind(window.console);
//export const debug = function(){};

File diff suppressed because one or more lines are too long

View File

@@ -23,18 +23,19 @@ 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
OPEN_AS_MD: "Open as Markdown",
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/CMD+CLICK to export)",
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/CMD+CLICK to export)",
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
LINK_BUTTON_CLICK_NO_TEXT: 'Select a an ImageElement, or select a TextElement that contains an internal or external link.\n'+
'SHIFT CLICK this button to open the link in a new pane.\n'+
'CTRL/META CLICK the Image or TextElement on the canvas has the same effect!',
'CTRL/CMD CLICK the Image or TextElement on the canvas has the same effect!',
TEXT_ELEMENT_EMPTY: "No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\  < > : | ?',
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
@@ -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,21 +70,35 @@ 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. " +
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively.",
MATCH_THEME_ALWAYS_NAME: "Existing drawings to match Obsidian theme",
MATCH_THEME_ALWAYS_DESC: "If theme is dark, drawings will be opened in dark mode. If your theme is light, they will be opened in light mode. ",
MATCH_THEME_TRIGGER_NAME: "Excalidraw to follow when Obsidian Theme changes",
MATCH_THEME_TRIGGER_DESC: "If this option is enabled open Excalidraw pane will switch to light/dark mode when Obsidian theme changes. ",
DEFAULT_OPEN_MODE_NAME: "Default mode when opening Excalidraw",
DEFAULT_OPEN_MODE_DESC: "Specifies the mode how Excalidraw opens: Normal, Zen, or View mode. You may also set this behaviour on a file level by " +
"adding the excalidraw-default-mode frontmatter key with a value of: normal,view, or zen to your document.",
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
ZOOM_TO_FIT_MAX_LEVEL_DESC: "Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
LINKS_HEAD: "Links and transclusion",
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
LINKS_DESC: "CTRL/CMD + CLICK on Text Elements to open them as links. " +
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
"If the text starts as a valid web link (i.e. https:// or http://), then " +
"the plugin will open it in a browser. " +
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
ADJACENT_PANE_NAME: "Open in adjacent pane",
ADJACENT_PANE_DESC: "When CTRL+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
ADJACENT_PANE_DESC: "When CTRL/CMD+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
"Excalidraw will first look too the right, then to the left, then down, then up. If no pane is found, Excalidraw will open " +
"a new pane.",
@@ -101,7 +114,7 @@ export default {
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
': "🌐 "\' to the file\'s frontmatter.',
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
LINK_CTRL_CLICK_NAME: "CTRL/CMD + CLICK on text to open them as links",
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
@@ -111,9 +124,14 @@ 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.",
PREVIEW_MATCH_OBSIDIAN_NAME: "Excalidraw preview to match Obsidian theme",
PREVIEW_MATCH_OBSIDIAN_DESC: "Image preview in documents should match the Obsidian theme. If enabled, when Obsidian is in dark mode, Excalidraw images will render in dark mode. "+
"When Obsidian is in light mode, Excalidraw will render light mode as well. You may want to switch 'Export image with background' off for a more Obsidian-integrated look and feel.",
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
@@ -163,8 +181,12 @@ 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.",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING: "EXCALIDRAW WARNING\nAborted loading embedded images due to infinite loop in file:\n",
};

View File

@@ -29,13 +29,13 @@ export default {
//ExcalidrawView.ts
OPEN_AS_MD: "打开为 Markdown 文件",
SAVE_AS_PNG: "保存成 PNG 文件到库里CTRL/META 加左键点击来指定导出位置)",
SAVE_AS_SVG: "保存成 SVG 文件到库里CTRL/META 加左键点击来指定导出位置)",
SAVE_AS_PNG: "保存成 PNG 文件到库里CTRL/CMD 加左键点击来指定导出位置)",
SAVE_AS_SVG: "保存成 SVG 文件到库里CTRL/CMD 加左键点击来指定导出位置)",
OPEN_LINK: "以链接的方式打开文本 \n按住 SHIFT 来在新面板中打开)",
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
LINK_BUTTON_CLICK_NO_TEXT: '选择带有外部链接或内部链接的文本。\n'+
'SHIFT 加左键点击按钮来在新面板中打开链接。\n'+
'CTRL/META 加左键在画布中点击文本元素也可以打开对应的链接。',
'CTRL/CMD 加左键在画布中点击文本元素也可以打开对应的链接。',
TEXT_ELEMENT_EMPTY: "文本元素没有链接任何东西.",
FILENAME_INVALID_CHARS: '文件名不能包含以下符号: * " \\ < > : | ?',
FILE_DOES_NOT_EXIST: "文件不存在。按住 ALT或者 ALT + SHIFT加左键点击来创建新文件。",
@@ -71,7 +71,7 @@ export default {
FILENAME_DATE_NAME: "文件名日期",
FILENAME_DATE_DESC: "文件名的第二部分",
LINKS_HEAD: "链接",
LINKS_DESC: "CTRL/META 加左键点击文本元素来打开链接。" +
LINKS_DESC: "CTRL/CMD 加左键点击文本元素来打开链接。" +
"如果选中的文本指向多个双链,只会打开其中第一个。" +
"如果选中的文本为超链接 (i.e. https:// or http://),然后" +
"插件会在浏览器中打开超链接。" +
@@ -85,7 +85,7 @@ export default {
LINK_PREFIX_DESC:"在预览(锁定)模式,如果文本元素包含链接,在文本之前加上这些字符。" +
"你可以在文件的 Frontmatter 中加入 \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
': "👉 "\' 单独更改',
LINK_CTRL_CLICK_NAME: "CTRL 加左键点击文本来打开链接",
LINK_CTRL_CLICK_NAME: "CTRL/CMD 加左键点击文本来打开链接",
LINK_CTRL_CLICK_DESC: "如果此功能干扰了您要使用的 Excalidraw 功能,您可以将其关闭。 如果" +
"关闭此选项,则只有绘图标题栏中的链接按钮可以让你打开链接。",
EMBED_HEAD: "嵌入 & 导出",

View File

@@ -12,9 +12,10 @@ import {
MenuItem,
TAbstractFile,
Tasks,
MarkdownRenderer,
ViewState,
Notice,
loadMathJax,
MarkdownRenderer,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -32,10 +33,11 @@ import {
FRONTMATTER,
JSON_parse,
nanoid,
DARK_BLANK_DRAWING
DARK_BLANK_DRAWING,
CTRL_OR_CMD
} from "./constants";
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
import {getJSON, getSVGString} from "./ExcalidrawData";
import {ExcalidrawData, getJSON, getMarkdownDrawingSection} from "./ExcalidrawData";
import {
ExcalidrawSettings,
DEFAULT_SETTINGS,
@@ -48,15 +50,23 @@ 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 { MigrationPrompt } from "./MigrationPrompt";
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, 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";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
declare module "obsidian" {
interface App {
@@ -73,15 +83,25 @@ 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 themeObserver: MutationObserver;
private fileExplorerObserver: MutationObserver;
public opencount:number = 0;
public ea:ExcalidrawAutomate;
//A master list of fileIds to facilitate copy / paste
public filesMaster:Map<FileId,{path:string,hasSVGwithBitmap:boolean}> = 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,{path:string,hasSVGwithBitmap:boolean}>();
this.equationsMaster = new Map<FileId,string>();
}
async onload() {
@@ -92,8 +112,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)
@@ -103,56 +123,62 @@ export default class ExcalidrawPlugin extends Plugin {
this.registerExtensions(["excalidraw"],VIEW_TYPE_EXCALIDRAW);
this.addMarkdownPostProcessor();
this.addThemeObserver();
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
this.registerCommands();
this.registerEventListeners();
//inspiration taken from kanban:
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
if(this.settings.loadCount<1) this.migrationNotice();
const electron:string = process.versions.electron;
if(electron.startsWith("8.")) {
new Notice(`You are running an older version of the electron Browser (${electron}). If Excalidraw does not start up, please reinstall Obsidian with the latest installer and try again.`,10000);
if(!this.app.isMobile) {
const electron:string = process?.versions?.electron;
if(electron && electron?.startsWith("8.")) {
new Notice(`You are running an older version of the electron Browser (${electron}). If Excalidraw does not start up, please reinstall Obsidian with the latest installer and try again.`,10000);
}
}
const patches = new OneOffs(this);
patches.migrationNotice();
patches.patchCommentBlock();
patches.wysiwygPatch();
patches.imageElementLaunchNotice();
this.switchToExcalidarwAfterLoad()
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
if(this.settings.patchCommentBlock) {
const self = this;
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings in 5 minutes");
setTimeout(async ()=>{
await self.loadSettings();
if (!self.settings.patchCommentBlock) {
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
return;
}
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
let i = 0;
const excalidrawFiles = this.app.vault.getFiles();
for (const f of (excalidrawFiles || []).filter((f:TFile) => self.isExcalidrawFile(f))) {
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
&& (self.app.workspace.getActiveFile() !== f)) { //file is currently being edited
let drawing = await self.app.vault.read(f);
const orig_drawing = drawing;
drawing = drawing.replaceAll("\r\n","\n").replaceAll("\r","\n"); //Win, Mac, Linux compatibility
drawing = drawing.replace("\n%%\n# Text Elements\n","\n# Text Elements\n");
if (drawing.search("\n%%\n# Drawing\n") === -1) {
const [json,pos] = getJSON(drawing);
drawing = drawing.substr(0,pos)+"\n%%\n# Drawing\n```json\n"+json+"\n```%%";
};
if (drawing !== orig_drawing) {
i++;
console.log("Excalidraw patched: " + f.path);
await self.app.vault.modify(f,drawing);
}
}
}
self.settings.patchCommentBlock = false;
self.saveSettings();
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
},300000) //5 minutes
}
const self = this;
this.loadMathJax();
process.env.REACT_APP_LIBRARY_URL = "https://libraries.excalidraw.com/";
process.env.REACT_APP_LIBRARY_BACKEND = "https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries";
}
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() {
@@ -169,19 +195,6 @@ export default class ExcalidrawPlugin extends Plugin {
});
}
private migrationNotice(){
const self = this;
this.app.workspace.onLayoutReady(async () => {
self.settings.loadCount++;
self.saveSettings();
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
if(files.length>0) {
const prompt = new MigrationPrompt(self.app, self);
prompt.open();
}
});
}
/**
* Displays a transcluded .excalidraw image in markdown preview mode
*/
@@ -216,37 +229,43 @@ export default class ExcalidrawPlugin extends Plugin {
withTheme: this.settings.exportWithTheme
}
const img = createEl("img");
img.setAttribute("width",imgAttributes.fwidth);
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
let style = `max-width:${imgAttributes.fwidth}px !important; width:100%;`;
if(imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
}
img.setAttribute("style",style);
img.addClass(imgAttributes.style);
const [scene,pos] = getJSON(content);
const svgSnapshot = getSVGString(content.substr(pos+scene.length));
//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) {
this.ea.reset();
const theme = this.settings.previewMatchObsidianTheme
? (isObsidianThemeDark() ? "dark" : "light")
: undefined;
if(theme) exportSettings.withTheme = true;
const loader = new EmbeddedFilesLoader(this,this.settings.previewMatchObsidianTheme?isObsidianThemeDark():undefined);
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);
if(width>=600) scale = 2;
if(width>=1200) scale = 3;
if(width>=1800) scale = 4;
if(width>=2400) scale = 5;
const png = await this.ea.createPNG(file.path,scale,exportSettings,loader,theme);
//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,loader,theme)).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);
@@ -256,6 +275,49 @@ export default class ExcalidrawPlugin extends Plugin {
return img;
}
const createImageDiv = async (attr:imgElementAttributes):Promise<HTMLDivElement> => {
const img = await getIMG(attr);
return createDiv(attr.style, (el)=>{
el.append(img);
el.setAttribute("src",attr.file.path);
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
if(attr.fheight) el.setAttribute("h",attr.fheight);
el.onClickEvent((ev)=>{
if(ev.target instanceof Element && ev.target.tagName.toLowerCase() != "img") return;
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev[CTRL_OR_CMD]);//.ctrlKey||ev.metaKey);
});
el.addEventListener(RERENDER_EVENT, async(e) => {
e.stopPropagation;
el.empty();
const img = await getIMG({
fname:el.getAttribute("src"),
fwidth:el.getAttribute("w"),
fheight:el.getAttribute("h"),
style:el.getAttribute("class")
});
el.append(img);
});
});
}
const tmpObsidianWYSIWYG = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => {
if(!ctx.frontmatter) return;
if(!ctx.frontmatter.hasOwnProperty("excalidraw-plugin")) return;
//@ts-ignore
if(ctx.remainingNestLevel<4) return;
if(!el.querySelector(".frontmatter")) {
el.style.display="none";
return;
}
const attr:imgElementAttributes={fname:ctx.sourcePath,fheight:"",fwidth:this.settings.width,style:"excalidraw-svg"};
attr.file = this.app.metadataCache.getFirstLinkpathDest(ctx.sourcePath,"");
const div = await createImageDiv(attr);
el.childNodes.forEach((child:HTMLElement)=>child.style.display="none");
el.appendChild(div)
}
/**
*
* @param el
@@ -263,13 +325,20 @@ export default class ExcalidrawPlugin extends Plugin {
*/
const markdownPostProcessor = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => {
const embeddedItems = el.querySelectorAll('.internal-embed');
if(embeddedItems.length==0) return;
if(embeddedItems.length===0) {
tmpObsidianWYSIWYG(el,ctx);
return;
}
let attr:imgElementAttributes={fname:"",fheight:"",fwidth:"",style:""};
let alt:string, parts, div, file:TFile;
for (const drawing of embeddedItems) {
attr.fname = drawing.getAttribute("src");
file = this.app.metadataCache.getFirstLinkpathDest(attr.fname, ctx.sourcePath);
if(!file && ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")) {
attr.fname = ctx.sourcePath;
file = this.app.metadataCache.getFirstLinkpathDest(attr.fname, ctx.sourcePath);
}
if(file && file instanceof TFile && this.isExcalidrawFile(file)) {
attr.fwidth = drawing.getAttribute("width") ? drawing.getAttribute("width") : this.settings.width;
attr.fheight = drawing.getAttribute("height");
@@ -289,29 +358,8 @@ export default class ExcalidrawPlugin extends Plugin {
}
attr.fname = file?.path;
const img = await getIMG(attr);
div = createDiv(attr.style, (el)=>{
el.append(img);
el.setAttribute("src",file.path);
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
if(attr.fheight) el.setAttribute("h",attr.fheight);
el.onClickEvent((ev)=>{
if(ev.target instanceof Element && ev.target.tagName.toLowerCase() != "img") return;
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
el.addEventListener(RERENDER_EVENT, async(e) => {
e.stopPropagation;
el.empty();
const img = await getIMG({
fname:el.getAttribute("src"),
fwidth:el.getAttribute("w"),
fheight:el.getAttribute("h"),
style:el.getAttribute("class")
});
el.append(img);
});
});
attr.file = file;
const div = await createImageDiv(attr);
drawing.parentElement.replaceChild(div,drawing);
}
}
@@ -341,47 +389,18 @@ 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.extension == "excalidraw") {
observerForLegacyFileFormat(m,file);
return;
}
if(!file) return;
if(!(file instanceof TFile)) return;
if(file.extension!=="excalidraw") return;
let i=0;
//@ts-ignore
while(i<m.length && m[i].target?.className!="markdown-preview-sizer markdown-preview-section") i++;
if(i==m.length) return;
if(m[i].addedNodes.length==0) return;
//@ts-ignore
if(m[i].addedNodes[0].childElementCount!=2) return;
//@ts-ignore
if(m[i].addedNodes[0].firstElementChild.className.indexOf("frontmatter")==-1) return;
//@ts-ignore
if(m[i].addedNodes[0].firstElementChild?.firstElementChild?.className=="excalidraw-svg") return;
//@ts-ignore
const hoverPopover = m[i].addedNodes[0].matchParent(".hover-popover");
if(!hoverPopover) return;
const node = m[i].addedNodes[0];
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
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("", async (el)=>{
const img = await getIMG({file:file,fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
});
node.insertBefore(div,node.firstChild)
});
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
//compatibility: .excalidraw file observer
let observerForLegacyFileFormat = async (m:MutationRecord[], file:TFile) => {
if(!this.hover.linkText) return;
if(m.length!=1) return;
if(m[0].addedNodes.length != 1) return;
@@ -399,15 +418,28 @@ export default class ExcalidrawPlugin extends Plugin {
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev[CTRL_OR_CMD]); //.ctrlKey||ev.metaKey);
});
});
node.appendChild(div);
}
this.observer.observe(document, {childList: true, subtree: true});
});
this.observer.observe(document, {childList: true, subtree: true});
}
private addThemeObserver() {
this.themeObserver = new MutationObserver(async (m)=>{
if(!this.settings.matchThemeTrigger) return;
const theme = isObsidianThemeDark() ? "dark":"light";
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf:WorkspaceLeaf)=> {
const excalidrawView = (leaf.view as ExcalidrawView);
if(excalidrawView.file && excalidrawView.excalidrawRef) {
excalidrawView.setTheme(theme);
}
});
});
this.themeObserver.observe(document.body, {attributeFilter:["class"]});
}
public experimentalFileTypeDisplayToggle(enabled: boolean) {
@@ -453,9 +485,10 @@ 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);
this.createDrawing(this.getNextDefaultFilename(), e[CTRL_OR_CMD]); //.ctrlKey||e.metaKey);
});
const fileMenuHandlerCreateNew = (menu: Menu, file: TFile) => {
@@ -695,6 +728,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"),
@@ -704,13 +755,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;
}
@@ -987,17 +1039,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)
@@ -1008,13 +1065,15 @@ export default class ExcalidrawPlugin extends Plugin {
onunload() {
destroyExcalidrawAutomate();
this.observer.disconnect();
this.themeObserver.disconnect();
if (this.fileExplorerObserver) this.fileExplorerObserver.disconnect();
const excalidrawLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
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();
}
@@ -1047,12 +1106,16 @@ export default class ExcalidrawPlugin extends Plugin {
await this.saveData(this.settings);
}
public getStencilLibrary():string {
return this.settings.library;
public getStencilLibrary():{} {
if(this.settings.library === "" || this.settings.library === "deprecated" ) {
return this.settings.library2;
}
return JSON_parse(this.settings.library);
}
public setStencilLibrary(library:string) {
this.settings.library = library;
public setStencilLibrary(library:{}) {
this.settings.library = "deprecated";
this.settings.library2=library;
}
public triggerEmbedUpdates(filepath?:string){
@@ -1104,24 +1167,10 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
if (this.settings.compatibilityMode) {
return this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING;
return this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
}
const blank = this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING;
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank,'<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%%';
const blank = this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
return FRONTMATTER + '\n' + getMarkdownDrawingSection(blank);
}
/**
@@ -1132,7 +1181,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;
@@ -1147,7 +1195,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> {
@@ -1166,10 +1214,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,10 +14,15 @@ export interface ExcalidrawSettings {
templateFilePath: string,
drawingFilenamePrefix: string,
drawingFilenameDateTime: string,
//displaySVGInPreview: boolean,
displaySVGInPreview: boolean,
previewMatchObsidianTheme: boolean,
width: string,
matchTheme: boolean,
matchThemeAlways: boolean,
matchThemeTrigger:boolean,
defaultMode: string,
zoomToFitOnResize: boolean,
zoomToFitMaxLevel: number,
openInAdjacentPane: boolean,
showLinkBrackets: boolean,
linkPrefix: string,
@@ -26,6 +30,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,
@@ -41,7 +46,11 @@ export interface ExcalidrawSettings {
loadCount: number, //version 1.2 migration counter
drawingOpenCount: number,
library: string,
library2: {},
patchCommentBlock: boolean, //1.3.12
imageElementNotice: boolean, //1.4.0
runWYSIWYGpatch: boolean, //1.4.9
fixInfinitePreviewLoop: boolean, //1.4.10
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
@@ -49,10 +58,15 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
templateFilePath: 'Excalidraw/Template.excalidraw',
drawingFilenamePrefix: 'Drawing ',
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
//displaySVGInPreview: true,
displaySVGInPreview: true,
previewMatchObsidianTheme: false,
width: '400',
matchTheme: false,
matchThemeAlways: false,
matchThemeTrigger: false,
defaultMode: "normal",
zoomToFitOnResize: true,
zoomToFitMaxLevel: 2,
linkPrefix: "📍",
urlPrefix: "🌐",
openInAdjacentPane: false,
@@ -60,6 +74,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
allowCtrlClick: true,
forceWrap: false,
pageTransclusionCharLimit: 200,
iframelyAllowed: true,
pngExportScale: 1,
exportWithTheme: true,
exportWithBackground: true,
@@ -74,8 +89,17 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
compatibilityMode: false,
loadCount: 0,
drawingOpenCount: 0,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
library: `deprecated`,
library2: {
type: "excalidrawlib",
version: 2,
source: "https://excalidraw.com",
libraryItems: []
},
patchCommentBlock: true,
imageElementNotice: true,
runWYSIWYGpatch: true,
fixInfinitePreviewLoop: true
}
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -112,7 +136,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
if(this.requestEmbedUpdate) this.plugin.triggerEmbedUpdates();
}
display(): void {
async display() {
await this.plugin.loadSettings(); //in case sync loaded changed settings in the background
this.requestEmbedUpdate = false;
this.requestReloadDrawings = false;
let {containerEl} = this;
@@ -190,7 +215,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")});
@@ -204,6 +229,40 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}));
new Setting(containerEl)
.setName(t("MATCH_THEME_ALWAYS_NAME"))
.setDesc(t("MATCH_THEME_ALWAYS_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matchThemeAlways)
.onChange(async (value) => {
this.plugin.settings.matchThemeAlways = value;
this.applySettingsUpdate();
}));
new Setting(containerEl)
.setName(t("MATCH_THEME_TRIGGER_NAME"))
.setDesc(t("MATCH_THEME_TRIGGER_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.matchThemeTrigger)
.onChange(async (value) => {
this.plugin.settings.matchThemeTrigger = value;
this.applySettingsUpdate();
}));
new Setting(containerEl)
.setName(t("DEFAULT_OPEN_MODE_NAME"))
.setDesc(t("DEFAULT_OPEN_MODE_DESC"))
.addDropdown(dropdown => dropdown
.addOption("normal","Normal Mode")
.addOption("zen","Zen Mode")
.addOption("view","View Mode")
.setValue(this.plugin.settings.defaultMode)
.onChange(async (value) => {
this.plugin.settings.defaultMode = value;
this.applySettingsUpdate();
}));
new Setting(containerEl)
.setName(t("ZOOM_TO_FIT_NAME"))
.setDesc(t("ZOOM_TO_FIT_DESC"))
@@ -214,6 +273,27 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}));
let zoomText:HTMLDivElement;
new Setting(containerEl)
.setName(t("ZOOM_TO_FIT_MAX_LEVEL_NAME"))
.setDesc(t("ZOOM_TO_FIT_MAX_LEVEL_DESC"))
.addSlider(slider => slider
.setLimits(0.5,10,0.5)
.setValue(this.plugin.settings.zoomToFitMaxLevel)
.onChange(async (value)=> {
zoomText.innerText = " " + value.toString();
this.plugin.settings.zoomToFitMaxLevel = value;
this.applySettingsUpdate();
}))
.settingEl.createDiv('',(el)=>{
zoomText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = " " + this.plugin.settings.zoomToFitMaxLevel.toString();
});
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
this.containerEl.createEl('p',{
text: t("LINKS_DESC")});
@@ -245,7 +325,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);
}));
@@ -305,10 +384,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
@@ -316,7 +405,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.displaySVGInPreview = value;
this.applySettingsUpdate();
}));*/
}));
new Setting(containerEl)
.setName(t("PREVIEW_MATCH_OBSIDIAN_NAME"))
.setDesc(t("PREVIEW_MATCH_OBSIDIAN_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.previewMatchObsidianTheme)
.onChange(async (value) => {
this.plugin.settings.previewMatchObsidianTheme = 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 {

View File

@@ -1,3 +1,4 @@
{
"1.3.20": "0.11.13"
"1.4.11": "0.12.16",
"1.4.2": "0.11.13"
}

133
yarn.lock
View File

@@ -1067,21 +1067,16 @@
dependencies:
"@types/estree" "*"
"@zsviczian/excalidraw@0.10.0-obsidian-2":
"integrity" "sha512-H9w7cB0ZgQIHujMB7Zwz82zoZl85ZGDtmxkX9swrPJXYcrJjx5j4oNqEK+dUSYwUlnN2iycdX9l93MjXjAK0+A=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-2.tgz"
"version" "0.10.0-obsidian-2"
"@zsviczian/excalidraw@0.10.0-obsidian-12":
"integrity" "sha512-0T6zCr/CTaTZxoh+hZKamSmph63WNnkxal1DUg7nVCjw02gz+Q6Y0qywJzy9B49K4r7CH4CewVqPmIFumUr5Kg=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-12.tgz"
"version" "0.10.0-obsidian-12"
"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"
@@ -3266,18 +3263,18 @@
"resolved" "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz"
"version" "1.0.0"
"currently-unhandled@^0.4.1":
"integrity" "sha1-mI3zP+qxke95mmE2nddsF635V+o="
"resolved" "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz"
"version" "0.4.1"
dependencies:
"array-find-index" "^1.0.1"
"css-color-names@0.0.4":
"integrity" "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA="
"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"
@@ -3390,6 +3387,13 @@
"resolved" "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz"
"version" "3.0.6"
"currently-unhandled@^0.4.1":
"integrity" "sha1-mI3zP+qxke95mmE2nddsF635V+o="
"resolved" "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz"
"version" "0.4.1"
dependencies:
"array-find-index" "^1.0.1"
"d@^1.0.1", "d@1":
"integrity" "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA=="
"resolved" "https://registry.npmjs.org/d/-/d-1.0.1.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"
@@ -9449,6 +9425,11 @@
"resolved" "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz"
"version" "2.3.1"
"tty-browserify@0.0.0":
"integrity" "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
"resolved" "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"
"version" "0.0.0"
"tunnel-agent@^0.6.0":
"integrity" "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0="
"resolved" "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"
@@ -9496,11 +9477,6 @@
"resolved" "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz"
"version" "4.4.3"
"tty-browserify@0.0.0":
"integrity" "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
"resolved" "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"
"version" "0.0.0"
"uglify-js@^2.8.29":
"integrity" "sha1-KcVzMUgFe7Th913zW3qcty5qWd0="
"resolved" "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.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"