Compare commits

..

49 Commits

Author SHA1 Message Date
Zsolt Viczian
cc7227d164 1.4.18 2021-12-04 16:38:21 +01:00
zsviczian
c75e7fb76c Merge pull request #293 from qifei9/patch-1
Update zh-cn.ts
2021-12-04 13:15:21 +01:00
Zsolt Viczian
6d3eb20ff1 check if exists before adding to embeddedFiles 2021-12-04 13:14:39 +01:00
qifei9
a603e4eeac Update zh-cn.ts
Fix the translation of `COMPATIBILITY_MODE_DESC` of zh-cn
2021-12-04 19:44:59 +08:00
zsviczian
ac260925dd Update ExcalidrawView.ts 2021-12-03 12:51:15 +01:00
Zsolt Viczian
1a2e7ac23f 1.4.17 2021-12-01 22:05:09 +01:00
Zsolt Viczian
1eb9b88f4b updated readme 2021-11-30 20:12:05 +01:00
Zsolt Viczian
a7814f383a 1.4.16 2021-11-30 19:57:50 +01:00
zsviczian
54e6d47df0 Update README.md 2021-11-30 14:59:19 +01:00
zsviczian
55ea1cf121 Update README.md 2021-11-30 14:57:02 +01:00
zsviczian
a2982f3406 Update EmbeddedFileLoader.ts 2021-11-30 14:52:21 +01:00
zsviczian
633ff1fea8 #286
https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/286#issuecomment-982179639
2021-11-30 08:46:05 +01:00
Zsolt Viczian
3312df0743 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2021-11-29 22:03:30 +01:00
Zsolt Viczian
0722bb8133 1.4.15 2021-11-29 22:02:59 +01:00
zsviczian
c9be4d95d7 Update README.md 2021-11-29 14:46:47 +01:00
Zsolt Viczian
023ddcec39 en.ts edited 2021-11-28 21:09:54 +01:00
zsviczian
9255643646 Update README.md 2021-11-28 21:09:04 +01:00
Zsolt Viczian
36ead43102 1.4.14 2021-11-28 20:54:51 +01:00
zsviczian
f61d000326 Update README.md 2021-11-28 07:52:44 +01:00
zsviczian
9bb254dc48 Update README.md 2021-11-28 07:42:21 +01:00
Zsolt Viczian
5023ed46f5 1.4.13: Markdown embed 2021-11-27 21:31:46 +01:00
Zsolt Viczian
73616e5084 debugging file update race condition 2021-11-27 18:29:56 +01:00
Zsolt Viczian
f7bbe2e446 svgMarkdownMVP 2021-11-26 20:20:08 +01:00
Zsolt Viczian
94c3011435 invers image cache 2021-11-24 21:30:59 +01:00
Zsolt Viczian
b348def6f6 1.4.12 - fixed lib export + preview without theme 2021-11-22 18:57:18 +01:00
Zsolt Viczian
d57c59e9eb markdown render 2021-11-21 22:46:37 +01:00
zsviczian
3ed25c221e Merge pull request #263 from LukeWeidenwalker/fix-typos
Fix three typos in English localization
2021-11-21 19:26:58 +01:00
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
Lukas Weidenholzer
200c2631cb fix three typos in English localization 2021-11-21 11:07:34 +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
23 changed files with 1897 additions and 718 deletions

View File

@@ -11,21 +11,23 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|[![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)|[![Image Elements](https://user-images.githubusercontent.com/14358394/138607067-ccb62f92-48a4-4880-ac6e-68c1bf86ac2c.png)](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
|[![LaTex Demo](https://user-images.githubusercontent.com/14358394/143732412-1c65227e-4381-406d-847a-b001ab3506ca.jpg)](https://youtu.be/r08wk-58DPk)|[![markdown embeds](https://user-images.githubusercontent.com/14358394/143732440-90bfa029-8615-462e-ada3-c903d71a82c9.jpg)](https://youtu.be/tsecSfnTMow)|[![markdownAdvanced](https://user-images.githubusercontent.com/14358394/143783906-15cee494-c6d5-4495-a2ca-74634e4e7355.jpg)](https://youtu.be/K6qZkTz8GHs)|
# 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.
- 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 customzie Excalidraw to your needs:
- 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
@@ -38,7 +40,7 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
- 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 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.
- 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.
@@ -58,6 +60,17 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
- `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
- Embed complete markdown files into your drawings
- Drag from the desired file from the Obsidian file explorer and hold down CTRL/CMD while dropping the file onto the canvas.
- Use the command palette action: `Insert markdown file from vault`
- Use custom woff, woff2 or TTF font to display the document, you can set the default font to use under Excalidraw Settings.
- You can set a custom css for rendering the snapshot image of your markdown document. Only operating system standard fonts are supported as the font-family ([Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list), [Mac & iOS](https://developer.apple.com/fonts/system-fonts/)), plus you can set one additional custom font using the setting explained above. (for a demonstration watch this [video](https://youtu.be/K6qZkTz8GHs) and check out this [sample css](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281)).
- To help with styling you can observe the SVG snapshot of the markdown document created by Excalidraw. Open Obsidian Developer Console (CTRL+Shift+i) and execute the following command: `ExcalidrawAutomate.mostRecentMarkdownSVG`
- You can control appearance of the embedded markdown file on a file by file bases by adding the following front matter keys to your markdown document:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`, you can find css color names [here](https://www.w3schools.com/colors/colors_names.asp).
- `excalidraw-css: "css-filename|css snippet"`
- Switch to markdown view or use CTRL/CMD+ALT/OPT click on the image to edit properties of the embed: `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
- 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.

View File

@@ -52,8 +52,20 @@ export interface ExcalidrawAutomate {
}
}
):Promise<string>;
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
createPNG (templatePath?:string):Promise<any>;
createSVG (
templatePath?:string,
embedFont?:boolean,
exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean)
loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
theme?:string
):Promise<SVGSVGElement>;
createPNG (
templatePath?:string,
scale?:number,
exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean)
loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
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;
@@ -84,7 +96,7 @@ export interface ExcalidrawAutomate {
}
):string ;
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
addLaTex(topX:number, topY:number, tex: string, color?:string):Promise<string>;
addLaTex(topX:number, topY:number, tex: string):Promise<string>;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
@@ -134,6 +146,10 @@ export interface ExcalidrawAutomate {
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
mostRecentMarkdownSVG:SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
//utility functions to generate EmbeddedFilesLoaderand ExportSettings objects
getEmbeddedFilesLoader(isDark?:boolean):EmbeddedFilesLoader;
getExportSettings(withBackground:boolean,withTheme:boolean):ExportSettings;
}
```

View File

@@ -1,8 +1,8 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.4.3",
"minAppVersion": "0.12.19",
"version": "1.4.18",
"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-8",
"@zsviczian/excalidraw": "0.10.0-obsidian-12",
"monkey-around": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -32,7 +32,6 @@
"@types/react-dom": "^17.0.9",
"cross-env": "^7.0.3",
"html2canvas": "^1.3.2",
"js-beautify": "1.13.3",
"nanoid": "^3.1.23",
"obsidian": "^0.12.16",
"rollup": "^2.52.3",

460
src/EmbeddedFileLoader.ts Normal file
View File

@@ -0,0 +1,460 @@
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import { CASCADIA_FONT, fileid, FRONTMATTER_KEY_FONT, FRONTMATTER_KEY_FONTCOLOR, FRONTMATTER_KEY_MD_STYLE, IMAGE_TYPES, nanoid, VIRGIL_FONT } from "./constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
import { ExportSettings } from "./ExcalidrawView";
import { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import {errorlog, getImageSize, getLinkParts, LinkParts, svgToBase64 } from "./Utils";
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
export type FileData = BinaryFileData & {
size: Size,
hasSVGwithBitmap: boolean
};
export type Size = {
height: number,
width: number,
}
export class EmbeddedFile {
public file:TFile = null;
public isSVGwithBitmap: boolean = false;
private img: string=""; //base64
private imgInverted: string=""; //base64
public mtime: number = 0; //modified time of the image
private plugin: ExcalidrawPlugin;
public mimeType: MimeType="application/octet-stream";
public size: Size ={height:0,width:0};
public linkParts: LinkParts;
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath:string) {
this.plugin = plugin;
this.resetImage(hostPath,imgPath);
}
public resetImage(hostPath: string, imgPath:string) {
this.imgInverted = this.img = "";
this.mtime = 0;
this.linkParts = getLinkParts(imgPath);
if(!this.linkParts.path) {
new Notice("Excalidraw Error\nIncorrect embedded filename: "+imgPath);
return;
}
if(!this.linkParts.width) this.linkParts.width = this.plugin.settings.mdSVGwidth;
if(!this.linkParts.height) this.linkParts.height = this.plugin.settings.mdSVGmaxHeight;
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(this.linkParts.path,hostPath);
if(!this.file) {
new Notice("Excalidraw Warning: could not find image file: "+imgPath,5000);
}
}
private fileChanged():boolean {
if(!this.file) return false;
return this.mtime !=this.file.stat.mtime;
}
public setImage(imgBase64:string,mimeType:MimeType,size:Size,isDark:boolean,isSVGwithBitmap:boolean) {
if(!this.file) return;
if(this.fileChanged()) this.imgInverted = this.img = "";
this.mtime = this.file.stat.mtime;
this.size = size;
this.mimeType = mimeType;
switch(isDark && isSVGwithBitmap) {
case true: this.imgInverted = imgBase64;break;
case false: this.img = imgBase64; break; //bitmaps and SVGs without an embedded bitmap do not need a negative image
}
this.isSVGwithBitmap = isSVGwithBitmap;
}
public isLoaded(isDark:boolean):boolean {
if(!this.file) return true;
if(this.fileChanged()) return false;
if (this.isSVGwithBitmap && isDark) return this.imgInverted !== "";
return this.img !=="";
}
public getImage(isDark:boolean) {
if(!this.file) return "";
if(isDark && this.isSVGwithBitmap) return this.imgInverted;
return this.img; //images that are not SVGwithBitmap, only the light string is stored, since inverted and non-inverted are ===
}
}
export class EmbeddedFilesLoader {
private plugin:ExcalidrawPlugin;
private processedFiles: Map<string,number> = new Map<string,number>();
private isDark:boolean;
public terminate=false;
public uid:string;
constructor(plugin: ExcalidrawPlugin, isDark?:boolean) {
this.plugin = plugin;
this.isDark = isDark;
this.uid = nanoid();
}
public async getObsidianImage (
inFile: TFile | EmbeddedFile
):Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
hasSVGwithBitmap: boolean,
size: {height: number, width: number},
}> {
if(!this.plugin || !inFile) return null;
const file:TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile;
const linkParts = inFile instanceof EmbeddedFile
? inFile.linkParts
: {
original: file.path,
path: file.path,
isBlockRef: false,
ref: null,
width: this.plugin.settings.mdSVGwidth,
height: this.plugin.settings.mdSVGmaxHeight
}
//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.isExcalidrawFile(file);
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile || file.extension==="md")) {
return null;
}
const ab = await app.vault.readBinary(file);
const getExcalidrawSVG = async (isDark:boolean) => {
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
const exportSettings:ExportSettings = {
withBackground: false,
withTheme: false,
};
const svg = await createSVG(
file.path,
true,
exportSettings,
this,
null,
null,
null,
[],
this.plugin
);
//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":
case "md" : mimeType = "image/svg+xml";break;
default: mimeType = "application/octet-stream";
}
}
const dataURL = excalidrawSVG
?? (file.extension==="svg"
? await getSVGData(app,file)
: (file.extension==="md"
? await convertMarkdownToSVG(this.plugin,file,linkParts)
: await getDataURL(ab,mimeType)
));
const size = await getImageSize(excalidrawSVG
?? (file.extension==="md"
? dataURL
: app.vault.getResourcePath(file)
));
return {
mimeType,
fileId: await generateIdFromFile(ab),
dataURL,
created: file.stat.mtime,
hasSVGwithBitmap,
size
}
}
public async loadSceneFiles (
excalidrawData: ExcalidrawData,
addFiles:Function
) {
const app = this.plugin.app;
const entries = excalidrawData.getFileEntries();
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,isDark:this.isDark,sceneTheme:excalidrawData.scene.appState.theme});
if(this.isDark===undefined) {
this.isDark = excalidrawData.scene.appState.theme==="dark";
}
let entry;
let files:FileData[] = [];
while(!this.terminate && !(entry = entries.next()).done) {
const embeddedFile:EmbeddedFile = entry.value[1];
const updateImage:boolean = !embeddedFile.isLoaded(this.isDark) || embeddedFile.isSVGwithBitmap;
if(!embeddedFile.isLoaded(this.isDark)) {
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"});
const data = await this.getObsidianImage(embeddedFile);
if(data) {
files.push({
mimeType : data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
created: data.created,
size: data.size,
hasSVGwithBitmap: data.hasSVGwithBitmap
});
}
} else if (embeddedFile.isSVGwithBitmap) {
files.push({
mimeType : embeddedFile.mimeType,
id: entry.value[0],
dataURL: embeddedFile.getImage(this.isDark) as DataURL,
created: embeddedFile.mtime,
size: embeddedFile.size,
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap
});
}
}
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;
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"add Files"});
try { //in try block because by the time files are loaded the user may have closed the view
addFiles(files,this.isDark);
} 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 convertMarkdownToSVG = async (plugin: ExcalidrawPlugin, file: TFile, linkParts: LinkParts): Promise<DataURL> => {
//1.
//get the markdown text
const [text,line] = await getTransclusion(linkParts,plugin.app,file);
//2.
//get styles
const fileCache = plugin.app.metadataCache.getFileCache(file);
let fontDef:string;
let fontName = plugin.settings.mdFont;
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_FONT]!=null) {
fontName = fileCache.frontmatter[FRONTMATTER_KEY_FONT];
}
switch(fontName){
case "Virgil": fontDef = VIRGIL_FONT; break;
case "Cascadia": fontDef = CASCADIA_FONT; break;
case "": fontDef = ""; break;
default:
const f = plugin.app.metadataCache.getFirstLinkpathDest(fontName,file.path);
if(f) {
const ab = await plugin.app.vault.readBinary(f);
const mimeType=f.extension.startsWith("woff")?"application/font-woff":"font/truetype";
fontName = f.basename;
fontDef = ` @font-face {font-family: "${fontName}";src: url("${await getDataURL(ab,mimeType)}") format("${f.extension==="ttf"?"truetype":f.extension}");}`;
const split = fontDef.split(";base64,",2);
fontDef = split[0]+";charset=utf-8;base64,"+split[1];
} else {
fontDef = "";
}
}
const fontColor = fileCache?.frontmatter ? fileCache.frontmatter[FRONTMATTER_KEY_FONTCOLOR]??plugin.settings.mdFontColor:plugin.settings.mdFontColor;
let style = fileCache?.frontmatter ? (fileCache.frontmatter[FRONTMATTER_KEY_MD_STYLE]??"") : "";
let frontmatterCSSisAfile = false;
if(style && style!="") {
const f = plugin.app.metadataCache.getFirstLinkpathDest(style,file.path);
if(f) {
style = await plugin.app.vault.read(f);
frontmatterCSSisAfile = true;
}
}
if(!frontmatterCSSisAfile && plugin.settings.mdCSS && plugin.settings.mdCSS!="") {
const f = plugin.app.metadataCache.getFirstLinkpathDest(plugin.settings.mdCSS,file.path);
if(f) {
style += "\n"+await plugin.app.vault.read(f);
}
}
//3.
//SVG helper functions
//the SVG will first have ~infinite height. After sizing this will be reduced
let svgStyle = ' width="'+linkParts.width+'px" height="100000"';
let foreignObjectStyle = ' width="'+linkParts.width+'px" height="100%"';
const svg = (xml:string,xmlFooter:string,style?:string) =>
'<svg xmlns="http://www.w3.org/2000/svg"'+svgStyle+'>'
+ (style?'<style>'+style+'</style>':'')
+ '<foreignObject x="0" y="0"'+foreignObjectStyle+'>'
+ xml
+ xmlFooter //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/286#issuecomment-982179639
+ '</foreignObject>'
+ (fontDef!==""
? ('<defs><style>' + fontDef + '</style></defs>')
: "")
+ '</svg>';
//4.
//create document div - this will be the contents of the foreign object
const mdDIV = createDiv();
mdDIV.setAttribute("xmlns","http://www.w3.org/1999/xhtml");
mdDIV.setAttribute("class","excalidraw-md-host");
// mdDIV.setAttribute("style",style);
if(fontName !== "") mdDIV.style.fontFamily = fontName;
mdDIV.style.overflow = "auto";
mdDIV.style.display = "block";
if(fontColor && fontColor!="") mdDIV.style.color = fontColor;
await MarkdownRenderer.renderMarkdown(text,mdDIV,file.path,plugin);
mdDIV.querySelectorAll(":scope > *[class^='frontmatter']").forEach((el)=>mdDIV.removeChild(el));
//5.1
//get SVG size.
//First I need to create a fully self contained copy of the document to convert
//blank styles into inline styles using computedStyle
const iframeHost = document.body.createDiv();
iframeHost.style.display = "none";
const iframe = iframeHost.createEl("iframe");
const iframeDoc = iframe.contentWindow.document;
if(style) {
const styleEl = iframeDoc.createElement("style");
styleEl.type = "text/css";
styleEl.innerHTML = style;
iframeDoc.head.appendChild(styleEl);
}
const stylingDIV = iframeDoc.importNode(mdDIV,true)
iframeDoc.body.appendChild(stylingDIV);
const footerDIV = createDiv();
footerDIV.setAttribute("class","excalidraw-md-footer");
iframeDoc.body.appendChild(footerDIV);
iframeDoc.body.querySelectorAll("*").forEach((el:HTMLElement)=>{
const elementStyle = el.style;
const computedStyle = window.getComputedStyle(el);
let style = "";
for (const prop in elementStyle) {
if (elementStyle.hasOwnProperty(prop)) {
style += prop + ": " + computedStyle[prop] + ";";
}
}
el.setAttribute("style",style);
});
const xmlINiframe = (new XMLSerializer().serializeToString(stylingDIV))
const xmlFooter = (new XMLSerializer().serializeToString(footerDIV))
document.body.removeChild(iframeHost);
//5.2
//get SVG size
const parser = new DOMParser();
const doc = parser.parseFromString(svg(xmlINiframe,xmlFooter),"image/svg+xml");
const svgEl = doc.firstElementChild;
const host = createDiv();
host.appendChild(svgEl);
document.body.appendChild(host);
const footerHeight = svgEl.querySelector(".excalidraw-md-footer").scrollHeight;
const height = svgEl.querySelector(".excalidraw-md-host").scrollHeight + footerHeight;
const svgHeight = height <= linkParts.height ? height : linkParts.height;
document.body.removeChild(host);
//finalize SVG
svgStyle = ' width="'+linkParts.width+'px" height="'+svgHeight+'px"';
foreignObjectStyle = ' width="'+linkParts.width+'px" height="'+svgHeight+'px"';
mdDIV.style.height = (svgHeight-footerHeight)+"px";
mdDIV.style.overflow = "hidden";
const xml = (new XMLSerializer().serializeToString(mdDIV))
const finalSVG = svg(xml,'<div class="excalidraw-md-footer"></div>',style);
plugin.ea.mostRecentMarkdownSVG = parser.parseFromString(finalSVG,"image/svg+xml").firstElementChild as SVGSVGElement;
return svgToBase64(finalSVG) 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) {
errorlog({where:"EmbeddedFileLoader.generateIdFromFile",error});
id = fileid() as FileId;
}
return id;
};

View File

@@ -4,12 +4,13 @@ import {
StrokeStyle,
StrokeSharpness,
ExcalidrawElement,
FileId,
} from "@zsviczian/excalidraw/types/element/types";
import {
normalizePath,
TFile
} from "obsidian"
import ExcalidrawView, { TextMode } from "./ExcalidrawView";
import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
import { ExcalidrawData} from "./ExcalidrawData";
import {
FRONTMATTER,
@@ -17,8 +18,10 @@ import {
VIEW_TYPE_EXCALIDRAW,
MAX_IMAGE_SIZE,
} from "./constants";
import { embedFontsInSVG, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, tex2dataURL, wrapText } from "./Utils";
import { AppState } from "@zsviczian/excalidraw/types/types";
import { debug, embedFontsInSVG, errorlog, getPNG, getSVG, isObsidianThemeDark, 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";
@@ -71,8 +74,20 @@ export interface ExcalidrawAutomate {
}
}
):Promise<string>;
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
createPNG (templatePath?:string):Promise<any>;
createSVG (
templatePath?:string,
embedFont?:boolean,
exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean)
loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
theme?:string
):Promise<SVGSVGElement>;
createPNG (
templatePath?:string,
scale?:number,
exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean)
loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
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;
@@ -103,7 +118,7 @@ export interface ExcalidrawAutomate {
}
):string ;
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
addLaTex(topX:number, topY:number, tex: string, color?:string):Promise<string>;
addLaTex(topX:number, topY:number, tex: string):Promise<string>;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
@@ -153,6 +168,10 @@ export interface ExcalidrawAutomate {
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
mostRecentMarkdownSVG:SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
//utility functions to generate EmbeddedFilesLoaderand ExportSettings objects
getEmbeddedFilesLoader(isDark?:boolean):EmbeddedFilesLoader;
getExportSettings(withBackground:boolean,withTheme:boolean):ExportSettings;
}
declare let window: any;
@@ -247,7 +266,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
return id;
},
async toClipboard(templatePath?:string) {
const template = templatePath ? (await getTemplate(this.plugin,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(
@@ -281,7 +300,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
}
}
):Promise<string> {
const template = params?.templatePath ? (await getTemplate(this.plugin,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;
@@ -336,53 +357,77 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
: frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t"))
);
},
async createSVG(templatePath?:string,embedFont:boolean = false):Promise<SVGSVGElement> {
const automateElements = this.getElements();
const template = templatePath ? (await getTemplate(this.plugin,templatePath,true)) : null;
let elements = template ? template.elements : [];
elements = elements.concat(automateElements);
const svg = await getSVG(
{//createDrawing
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: template?.appState?.theme ?? this.canvas.theme,
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
},
files: template?.files ?? {}
},
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme
async createSVG(
templatePath?:string,
embedFont:boolean = false,
exportSettings?:ExportSettings,
loader?:EmbeddedFilesLoader,
theme?:string,
):Promise<SVGSVGElement> {
if(!theme) {
theme = this.plugin.settings.previewMatchObsidianTheme
? (isObsidianThemeDark() ? "dark" : "light")
: (!this.plugin.settings.exportWithTheme
? "light"
: undefined);
}
if(theme && !exportSettings) {
exportSettings = {
withBackground: this.plugin.settings.exportBackground,
withTheme: true
}
)
return embedFont ? embedFontsInSVG(svg) : svg;
}
if(!loader) {
const loader = new EmbeddedFilesLoader(this.plugin,theme?(theme==="dark"):undefined);
}
return await createSVG(
templatePath,
embedFont,
exportSettings,
loader,
theme,
this.canvas.theme,
this.canvas.viewBackgroundColor,
this.getElements(),
this.plugin
);
},
async createPNG(templatePath?:string, scale:number=1) {
const automateElements = this.getElements();
const template = templatePath ? (await getTemplate(this.plugin,templatePath,true)) : null;
let elements = template ? template.elements : [];
elements = elements.concat(automateElements);
return getPNG(
{
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: template?.appState?.theme ?? this.canvas.theme,
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
},
files: template?.files ?? {}
},
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme
},
scale
)
async createPNG(
templatePath?:string,
scale:number=1,
exportSettings?:ExportSettings,
loader?:EmbeddedFilesLoader,
theme?:string
) {
if(!theme) {
theme = this.plugin.settings.previewMatchObsidianTheme
? (isObsidianThemeDark() ? "dark" : "light")
: (!this.plugin.settings.exportWithTheme
? "light"
: undefined);
}
if(theme && !exportSettings) {
exportSettings = {
withBackground: this.plugin.settings.exportBackground,
withTheme: true
}
}
if(!loader) {
const loader = new EmbeddedFilesLoader(this.plugin,theme?(theme==="dark"):undefined);
}
return await createPNG(
templatePath,
scale,
exportSettings,
loader,
theme,
this.canvas.theme,
this.canvas.viewBackgroundColor,
this.getElements(),
this.plugin
)
},
wrapText(text:string, lineLen:number):string {
return wrapText(text,lineLen,this.plugin.settings.forceWrap);
@@ -528,7 +573,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
},
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
const id = nanoid();
const image = await getObsidianImage(this.plugin,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,
@@ -536,7 +582,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
dataURL: image.dataURL,
created: image.created,
file: imageFile.path,
tex: null
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,9 +595,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
this.elementsDict[id].scale = [1,1];
return id;
},
async addLaTex(topX:number, topY:number, tex:string, color:string = "black"):Promise<string> {
async addLaTex(topX:number, topY:number, tex:string):Promise<string> {
const id = nanoid();
const image = await tex2dataURL(tex, color);
const image = await tex2dataURL(tex, this.plugin);
if(!image) return null;
this.imagesDict[image.fileId] = {
mimeType: image.mimeType,
@@ -558,7 +605,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
dataURL: image.dataURL,
created: image.created,
file: null,
tex: tex
hasSVGwithBitmap: false,
latex: tex
}
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
this.elementsDict[id].fileId = image.fileId;
@@ -738,6 +786,13 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
},
onDropHook:null,
mostRecentMarkdownSVG:null,
getEmbeddedFilesLoader(isDark?:boolean):EmbeddedFilesLoader {
return new EmbeddedFilesLoader(this.plugin,isDark);
},
getExportSettings(withBackground:boolean,withTheme:boolean):ExportSettings{
return {withBackground,withTheme};
},
};
await initFonts();
return window.ExcalidrawAutomate;
@@ -835,17 +890,23 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
return {w: width, h: height, baseline: baseline };
};
async function getTemplate(plugin: ExcalidrawPlugin, 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 = plugin.app;
const vault = app.vault;
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(plugin);
@@ -857,7 +918,7 @@ async function getTemplate(plugin: ExcalidrawPlugin, fileWithPath:string, loadFi
appState: excalidrawData.scene.appState,
frontmatter: "",
files: excalidrawData.scene.files,
svgSnapshot: null,
hasSVGwithBitmap
};
}
@@ -867,22 +928,31 @@ async function getTemplate(plugin: ExcalidrawPlugin, fileWithPath:string, loadFi
let trimLocation = data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
let scene = excalidrawData.scene;
if(loadFiles) {
await loadSceneFiles(plugin,excalidrawData.files, excalidrawData.equations, null, (fileArray:any, view:any)=>{
//debug({where:"getTemplate",template:file.name,loader:loader.uid});
await loader.loadSceneFiles(excalidrawData, (fileArray:FileData[],isDark:boolean)=>{
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);
},templatePath);
[foo,scene] = scaleLoadedImage(excalidrawData.scene,fileArray);
});
}
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 {
@@ -890,10 +960,82 @@ async function getTemplate(plugin: ExcalidrawPlugin, fileWithPath:string, loadFi
appState: {},
frontmatter: null,
files: [],
svgSnapshot: null,
hasSVGwithBitmap
}
}
export async function createPNG(
templatePath:string = undefined,
scale:number=1,
exportSettings:ExportSettings,
loader:EmbeddedFilesLoader,
forceTheme:string = undefined,
canvasTheme: string = undefined,
canvasBackgroundColor: string = undefined,
automateElements: ExcalidrawElement[] = [],
plugin: ExcalidrawPlugin,
) {
if(!loader) loader = new EmbeddedFilesLoader(plugin);
const template = templatePath ? (await getTemplate(plugin,templatePath,true,loader)) : null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
return await getPNG(
{
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: forceTheme??(template?.appState?.theme ?? canvasTheme),
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
},
files: template?.files ?? {}
},
{
withBackground: exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme
},
scale
)
}
export async function createSVG(
templatePath:string = undefined,
embedFont:boolean = false,
exportSettings:ExportSettings,
loader:EmbeddedFilesLoader,
forceTheme:string = undefined,
canvasTheme: string = undefined,
canvasBackgroundColor: string = undefined,
automateElements: ExcalidrawElement[] = [],
plugin: ExcalidrawPlugin,
):Promise<SVGSVGElement> {
if(!loader) loader = new EmbeddedFilesLoader(plugin);
const template = templatePath ? (await getTemplate(plugin,templatePath,true,loader)) : null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
const svg = await getSVG(
{//createDrawing
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: forceTheme??(template?.appState?.theme ?? canvasTheme),
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
},
files: template?.files ?? {}
},
{
withBackground: exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme
}
)
if(template?.hasSVGwithBitmap) svg.setAttribute("hasbitmap","true");
return embedFont ? embedFontsInSVG(svg) : svg;
}
function estimateLineBound(points:any):[number,number,number,number] {
let minX = Infinity;
let maxX = -Infinity;
@@ -953,12 +1095,12 @@ export function repositionElementsToCursor (elements:ExcalidrawElement[],newPosi
function errorMessage(message: string, source: string) {
switch(message) {
case "targetView not set":
console.log(source, "ExcalidrawAutomate: targetView not set, or no longer active. Use setView before calling this function");
errorlog({where:"ExcalidrawAutomate",source, message:"targetView not set, or no longer active. Use setView before calling this function"});
break;
case "mobile not supported":
console.log(source, "ExcalidrawAutomate: this function is not avalable on Obsidian Mobile");
errorlog({where:"ExcalidrawAutomate",source, message:"this function is not avalable on Obsidian Mobile"});
break;
default:
console.log(source, "ExcalidrawAutomate: unknown error");
errorlog({where:"ExcalidrawAutomate",source, message:"unknown error"});
}
}

View File

@@ -1,9 +1,10 @@
import { App, normalizePath, TFile } from "obsidian";
import { App, Notice, 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,9 +12,11 @@ import {
JSON_parse
} from "./constants";
import { TextMode } from "./ExcalidrawView";
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, isObsidianThemeDark, wrapText } from "./Utils";
import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, getIMGFilename, getLinkParts, isObsidianThemeDark, LinkParts, wrapText } from "./Utils";
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types";
import { nonWhiteSpace } from "html2canvas/dist/types/css/syntax/parser";
import { EmbeddedFile, EmbeddedFilesLoader } from "./EmbeddedFileLoader";
type SceneDataWithFiles = SceneData & { files: BinaryFiles};
@@ -54,7 +57,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,36 +76,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].replaceAll("\n","");
}
return null;
}
export function getMarkdownDrawingSection(jsonString: string,svgString: string) {
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)
+ (svgString ? //&& this.settings.saveSVGSnapshots
'\n\n# SVG snapshot\n'
+ "==⚠ Remove all linebreaks from SVG string before use. Linebreaks were added to improve markdown view speed. ⚠==\n"
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'html\n'
+ svgString + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
: '')
+ '\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;
@@ -113,15 +94,15 @@ export class ExcalidrawData {
private textMode: TextMode = TextMode.raw;
private plugin: ExcalidrawPlugin;
public loaded: boolean = false;
public files:Map<FileId,string> = null; //fileId, path
public equations:Map<FileId,string> = null; //fileId, path
private files:Map<FileId,EmbeddedFile> = 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.equations = new Map<FileId,string>();
this.files = new Map<FileId,EmbeddedFile>();
this.equations = new Map<FileId,{latex:string,isLoaded:boolean}>();
}
/**
@@ -131,10 +112,12 @@ export class ExcalidrawData {
*/
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
this.loaded = false;
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
this.files.clear();
this.equations.clear();
if(this.file!=file) { //this is a reload - files and equations will take care of reloading when needed
this.files.clear();
this.equations.clear();
}
this.file = file;
this.compatibilityMode = false;
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
@@ -175,8 +158,6 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
this.svgSnapshot = getSVGString(data.substr(pos+scene.length));
data = data.substring(0,pos);
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
@@ -215,14 +196,15 @@ export class ExcalidrawData {
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
res = data.matchAll(REG_FILEID_FILEPATH);
while(!(parts = res.next()).done) {
this.files.set(parts.value[1] as FileId,parts.value[2]);
const embeddedFile = new EmbeddedFile(this.plugin,this.file.path,parts.value[2]);
this.setFile(parts.value[1] as FileId,embeddedFile);
}
//Load Equations
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$(.*)(\$\$\s*\n)/gm;
res = data.matchAll(REG_FILEID_EQUATION);
while(!(parts = res.next()).done) {
this.equations.set(parts.value[1] as FileId,parts.value[2]);
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
@@ -281,7 +263,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
}
}
@@ -295,7 +277,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;
}
/**
@@ -375,50 +357,10 @@ export class ExcalidrawData {
* @param text
* @returns [string,number] - the transcluded text, and the line number for the location of the text
*/
public async getTransclusion (text:string):Promise<[string,number]> {
//file-name#^blockref
//1 2 3
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
if(!parts.done && !parts.value[1]) return [text,0]; //filename not found
const filename = parts.done ? text : parts.value[1];
const file = this.app.metadataCache.getFirstLinkpathDest(filename,this.file.path);
if(!file || !(file instanceof TFile)) return [text,0];
const contents = await this.app.vault.cachedRead(file);
if(parts.done) { //no blockreference
return([contents.substr(0,this.plugin.settings.pageTransclusionCharLimit),0]);
}
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
const id = parts.value[3]; //the block ID or heading text
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
if(!blocks) return [text,0];
if(isParagraphRef) {
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
if(!para) return [text,0];
if(["blockquote","listItem"].includes(para.type)) para = para.children[0]; //blockquotes are special, they have one child, which has the paragraph
const startPos = para.position.start.offset;
const lineNum = para.position.start.line;
const endPos = para.children[para.children.length-1]?.position.start.offset-1; //alternative: filter((c:any)=>c.type=="blockid")[0]
return [contents.substr(startPos,endPos-startPos),lineNum]
} else {
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
let startPos:number = null;
let lineNum:number = 0;
let endPos:number = null;
for(let i=0;i<headings.length;i++) {
if(startPos && !endPos) {
endPos = headings[i].node.position.start.offset-1;
return [contents.substr(startPos,endPos-startPos),lineNum];
}
if(!startPos && headings[i].node.children[0]?.value == id) {
startPos = headings[i].node.children[0]?.position.start.offset; //
lineNum = headings[i].node.children[0]?.position.start.line; //
}
}
if(startPos) return [contents.substr(startPos),lineNum];
return [text,0];
}
public async getTransclusion (link:string):Promise<[string,number]> {
const linkParts = getLinkParts(link);
const file = this.app.metadataCache.getFirstLinkpathDest(linkParts.path,this.file.path);
return await getTransclusion(getLinkParts(link),this.app,file,this.plugin.settings.pageTransclusionCharLimit);
}
/**
@@ -518,19 +460,19 @@ export class ExcalidrawData {
outString += (this.equations.size>0 || this.files.size>0) ? '\n# Embedded files\n' : '';
if(this.equations.size>0) {
for(const key of this.equations.keys()) {
outString += key +': $$'+this.equations.get(key) + '$$\n';
outString += key +': $$'+this.equations.get(key).latex + '$$\n';
}
}
if(this.files.size>0) {
for(const key of this.files.keys()) {
outString += key +': [['+this.files.get(key) + ']]\n';
outString += key +': [['+this.files.get(key).linkParts.original + ']]\n';
}
}
outString += (this.equations.size>0 || this.files.size>0) ? '\n' : '';
const sceneJSONstring = JSON.stringify(this.scene,null,"\t");
return outString + getMarkdownDrawingSection(sceneJSONstring,this.svgSnapshot);
return outString + getMarkdownDrawingSection(sceneJSONstring);
}
private async syncFiles(scene:SceneDataWithFiles):Promise<boolean> {
@@ -556,10 +498,11 @@ export class ExcalidrawData {
if(!scene.files || scene.files == {}) return false;
for(const key of Object.keys(scene.files)) {
if(!(this.files.has(key as FileId) || this.equations.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) {
const mimeType = scene.files[key].mimeType;
switch(mimeType) {
case "image/png": fname += ".png"; break;
case "image/jpeg": fname += ".jpg"; break;
case "image/svg+xml": fname += ".svg"; break;
@@ -567,8 +510,17 @@ export class ExcalidrawData {
default: fname += ".png";
}
const [folder,filepath] = await getAttachmentsFolderAndFilePath(this.app,this.file.path,fname);
await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(scene.files[key].dataURL));
this.files.set(key as FileId,filepath);
const dataURL = scene.files[key].dataURL;
await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(dataURL));
const embeddedFile = new EmbeddedFile(this.plugin,this.file.path,filepath);
embeddedFile.setImage(
dataURL,
mimeType,
{height:0,width:0},
scene.appState?.theme==="dark",
mimeType === "image/svg+xml" //this treat all SVGs as if they had embedded images REF:addIMAGE
);
this.setFile(key as FileId,embeddedFile);
}
}
return dirty;
@@ -630,6 +582,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);
@@ -663,4 +629,127 @@ 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:EmbeddedFile) {
//always store absolute path because in case of paste, relative path may not resolve ok
if(!data) return;
this.files.set(fileId,data);
if(!data.file) return;
this.plugin.filesMaster.set(
fileId,
{
path:data.file.path,
hasSVGwithBitmap:data.isSVGwithBitmap
});
}
public getFile(fileId:FileId):EmbeddedFile {
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)) {
const fileMaster = this.plugin.filesMaster.get(fileId);
const embeddedFile = new EmbeddedFile(this.plugin,this.file.path,fileMaster.path);
this.files.set(fileId,embeddedFile);
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;
}
}
export const getTransclusion = async (linkParts:LinkParts,app:App,file:TFile,charCountLimit?:number):Promise<[string,number]> => {
//file-name#^blockref
//1 2 3
if(!linkParts.path) return [linkParts.original.trim(),0]; //filename not found
if(!file || !(file instanceof TFile)) return [linkParts.original.trim(),0];
const contents = await app.vault.read(file);
if(!linkParts.ref) { //no blockreference
return charCountLimit ? [contents.substr(0,charCountLimit).trim(),0] : [contents.trim(),0];
}
//const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
//const id = parts.value[3]; //the block ID or heading text
const blocks = (await app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
if(!blocks) return [linkParts.original.trim(),0];
if(linkParts.isBlockRef) {
let para = blocks.filter((block:any)=>block.node.id == linkParts.ref)[0]?.node;
if(!para) return [linkParts.original.trim(),0];
if(["blockquote","listItem"].includes(para.type)) para = para.children[0]; //blockquotes are special, they have one child, which has the paragraph
const startPos = para.position.start.offset;
const lineNum = para.position.start.line;
const endPos = para.children[para.children.length-1]?.position.start.offset-1; //alternative: filter((c:any)=>c.type=="blockid")[0]
return [contents.substr(startPos,endPos-startPos).trim(),lineNum]
} else {
const headings = blocks.filter((block:any)=>block.display.search(/^#+\s/)===0);// startsWith("#"));
let startPos:number = null;
let lineNum:number = 0;
let endPos:number = null;
for(let i=0;i<headings.length;i++) {
if(startPos && !endPos) {
endPos = headings[i].node.position.start.offset-1;
return [contents.substr(startPos,endPos-startPos).trim(),lineNum];
}
const c = headings[i].node.children[0];
const cc = c?.children;
if(!startPos && (c?.value === linkParts.ref || (cc?cc[0]?.value===linkParts.ref:false) ) ) {
startPos = headings[i].node.children[0]?.position.start.offset; //
lineNum = headings[i].node.children[0]?.position.start.line; //
}
}
if(startPos) return [contents.substr(startPos).trim(),lineNum];
return [linkParts.original.trim(),0];
}
}

View File

@@ -19,7 +19,6 @@ import {
import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
EXCALIDRAW_LIB_HEADER,
DISK_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME,
@@ -28,15 +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, updateEquation, viewportCoordsToSceneCoords } from "./Utils";
import { checkAndCreateFolder, debug, download, embedFontsInSVG, errorlog, getIMGFilename, getLinkParts, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, rotatedDimensions, scaleLoadedImage, splitFolderAndFilename, svgToBase64, viewportCoordsToSceneCoords } from "./Utils";
import { Prompt } from "./Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { updateEquation } from "./LaTeX";
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
export enum TextMode {
parsed,
@@ -54,24 +56,43 @@ export interface ExportSettings {
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export const addFiles = (files:any, view: ExcalidrawView) => {
export const addFiles = async (files:FileData[], view: ExcalidrawView,isDark?:boolean) => {
if(!files || files.length === 0 || !view) return;
files = files.filter((f)=>(f.size.height>0) && (f.size.width>0)); //height will be zero when file does not exisig in case of broken embedded file links
if(files.length === 0) return;
const [dirty, scene] = scaleLoadedImage(view.getScene(),files);
if(isDark===undefined) isDark = scene.appState.theme;
if(dirty) {
//debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState})
view.excalidrawAPI.updateScene({
elements: scene.elements,
appState: scene.appState,
commitToHistory: false,
});
}
for(const f of files) {
if(view.excalidrawData.hasFile(f.id)) {
const embeddedFile = view.excalidrawData.getFile(f.id);
embeddedFile.setImage(
f.dataURL,
f.mimeType,
f.size,
isDark,
f.hasSVGwithBitmap
);
}
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;
public excalidrawData: ExcalidrawData;
public getScene: Function = null;
public addElements: Function = null; //add elements to the active Excalidraw drawing
private getSelectedTextElement: Function = null;
@@ -120,7 +141,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 = {
@@ -142,7 +163,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 () => {
@@ -159,6 +180,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();
@@ -167,10 +189,9 @@ export default class ExcalidrawView extends TextFileView {
await this.excalidrawData.syncElements(scene);
} else {
if(await this.excalidrawData.syncElements(scene) && !this.autosaving) {
//debug({where:"ExcalidrawView.save",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"loadDrawing(false)"})
await this.loadDrawing(false);
}
//generate SVG preview snapshot
this.excalidrawData.svgSnapshot = await generateSVGString(this.getScene(),this.plugin.settings);
}
await super.save();
}
@@ -194,8 +215,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) {
@@ -269,20 +298,39 @@ export default class ExcalidrawView extends TextFileView {
} else {
const selectedImage = this.getSelectedImageElement();
if(selectedImage?.id) {
if(this.excalidrawData.equations.has(selectedImage.fileId)) {
const equation = this.excalidrawData.equations.get(selectedImage.fileId);
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.equations.set(selectedImage.fileId,formula);
this.excalidrawData.setEquation(selectedImage.fileId,{latex:formula,isLoaded:false});
await this.save(true);
await updateEquation(formula,selectedImage.fileId,this,addFiles);
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)) {
if(ev.altKey) {
const ef = this.excalidrawData.getFile(selectedImage.fileId);
if(ef.file.extension==="md" && !this.plugin.isExcalidrawFile(ef.file)) {
const prompt = new Prompt(
this.app,
"Customize the link",
ef.linkParts.original,
'',
"Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>"
);
prompt.openAndGetValue( async (link:string)=> {
if(!link) return;
ef.resetImage(this.file.path,link);
await this.save(true);
await this.loadSceneFiles();
});
return;
}
}
linkText = this.excalidrawData.getFile(selectedImage.fileId).file.path;
}
}
}
@@ -322,6 +370,7 @@ export default class ExcalidrawView extends TextFileView {
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
await this.save(false);
this.plugin.triggerEmbedUpdates();
this.loadSceneFiles();
});
this.textIsRaw_Element = this.addAction(TEXT_DISPLAY_RAW_ICON_NAME,t("RAW"), (ev) => this.changeTextMode(TextMode.parsed));
@@ -350,6 +399,20 @@ export default class ExcalidrawView extends TextFileView {
this.setupAutosaveTimer();
}
public setTheme(theme:string) {
if(!this.excalidrawRef) return;
const st:AppState = this.excalidrawAPI.getAppState();
this.excalidrawData.scene.theme = theme;
//debug({where:"ExcalidrawView.setTheme",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene"});
this.excalidrawAPI.updateScene({
appState: {
...st,
theme: theme,
},
commitToHistory: false,
});
}
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
this.textMode = textMode;
if(textMode == TextMode.parsed) {
@@ -400,6 +463,8 @@ export default class ExcalidrawView extends TextFileView {
if(file) this.data = await this.app.vault.cachedRead(file);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
else await this.excalidrawData.setTextMode(this.textMode);
this.excalidrawData.scene.appState.theme = this.excalidrawAPI.getAppState().theme;
//debug({where:"ExcalidrawView.reload",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"loadDrawing(false)"})
await this.loadDrawing(false);
this.dirty = null;
}
@@ -407,18 +472,21 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
if(!this.excalidrawRef) return;
if(this.activeLoader) this.activeLoader.terminate=true;
this.nextLoader = null;
this.excalidrawAPI.resetScene();
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();
@@ -427,11 +495,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")
@@ -442,20 +511,54 @@ export default class ExcalidrawView extends TextFileView {
return;
}
}
await this.loadDrawing(true)
//debug({where:"ExcalidrawView.setViewData",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"loadDrawing(true)"})
await this.loadDrawing(true);
this.isLoaded=true;
});
}
public activeLoader:EmbeddedFilesLoader = null;
private nextLoader:EmbeddedFilesLoader = null;
public async loadSceneFiles() {
const loader = new EmbeddedFilesLoader(this.plugin);
//debug({where:"ExcalidrawView.loadSceneFiles",status:"loader created",file:this.file.name,loader:loader.uid});
const runLoader = (l:EmbeddedFilesLoader) => {
this.nextLoader = null;
this.activeLoader = l;
//debug({where:"ExcalidrawView.loadSceneFiles",status:"loader initiated",file:this.file.name,loader:l.uid});
//debug({where:"ExcalidrawView.loadSceneFiles",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"loader.loadSceneFiles",isDark})
l.loadSceneFiles(
this.excalidrawData,
(files:FileData[], isDark:boolean) => {
if(!files) return;
addFiles(files,this,isDark);
this.activeLoader = null;
if(this.nextLoader) {
runLoader(this.nextLoader);
}
});
}
if(!this.activeLoader) {
runLoader(loader);
} else {
this.nextLoader=loader;
}
}
/**
*
* @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;
//debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,dataTheme:excalidrawData.appState.theme,before:"updateScene"})
this.excalidrawAPI.updateScene({
elements: excalidrawData.elements,
appState: {
@@ -469,18 +572,16 @@ export default class ExcalidrawView extends TextFileView {
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
this.excalidrawWrapperRef.current.focus();
}
loadSceneFiles(
this.plugin,
this.excalidrawData.files,
this.excalidrawData.equations,
this,
(files:any, view:ExcalidrawView) => addFiles(files,view),
this.file?.path
);
//debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"});
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(),
});
@@ -567,7 +668,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
@@ -592,7 +693,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
@@ -611,8 +712,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??[]);
}
@@ -657,14 +760,9 @@ export default class ExcalidrawView extends TextFileView {
React.useEffect(() => {
excalidrawRef.current.readyPromise.then((api) => {
this.excalidrawAPI = api;
loadSceneFiles(
this.plugin,
this.excalidrawData.files,
this.excalidrawData.equations,
this,
(files:any, view:ExcalidrawView)=>addFiles(files,view),
this.file?.path
);
//console.log({where:"ExcalidrawView.React.ReadyPromise"});
//debug({where:"ExcalidrawView.React.useEffect",file:this.file.name,before:"this.loadSceneFiles"});
this.loadSceneFiles();
});
}, [excalidrawRef]);
@@ -766,10 +864,12 @@ export default class ExcalidrawView extends TextFileView {
}
};
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
const newIds = newElements.map((e)=>e.id);
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements().filter((e:ExcalidrawElement)=>!newIds.includes(e.id));
let st: AppState = this.excalidrawAPI.getAppState();
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
//debug({where:"ExcalidrawView.addElements",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene",state:st})
this.excalidrawAPI.updateScene({
elements: el.concat(newElements),
appState: st,
@@ -785,10 +885,23 @@ export default class ExcalidrawView extends TextFileView {
created: images[k].created
});
if(images[k].file) {
this.excalidrawData.files.set(images[k].id,images[k].file);
const embeddedFile = new EmbeddedFile(this.plugin,this.file.path,images[k].file);
embeddedFile.setImage(
images[k].dataURL,
images[k].mimeType,
images[k].size,
st.theme==="dark",
images[k].hasSVGwithBitmap
);
this.excalidrawData.setFile(images[k].id,embeddedFile);
}
if(images[k].tex) {
this.excalidrawData.equations.set(images[k].id,images[k].tex);
if(images[k].latex) {
this.excalidrawData.setEquation(
images[k].id,
{
latex:images[k].latex,
isLoaded:true
});
}
});
this.excalidrawAPI.addFiles(files);
@@ -950,26 +1063,33 @@ 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.metaKey) && !e.shiftKey && !e.altKey) {
if(e[CTRL_OR_CMD] && !e.shiftKey && !e.altKey) { //.ctrlKey||e.metaKey) && !e.shiftKey && !e.altKey) {
let linktext = "";
const selectedElement = getTextElementAtPointer(currentPosition);
if(!selectedElement) return;
if(!selectedElement || !selectedElement.text) {
const selectedImgElement = getImageElementAtPointer(currentPosition)
if(!selectedImgElement || !selectedImgElement.fileId) return;
if(!this.excalidrawData.hasFile(selectedImgElement.fileId)) return;
const ef = this.excalidrawData.getFile(selectedImgElement.fileId);
const ref = ef.linkParts.ref ? "#"+(ef.linkParts.isBlockRef?"^":"")+ef.linkParts.ref:"";
linktext = this.excalidrawData.getFile(selectedImgElement.fileId).file.path+ref;
} else {
const text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(selectedElement.id)
: selectedElement.text;
const text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(selectedElement.id)
: selectedElement.text;
if(!text) return;
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
if(!text) return;
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
const parts = REGEX_LINK.getRes(text).next();
if(!parts.value) return;
let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
if(linktext.match(REG_LINKINDEX_HYPERLINK)) return;
const parts = REGEX_LINK.getRes(text).next();
if(!parts.value) return;
linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
if(linktext.match(REG_LINKINDEX_HYPERLINK)) return;
}
this.plugin.hover.linkText = linktext;
this.plugin.hover.sourcePath = this.file.path;
@@ -993,12 +1113,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 => {
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);
@@ -1083,7 +1203,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();
})();
},
@@ -1094,6 +1220,11 @@ export default class ExcalidrawView extends TextFileView {
}
return true;
},
onThemeChange: async (newTheme:string) => {
//debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme});
this.excalidrawData.scene.appState.theme = newTheme;
this.loadSceneFiles();
},
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
const st: AppState = this.excalidrawAPI.getAppState();
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
@@ -1118,7 +1249,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 {
@@ -1130,9 +1261,9 @@ 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))) {
|| draggable.file.extension==="md")) {//this.plugin.isExcalidrawFile(draggable.file)
const f = draggable.file;
const topX = currentPosition.x;
const topY = currentPosition.y;
@@ -1140,6 +1271,7 @@ export default class ExcalidrawView extends TextFileView {
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);
})();
@@ -1265,4 +1397,9 @@ export default class ExcalidrawView extends TextFileView {
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;
}

View File

@@ -42,6 +42,7 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
const ea = this.plugin.ea;
ea.reset();
ea.setView(this.view);
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
(async () => {
await ea.addImage(0,0,item);
ea.addElementsToView(true,false);

54
src/InsertMDDialog.ts Normal file
View File

@@ -0,0 +1,54 @@
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 InsertMDDialog 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_MD"));
this.emptyStateText = t("NO_MATCH");
}
getItems(): TFile[] {
return (this.app.vault.getFiles() || []).filter((f:TFile) => (f.extension==="md") && !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,6 +1,9 @@
import { App, Modal, Notice, TFile } from "obsidian";
import { getJSON } from "./ExcalidrawData";
import { FRONTMATTER_KEY } from "./constants";
import { ExcalidrawData, getJSON } from "./ExcalidrawData";
import { getTextMode, TextMode } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { errorlog } from "./Utils";
export class OneOffs {
private plugin:ExcalidrawPlugin
@@ -72,6 +75,66 @@ export class OneOffs {
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) {
errorlog({where:"OneOffs.wysiwygPatch",message:"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 {

View File

@@ -4,7 +4,7 @@ export class Prompt extends Modal {
private promptEl: HTMLInputElement;
private resolve: (value: string) => void;
constructor(app: App, private prompt_text: string, private default_value: string, private placeholder:string) {
constructor(app: App, private prompt_text: string, private default_value: string, private placeholder:string, private prompt_desc?:string) {
super(app);
}
@@ -18,9 +18,14 @@ export class Prompt extends Modal {
}
createForm(): void {
const div = this.contentEl.createDiv();
let div = this.contentEl.createDiv();
div.addClass("excalidraw-prompt-div");
if(this.prompt_desc) {
div = div.createDiv();
div.style.width = "100%";
const p=div.createEl("p");
p.innerHTML = this.prompt_desc;
}
const form = div.createEl("form");
form.addClass("excalidraw-prompt-form");
form.type = "submit";

View File

@@ -1,14 +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 { CASCADIA_FONT, fileid, IMAGE_TYPES, VIRGIL_FONT } from "./constants";
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 ExcalidrawView, { ExportSettings } from "./ExcalidrawView";
import { ExcalidrawSettings } from "./settings";
import { html_beautify } from "js-beautify";
import html2canvas from "html2canvas";
import { ExportSettings } from "./ExcalidrawView";
declare module "obsidian" {
interface Workspace {
@@ -19,16 +16,11 @@ declare module "obsidian" {
}
}
declare let window: any;
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)),
@@ -154,7 +146,6 @@ export const rotatedDimensions = (
];
}
export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number },
{
@@ -189,99 +180,9 @@ export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLe
return plugin.app.workspace.createLeafBySplit(leaf);
}
export const getObsidianImage = async (plugin: ExcalidrawPlugin, file: TFile)
:Promise<{
mimeType: MimeType,
fileId: FileId,
dataURL: DataURL,
created: number,
size: {height: number, width: number},
}> => {
if(!plugin || !file) return null;
const app = plugin.app;
const isExcalidrawFile = plugin.ea.isExcalidrawFile(file);
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
return null;
}
const ab = await app.vault.readBinary(file);
const getExcalidrawSVG = async () => {
plugin.ea.reset();
return svgToBase64((await plugin.ea.createSVG(file.path,true)).outerHTML) as DataURL;
}
const excalidrawSVG = isExcalidrawFile
? await getExcalidrawSVG()
: null;
let mimeType:MimeType = "image/svg+xml";
if (!isExcalidrawFile) {
switch (file.extension) {
case "png": mimeType = "image/png";break;
case "jpeg":mimeType = "image/jpeg";break;
case "jpg": mimeType = "image/jpeg";break;
case "gif": mimeType = "image/gif";break;
case "svg": mimeType = "image/svg+xml";break;
default: mimeType = "application/octet-stream";
}
}
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 = fileid() 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;
@@ -312,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,
@@ -326,33 +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 wrapText(html_beautify(svg.outerHTML,{"indent_with_tabs": true}),4096,true);// 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) {
errorlog({where:"Utils.getPNG",error});
return null;
}
}
@@ -368,80 +256,18 @@ export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => {
return svg;
}
export const loadSceneFiles = async (
plugin:ExcalidrawPlugin,
filesMap: Map<FileId, string>,
equationsMap: Map<FileId, string>,
view: ExcalidrawView,
addFiles:Function,
sourcePath:string
) => {
const app = plugin.app;
let entries = filesMap.entries();
let entry;
let files:BinaryFileData[] = [];
while(!(entry = entries.next()).done) {
const file = app.metadataCache.getFirstLinkpathDest(entry.value[1],sourcePath)
if(file && file instanceof TFile) {
const data = await getObsidianImage(plugin,file);
files.push({
mimeType : data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
created: data.created,
//@ts-ignore
size: data.size,
});
}
}
entries = equationsMap.entries();
while(!(entry = entries.next()).done) {
const tex = entry.value[1];
const data = await tex2dataURL(tex);
if(data) {
files.push({
mimeType : data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
created: data.created,
//@ts-ignore
size: data.size,
});
}
}
try { //in try block because by the time files are loaded the user may have closed the view
addFiles(files,view);
} catch(e) {
}
}
export const updateEquation = async (
equation: string,
fileId: string,
view: ExcalidrawView,
addFiles:Function
) => {
const data = await tex2dataURL(equation);
if(data) {
let files:BinaryFileData[] = [];
files.push({
mimeType : data.mimeType,
id: fileId as FileId,
dataURL: data.dataURL,
created: data.created,
//@ts-ignore
size: data.size,
});
addFiles(files,view);
}
export 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;
@@ -467,35 +293,39 @@ export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark");
export async function tex2dataURL(tex:string, color:string="black"):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 = color;
export function getIMGFilename(path:string,extension:string):string {
return path.substring(0,path.lastIndexOf('.')) + '.' + extension;
}
//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);
export type LinkParts = {
original: string,
path: string,
isBlockRef: boolean,
ref: string,
width: number,
height: number
}
export const getLinkParts = (fname:string):LinkParts => {
const REG = /(^[^#\|]+)#?(\^)?([^\|]*)?\|?(\d*)x?(\d*)/;
const parts = fname.match(REG)
return {
mimeType: "image/png",
fileId: fileid() as FileId,
dataURL: canvas.toDataURL() as DataURL,
created: Date.now(),
size: {height: canvas.height, width: canvas.width}
original: fname,
path: parts[1],
isBlockRef: parts[2]==="^",
ref: parts[3],
width: parts[4]?parseInt(parts[4]):undefined,
height: parts[5]?parseInt(parts[5]):undefined
}
}
export const errorlog = (data:{}) => {
console.error({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

@@ -1,6 +1,7 @@
//Solution copied from obsidian-kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/lang/helpers.ts
import { moment } from "obsidian";
import { errorlog } from "src/Utils";
import ar from "./locale/ar";
import cz from "./locale/cz";
import da from "./locale/da";
@@ -55,7 +56,7 @@ const locale = localeMap[moment.locale()];
export function t(str: keyof typeof en): string {
if (!locale) {
console.error("Error: Excalidraw locale not found", moment.locale());
errorlog({where:"helpers.t",message:"Error: Excalidraw locale not found", locale: moment.locale()});
}
return (locale && locale[str]) || en[str];

View File

@@ -24,6 +24,7 @@ export default {
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
INSERT_LINK: "Insert link to file",
INSERT_IMAGE: "Insert image from vault",
INSERT_MD: "Insert markdown file from vault",
INSERT_LATEX: "Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
ENTER_LATEX: "Enter a valid LaTeX expression",
@@ -80,13 +81,18 @@ export default {
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_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/CMD + 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. " +
@@ -109,7 +115,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/CMD + CLICK on text to open them as links",
LINK_CTRL_CLICK_NAME: "CTRL/CMD + CLICK on text with [[links]] or [](links) to open them",
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",
@@ -121,9 +127,36 @@ export default {
"![[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",
MD_HEAD: "Markdown-embed settings",
MD_HEAD_DESC: "You can transclude formatted markdown documents into drawings as images CTRL/CMD drop from the file explorer or using "+
"the command palette action.",
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
MD_TRANSCLUDE_WIDTH_DESC: "The width of the markdown page. This effects the word wrapping when transcluding longer paragraphs, and the width of " +
"the image element. You can override the default width of " +
"an embedded file using the [[filename#heading|WIDTHxMAXHEIGHT]] syntax in markdown view mode under embedded files.",
MD_TRANSCLUDE_HEIGHT_NAME: "Default maximum height of a transcluded markdown document",
MD_TRANSCLUDE_HEIGHT_DESC: "The embedded image will be as high as the markdown text requries, but not higher than this value. " +
"You can override this value by editing the embedded image link in markdown view mode with the following syntax [[filename#^blockref|WIDTHxMAXHEIGHT]].",
MD_DEFAULT_FONT_NAME: "The default font typeface to use for embedded markdown files.",
MD_DEFAULT_FONT_DESC: 'Set this value to "Virgil" or "Cascadia" or the filename of a valid .ttf, .woff, or .woff2 font e.g. "MyFont.woff2" ' +
'You can override this setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-font: font_or_filename"',
MD_DEFAULT_COLOR_NAME: "The default font color to use for embedded markdown files.",
MD_DEFAULT_COLOR_DESC: 'Set this to allowed css color names e.g. "steelblue" (https://www.w3schools.com/colors/colors_names.asp), or a valid hexadecimal color e.g. "#e67700". ' +
'You can override this setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-font-color: color_name_or_rgbhex"',
MD_CSS_NAME: "CSS file",
MD_CSS_DESC: "The filename of the CSS to apply to markdown embeds. Provide the filename with extension (e.g. 'md-embed.css'). The css file may also be a plain " +
"markdown file (e.g. 'md-embed-css.md'), just make sure the content is written using valid css syntax. " +
"If you need to look at the HTML code you are applying the CSS to, then open Obsidian Developer Console (CTRL+SHIFT+i) and type in the follwoing command: " +
'"ExcalidrawAutomate.mostRecentMarkdownSVG". This will display the most recent SVG generated by Excalidraw. ' +
"Setting the font-family in the css is has limitations. By default only your operating system's standard fonts are available (see README for details). "+
"You can add one custom font beyond that using the setting above. " +
'You can override this css setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-css: css_file_in_valut|css-snippet".',
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 " +
@@ -131,7 +164,7 @@ export default {
EMBED_TYPE_NAME: "Type of file to insert into the document",
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
@@ -139,10 +172,10 @@ export default {
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
EXPORT_THEME_NAME: "Export image with theme",
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
"drawings created in drak mode will appear as they would in light mode.",
"drawings created in dark mode will appear as they would in light mode.",
EXPORT_HEAD: "Export Settings",
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
EXPORT_SYNC_DESC:"When turned on, the plugin will automatically update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
EXPORT_SVG_NAME: "Auto-export SVG",
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
@@ -178,4 +211,8 @@ export default {
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.",
SELECT_MD: "Select the markdown document you want to insert",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING: "EXCALIDRAW WARNING\nAborted loading embedded images due to infinite loop in file:\n",
};

View File

@@ -118,9 +118,9 @@ export default {
SYNC_EXCALIDRAW_DESC: "如果 *.excalidraw 文件的修改比 *.md 文件的修改更新" +
",会根据 .excalidraw 文件更新 .md 文件中的绘图",
COMPATIBILITY_MODE_NAME: "以旧格式创建新绘图",
COMPATIBILITY_MODE_DESC: "通过启用此功能图形,您可以使用功能区图标、命令面板操作、 "+
"并且文件浏览器将仍旧保留 *.excalidraw 文件。 此设置还将" +
"关闭你打开旧格式绘图时的提醒消息",
COMPATIBILITY_MODE_DESC: "启用此功能后,你使用功能区图标、命令面板"+
"文件浏览器创建的绘图都将是旧格式 *.excalidraw 文件。 此设置还将" +
"关闭你打开并编辑旧格式绘图文件时的提醒消息",
EXPERIMENTAL_HEAD: "实验性特性",
EXPERIMENTAL_DESC: "这些设置不会立即生效,只有在刷新文件资源管理器或重新启动 Obsidian 时才会生效。",
FILETYPE_NAME: "在文件浏览器中给所有的 Excalidraw 文件加上 ✏️ 标识符",

View File

@@ -33,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, getMarkdownDrawingSection, getSVGString} from "./ExcalidrawData";
import {ExcalidrawData, getJSON, getMarkdownDrawingSection} from "./ExcalidrawData";
import {
ExcalidrawSettings,
DEFAULT_SETTINGS,
@@ -52,16 +53,25 @@ import {
import {
InsertImageDialog
} from "./InsertImageDialog";
import {
InsertMDDialog
} from "./InsertMDDialog";
import {
initExcalidrawAutomate,
destroyExcalidrawAutomate,
ExcalidrawAutomate
ExcalidrawAutomate,
createSVG,
createPNG
} from "./ExcalidrawAutomate";
import { Prompt } from "./Prompt";
import { around } from "monkey-around";
import { t } from "./lang/helpers";
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, splitFolderAndFilename, svgToBase64 } from "./Utils";
import { checkAndCreateFolder, debug, 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 {
@@ -79,16 +89,25 @@ export default class ExcalidrawPlugin extends Plugin {
private openDialog: OpenFileDialog;
private insertLinkDialog: InsertLinkDialog;
private insertImageDialog: InsertImageDialog;
private insertMDDialog: InsertMDDialog;
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() {
@@ -110,6 +129,7 @@ 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();
@@ -128,14 +148,43 @@ export default class ExcalidrawPlugin extends Plugin {
const patches = new OneOffs(this);
patches.migrationNotice();
patches.patchCommentBlock();
patches.wysiwygPatch();
patches.imageElementLaunchNotice();
this.switchToExcalidarwAfterLoad()
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() {
@@ -186,37 +235,65 @@ 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) {
const theme = this.settings.previewMatchObsidianTheme
? (isObsidianThemeDark() ? "dark" : "light")
: (!this.settings.exportWithTheme
? "light"
: undefined);
if(theme) exportSettings.withTheme = true;
const loader = new EmbeddedFilesLoader(this,theme?(theme==="dark"):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 createPNG(
file.path,
scale,
exportSettings,
loader,
theme,
null,
null,
[],
this
);
//const png = await getPNG(JSON_parse(scene),exportSettings, scale);
if(!png) return null;
img.src = URL.createObjectURL(png);
return img;
}*/
}
const svgSnapshot = (
await createSVG(
file.path,
true,
exportSettings,
loader,
theme,
null,
null,
[],
this
)).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);
@@ -226,6 +303,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
@@ -233,13 +353,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");
@@ -259,29 +386,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);
}
}
@@ -311,47 +417,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;
@@ -369,15 +446,32 @@ 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:MutationRecord[])=>{
if(!this.settings.matchThemeTrigger) return;
//@ts-ignore
if(m[0]?.oldValue === m[0]?.target?.getAttribute("class")) return;
//@ts-ignore
if(m[0]?.oldValue?.includes("theme-dark") === m[0]?.target?.classList?.contains("theme-dark")) 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, {attributeOldValue:true, attributeFilter:["class"]});
}
public experimentalFileTypeDisplayToggle(enabled: boolean) {
@@ -395,8 +489,11 @@ export default class ExcalidrawPlugin extends Plugin {
private experimentalFileTypeDisplay() {
const insertFiletype = (el: HTMLElement) => {
if(el.childElementCount != 1) return;
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
const filename = el.getAttribute("data-path");
if(!filename) return;
const f = this.app.vault.getAbstractFileByPath(filename);
if(!f || !(f instanceof TFile)) return;
if(this.isExcalidrawFile(f)) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
};
@@ -424,9 +521,10 @@ export default class ExcalidrawPlugin extends Plugin {
this.openDialog = new OpenFileDialog(this.app, this);
this.insertLinkDialog = new InsertLinkDialog(this.app);
this.insertImageDialog = new InsertImageDialog(this);
this.insertMDDialog = new InsertMDDialog(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) => {
@@ -494,7 +592,7 @@ export default class ExcalidrawPlugin extends Plugin {
});
return;
}
download('data:text/plain;charset=utf-8',encodeURIComponent(this.settings.library), 'my-obsidian-library.excalidrawlib');
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.settings.library2,null,"\t")), 'my-obsidian-library.excalidrawlib');
},
});
@@ -684,6 +782,24 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "insert-md",
name: t("INSERT_MD"),
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.insertMDDialog.start(view);
return true;
}
else return false;
}
},
});
this.addCommand({
id: "insert-LaTeX-symbol",
name: t("INSERT_LATEX"),
@@ -921,9 +1037,10 @@ export default class ExcalidrawPlugin extends Plugin {
leaves.forEach((leaf:WorkspaceLeaf)=> {
const excalidrawView = (leaf.view as ExcalidrawView);
if(excalidrawView.file
&& (excalidrawView.file.path == file.path
|| (file.extension=="excalidraw"
&& file.path.substring(0,file.path.lastIndexOf('.excalidraw'))+'.md' == excalidrawView.file.path))) {
&& (excalidrawView.file.path === file.path
|| (file.extension === "excalidraw"
&& file.path.substring(0,file.path.lastIndexOf('.excalidraw'))+'.md' === excalidrawView.file.path))) {
//debug({where:"ExcalidrawPlugin.modifyEventHandler",file:file.name,reloadfile:excalidrawView.file,before:"reload(true)"});
excalidrawView.reload(true,excalidrawView.file);
}
});
@@ -973,26 +1090,39 @@ export default class ExcalidrawPlugin extends Plugin {
self.registerEvent(
self.app.workspace.on("quit",quitEventHandler)
);
//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 : null;
self.activeExcalidrawView = newActiveview;
if(newActiveview) {
self.lastActiveExcalidrawFilePath = newActiveview.file?.path;
const previouslyActiveEV = self.activeExcalidrawView;
const newActiveviewEV:ExcalidrawView = (leaf.view instanceof ExcalidrawView) ? leaf.view : null;
self.activeExcalidrawView = newActiveviewEV;
if(newActiveviewEV) {
self.lastActiveExcalidrawFilePath = newActiveviewEV.file?.path;
}
if(activeExcalidrawView && activeExcalidrawView != newActiveview) {
if(activeExcalidrawView.leaf != leaf) {
if(previouslyActiveEV && previouslyActiveEV != newActiveviewEV) {
if(previouslyActiveEV.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);
await previouslyActiveEV.save(true); //this will update transclusions in the drawing
}
if(activeExcalidrawView.file) {
self.triggerEmbedUpdates(activeExcalidrawView.file.path);
if(previouslyActiveEV.file) {
self.triggerEmbedUpdates(previouslyActiveEV.file.path);
}
}
if(newActiveviewEV && (!previouslyActiveEV || previouslyActiveEV.leaf != leaf)) {
//the user switched to a new leaf
//timeout gives time to the view being exited to finish saving
const f = newActiveviewEV.file;
if(newActiveviewEV.file) setTimeout(()=>{
//@ts-ignore
if(!newActiveviewEV || !newActiveviewEV._loaded) return;
if(newActiveviewEV.file?.path !== f?.path) return;
if(newActiveviewEV.activeLoader) return;
newActiveviewEV.loadSceneFiles()
},2000); //refresh embedded files
}
};
self.registerEvent(
self.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler)
@@ -1003,13 +1133,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();
}
@@ -1042,12 +1174,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){
@@ -1102,7 +1238,7 @@ export default class ExcalidrawPlugin extends Plugin {
return this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
}
const blank = this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
return FRONTMATTER + '\n' + getMarkdownDrawingSection(blank,'<SVG></SVG>');
return FRONTMATTER + '\n' + getMarkdownDrawingSection(blank);
}
/**
@@ -1113,7 +1249,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;
@@ -1128,7 +1263,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + 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> {

5
src/mathjax.ts Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,23 +3,25 @@ import {
DropdownComponent,
PluginSettingTab,
Setting,
TFile
} from 'obsidian';
import { VIEW_TYPE_EXCALIDRAW } from './constants';
import ExcalidrawView from './ExcalidrawView';
import { t } from './lang/helpers';
import type ExcalidrawPlugin from "./main";
import { debug } from './Utils';
export interface ExcalidrawSettings {
folder: string,
templateFilePath: string,
drawingFilenamePrefix: string,
drawingFilenameDateTime: string,
// saveSVGSnapshots: boolean,
//displaySVGInPreview: boolean,
displaySVGInPreview: boolean,
previewMatchObsidianTheme: boolean,
width: string,
matchTheme: boolean,
matchThemeAlways: boolean,
matchThemeTrigger:boolean,
defaultMode: string,
zoomToFitOnResize: boolean,
zoomToFitMaxLevel: number,
openInAdjacentPane: boolean,
@@ -45,8 +47,16 @@ 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
mdSVGwidth: number,
mdSVGmaxHeight: number,
mdFont: string,
mdFontColor: string,
mdCSS: string,
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
@@ -54,11 +64,13 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
templateFilePath: 'Excalidraw/Template.excalidraw',
drawingFilenamePrefix: 'Drawing ',
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
// saveSVGSnapshots: true,
//displaySVGInPreview: true,
displaySVGInPreview: true,
previewMatchObsidianTheme: false,
width: '400',
matchTheme: false,
matchThemeAlways: false,
matchThemeTrigger: false,
defaultMode: "normal",
zoomToFitOnResize: true,
zoomToFitMaxLevel: 2,
linkPrefix: "📍",
@@ -83,9 +95,22 @@ 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,
mdSVGwidth: 500,
mdSVGmaxHeight: 800,
mdFont: "Virgil",
mdFontColor: "Black",
mdCSS: ""
}
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -104,7 +129,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
const plugin = this.plugin;
this.applyDebounceTimer = window.setTimeout(() => {
plugin.saveSettings();
}, 200);
}, 100);
if(requestReloadDrawings) this.requestReloadDrawings = true;
}
@@ -114,6 +139,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
for(const v of exs) {
if(v.view instanceof ExcalidrawView) {
await v.view.save(false);
//debug({where:"ExcalidrawSettings.hide",file:v.view.file.name,before:"reload(true)"})
await v.view.reload(true);
}
}
@@ -122,7 +148,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;
@@ -200,18 +227,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text.setValue(this.plugin.settings.drawingFilenameDateTime);
filenameEl.innerHTML = getFilenameSample();
this.applySettingsUpdate();
}));
/* new Setting(containerEl)
.setName(t("SVG_IN_MD_NAME"))
.setDesc(t("SVG_IN_MD_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.saveSVGSnapshots)
.onChange(async (value) => {
this.plugin.settings.saveSVGSnapshots = value;
this.applySettingsUpdate();
}));*/
}));
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
@@ -233,7 +249,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.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"))
@@ -267,8 +307,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
this.containerEl.createEl('p',{
text: t("LINKS_DESC")});
this.containerEl.createEl('p',{text: t("LINKS_DESC")});
new Setting(containerEl)
.setName(t("ADJACENT_PANE_NAME"))
@@ -297,7 +336,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);
}));
@@ -367,10 +405,97 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}));
this.containerEl.createEl('h1', {text: t("MD_HEAD")});
this.containerEl.createEl('p',{text: t("MD_HEAD_DESC")});
new Setting(containerEl)
.setName(t("MD_TRANSCLUDE_WIDTH_NAME"))
.setDesc(t("MD_TRANSCLUDE_WIDTH_DESC"))
.addText(text => text
.setPlaceholder('Enter a number e.g. 500')
.setValue(this.plugin.settings.mdSVGwidth.toString())
.onChange(async (value) => {
const intVal = parseInt(value);
if(isNaN(intVal) && value!=="") {
text.setValue(this.plugin.settings.mdSVGwidth.toString());
return;
}
this.requestEmbedUpdate = true;
if(value === "") {
this.plugin.settings.mdSVGwidth = 500;
this.applySettingsUpdate(true);
return;
}
this.plugin.settings.mdSVGwidth = intVal;
this.requestReloadDrawings=true;
text.setValue(this.plugin.settings.mdSVGwidth.toString());
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
.setName(t("MD_TRANSCLUDE_HEIGHT_NAME"))
.setDesc(t("MD_TRANSCLUDE_HEIGHT_DESC"))
.addText(text => text
.setPlaceholder('Enter a number e.g. 800')
.setValue(this.plugin.settings.mdSVGmaxHeight.toString())
.onChange(async (value) => {
const intVal = parseInt(value);
if(isNaN(intVal) && value!=="") {
text.setValue(this.plugin.settings.mdSVGmaxHeight.toString());
return;
}
this.requestEmbedUpdate = true;
if(value === "") {
this.plugin.settings.mdSVGmaxHeight = 800;
this.applySettingsUpdate(true);
return;
}
this.plugin.settings.mdSVGmaxHeight = intVal;
this.requestReloadDrawings=true;
text.setValue(this.plugin.settings.mdSVGmaxHeight.toString());
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
.setName(t("MD_DEFAULT_FONT_NAME"))
.setDesc(t("MD_DEFAULT_FONT_DESC"))
.addText(text => text
.setPlaceholder("Virgil|Cascadia|Filename")
.setValue(this.plugin.settings.mdFont)
.onChange((value) => {
this.requestReloadDrawings=true;
this.plugin.settings.mdFont = value;
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
.setName(t("MD_DEFAULT_COLOR_NAME"))
.setDesc(t("MD_DEFAULT_COLOR_DESC"))
.addText(text => text
.setPlaceholder("CSS Color-name|RGB-HEX")
.setValue(this.plugin.settings.mdFontColor)
.onChange((value) => {
this.requestReloadDrawings=true;
this.plugin.settings.mdFontColor = value;
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
.setName(t("MD_CSS_NAME"))
.setDesc(t("MD_CSS_DESC"))
.addText(text => text
.setPlaceholder("filename of css file in vault")
.setValue(this.plugin.settings.mdCSS)
.onChange((value) => {
this.requestReloadDrawings=true;
this.plugin.settings.mdCSS = value;
this.applySettingsUpdate(true);
}));
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
@@ -378,7 +503,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

@@ -1,4 +1,4 @@
{
"1.4.2": "0.11.13",
"1.4.3": "0.12.19"
"1.4.18": "0.12.16",
"1.4.2": "0.11.13"
}

View File

@@ -1067,21 +1067,16 @@
dependencies:
"@types/estree" "*"
"@zsviczian/excalidraw@0.10.0-obsidian-8":
"integrity" "sha512-GLyOdnA2yH+acXjTaIuy6oeUz9JL8Wj8n/qzdTfqGhAWz6wPoXXEewTvZIsW4pij/0MC0A+unvHNK/k+YDbpBA=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-8.tgz"
"version" "0.10.0-obsidian-8"
"@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"
@@ -3003,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"
@@ -3063,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"
@@ -3276,13 +3263,6 @@
"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"
@@ -3407,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"
@@ -3790,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"
@@ -6089,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"
@@ -6488,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"
@@ -6716,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"
@@ -6873,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"
@@ -7886,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"
@@ -8657,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"
@@ -8815,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"
@@ -9481,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"
@@ -9528,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"