mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2e7ac23f | ||
|
|
1eb9b88f4b | ||
|
|
a7814f383a | ||
|
|
54e6d47df0 | ||
|
|
55ea1cf121 | ||
|
|
a2982f3406 | ||
|
|
633ff1fea8 | ||
|
|
3312df0743 | ||
|
|
0722bb8133 | ||
|
|
c9be4d95d7 | ||
|
|
023ddcec39 | ||
|
|
9255643646 | ||
|
|
36ead43102 | ||
|
|
f61d000326 | ||
|
|
9bb254dc48 | ||
|
|
5023ed46f5 | ||
|
|
73616e5084 | ||
|
|
f7bbe2e446 | ||
|
|
94c3011435 | ||
|
|
b348def6f6 | ||
|
|
d57c59e9eb | ||
|
|
3ed25c221e | ||
|
|
200c2631cb |
13
README.md
13
README.md
@@ -11,6 +11,8 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
||||
|[](https://youtu.be/QOL1KF7-kdc)|[](https://youtu.be/aSgcbfspvfo)|[](https://youtu.be/MaJ5jJwBRWs)|
|
||||
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
||||
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)|[](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
|
||||
|[](https://youtu.be/r08wk-58DPk)|[](https://youtu.be/tsecSfnTMow)|[](https://youtu.be/K6qZkTz8GHs)|
|
||||
|
||||
|
||||
# Key features
|
||||
- The plugin aims to integrate Excalidraw seamlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.4.11",
|
||||
"version": "1.4.17",
|
||||
"minAppVersion": "0.12.16",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,37 +1,102 @@
|
||||
import { exportToBlob } from "@zsviczian/excalidraw";
|
||||
import { FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { App, Notice, TFile } from "obsidian";
|
||||
import { fileid, IMAGE_TYPES } from "./constants";
|
||||
import { ExcalidrawData } from "./ExcalidrawData";
|
||||
import ExcalidrawView, { ExportSettings } from "./ExcalidrawView";
|
||||
import { 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, svgToBase64 } from "./Utils";
|
||||
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: {
|
||||
height: number;
|
||||
width: number;
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
private fileChanged():boolean {
|
||||
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 (
|
||||
file: TFile
|
||||
inFile: TFile | EmbeddedFile
|
||||
):Promise<{
|
||||
mimeType: MimeType,
|
||||
fileId: FileId,
|
||||
@@ -40,7 +105,18 @@ export class EmbeddedFilesLoader {
|
||||
hasSVGwithBitmap: boolean,
|
||||
size: {height: number, width: number},
|
||||
}> {
|
||||
if(!this.plugin || !file) return null;
|
||||
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) {
|
||||
@@ -50,19 +126,29 @@ export class EmbeddedFilesLoader {
|
||||
this.processedFiles.set(file.path,count+1);
|
||||
let hasSVGwithBitmap = false;
|
||||
const app = this.plugin.app;
|
||||
const isExcalidrawFile = this.plugin.ea.isExcalidrawFile(file);
|
||||
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
|
||||
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,
|
||||
};
|
||||
this.plugin.ea.reset();
|
||||
const svg = await this.plugin.ea.createSVG(file.path,true,exportSettings,this,null);
|
||||
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;
|
||||
@@ -90,12 +176,23 @@ export class EmbeddedFilesLoader {
|
||||
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;
|
||||
case "svg":
|
||||
case "md" : mimeType = "image/svg+xml";break;
|
||||
default: mimeType = "application/octet-stream";
|
||||
}
|
||||
}
|
||||
const dataURL = excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab,mimeType));
|
||||
const size = await getImageSize(excalidrawSVG??app.vault.getResourcePath(file));
|
||||
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),
|
||||
@@ -108,34 +205,44 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
public async loadSceneFiles (
|
||||
excalidrawData: ExcalidrawData,
|
||||
view: ExcalidrawView,
|
||||
addFiles:Function,
|
||||
sourcePath:string,
|
||||
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) {
|
||||
if(!entry.value[1].isLoaded || entry.value[1].hasSVGwithBitmap) {
|
||||
const file = app.metadataCache.getFirstLinkpathDest(entry.value[1].path,sourcePath);
|
||||
if(file && file instanceof TFile) {
|
||||
const data = await this.getObsidianImage(file);//,theme);
|
||||
if(data) {
|
||||
files.push({
|
||||
mimeType : data.mimeType,
|
||||
id: entry.value[0],
|
||||
dataURL: data.dataURL,
|
||||
created: data.created,
|
||||
size: data.size,
|
||||
hasSVGwithBitmap: data.hasSVGwithBitmap
|
||||
});
|
||||
}
|
||||
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;
|
||||
@@ -158,8 +265,9 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
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,view);
|
||||
addFiles(files,this.isDark);
|
||||
} catch(e) {
|
||||
errorlog({where:"EmbeddedFileLoader.loadSceneFiles", error: e});
|
||||
}
|
||||
@@ -171,6 +279,150 @@ const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||
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();
|
||||
@@ -179,7 +431,7 @@ const getDataURL = async (file: ArrayBuffer,mimeType: string): Promise<DataURL>
|
||||
resolve(dataURL);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(new Blob([new Uint8Array(file)],{type:'mimeType'}));
|
||||
reader.readAsDataURL(new Blob([new Uint8Array(file)],{type:mimeType}));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -197,7 +449,7 @@ const generateIdFromFile = async (file: ArrayBuffer):Promise<FileId> => {
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorlog({where:"EmbeddedFileLoader.generateIdFromFile",error});
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
MAX_IMAGE_SIZE,
|
||||
} from "./constants";
|
||||
import { embedFontsInSVG, getPNG, getSVG, scaleLoadedImage, wrapText } from "./Utils";
|
||||
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";
|
||||
@@ -77,15 +77,15 @@ export interface ExcalidrawAutomate {
|
||||
createSVG (
|
||||
templatePath?:string,
|
||||
embedFont?:boolean,
|
||||
exportSettings?:ExportSettings,
|
||||
loader?:EmbeddedFilesLoader,
|
||||
exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
theme?:string
|
||||
):Promise<SVGSVGElement>;
|
||||
createPNG (
|
||||
templatePath?:string,
|
||||
scale?:number,
|
||||
exportSettings?:ExportSettings,
|
||||
loader?:EmbeddedFilesLoader,
|
||||
exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
theme?:string
|
||||
):Promise<any>;
|
||||
wrapText (text:string, lineLen:number):string;
|
||||
@@ -168,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;
|
||||
@@ -357,62 +361,73 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
|
||||
templatePath?:string,
|
||||
embedFont:boolean = false,
|
||||
exportSettings?:ExportSettings,
|
||||
loader:EmbeddedFilesLoader = new EmbeddedFilesLoader(this.plugin),
|
||||
loader?:EmbeddedFilesLoader,
|
||||
theme?:string,
|
||||
):Promise<SVGSVGElement> {
|
||||
const automateElements = this.getElements();
|
||||
const template = templatePath ? (await getTemplate(this.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: theme??(template?.appState?.theme ?? this.canvas.theme),
|
||||
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||
},
|
||||
files: template?.files ?? {}
|
||||
},
|
||||
{
|
||||
withBackground: exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme
|
||||
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(template?.hasSVGwithBitmap) svg.setAttribute("hasbitmap","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,
|
||||
exportSettings?:ExportSettings,
|
||||
loader:EmbeddedFilesLoader = new EmbeddedFilesLoader(this.plugin),
|
||||
loader?:EmbeddedFilesLoader,
|
||||
theme?:string
|
||||
) {
|
||||
const automateElements = this.getElements();
|
||||
const template = templatePath ? (await getTemplate(this.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: theme??(template?.appState?.theme ?? this.canvas.theme),
|
||||
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||
},
|
||||
files: template?.files ?? {}
|
||||
},
|
||||
{
|
||||
withBackground: exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme
|
||||
},
|
||||
scale
|
||||
)
|
||||
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);
|
||||
@@ -771,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;
|
||||
@@ -908,7 +930,8 @@ async function getTemplate(
|
||||
|
||||
let scene = excalidrawData.scene;
|
||||
if(loadFiles) {
|
||||
await loader.loadSceneFiles(excalidrawData, null, (fileArray:FileData[], 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) {
|
||||
if(f.hasSVGwithBitmap) hasSVGwithBitmap = true;
|
||||
@@ -921,7 +944,7 @@ async function getTemplate(
|
||||
}
|
||||
let foo;
|
||||
[foo,scene] = scaleLoadedImage(excalidrawData.scene,fileArray);
|
||||
},templatePath);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -941,6 +964,78 @@ async function getTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1000,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"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, getIMGFilename, isObsidianThemeDark, wrapText } from "./Utils";
|
||||
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};
|
||||
|
||||
@@ -92,14 +94,14 @@ export class ExcalidrawData {
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
public loaded: boolean = false;
|
||||
private files:Map<FileId,{path:string,hasSVGwithBitmap:boolean,isLoaded:boolean}> = 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,{path:string,hasSVGwithBitmap:boolean,isLoaded:boolean}>();
|
||||
this.files = new Map<FileId,EmbeddedFile>();
|
||||
this.equations = new Map<FileId,{latex:string,isLoaded:boolean}>();
|
||||
}
|
||||
|
||||
@@ -110,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
|
||||
@@ -192,13 +196,8 @@ export class ExcalidrawData {
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
|
||||
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||
while(!(parts = res.next()).done) {
|
||||
this.setFile(
|
||||
parts.value[1] as FileId,
|
||||
{
|
||||
path:parts.value[2],
|
||||
hasSVGwithBitmap:undefined,
|
||||
isLoaded:false
|
||||
});
|
||||
const embeddedFile = new EmbeddedFile(this.plugin,this.file.path,parts.value[2]);
|
||||
this.setFile(parts.value[1] as FileId,embeddedFile);
|
||||
}
|
||||
|
||||
//Load Equations
|
||||
@@ -358,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -506,7 +465,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
if(this.files.size>0) {
|
||||
for(const key of this.files.keys()) {
|
||||
outString += key +': [['+this.files.get(key).path + ']]\n';
|
||||
outString += key +': [['+this.files.get(key).linkParts.original + ']]\n';
|
||||
}
|
||||
}
|
||||
outString += (this.equations.size>0 || this.files.size>0) ? '\n' : '';
|
||||
@@ -542,7 +501,8 @@ export class ExcalidrawData {
|
||||
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;
|
||||
@@ -550,14 +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.setFile(
|
||||
key as FileId,
|
||||
{
|
||||
path:filepath,
|
||||
hasSVGwithBitmap:false,
|
||||
isLoaded:true
|
||||
});
|
||||
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;
|
||||
@@ -676,26 +639,18 @@ export class ExcalidrawData {
|
||||
// of copying an image or equation from one drawing to another within the same vault
|
||||
// this is going to do the job
|
||||
*/
|
||||
public setFile(fileId:FileId, data:{path:string,hasSVGwithBitmap:boolean,isLoaded:boolean}) {
|
||||
public setFile(fileId:FileId, data:EmbeddedFile) {
|
||||
//always store absolute path because in case of paste, relative path may not resolve ok
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(data.path,this.file.path);
|
||||
const p = file?.path ?? data.path;
|
||||
this.files.set(
|
||||
fileId,
|
||||
{
|
||||
path:p,
|
||||
hasSVGwithBitmap:data.hasSVGwithBitmap,
|
||||
isLoaded:data.isLoaded
|
||||
});
|
||||
this.files.set(fileId,data);
|
||||
this.plugin.filesMaster.set(
|
||||
fileId,
|
||||
{
|
||||
path:p,
|
||||
hasSVGwithBitmap:data.hasSVGwithBitmap
|
||||
path:data.file.path,
|
||||
hasSVGwithBitmap:data.isSVGwithBitmap
|
||||
});
|
||||
}
|
||||
|
||||
public getFile(fileId:FileId):{path:string,hasSVGwithBitmap?:boolean,isLoaded:boolean} {
|
||||
public getFile(fileId:FileId):EmbeddedFile {
|
||||
return this.files.get(fileId);
|
||||
}
|
||||
|
||||
@@ -713,12 +668,9 @@ export class ExcalidrawData {
|
||||
public hasFile(fileId:FileId):boolean {
|
||||
if(this.files.has(fileId)) return true;
|
||||
if(this.plugin.filesMaster.has(fileId)) {
|
||||
this.files.set(
|
||||
fileId,
|
||||
{
|
||||
isLoaded:false,
|
||||
...this.plugin.filesMaster.get(fileId)
|
||||
});
|
||||
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;
|
||||
@@ -752,4 +704,49 @@ export class ExcalidrawData {
|
||||
}
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,11 @@ import ExcalidrawPlugin from './main';
|
||||
import { repositionElementsToCursor} from './ExcalidrawAutomate';
|
||||
import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
||||
import { checkAndCreateFolder, download, embedFontsInSVG, errorlog, getIMGFilename, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, rotatedDimensions, scaleLoadedImage, splitFolderAndFilename, svgToBase64, 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 { EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
|
||||
|
||||
export enum TextMode {
|
||||
parsed,
|
||||
@@ -56,27 +56,34 @@ export interface ExportSettings {
|
||||
|
||||
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
|
||||
|
||||
export const addFiles = (files:FileData[], view: ExcalidrawView) => {
|
||||
export const addFiles = async (files:FileData[], view: ExcalidrawView,isDark?:boolean) => {
|
||||
if(!files || files.length === 0 || !view) 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,
|
||||
});
|
||||
}
|
||||
files.forEach((f:FileData)=>{
|
||||
for(const f of files) {
|
||||
if(view.excalidrawData.hasFile(f.id)) {
|
||||
const path = view.excalidrawData.getFile(f.id).path;
|
||||
view.excalidrawData.setFile(f.id,{path,hasSVGwithBitmap:f.hasSVGwithBitmap,isLoaded:true});
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -179,6 +186,7 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -300,7 +308,26 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
await this.save(true); //in case pasted images haven't been saved yet
|
||||
if(this.excalidrawData.hasFile(selectedImage.fileId)) {
|
||||
linkText = this.excalidrawData.getFile(selectedImage.fileId).path;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,6 +367,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));
|
||||
@@ -371,6 +399,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
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,
|
||||
@@ -430,6 +460,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;
|
||||
}
|
||||
@@ -437,6 +469,8 @@ 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();
|
||||
}
|
||||
@@ -474,27 +508,39 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return;
|
||||
}
|
||||
}
|
||||
//debug({where:"ExcalidrawView.setViewData",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"loadDrawing(true)"})
|
||||
await this.loadDrawing(true);
|
||||
this.isLoaded=true;
|
||||
});
|
||||
}
|
||||
|
||||
private activeLoader:EmbeddedFilesLoader = null;
|
||||
private async loadSceneFiles(isDark?:boolean) {
|
||||
if(this.activeLoader) this.activeLoader.terminate=true;
|
||||
const loader = new EmbeddedFilesLoader(this.plugin,isDark);
|
||||
this.activeLoader = loader;
|
||||
|
||||
loader.loadSceneFiles(
|
||||
this.excalidrawData,
|
||||
this,
|
||||
(files:FileData[], view:ExcalidrawView) => {
|
||||
this.activeLoader = null;
|
||||
if(!files || !view) return;
|
||||
addFiles(files,view);
|
||||
},
|
||||
this.file?.path
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,6 +555,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//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: {
|
||||
@@ -522,6 +569,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
|
||||
this.excalidrawWrapperRef.current.focus();
|
||||
}
|
||||
//debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"});
|
||||
this.loadSceneFiles();
|
||||
} else {
|
||||
this.instantiateExcalidraw({
|
||||
@@ -710,6 +758,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
excalidrawRef.current.readyPromise.then((api) => {
|
||||
this.excalidrawAPI = api;
|
||||
//console.log({where:"ExcalidrawView.React.ReadyPromise"});
|
||||
//debug({where:"ExcalidrawView.React.useEffect",file:this.file.name,before:"this.loadSceneFiles"});
|
||||
this.loadSceneFiles();
|
||||
});
|
||||
}, [excalidrawRef]);
|
||||
@@ -816,6 +865,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
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,
|
||||
@@ -831,15 +881,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
created: images[k].created
|
||||
});
|
||||
if(images[k].file) {
|
||||
this.excalidrawData.setFile(
|
||||
images[k].id,
|
||||
{
|
||||
path:images[k].file,
|
||||
hasSVGwithBitmap:images[k].hasSVGwithBitmap,
|
||||
isLoaded:true
|
||||
});
|
||||
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) {
|
||||
if(images[k].latex) {
|
||||
this.excalidrawData.setEquation(
|
||||
images[k].id,
|
||||
{
|
||||
@@ -1012,21 +1064,28 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.altKeyDown = 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;
|
||||
@@ -1158,7 +1217,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return true;
|
||||
},
|
||||
onThemeChange: async (newTheme:string) => {
|
||||
this.loadSceneFiles(newTheme==="dark");
|
||||
//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();
|
||||
@@ -1198,7 +1259,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!onDropHook("file",[draggable.file],null)) {
|
||||
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;
|
||||
|
||||
@@ -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
54
src/InsertMDDialog.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -123,7 +124,7 @@ export class OneOffs {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Unable to process: "+f.path,{error:e});
|
||||
errorlog({where:"OneOffs.wysiwygPatch",message:"Unable to process: "+f.path,error:e});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
28
src/Utils.ts
28
src/Utils.ts
@@ -240,7 +240,7 @@ export const getPNG = async (scene:any, exportSettings:ExportSettings, scale:num
|
||||
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
errorlog({where:"Utils.getPNG",error});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -297,13 +297,35 @@ export function getIMGFilename(path:string,extension:string):string {
|
||||
return path.substring(0,path.lastIndexOf('.')) + '.' + extension;
|
||||
}
|
||||
|
||||
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 {
|
||||
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.log({plugin:"Excalidraw",...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 = console.log.bind(window.console);
|
||||
//export const debug = function(){};
|
||||
@@ -13,6 +13,9 @@ export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
|
||||
export const FRONTMATTER_KEY_DEFAULT_MODE = "excalidraw-default-mode";
|
||||
export const FRONTMATTER_KEY_FONT = "excalidraw-font";
|
||||
export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color";
|
||||
export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css";
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const MAX_COLORS = 5;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -91,7 +92,7 @@ export default {
|
||||
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. " +
|
||||
@@ -114,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",
|
||||
@@ -126,6 +127,30 @@ 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.",
|
||||
@@ -139,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",
|
||||
@@ -147,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. " +
|
||||
@@ -186,6 +211,7 @@ 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",
|
||||
|
||||
124
src/main.ts
124
src/main.ts
@@ -53,15 +53,20 @@ 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, getAttachmentsFolderAndFilePath, getIMGFilename, 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";
|
||||
@@ -84,6 +89,7 @@ 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};
|
||||
@@ -149,8 +155,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
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";
|
||||
// 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() {
|
||||
@@ -238,13 +244,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
img.addClass(imgAttributes.style);
|
||||
|
||||
const [scene,pos] = getJSON(content);
|
||||
this.ea.reset();
|
||||
|
||||
const theme = this.settings.previewMatchObsidianTheme
|
||||
? (isObsidianThemeDark() ? "dark" : "light")
|
||||
: undefined;
|
||||
: (!this.settings.exportWithTheme
|
||||
? "light"
|
||||
: undefined);
|
||||
if(theme) exportSettings.withTheme = true;
|
||||
const loader = new EmbeddedFilesLoader(this,this.settings.previewMatchObsidianTheme?isObsidianThemeDark():undefined);
|
||||
const loader = new EmbeddedFilesLoader(this,theme?(theme==="dark"):undefined);
|
||||
|
||||
if(!this.settings.displaySVGInPreview) {
|
||||
const width = parseInt(imgAttributes.fwidth);
|
||||
@@ -253,13 +260,34 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if(width>=1200) scale = 3;
|
||||
if(width>=1800) scale = 4;
|
||||
if(width>=2400) scale = 5;
|
||||
const png = await this.ea.createPNG(file.path,scale,exportSettings,loader,theme);
|
||||
const png = await 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 this.ea.createSVG(file.path,true,exportSettings,loader,theme)).outerHTML;
|
||||
const svgSnapshot = (
|
||||
await createSVG(
|
||||
file.path,
|
||||
true,
|
||||
exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
this
|
||||
)).outerHTML;
|
||||
let svg:SVGSVGElement = null;
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = svgSnapshot;
|
||||
@@ -428,8 +456,12 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private addThemeObserver() {
|
||||
this.themeObserver = new MutationObserver(async (m)=>{
|
||||
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)=> {
|
||||
@@ -439,7 +471,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
});
|
||||
});
|
||||
this.themeObserver.observe(document.body, {attributeFilter:["class"]});
|
||||
this.themeObserver.observe(document.body, {attributeOldValue:true, attributeFilter:["class"]});
|
||||
}
|
||||
|
||||
public experimentalFileTypeDisplayToggle(enabled: boolean) {
|
||||
@@ -457,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);
|
||||
}
|
||||
};
|
||||
@@ -486,6 +521,7 @@ 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[CTRL_OR_CMD]); //.ctrlKey||e.metaKey);
|
||||
@@ -556,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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -746,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"),
|
||||
@@ -983,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);
|
||||
}
|
||||
});
|
||||
@@ -1035,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)
|
||||
|
||||
106
src/settings.ts
106
src/settings.ts
@@ -8,6 +8,7 @@ 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,
|
||||
@@ -51,6 +52,11 @@ export interface ExcalidrawSettings {
|
||||
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 = {
|
||||
@@ -99,7 +105,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
patchCommentBlock: true,
|
||||
imageElementNotice: true,
|
||||
runWYSIWYGpatch: true,
|
||||
fixInfinitePreviewLoop: true
|
||||
fixInfinitePreviewLoop: true,
|
||||
mdSVGwidth: 500,
|
||||
mdSVGmaxHeight: 800,
|
||||
mdFont: "Virgil",
|
||||
mdFontColor: "Black",
|
||||
mdCSS: ""
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -118,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;
|
||||
}
|
||||
|
||||
@@ -128,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);
|
||||
}
|
||||
}
|
||||
@@ -295,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"))
|
||||
@@ -394,6 +405,93 @@ 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")});
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"1.4.11": "0.12.16",
|
||||
"1.4.17": "0.12.16",
|
||||
"1.4.2": "0.11.13"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user