Compare commits

..

37 Commits

Author SHA1 Message Date
Zsolt Viczian
65af29c2ef Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2021-08-29 10:31:59 +02:00
Zsolt Viczian
b19e1b6dcb 1.2.22 2021-08-29 10:31:35 +02:00
zsviczian
f7263543fa Update README.md 2021-08-28 16:31:41 +02:00
Zsolt Viczian
c6339b28ac 1.2.21 - blockquote transclusion fixed 2021-08-28 16:20:43 +02:00
Zsolt Viczian
f24e4fce9c 1.2.20 wrapText() 2021-08-28 15:56:45 +02:00
Zsolt Viczian
5eff9b2e54 1.2.19 2021-08-28 11:57:09 +02:00
Zsolt Viczian
d89c019612 1.2.18 2021-08-27 19:12:29 +02:00
Zsolt Viczian
01d3c13cce 1.2.17 2021-08-25 20:48:36 +02:00
Zsolt Viczian
de68ebbe7d 1.2.16 2021-08-22 13:05:47 +02:00
Zsolt Viczian
caee4f7500 1.2.16 2021-08-22 12:38:09 +02:00
Zsolt Viczian
6d28546677 1.2.15 - link preview inside Excalidraw 2021-08-21 23:01:12 +02:00
Zsolt Viczian
ec5a13f9e4 1.2.14 2021-08-10 20:57:12 +02:00
Zsolt Viczian
b788118880 1.2.13 2021-08-05 21:47:44 +02:00
Zsolt Viczian
6e22e0a428 1.2.13 draft 2021-08-04 22:50:26 +02:00
zsviczian
8b8b469569 Merge pull request #117 from Quorafind/master
Update zh-cn.ts
2021-08-04 22:30:44 +02:00
Boninall
789851c0c4 Update zh-cn.ts
Add some period and Fix some language error
2021-08-04 22:46:01 +08:00
Zsolt Viczian
816de255ee 1.2.12 zh-cn translation 2021-08-03 20:16:56 +02:00
zsviczian
54d4eb7ab4 Merge pull request #113 from Quorafind/master
Update zh-cn.ts
2021-08-03 19:59:30 +02:00
Boninall
e3242ebfb7 Merge branch 'zsviczian:master' into master 2021-08-03 12:51:58 +08:00
Zsolt Viczian
2e36d83abc 1.2.11 2021-08-02 20:34:56 +02:00
Boninall
6552e6bd7b Update zh-cn.ts
Add zh-cn Language
2021-08-02 18:42:58 +08:00
Zsolt Viczian
21ff1833a8 1.2.10 2021-08-01 19:32:37 +02:00
Zsolt Viczian
1b931abd38 changed to my own Excalidraw package 2021-07-31 19:21:06 +02:00
zsviczian
fe28098776 1.2.9 release 2021-07-23 20:10:44 +02:00
zsviczian
72c158e08b save.settings 2021-07-23 19:13:52 +02:00
Zsolt Viczian
5133ab028b style changes context menu 2021-07-14 21:26:47 +02:00
Zsolt Viczian
5fc0f70ded 1.2.8 2021-07-14 20:47:23 +02:00
Zsolt Viczian
08f489f1c5 1.2.8 autosave safeguard, merged file error 2021-07-14 20:04:08 +02:00
zsviczian
dd476b564a workaround to solve issue #94
trim part after final closing } curly bracket
2021-07-14 11:55:40 +02:00
zsviczian
b7a7c9473e dirty changed to file.path
to ensure the plugin won't accidentally overwrite the next file
2021-07-14 11:29:30 +02:00
Zsolt Viczian
cc7dc16810 1.2.7 autosave+template warning 2021-07-13 22:47:38 +02:00
Zsolt Viczian
c45faef141 1.2.6 2021-07-12 23:07:40 +02:00
Zsolt Viczian
ae31ad0870 1.2.5 fame update for .png and .svg when migrating 2021-07-12 07:03:31 +02:00
Zsolt Viczian
ba88ced2ba 1.2.4 2021-07-11 23:28:46 +02:00
Zsolt Viczian
803fb9e234 1.2.3 2021-07-11 08:42:29 +02:00
Zsolt Viczian
d77249088f 1.2.2 text lock/unlock solved 2021-07-10 23:32:25 +02:00
Zsolt Viczian
e6ad7aa304 Excalidraw 0.9 2021-07-10 20:45:09 +02:00
19 changed files with 1349 additions and 557 deletions

View File

@@ -41,7 +41,10 @@ To convert files you have the following options:
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enalbled.
- Links in drawings will show up in backlinks of documents
- Transclusions are supported i.e. `![[myfile#^blockref]]` will convert in the drawing into the transcluded text
- Transclusions are supported
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
- `![[myfile#section]]` also works, this will transclude the section
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience you can also use the command palette to insert links into drawings
- CTRL/META + CLICK a text element to open it as a link.
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
@@ -54,6 +57,10 @@ To convert files you have the following options:
- You can add metadata to the YAML front matter of drawings
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
- Excalidraw documents now show in graph view.
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.

View File

@@ -14,7 +14,7 @@ Returns the `id` of the object. The `id` is required when connecting objects wit
### addText()
```typescript
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
```
Adds text to the drawing.

View File

@@ -1,5 +1,11 @@
# [◀ Excalidraw Automate How To](../readme.md)
## Utility functions
### isExcalidrawFile()
```typescript
isExcalidrawFile(f:TFile): boolean
```
Returns true if the file provided is a valid Excalidraw file (either a legacy `*.excalidraw` file or a markdown file with the excalidraw key in the front-matter).
### clear()
`clear()` will clear objects from cache, but will retain element style settings.
@@ -38,6 +44,6 @@ Returns an HTML SVGSVGElement containing the generated drawing.
### createPNG()
```typescript
async createPNG(templatePath?:string)
async createPNG(templatePath?:string, scale:number=1)
```
Returns a blob containing a PNG image of the generated drawing.

View File

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

View File

@@ -11,11 +11,12 @@
"author": "",
"license": "MIT",
"dependencies": {
"@excalidraw/excalidraw": "^0.8.0",
"@zsviczian/excalidraw": "^0.9.0-onTextEditEvents-4",
"monkey-around": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^1.1.5"
"react-scripts": "^1.1.5",
"roughjs": "4.4.1"
},
"devDependencies": {
"@babel/core": "^7.14.6",
@@ -30,7 +31,7 @@
"@types/react-dom": "^17.0.8",
"cross-env": "^7.0.3",
"nanoid": "^3.1.23",
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
"obsidian": "^0.12.11",
"rollup": "^2.52.3",
"rollup-plugin-visualizer": "^5.5.0",
"tslib": "^2.3.0",

View File

@@ -3,10 +3,10 @@ import {
FillStyle,
StrokeStyle,
StrokeSharpness,
FontFamily,
} from "@excalidraw/excalidraw/types/element/types";
} from "@zsviczian/excalidraw/types/element/types";
import {
normalizePath,
Notice,
TFile
} from "obsidian"
import ExcalidrawView from "./ExcalidrawView";
@@ -34,7 +34,7 @@ export interface ExcalidrawAutomate extends Window {
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: FontFamily;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
@@ -55,7 +55,7 @@ export interface ExcalidrawAutomate extends Window {
addRect(topX:number, topY:number, width:number, height:number):string;
addDiamond(topX:number, topY:number, width:number, height:number):string;
addEllipse(topX:number, topY:number, width:number, height:number):string;
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string;
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
addLine(points: [[x:number,y:number]]):void;
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
@@ -177,7 +177,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
params?.onNewPane ? params.onNewPane : false,
params?.foldername ? params.foldername : this.plugin.settings.folder,
FRONTMATTER + exportSceneToMD(
FRONTMATTER + plugin.exportSceneToMD(
JSON.stringify({
type: "excalidraw",
version: 2,
@@ -210,7 +210,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
for (let i=0;i<this.elementIds.length;i++) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getSVG(
return await ExcalidrawView.getSVG(
{//createDrawing
"type": "excalidraw",
"version": 2,
@@ -227,7 +227,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
}
)
},
async createPNG(templatePath?:string) {
async createPNG(templatePath?:string, scale:number=1) {
const template = templatePath ? (await getTemplate(templatePath)) : null;
let elements = template ? template.elements : [];
for (let i=0;i<this.elementIds.length;i++) {
@@ -247,7 +247,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme
}
},
scale
)
},
addRect(topX:number, topY:number, width:number, height:number):string {
@@ -268,8 +269,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
return id;
},
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string {
const id = nanoid();
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string {
if(!id) id = nanoid();
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
const width = formatting?.width ? formatting.width : w;
const height = formatting?.height ? formatting.height : h;
@@ -376,7 +377,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
this.canvas.theme = "light";
this.canvas.viewBackgroundColor="#FFFFFF";
},
isExcalidrawFile(f:TFile) {
isExcalidrawFile(f:TFile):boolean {
return this.plugin.isExcalidrawFile(f);
}
@@ -491,31 +492,3 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
}
}
/**
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
* @param {string} data - Excalidraw scene JSON string
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
*/
export function exportSceneToMD(data:string): string {
if(!data) return "";
const excalidrawData = JSON_parse(data);
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
let outString = '# Text Elements\n';
let id:string;
for (const te of textElements) {
id = te.id;
//replacing Excalidraw text IDs with my own, because default IDs may contain
//characters not recognized by Obsidian block references
//also Excalidraw IDs are inconveniently long
if(te.id.length>8) {
id=nanoid();
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + '# Drawing\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
+ data + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
//+data.replaceAll("[","&#91;");
}

View File

@@ -3,6 +3,7 @@ import {
nanoid,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
} from "./constants";
import { measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
@@ -10,17 +11,53 @@ import { ExcalidrawSettings } from "./settings";
import {
JSON_parse
} from "./constants";
import { TextMode } from "./ExcalidrawView";
import { wrapText } from "./Utils";
//![[link|alias]]![alias](link)
//1 2 3 4 5 6
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
declare module "obsidian" {
interface MetadataCache {
blockCache: {
getForFile(x:any,f:TAbstractFile):any;
}
}
}
const DRAWING_REG = /[\r\n]# Drawing[\r\n](```json[\r\n])?(.*)(```)?(%%)?/gm;
export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
//12 3 4 5 6 7 8
EXPR: /(!)?(\[\[([^|\]]+)\|?(.+)?]]|\[(.*)\]\((.*)\))(\{(\d+)\})?/g,
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
return parts.value[1] ? true:false;
},
getLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
return parts.value[3] ? parts.value[3] : parts.value[6];
},
isWikiLink: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
return parts.value[3] ? true:false;
},
getAliasOrLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
return REGEX_LINK.isWikiLink(parts)
? (parts.value[4] ? parts.value[4] : parts.value[3])
: (parts.value[5] ? parts.value[5] : parts.value[6]);
},
getWrapLength: (parts: IteratorResult<RegExpMatchArray, any>):number => {
return parts.value[8];
}
}
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
export function getJSON(data:string):string {
const findJSON = /\n# Drawing\n(```json\n)?(.*)(```)?/gm // /\n# Drawing\n(.*)/gm
const res = data.matchAll(findJSON);
const res = data.matchAll(DRAWING_REG);
const parts = res.next();
if(parts.value && parts.value.length>1) {
return parts.value[2];
const result = parts.value[2];
return result.substr(0,result.lastIndexOf("}")+1); //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
}
return data;
}
@@ -29,14 +66,15 @@ export class ExcalidrawData {
private textElements:Map<string,{raw:string, parsed:string}> = null;
public scene:any = null;
private file:TFile = null;
private settings:ExcalidrawSettings;
private app:App;
private showLinkBrackets: boolean;
private linkPrefix: string;
private allowParse: boolean = false;
private urlPrefix: string;
private textMode: TextMode = TextMode.raw;
private plugin: ExcalidrawPlugin;
constructor(plugin: ExcalidrawPlugin) {
this.settings = plugin.settings;
this.plugin = plugin;
this.app = plugin.app;
}
@@ -45,8 +83,7 @@ export class ExcalidrawData {
* @param {TFile} file - the MD file containing the Excalidraw drawing
* @returns {boolean} - true if file was loaded, false if there was an error
*/
public async loadData(data: string,file: TFile, allowParse:boolean):Promise<boolean> {
//console.log("Excalidraw.Data.loadData()",{data:data,allowParse:allowParse,file:file});
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
@@ -55,9 +92,15 @@ export class ExcalidrawData {
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
this.setShowLinkBrackets();
this.setLinkPrefix();
this.setUrlPrefix();
this.scene = null;
if (this.settings.syncExcalidraw) {
//In compatibility mode if the .excalidraw file was more recently updated than the .md file, then the .excalidraw file
//should be loaded as the scene.
//This feature is mostly likely only relevant to people who use Obsidian and Logseq on the same vault and edit .excalidraw
//drawings in Logseq.
if (this.plugin.settings.syncExcalidraw) {
const excalfile = file.path.substring(0,file.path.lastIndexOf('.md')) + '.excalidraw';
const f = this.app.vault.getAbstractFileByPath(excalfile);
if(f && f instanceof TFile && f.stat.mtime>file.stat.mtime) { //the .excalidraw file is newer then the .md file
@@ -67,23 +110,27 @@ export class ExcalidrawData {
}
//Load scene: Read the JSON string after "# Drawing"
let parts = data.matchAll(/\n# Drawing\n(```json\n)?(.*)(```)?/gm).next();
let parts = data.matchAll(DRAWING_REG).next();
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
if(!this.scene) { //scene was not loaded from .excalidraw
this.scene = JSON_parse(parts.value[2]);
const scene = parts.value[2];
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
//using JSON_parse for legacy compatibiltiy. In an earlier version Excalidraw JSON was not enclosed in a codeblock
}
//Trim data to remove the JSON string
data = data.substring(0,parts.value.index);
//The Markdown # Text Elements take priority over the JSON text elements.
//i.e. if the JSON is modified to reflect the MD in case of difference
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
//The .excalidraw JSON is modified to reflect the MD in case of difference
//Read the text elements into the textElements Map
let position = data.search("# Text Elements");
if(position==-1) return true; //Text Elements header does not exist
position += "# Text Elements\n".length;
//iterating through all the text elements in .md
//Text elements always contain the raw value
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
const res = data.matchAll(/\s\^(.{8})\n/g);
const res = data.matchAll(/\s\^(.{8})[\r\n]/g);
while(!(parts = res.next()).done) {
const text = data.substring(position,parts.value.index);
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
@@ -93,7 +140,7 @@ export class ExcalidrawData {
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
//e.g. if the entire text elements section was deleted.
this.findNewTextElementsInScene();
await this.setAllowParse(allowParse,true);
await this.setTextMode(textMode,true);
return true;
}
@@ -102,19 +149,26 @@ export class ExcalidrawData {
this.textElements = new Map<string,{raw:string, parsed:string}>();
this.setShowLinkBrackets();
this.setLinkPrefix();
this.setUrlPrefix();
this.scene = JSON.parse(data);
this.findNewTextElementsInScene();
await this.setAllowParse(false,true);
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
return true;
}
public async setAllowParse(allowParse:boolean,forceupdate:boolean=false) {
this.allowParse = allowParse;
public async setTextMode(textMode:TextMode,forceupdate:boolean=false) {
this.textMode = textMode;
await this.updateSceneTextElements(forceupdate);
}
/**
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
* Depending on textMode, TextElements will receive their raw or parsed values
* @param forceupdate : will update text elements even if text contents has not changed, this will
* correct sizing issues
*/
private async updateSceneTextElements(forceupdate:boolean=false) {
//console.log("Excalidraw.Data.updateSceneTextElements(), forceupdate",forceupdate);
//update a single text element in the scene if the newText is different
const update = (sceneTextElement:any, newText:string) => {
if(forceupdate || newText!=sceneTextElement.text) {
@@ -135,8 +189,8 @@ export class ExcalidrawData {
}
private async getText(id:string):Promise<string> {
if (this.allowParse) {
if(!this.textElements.get(id)?.parsed) {
if (this.textMode == TextMode.parsed) {
if(!this.textElements.get(id).parsed) {
const raw = this.textElements.get(id).raw;
this.textElements.set(id,{raw:raw, parsed: await this.parse(raw)})
}
@@ -170,14 +224,19 @@ export class ExcalidrawData {
id=nanoid();
jsonString = jsonString.replaceAll(te.id,id); //brute force approach to replace all occurances (e.g. links, groups,etc.)
}
if(!this.textElements.has(id)) {
if(te.id.length > 8 && this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit
const element = this.textElements.get(te.id);
this.textElements.set(id,{raw: element.raw, parsed: element.parsed})
this.textElements.delete(te.id); //delete the old ID from the Map
dirty = true;
} else if(!this.textElements.has(id)) {
dirty = true;
this.textElements.set(id,{raw: te.text, parsed: null});
this.parseasync(id,te.text);
}
}
if(dirty) { //reload scene json in case it has changed
this.scene = JSON_parse(jsonString);
this.scene = JSON.parse(jsonString);
}
return dirty;
@@ -188,7 +247,6 @@ export class ExcalidrawData {
* and updating the textElement map based on the text updated in the scene
*/
private async updateTextElementsFromScene() {
//console.log("Excalidraw.Data.updateTextElementesFromScene()");
for(const key of this.textElements.keys()){
//find text element in the scene
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
@@ -207,36 +265,30 @@ export class ExcalidrawData {
}
}
/**
* update text element map by deleting entries that are no long in the scene
* and updating the textElement map based on the text updated in the scene
*/
private updateTextElementsFromSceneRawOnly() {
//console.log("Excalidraw.Data.updateTextElementsFromSceneRawOnly()");
for(const key of this.textElements.keys()){
//find text element in the scene
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
if(el.length==0) {
this.textElements.delete(key); //if no longer in the scene, delete the text element
} else {
if(!this.textElements.has(key)) {
this.textElements.set(key,{raw: el[0].text,parsed: null});
this.parseasync(key,el[0].text);
} else {
const text = this.allowParse ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
if(text != el[0].text) {
this.textElements.set(key,{raw: el[0].text,parsed: null});
this.parseasync(key,el[0].text);
}
}
}
}
}
private async parseasync(key:string, raw:string) {
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
}
private parseLinks(text:string, position:number, parts:any):string {
return text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
REGEX_LINK.getAliasOrLink(parts) +
(this.showLinkBrackets ? "]]" : "");
/*let outString = null;
if (REGEX_LINK.isWikiLink(parts)) { //parts.value[2]
outString = text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
} else {
outString = text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
}
return outString;*/
}
/**
* Process aliases and block embeds
* @param text
@@ -245,40 +297,103 @@ export class ExcalidrawData {
private async parse(text:string):Promise<string>{
const getTransclusion = async (text:string) => {
//file-name#^blockref
//1 2
const REG_FILE_BLOCKREF = /(.*)#\^(.*)/g;
//1 2 3
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
if(parts.done || !parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
if(parts.done || !parts.value[1] || !parts.value[3]) return text; //filename and/or blockref not found
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
const contents = await this.app.vault.cachedRead(file);
//get transcluded line and take the part before ^blockref
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2]);
const res = contents.match(REG_TRANSCLUDE);
if(res) return res[1];
return text;//if blockref not found in file, return the input string
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;
if(isParagraphRef) {
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
if(!para) return text;
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 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)
} else {
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
let startPos:number = null;
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)
}
if(!startPos && headings[i].node.children[0]?.value == id) startPos = headings[i].node.children[0]?.position.start.offset; //
}
if(startPos) return contents.substr(startPos);
return text;
}
}
let outString = "";
let position = 0;
const res = text.matchAll(REG_LINK_BACKETS);
const res = text.matchAll(REGEX_LINK.EXPR);
let linkIcon = false;
let urlIcon = false;
let parts;
while(!(parts=res.next()).done) {
if (parts.value[1] || parts.value[4]) { //transclusion
if (REGEX_LINK.isTransclusion(parts)) { //transclusion //parts.value[1] || parts.value[4]
outString += text.substring(position,parts.value.index) +
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
} else if (parts.value[2]) {
linkIcon = true;
outString += text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
wrapText(await getTransclusion(REGEX_LINK.getLink(parts)),REGEX_LINK.getWrapLength(parts));
} else {
linkIcon = true;
outString += text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
const parsedLink = this.parseLinks(text,position,parts);
if(parsedLink) {
outString += parsedLink;
if(!(urlIcon || linkIcon))
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
else linkIcon = true;
}
}
position = parts.value.index + parts.value[0].length;
}
outString += text.substring(position,text.length);
if (linkIcon) {
outString = this.linkPrefix + outString;
}
if (urlIcon) {
outString = this.urlPrefix + outString;
}
return outString;
}
/**
* Does a quick parse of the raw text. Returns the parsed string if raw text does not include a transclusion.
* Return null if raw text includes a transclusion.
* This is implemented in a separate function, because by nature resolving a transclusion is an asynchronious
* activity. Quick parse gets the job done synchronously if possible.
* @param text
*/
private quickParse(text:string):string {
const hasTransclusion = (text:string):boolean => {
const res = text.matchAll(REGEX_LINK.EXPR);
let parts;
while(!(parts=res.next()).done) {
if (REGEX_LINK.isTransclusion(parts)) return true;
}
return false;
}
if (hasTransclusion(text)) return null;
let outString = "";
let position = 0;
const res = text.matchAll(REGEX_LINK.EXPR);
let linkIcon = false;
let urlIcon = false;
let parts;
while(!(parts=res.next()).done) {
const parsedLink = this.parseLinks(text,position,parts);
if(parsedLink) {
outString += parsedLink;
if(!(urlIcon || linkIcon))
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
else linkIcon = true;
}
position = parts.value.index + parts.value[0].length;
}
@@ -286,38 +401,37 @@ export class ExcalidrawData {
if (linkIcon) {
outString = this.linkPrefix + outString;
}
if (urlIcon) {
outString = this.urlPrefix + outString;
}
return outString;
}
/**
* Generate markdown file representation of excalidraw drawing
* @returns markdown string
*/
generateMD():string {
//console.log("Excalidraw.Data.generateMD()");
let outString = '# Text Elements\n';
for(const key of this.textElements.keys()){
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
}
return outString + '# Drawing\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
+ JSON.stringify(this.scene) + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene));
}
public syncElements(newScene:any):boolean {
public async syncElements(newScene:any):Promise<boolean> {
//console.log("Excalidraw.Data.syncElements()");
this.scene = newScene;//JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
this.updateTextElementsFromSceneRawOnly();
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
await this.updateTextElementsFromScene();
return result;
}
public async updateScene(newScene:any){
//console.log("Excalidraw.Data.updateScene()");
this.scene = JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
await this.updateTextElementsFromScene();
if(result) {
await this.updateSceneTextElements();
@@ -329,6 +443,28 @@ export class ExcalidrawData {
public getRawText(id:string) {
return this.textElements.get(id)?.raw;
}
public getParsedText(id:string):string {
return this.textElements.get(id)?.parsed;
}
public setTextElement(elementID:string, rawText:string, updateScene:Function):string {
const parseResult = this.quickParse(rawText); //will return the parsed result if raw text does not include transclusion
if(parseResult) { //No transclusion
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
return parseResult;
}
//transclusion needs to be resolved asynchornously
this.parse(rawText).then((parsedText:string)=> {
this.textElements.set(elementID,{raw: rawText,parsed: parsedText});
if(parsedText) updateScene(parsedText);
});
return null;
}
public deleteTextElement(id:string) {
this.textElements.delete(id);
}
private setLinkPrefix():boolean {
const linkPrefix = this.linkPrefix;
@@ -336,20 +472,33 @@ export class ExcalidrawData {
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX]!=null) {
this.linkPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX];
} else {
this.linkPrefix = this.settings.linkPrefix;
this.linkPrefix = this.plugin.settings.linkPrefix;
}
return linkPrefix != this.linkPrefix;
}
private setUrlPrefix():boolean {
const urlPrefix = this.urlPrefix;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_URL_PREFIX]!=null) {
this.urlPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_URL_PREFIX];
} else {
this.urlPrefix = this.plugin.settings.urlPrefix;
}
return urlPrefix != this.urlPrefix;
}
private setShowLinkBrackets():boolean {
const showLinkBrackets = this.showLinkBrackets;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=null) {
this.showLinkBrackets=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=false;
} else {
this.showLinkBrackets = this.settings.showLinkBrackets;
this.showLinkBrackets = this.plugin.settings.showLinkBrackets;
}
return showLinkBrackets != this.showLinkBrackets;
}
}
}

View File

@@ -9,12 +9,12 @@ import {
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
import {
AppState,
LibraryItems
} from "@excalidraw/excalidraw/types/types";
} from "@zsviczian/excalidraw/types/types";
import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
@@ -25,17 +25,26 @@ import {
PNG_ICON_NAME,
SVG_ICON_NAME,
FRONTMATTER_KEY,
UNLOCK_ICON_NAME,
LOCK_ICON_NAME,
JSON_parse
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
FULLSCREEN_ICON_NAME,
JSON_parse,
nanoid
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
import { Prompt } from "./Prompt";
declare let window: ExcalidrawAutomate;
export enum TextMode {
parsed,
raw
}
interface WorkspaceItemExt extends WorkspaceItem {
containerEl: HTMLElement;
}
@@ -45,26 +54,31 @@ export interface ExportSettings {
withTheme: boolean
}
const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export default class ExcalidrawView extends TextFileView {
private excalidrawData: ExcalidrawData;
private getScene: Function = null;
private getSelectedText: Function = null;
private getSelectedId: Function = null;
private getSelectedTextElement: Function = null;
public addText:Function = null;
private refresh: Function = null;
private excalidrawRef: React.MutableRefObject<any> = null;
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
private justLoaded: boolean = false;
private plugin: ExcalidrawPlugin;
private dirty: boolean = false;
private dirty: string = null;
public autosaveTimer: any = null;
public isTextLocked:boolean = false;
private lockedElement:HTMLElement;
private unlockedElement:HTMLElement;
public autosaving:boolean = false;
public textMode:TextMode = TextMode.raw;
private textIsParsed_Element:HTMLElement;
private textIsRaw_Element:HTMLElement;
private preventReload:boolean = true;
public compatibilityMode: boolean = false;
//store key state for view mode link resolution
private ctrlKeyDown = false;
private shiftKeyDown = false;
private altKeyDown = false;
private mouseEvent:any = null;
id: string = (this.leaf as any).id;
@@ -81,8 +95,8 @@ export default class ExcalidrawView extends TextFileView {
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));//data.replaceAll("&#91;","["));
else this.app.vault.create(filepath,JSON.stringify(scene));//.replaceAll("&#91;","["));
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));
else this.app.vault.create(filepath,JSON.stringify(scene));
}
public async saveSVG(scene?: any) {
@@ -96,7 +110,7 @@ export default class ExcalidrawView extends TextFileView {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const svg = ExcalidrawView.getSVG(scene,exportSettings);
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
if(!svg) return;
const svgString = ExcalidrawView.embedFontsInSVG(svg).outerHTML;
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
@@ -123,7 +137,7 @@ export default class ExcalidrawView extends TextFileView {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(scene,exportSettings);
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
@@ -132,7 +146,17 @@ export default class ExcalidrawView extends TextFileView {
}
async save(preventReload:boolean=true) {
if(!this.getScene) return;
this.preventReload = preventReload;
this.dirty = null;
if(this.compatibilityMode) {
await this.excalidrawData.syncElements(this.getScene());
} else {
if(await this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
await this.loadDrawing(false);
}
}
await super.save();
}
@@ -141,55 +165,66 @@ export default class ExcalidrawView extends TextFileView {
// if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
getViewData () {
//console.log("ExcalidrawView.getViewData()");
if(this.getScene && !this.compatibilityMode) {
if(this.excalidrawData.syncElements(this.getScene())) {
this.loadDrawing(false);
}
if(!this.getScene) return this.data;
if(!this.compatibilityMode) {
let trimLocation = this.data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
if(trimLocation == -1) return this.data;
const scene = this.excalidrawData.scene;
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
if(!this.autosaving) {
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
}
const header = this.data.substring(0,trimLocation)
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + (this.isTextLocked ? "locked\n" : "unlocked\n"));
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (this.textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
return header + this.excalidrawData.generateMD();
}
if(this.getScene && this.compatibilityMode) {
this.excalidrawData.syncElements(this.getScene());
if(this.compatibilityMode) {
const scene = this.excalidrawData.scene;
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
if(!this.autosaving) {
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
}
return JSON.stringify(scene);
}
else return this.data;
return this.data;
}
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
let text:string = this.isTextLocked
? this.excalidrawData.getRawText(this.getSelectedId())
: this.getSelectedText();
let text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(this.getSelectedTextElement().id)
: this.getSelectedTextElement().text;
if(!text) {
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
return;
}
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
return; }
return;
}
//![[link|alias]]![alias](link)
//1 2 3 4 5 6
const parts = text.matchAll(REG_LINK_BACKETS).next();
const parts = text.matchAll(REGEX_LINK.EXPR).next();
if(!parts.value) {
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
if(!tags.value || tags.value.length<2) {
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
return;
}
const search=this.app.workspace.getLeavesOfType("search");
if(search.length==0) return;
//@ts-ignore
search[0].view.setQuery("tag:"+tags.value[1]);
this.app.workspace.revealLeaf(search[0]);
//if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
document.exitFullscreen();
this.zoomToFit();
return;
}
text = parts.value[2] ? parts.value[2]:parts.value[6];
text = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
@@ -210,82 +245,87 @@ export default class ExcalidrawView extends TextFileView {
}
try {
const f = view.file;
if(ev.shiftKey) {
document.exitFullscreen();
this.zoomToFit();
}
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
} catch (e) {
new Notice(e,4000);
}
}
download(encoding:string,data:any,filename:string) {
let element = document.createElement('a');
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
onload() {
//console.log("ExcalidrawView.onload()");
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
await this.save();
await this.save(false);
this.plugin.triggerEmbedUpdates();
});
this.unlockedElement = this.addAction(UNLOCK_ICON_NAME,t("LOCK"), (ev) => this.lock(true));
this.lockedElement = this.addAction(LOCK_ICON_NAME,t("UNLOCK"), (ev) => this.lock(false));
this.textIsRaw_Element = this.addAction(TEXT_DISPLAY_RAW_ICON_NAME,t("RAW"), (ev) => this.changeTextMode(TextMode.parsed));
this.textIsParsed_Element = this.addAction(TEXT_DISPLAY_PARSED_ICON_NAME,t("PARSED"), (ev) => this.changeTextMode(TextMode.raw));
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
//@ts-ignore
if(!this.app.isMobile) {
this.addAction(FULLSCREEN_ICON_NAME,"Press ESC to exit fullscreen mode",()=>{
this.contentEl.requestFullscreen();//{navigationUI: "hide"});
if(this.excalidrawWrapperRef) this.excalidrawWrapperRef.current.focus();
});
this.contentEl.onfullscreenchange = () => {
this.zoomToFit();
}
}
//this is to solve sliding panes bug
if (this.app.workspace.layoutReady) {
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
} else {
this.registerEvent(this.app.workspace.on('layout-ready', async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})));
this.app.workspace.onLayoutReady(
async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})
);
}
this.setupAutosaveTimer();
}
public async lock(locked:boolean,reload:boolean=true) {
//console.log("ExcalidrawView.lock(), locked",locked, "reload",reload);
this.isTextLocked = locked;
if(locked) {
this.unlockedElement.hide();
this.lockedElement.show();
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
this.textMode = textMode;
if(textMode == TextMode.parsed) {
this.textIsRaw_Element.hide();
this.textIsParsed_Element.show();
} else {
this.unlockedElement.show();
this.lockedElement.hide();
this.textIsRaw_Element.show();
this.textIsParsed_Element.hide();
}
if(reload) {
await this.save(false);
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
}
}
public setupAutosaveTimer() {
const timer = async () => {
//console.log("ExcalidrawView.autosaveTimer(), dirty", this.dirty);
if(this.dirty) {
console.log("autosave",Date.now());
this.dirty = false;
if(this.dirty && (this.dirty == this.file?.path)) {
this.dirty = null;
this.autosaving=true;
if(this.excalidrawRef) await this.save();
this.plugin.triggerEmbedUpdates();
this.autosaving=false;
}
}
if(this.plugin.settings.autosave) {
this.autosaveTimer = setInterval(timer,30000);
}
if(this.autosaveTimer) clearInterval(this.autosaveTimer); // clear previous timer if one exists
this.autosaveTimer = setInterval(timer,20000);
}
//save current drawing when user closes workspace leaf
async onunload() {
//console.log("ExcalidrawView.onunload()");
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
//if(this.excalidrawRef) await this.save();
if(this.autosaveTimer) {
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
}
public async reload(fullreload:boolean = false, file?:TFile){
//console.log("ExcalidrawView.reload(), fullreload",fullreload,"preventReload",this.preventReload);
if(this.preventReload) {
this.preventReload = false;
return;
@@ -293,9 +333,10 @@ export default class ExcalidrawView extends TextFileView {
if(!this.excalidrawRef) return;
if(!this.file) return;
if(file) this.data = await this.app.vault.read(file);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.isTextLocked);
else await this.excalidrawData.setAllowParse(this.isTextLocked);
this.loadDrawing(false);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
else await this.excalidrawData.setTextMode(this.textMode);
await this.loadDrawing(false);
this.dirty = null;
}
// clear the view content
@@ -306,44 +347,60 @@ export default class ExcalidrawView extends TextFileView {
async setViewData (data: string, clear: boolean = false) {
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
this.dirty = null;
this.compatibilityMode = this.file.extension == "excalidraw";
this.plugin.settings.drawingOpenCount++;
this.plugin.saveSettings();
await this.plugin.loadSettings();
this.plugin.opencount++;
if(this.compatibilityMode) {
this.unlockedElement.hide();
this.lockedElement.hide();
this.textIsRaw_Element.hide();
this.textIsParsed_Element.hide();
await this.excalidrawData.loadLegacyData(data,this.file);
if (!this.plugin.settings.compatibilityMode) {
new Notice(t("COMPATIBILITY_MODE"),4000);
}
} else {
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
}
if(clear) this.clear();
this.loadDrawing(true)
this.dirty = false;
await this.loadDrawing(true)
});
}
private loadDrawing (justloaded:boolean) {
//console.log("ExcalidrawView.loadDrawing, justloaded", justloaded);
this.justLoaded = justloaded; //a flag to trigger zoom to fit after the drawing has been loaded
/**
*
* @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
*/
private async loadDrawing(justloaded:boolean) {
const excalidrawData = this.excalidrawData.scene;
if(this.excalidrawRef) {
const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled;
const zenModeEnabled = this.excalidrawRef.current.getAppState().zenModeEnabled;
if(justloaded) {
this.excalidrawRef.current.resetScene();
this.excalidrawRef.current.history.clear();
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it here
}
this.excalidrawRef.current.updateScene({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
appState: {
zenModeEnabled: zenModeEnabled,
viewModeEnabled: viewModeEnabled,
... excalidrawData.appState,
},
commitToHistory: true,
});
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
this.excalidrawWrapperRef.current.focus();
}
} else {
(async() => {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
// scrollToContent: true,
libraryItems: await this.getLibrary(),
});
})();
this.justLoaded = justloaded;
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
libraryItems: await this.getLibrary(),
});
}
}
@@ -387,7 +444,21 @@ export default class ExcalidrawView extends TextFileView {
.setIcon(ICON_NAME)
.onClick( async (ev) => {
if(!this.getScene || !this.file) return;
this.download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
//@ts-ignore
if(this.app.isMobile) {
const prompt = new Prompt(this.app, "Please provide filename",this.file.basename,'filename, leave blank to cancel action');
prompt.openAndGetValue( async (filename:string)=> {
if(!filename) return;
filename = filename + ".excalidraw";
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
this.app.vault.create(fname,JSON.stringify(this.getScene()));
new Notice("Exported to " + fname,6000);
});
return;
}
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
});
});
} else {
@@ -413,14 +484,14 @@ export default class ExcalidrawView extends TextFileView {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings);
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
let reader = new FileReader();
reader.readAsDataURL(png);
const self = this;
reader.onloadend = function() {
let base64data = reader.result;
self.download(null,base64data,self.file.basename+'.png');
download(null,base64data,self.file.basename+'.png');
}
return;
}
@@ -438,10 +509,10 @@ export default class ExcalidrawView extends TextFileView {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
let svg = ExcalidrawView.getSVG(this.getScene(),exportSettings);
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
this.download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
return;
}
this.saveSVG()
@@ -452,14 +523,14 @@ export default class ExcalidrawView extends TextFileView {
}
async getLibrary() {
const data = JSON_parse(this.plugin.settings.library);
const data = JSON_parse(this.plugin.getStencilLibrary());
return data?.library ? data.library : [];
}
private instantiateExcalidraw(initdata: any) {
//console.log("ExcalidrawView.instantiateExcalidraw()");
this.dirty = false;
this.dirty = null;
const reactElement = React.createElement(() => {
let previousSceneVersion = 0;
let currentPosition = {x:0, y:0};
@@ -471,6 +542,8 @@ export default class ExcalidrawView extends TextFileView {
});
this.excalidrawRef = excalidrawRef;
this.excalidrawWrapperRef = excalidrawWrapperRef;
React.useEffect(() => {
setDimensions({
width: this.contentEl.clientWidth,
@@ -490,58 +563,63 @@ export default class ExcalidrawView extends TextFileView {
}, [excalidrawWrapperRef]);
this.getSelectedId = ():string => {
if(!excalidrawRef?.current) return null;
this.getSelectedTextElement = ():{id: string, text:string} => {
if(!excalidrawRef?.current) return {id:null,text:null};
if(this.excalidrawRef.current.getAppState().viewModeEnabled) {
if(selectedTextElement) {
const retval = selectedTextElement;
selectedTextElement == null;
return retval;
}
return {id:null,text:null};
}
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type == "text") return selectedElement[0].id; //a text element was selected. Retrun text
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
if(selectedElement.length==0) return {id:null,text:null};
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Retrun text
if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group?
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
const textElement = excalidrawRef
.current
.getSceneElements()
.filter((el:any)=>el.groupIds?.includes(group))
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
if(textElement.length==0) return null; //the group had no text element member
return textElement[0].id; //return text element text
if(textElement.length==0) return {id:null,text:null}; //the group had no text element member
return {id:selectedElement[0].id, text:selectedElement[0].text}; //return text element text
};
this.getSelectedText = (textonly:boolean=false):string => {
if(!excalidrawRef?.current) return null;
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type == "text") return selectedElement[0].text; //a text element was selected. Retrun text
if(textonly) return null;
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
const textElement = excalidrawRef
.current
.getSceneElements()
.filter((el:any)=>el.groupIds?.includes(group))
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
if(textElement.length==0) return null; //the group had no text element member
return textElement[0].text; //return text element text
};
this.addText = (text:string, fontFamily?:1|2|3) => {
if(!excalidrawRef?.current) {
return;
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
const id = nanoid();
window.ExcalidrawAutomate.reset();
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
const id = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
//@ts-ignore
el.push(window.ExcalidrawAutomate.elementsDict[id]);
excalidrawRef.current.updateScene({
elements: el,
appState: st,
const addText = (text:string) => {
window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text,null,id);
//@ts-ignore
const textElement = window.ExcalidrawAutomate.elementsDict[id];
el.push(textElement);
excalidrawRef.current.updateScene({
elements: el,
appState: st,
});
this.save(false);
}
const self = this;
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(id, text,async (parsedText:string)=>{
addText(self.textMode==TextMode.parsed?parsedText:text);
});
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
addText(self.textMode==TextMode.parsed?parseResult:text);
}
}
this.getScene = () => {
@@ -581,89 +659,265 @@ export default class ExcalidrawView extends TextFileView {
if(!excalidrawRef?.current) return;
excalidrawRef.current.refresh();
};
let timestamp = (new Date()).getTime();
//variables used to handle click events in view mode
let selectedTextElement:{id:string,text:string} = null;
let timestamp = 0;
let blockOnMouseButtonDown = false;
const getTextElementAtPointer = (pointer:any) => {
const elements = this.excalidrawRef.current.getSceneElements()
.filter((e:ExcalidrawElement)=>{
return e.type == "text"
&& e.x<=pointer.x && (e.x+e.width)>=pointer.x
&& e.y<=pointer.y && (e.y+e.height)>=pointer.y;
});
if(elements.length==0) return null;
return {id:elements[0].id,text:elements[0].text};
}
let hoverPoint = {x:0,y:0};
let hoverPreviewTarget:EventTarget = null;
const clearHoverPreview = () => {
if(hoverPreviewTarget) {
const event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true,
});
hoverPreviewTarget.dispatchEvent(event);
hoverPreviewTarget = null;
}
}
const excalidrawDiv = React.createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
key: "abc",
tabIndex: 0,
onKeyDown: (e:any) => {
if(document.fullscreenEnabled && e.keyCode==27) document.exitFullscreen();
this.ctrlKeyDown = e.ctrlKey;
this.shiftKeyDown = e.shiftKey;
this.altKeyDown = e.altKey;
if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
const selectedElement = getTextElementAtPointer(currentPosition);
if(!selectedElement) return;
const text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(selectedElement.id)
: selectedElement.text;
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
const parts = text.matchAll(REGEX_LINK.EXPR).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;
this.plugin.hover.linkText = linktext;
this.plugin.hover.sourcePath = this.file.path;
hoverPreviewTarget = this.contentEl; //e.target;
this.app.workspace.trigger('hover-link', {
event: this.mouseEvent,
source: VIEW_TYPE_EXCALIDRAW,
hoverParent: hoverPreviewTarget,
targetEl: hoverPreviewTarget,
linktext: this.plugin.hover.linkText,
sourcePath: this.plugin.hover.sourcePath
});
hoverPoint = currentPosition;
if(document.fullscreenElement === this.contentEl) {
const self = this;
setTimeout(()=>{
const popover = document.body.querySelector("div.popover");
if(popover) self.contentEl.append(popover);
},100)
}
}
},
onKeyUp: (e:any) => {
this.ctrlKeyDown = e.ctrlKey;
this.shiftKeyDown = e.shiftKey;
this.altKeyDown = e.altKey;
},
onClick: (e:MouseEvent):any => {
//@ts-ignore
if(!(e.ctrlKey||e.metaKey)) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!this.getSelectedTextElement().id) return;
this.handleLinkClick(this,e);
},
onMouseMove: (e:MouseEvent) => {
//@ts-ignore
this.mouseEvent = e.nativeEvent;
},
onMouseOver: (e:MouseEvent) => {
clearHoverPreview();
//console.log(e);
},
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
width: dimensions.width,
height: dimensions.height,
UIOptions: {
canvasActions: {
loadScene: false,
saveScene: false,
saveAsScene: false,
export: { saveFileToDisk: false },
saveAsImage: false,
saveToActiveFile: false,
},
},
initialData: initdata,
detectScroll: true,
onPointerUpdate: (p:any) => {
currentPosition = p.pointer;
if(hoverPreviewTarget && (Math.abs(hoverPoint.x-p.pointer.x)>50 || Math.abs(hoverPoint.y-p.pointer.y)>50)) clearHoverPreview();
if(!this.excalidrawRef.current.getAppState().viewModeEnabled) return;
const handleLinkClick = () => {
selectedTextElement = getTextElementAtPointer(p.pointer);
if(selectedTextElement) {
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
this.handleLinkClick(this,event);
selectedTextElement = null;
}
}
const buttonDown = !blockOnMouseButtonDown && p.button=="down";
if(buttonDown) {
blockOnMouseButtonDown = true;
//ctrl click
if(this.ctrlKeyDown) {
handleLinkClick();
return;
}
//dobule click
const now = (new Date()).getTime();
if(now-timestamp < 600) {
handleLinkClick();
}
timestamp = now;
return;
}
if (p.button=="up") {
blockOnMouseButtonDown=false;
}
},
onChange: (et:ExcalidrawElement[],st:AppState) => {
if(this.justLoaded) {
this.justLoaded = false;
this.zoomToFit();
previousSceneVersion = getSceneVersion(et);
return;
}
if (st.editingElement == null && st.resizingElement == null &&
st.draggingElement == null && st.editingGroupId == null &&
st.editingLinearElement == null ) {
const sceneVersion = getSceneVersion(et);
if(sceneVersion != previousSceneVersion) {
previousSceneVersion = sceneVersion;
this.dirty=this.file?.path;
}
}
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
await this.plugin.saveSettings();
})();
},
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
console.log(data,event);
return true;
},*/
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
return null;
},
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
if(isDeleted) {
this.excalidrawData.deleteTextElement(textElement.id);
this.dirty=this.file?.path;
this.setupAutosaveTimer();
return;
}
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
if(text!=textElement.text) { //the user made changes to the text
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
await this.save(false);
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
//thus I only check if TextMode.parsed, text is always != with parseResult
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
this.setupAutosaveTimer();
});
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
this.setupAutosaveTimer();
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
this.excalidrawRef.current.history.clear();
return parseResult;
}
return;
}
this.setupAutosaveTimer();
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
}
})
);
return React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
key: "abc",
onClick: (e:MouseEvent):any => {
if(this.isTextLocked && (e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
const now = (new Date()).getTime();
if(now-timestamp < 600) { //double click
let event = new MouseEvent('dblclick', {
'view': window,
'bubbles': true,
'cancelable': true,
});
e.target.dispatchEvent(event);
new Notice(t("UNLOCK_TO_EDIT"));
timestamp = now;
return;
}
timestamp = now;
}
if(!(e.ctrlKey||e.metaKey)) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!this.getSelectedId()) return;
this.handleLinkClick(this,e);
},
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
width: dimensions.width,
height: dimensions.height,
UIOptions: {
canvasActions: {
loadScene: false,
saveScene: false,
saveAsScene: false,
export: false
},
},
initialData: initdata,
detectScroll: true,
onPointerUpdate: (p:any) => {
currentPosition = p.pointer;
},
onChange: (et:ExcalidrawElement[],st:AppState) => {
if(this.justLoaded) {
this.justLoaded = false;
previousSceneVersion = Excalidraw.getSceneVersion(et);
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
}
if (st.editingElement == null && st.resizingElement == null &&
st.draggingElement == null && st.editingGroupId == null &&
st.editingLinearElement == null ) {
const sceneVersion = Excalidraw.getSceneVersion(et);
if(sceneVersion != previousSceneVersion) {
previousSceneVersion = sceneVersion;
this.dirty=true;
}
}
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
await this.plugin.saveSettings();
})();
}
})
)
excalidrawDiv
);
});
ReactDOM.render(reactElement,(this as any).contentEl);
ReactDOM.render(reactElement,this.contentEl,()=>this.excalidrawWrapperRef.current.focus());
}
public static getSVG(scene:any, exportSettings:ExportSettings):SVGSVGElement {
private zoomToFit() {
//when viewmode is enabled Excalidraw only listens to Alt+R
const el = this.containerEl;
const self = this;
const pattern = this.excalidrawRef.current.getAppState().viewModeEnabled
? [100,200,300] : [null,100,null];
if(pattern[0])
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, altKey : true, code:"KeyR"});
el.querySelector("canvas")?.dispatchEvent(e);
self.altKeyDown = false;
},pattern[0]);
if(pattern[1])
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
el.querySelector("canvas")?.dispatchEvent(e);
self.shiftKeyDown = false;
},pattern[1])
if(pattern[2])
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, altKey : true, code:"KeyR"});
el.querySelector("canvas")?.dispatchEvent(e);
self.altKeyDown=false;
},pattern[2]);
}
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
try {
return exportToSvg({
elements: scene.elements,
@@ -672,14 +926,13 @@ export default class ExcalidrawView extends TextFileView {
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
exportPadding:10,
metadata: "Generated by Excalidraw-Obsidian plugin",
});
} catch (error) {
return null;
}
}
public static async getPNG(scene:any, exportSettings:ExportSettings) {
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
try {
return await Excalidraw.exportToBlob({
elements: scene.elements,
@@ -690,9 +943,10 @@ export default class ExcalidrawView extends TextFileView {
mimeType: "image/png",
exportWithDarkMode: "true",
metadata: "Generated by Excalidraw-Obsidian plugin",
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
});
} catch (error) {
return null;
}
}
}
}

View File

@@ -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) {
constructor(app: App, private prompt_text: string, private default_value: string, private placeholder:string) {
super(app);
}
@@ -32,7 +32,7 @@ export class Prompt extends Modal {
this.promptEl = form.createEl("input");
this.promptEl.type = "text";
this.promptEl.placeholder = "$\\theta$";
this.promptEl.placeholder = this.placeholder;
this.promptEl.value = this.default_value ?? "";
this.promptEl.addClass("excalidraw-prompt-input")
this.promptEl.select();

90
src/Utils.ts Normal file
View File

@@ -0,0 +1,90 @@
import { normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
import { Random } from "roughjs/bin/math";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
* @param filepath
*/
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
let folderpath: string, filename:string;
const lastIndex = filepath.lastIndexOf("/");
return {
folderpath: normalizePath(filepath.substr(0,lastIndex)),
filename: lastIndex==-1 ? filepath : filepath.substr(lastIndex+1),
};
}
/**
* Download data as file from Obsidian, to store on local device
* @param encoding
* @param data
* @param filename
*/
export function download(encoding:string,data:any,filename:string) {
let element = document.createElement('a');
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* Generates the image filename based on the excalidraw filename
* @param excalidrawPath - Full filepath of ExclidrawFile
* @param newExtension - extension of IMG file in ".extension" format
* @returns
*/
export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension:string):string {
const isLegacyFile:boolean = excalidrawPath.endsWith(".excalidraw");
const replaceExtension:string = isLegacyFile ? ".excalidraw" : ".md";
return excalidrawPath.substring(0,excalidrawPath.lastIndexOf(replaceExtension)) + newExtension;
}
/**
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
* @param filename
* @param folderpath
* @returns
*/
export function getNewUniqueFilepath(vault:Vault, filename:string, folderpath:string):string {
let fname = normalizePath(folderpath +'/'+ filename);
let file:TAbstractFile = vault.getAbstractFileByPath(fname);
let i = 0;
while(file) {
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
i++;
file = vault.getAbstractFileByPath(fname);
}
return fname;
}
/**
* Open or create a folderpath if it does not exist
* @param folderpath
*/
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
folderpath = normalizePath(folderpath);
let folder = vault.getAbstractFileByPath(folderpath);
if(folder && folder instanceof TFolder) return;
await vault.createFolder(folderpath);
}
let random = new Random(Date.now());
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
export function wrapText(text:string, lineLen:number):string {
if(!lineLen) return text;
let outstring = "";
// 1 2
const reg = new RegExp(`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]+)(\\s+|$\\n?)`,'gm');
const res = text.matchAll(reg);
let parts;
while(!(parts = res.next()).done) {
outstring += parts.value[1] ? parts.value[1].trimEnd() : parts.value[3].trimEnd();
outstring += (parts.value[2]=='\n' || parts.value[4]=='\n') ?'\n\n':'\n';
}
return outstring.trimEnd();
}

View File

@@ -1,11 +1,11 @@
//This is to avoid brackets littering graph view with links
//export function JSON_stringify(x:any):string {return JSON.stringify(x).replaceAll("[","&#91;");}
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("&#91;","["));}
import {customAlphabet} from "nanoid";
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
export const FRONTMATTER_KEY = "excalidraw-plugin";
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
export const ICON_NAME = "excalidraw-icon";
@@ -13,12 +13,12 @@ export const MAX_COLORS = 5;
export const COLOR_FREQ = 6;
export const RERENDER_EVENT = "excalidraw-embed-rerender";
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "", ""].join("\n");
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
export const LOCK_ICON_NAME = "lock";
export const LOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M35.715 42.855h28.57v-10.71c0-3.946-1.394-7.313-4.183-10.102-2.793-2.79-6.157-4.188-10.102-4.188-3.945 0-7.309 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102zm46.43 5.36v32.14c0 1.489-.524 2.754-1.563 3.797-1.043 1.043-2.309 1.563-3.797 1.563h-53.57c-1.488 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.309-1.563 3.797-1.563H25v-10.71c0-6.848 2.457-12.727 7.367-17.637S43.157 7.145 50 7.145c6.844 0 12.723 2.453 17.633 7.363C72.543 19.418 75 25.297 75 32.145v10.71h1.785c1.488 0 2.754.524 3.797 1.563 1.04 1.043 1.563 2.309 1.563 3.797zm0 0"/>`
export const UNLOCK_ICON_NAME = "unlock";
export const UNLOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M96.43 32.145V46.43c0 .965-.356 1.804-1.063 2.511-.707.707-1.543 1.059-2.512 1.059h-3.57c-.965 0-1.805-.352-2.512-1.059-.707-.707-1.058-1.546-1.058-2.511V32.145c0-3.946-1.395-7.313-4.188-10.102-2.789-2.79-6.156-4.188-10.097-4.188-3.946 0-7.313 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102v10.71H62.5c1.488 0 2.754.524 3.793 1.563 1.043 1.043 1.562 2.309 1.562 3.797v32.14c0 1.489-.52 2.754-1.562 3.797-1.04 1.043-2.305 1.563-3.793 1.563H8.93c-1.489 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.308-1.563 3.797-1.563h37.5v-10.71c0-6.883 2.445-12.77 7.336-17.665 4.894-4.89 10.78-7.335 17.664-7.335 6.882 0 12.77 2.445 17.66 7.335 4.894 4.895 7.34 10.782 7.34 17.665zm0 0"/>`;
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
export const FULLSCREEN_ICON_NAME="fullscreen";
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";
export const DISK_ICON_NAME = "disk";
export const DISK_ICON = `<path fill="none" stroke="currentColor" fill="#fff" d="M0 0h100v100H0z"/><path fill="none" stroke="currentColor" d="M20.832 4.168c21.824.145 43.645.289 74.68.5m-74.68-.5c17.09.113 34.176.227 74.68.5m0 0c.094 27.3.191 54.602.32 91.164m-.32-91.164c.113 32.633.23 65.27.32 91.164m0 0H4.168m91.664 0H4.168m0 0v-75m0 75v-75m0 0L20.832 4.168M4.168 20.832L20.832 4.168M20.832 4.168h58.336m-58.336 0h58.336m0 0v25m0-25v25m0 0H20.832m58.336 0H20.832m0 0v-25m0 25v-25" stroke-width="1.66668" /><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664v16.664H29.168"/><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664m-16.664 0h16.664m0 0v16.664m0-16.664v16.664m0 0H29.168m16.664 0H29.168m0 0V4.168m0 16.664V4.168M12.5 54.168h75m-75 0h75m0 0v41.664m0-41.664v41.664m0 0h-75m75 0h-75m0 0V54.168m0 41.664V54.168M20.832 62.5c20.11-.18 40.219-.36 55.68-.5m-55.68.5c14.656-.133 29.313-.262 55.68-.5M20.832 71.332c13.098-.117 26.2-.234 55.68-.5m-55.68.5l55.68-.5M21.117 79.582c20.645-.184 41.285-.371 55.68-.5m-55.68.5c18.153-.16 36.301-.324 55.68-.5" stroke-width="1.66668"/>`;
export const PNG_ICON_NAME = "save-png";

View File

@@ -1,4 +1,4 @@
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
// English
export default {
@@ -10,6 +10,7 @@ export default {
CREATE_NEW : "New Excalidraw drawing",
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
TRANSCLUDE: "Transclude (embed) a drawing",
@@ -31,7 +32,6 @@ export default {
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
UNLOCK_TO_EDIT: "UNLOCK Text Elements to edit",
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
'SHIFT CLICK this button to open the link in a new pane.\n'+
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
@@ -39,8 +39,8 @@ export default {
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\  < > : | ?',
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
LOCK: "Text Elements are unlocked. Click to LOCK.",
UNLOCK: "Text Elements are locked. Click to UNLOCK.",
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
NOFILE: "Excalidraw (no file)",
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
CONVERT_FILE: "Convert to new format",
@@ -77,21 +77,29 @@ export default {
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
LINK_BRACKETS_DESC: "In preview (locked) mode, when parsing Text Elements, place brackets around links. " +
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
": true/false' to the file\'s frontmatter.",
LINK_PREFIX_NAME:"Link prefix",
LINK_PREFIX_DESC:"In preview (locked) mode, if the Text Element contains a link, precede the text with these characters. " +
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
': "👉 "\' to the file\'s frontmatter.',
': "📍 "\' to the file\'s frontmatter.',
URL_PREFIX_NAME:"URL prefix",
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
': "🌐 "\' to the file\'s frontmatter.',
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
LINK_CTRL_CLICK_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.",
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.",
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
"[[drawing.excalidraw|100x100]] format.",
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
EXPORT_BACKGROUND_NAME: "Export image with background",
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
EXPORT_THEME_NAME: "Export image with theme",
@@ -124,7 +132,7 @@ export default {
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
FILETAG_DESC: "The text or emojii to display as type indicator.",
INSERT_EMOJI: "Insert an emoji",
//openDrawings.ts

View File

@@ -1,3 +1,140 @@
// 简体中文
export default {};
// 简体中文
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
export default {
// main.ts
OPEN_AS_EXCALIDRAW: "打开为 Excalidraw 绘图",
TOGGLE_MODE: "在 Excalidraw 和 Markdown 模式之间切换",
CONVERT_NOTE_TO_EXCALIDRAW: "转换空白笔记为 Excalidraw 绘图",
CONVERT_EXCALIDRAW: "转换 *.excalidraw 为 *.md 文件",
CREATE_NEW : "新建 Excalidraw 绘图",
CONVERT_FILE_KEEP_EXT: "*.excalidraw 格式 => *.excalidraw.md 格式",
CONVERT_FILE_REPLACE_EXT: "*.excalidraw 格式 => *.md (Logseq compatibility) 格式",
DOWNLOAD_LIBRARY: "导出 stencil 库为 *.excalidrawlib 文件",
OPEN_EXISTING_NEW_PANE: "在新面板中打开已存在的绘图",
OPEN_EXISTING_ACTIVE_PANE: "在当前面板中打开已存在的绘图",
TRANSCLUDE: "嵌入绘图",
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑的绘图",
NEW_IN_NEW_PANE: "在新面板中创建已存在的绘图",
NEW_IN_ACTIVE_PANE: "在当前面板中创建已存在的绘图",
NEW_IN_NEW_PANE_EMBED: "在新面板中创建已存在的绘图且嵌入到当前笔记中",
NEW_IN_ACTIVE_PANE_EMBED: "在当前面板中创建已存在的绘图且嵌入到当前笔记中",
EXPORT_SVG: "导出 SVG 文件到当前文件的目录中",
EXPORT_PNG: "导出 PNG 文件到当前文件的目录中",
TOGGLE_LOCK: "切换文本元素锁定模式",
INSERT_LINK: "在文件中插入链接",
INSERT_LATEX: "在文件中插入 LaTeX 符号 (e.g. $\\theta$)",
ENTER_LATEX: "输入一个 LaTeX 表达式",
//ExcalidrawView.ts
OPEN_AS_MD: "打开为 Markdown 文件",
SAVE_AS_PNG: "保存成 PNG 文件到库里CTRL/META 加左键点击来指定导出位置)",
SAVE_AS_SVG: "保存成 SVG 文件到库里CTRL/META 加左键点击来指定导出位置)",
OPEN_LINK: "以链接的方式打开文本 \n按住 SHIFT 来在新面板中打开)",
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
LINK_BUTTON_CLICK_NO_TEXT: '选择带有外部链接或内部链接的文本。\n'+
'SHIFT 加左键点击按钮来在新面板中打开链接。\n'+
'CTRL/META 加左键在画布中点击文本元素也可以打开对应的链接。',
TEXT_ELEMENT_EMPTY: "文本元素没有链接任何东西.",
FILENAME_INVALID_CHARS: '文件名不能包含以下符号: * " \\ < > : | ?',
FILE_DOES_NOT_EXIST: "文件不存在。按住 ALT或者 ALT + SHIFT加左键点击来创建新文件。",
FORCE_SAVE: "强制保存以更新相邻面板中的嵌入。\n请注意自动保存始终处于开启状态",
RAW: "文本元素正以原文模式显示。 单击按钮更改为预览模式。",
PARSED: "文本元素正以预览模式显示。 单击按钮更改为原文模式。",
NOFILE: "Excalidraw (没有文件)",
COMPATIBILITY_MODE: "*.excalidraw 文件以兼容模式打开。转换为新格式以获得完整的插件功能。",
CONVERT_FILE: "转换为新格式",
//settings.ts
FOLDER_NAME: "Excalidraw 文件夹",
FOLDER_DESC: "新绘图的默认位置。如果此处为空,将在 Vault 根目录中创建绘图。",
TEMPLATE_NAME: "Excalidraw 模板文件",
TEMPLATE_DESC: "Excalidraw 模板的完整文件路径。" +
"例如:如果您的模板在默认的 Excalidraw 文件夹中且它的名称是" +
"Template.md你应当设置为Excalidraw/Template.md。" +
"如果您在兼容模式下使用 Excalidraw那么您的模板也必须是旧的 excalidraw 文件," +
"例如 Excalidraw/Template.excalidraw。",
AUTOSAVE_NAME: "自动保存",
AUTOSAVE_DESC: "每 30 秒自动保存编辑中的绘图。当您关闭 Excalidraw 或 Obsidian 或焦点移动到另一个面板时,通常会引发保存"+
"在极少数情况下自动保存可能会稍微扰乱绘图流程。我在创建此功能时考虑到了手机端(安卓)," +
"其中“滑到另一个应用程序”会导致一些数据丢失,并且因为我无法在手机上的应用程序" +
" 终止时强制保存。如果您在桌面上使用 Excalidraw这你可以关掉它。",
FILENAME_HEAD: "文件名",
FILENAME_DESC: "<p>自动生成的文件名包括一个前缀和一个日期。" +
"例如 'Drawing 2021-05-24 12.58.07'。</p>"+
"<p>点击<a href='https://momentjs.com/docs/#/displaying/format/'>"+
"日期和时间格式参考</a>来查看如何修改。</p>",
FILENAME_SAMPLE: "当前文件名的格式为:<b>",
FILENAME_PREFIX_NAME: "文件名前缀",
FILENAME_PREFIX_DESC: "文件名的第一部分",
FILENAME_DATE_NAME: "文件名日期",
FILENAME_DATE_DESC: "文件名的第二部分",
LINKS_HEAD: "链接",
LINKS_DESC: "CTRL/META 加左键点击文本元素来打开链接。" +
"如果选中的文本指向多个双链,只会打开其中第一个。" +
"如果选中的文本为超链接 (i.e. https:// or http://),然后" +
"插件会在浏览器中打开超链接。" +
"当对应的文件名修改时,匹配的链接也会修改。" +
"如果你不希望你自己的链接文本突然修改,用别名来替代",
LINK_BRACKETS_NAME: "在链接上显示双链符号[[",
LINK_BRACKETS_DESC: "在预览(锁定)模式,当解析文本元素,在链接左右展示中括号。" +
"你可以在文件的 Frontmatter 中加入'" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
": true/false' 来单独控制某个文件。",
LINK_PREFIX_NAME:"链接前缀",
LINK_PREFIX_DESC:"在预览(锁定)模式,如果文本元素包含链接,在文本之前加上这些字符。" +
"你可以在文件的 Frontmatter 中加入 \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
': "👉 "\' 单独更改',
LINK_CTRL_CLICK_NAME: "CTRL 加左键点击文本来打开链接",
LINK_CTRL_CLICK_DESC: "如果此功能干扰了您要使用的 Excalidraw 功能,您可以将其关闭。 如果" +
"关闭此选项,则只有绘图标题栏中的链接按钮可以让你打开链接。",
EMBED_HEAD: "嵌入 & 导出",
EMBED_WIDTH_NAME: "嵌入图像的默认宽度",
EMBED_WIDTH_DESC: "嵌入图形的默认宽度。您可以在使用" +
"![[drawing.excalidraw|100]] 或 [[drawing.excalidraw|100x100]]" +
"格式指定嵌入图像时的宽度。",
EXPORT_PNG_SCALE_NAME: "PNG 导出图像比例",
EXPORT_PNG_SCALE_DESC: "导出的 PNG 图像的大小比例",
EXPORT_BACKGROUND_NAME: "导出带有背景的图像",
EXPORT_BACKGROUND_DESC: "如果关闭,导出的图像的背景将是透明的。",
EXPORT_THEME_NAME: "导出带有主题的图像",
EXPORT_THEME_DESC: "导出与绘图的暗/亮主题匹配的图像。" +
"如果关闭,在深色模式下导出的绘图将和浅色模式下导出的图像一样",
EXPORT_HEAD: "导出设置",
EXPORT_SYNC_NAME:"保持 .SVG 和/或 .PNG 文件名与绘图文件同步",
EXPORT_SYNC_DESC:"打开后,当同一文件夹且同名的绘图被重命名时,插件将自动更新对应的 .SVG 和/或 .PNG 文件的文件名。" +
"当同一文件夹的同一名称的绘图被删除时,该插件还将自动删除对应的 .SVG 和/或 .PNG 文件。",
EXPORT_SVG_NAME: "自动导出 SVG",
EXPORT_SVG_DESC: "自动导出和你文件同名的 SVG 文件" +
"插件会将 SVG 文件保存到对应的 Excalidraw 所在的文件夹中"+
"将 .svg 文件嵌入到文档中,而不是 excalidraw使您嵌入的页面独立开来" +
"当自动导出开关打开时,每次您编辑对应的 excalidraw 绘图时,此文件都会更新。",
EXPORT_PNG_NAME: "自动导出 PNG",
EXPORT_PNG_DESC: "和自动导出 SVG 一样,但面向 *.PNG",
COMPATIBILITY_HEAD: "兼容特性",
EXPORT_EXCALIDRAW_NAME: "自动导出 Excalidraw 文件",
EXPORT_EXCALIDRAW_DESC: "和自动导出 SVG 一样,但面向 *.Excalidraw",
SYNC_EXCALIDRAW_NAME: "同步 .md 格式以及 .excalidraw 格式",
SYNC_EXCALIDRAW_DESC: "如果 *.excalidraw 文件的修改比 *.md 文件的修改更新" +
",会根据 .excalidraw 文件更新 .md 文件中的绘图",
COMPATIBILITY_MODE_NAME: "以旧格式创建新绘图",
COMPATIBILITY_MODE_DESC: "通过启用此功能图形,您可以使用功能区图标、命令面板操作、 "+
"并且文件浏览器将仍旧保留 *.excalidraw 文件。 此设置还将" +
"关闭你打开旧格式绘图时的提醒消息",
EXPERIMENTAL_HEAD: "实验性特性",
EXPERIMENTAL_DESC: "这些设置不会立即生效,只有在刷新文件资源管理器或重新启动 Obsidian 时才会生效。",
FILETYPE_NAME: "在文件浏览器中给所有的 Excalidraw 文件加上 ✏️ 标识符",
FILETYPE_DESC: "Excalidraw 文件将使用下一个设置中定义的表情符号或文本来做标识。",
FILETAG_NAME: "给 Excalidraw 文件设置标识符",
FILETAG_DESC: "要显示为标识符的文本或表情符号。",
//openDrawings.ts
SELECT_FILE: "选择一个文件后按回车。",
NO_MATCH: "没有文件匹配你的索引。",
SELECT_FILE_TO_LINK: "选择要为其插入链接的文件。",
TYPE_FILENAME: "键入要选择的绘图名称。",
SELECT_FILE_OR_TYPE_NEW: "选择现有绘图或新绘图的类型名称,然后按回车。",
SELECT_TO_EMBED: "选择要插入到当前文档中的绘图。",
};

View File

@@ -1,6 +1,5 @@
import {
TFile,
TFolder,
Plugin,
WorkspaceLeaf,
addIcon,
@@ -32,13 +31,10 @@ import {
RERENDER_EVENT,
FRONTMATTER_KEY,
FRONTMATTER,
LOCK_ICON,
LOCK_ICON_NAME,
UNLOCK_ICON_NAME,
UNLOCK_ICON,
JSON_parse
JSON_parse,
nanoid
} from "./constants";
import ExcalidrawView, {ExportSettings} from "./ExcalidrawView";
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
import {getJSON} from "./ExcalidrawData";
import {
ExcalidrawSettings,
@@ -51,24 +47,38 @@ import {
} from "./openDrawing";
import {
initExcalidrawAutomate,
destroyExcalidrawAutomate,
exportSceneToMD,
destroyExcalidrawAutomate
} from "./ExcalidrawAutomate";
import { Prompt } from "./Prompt";
import { around } from "monkey-around";
import { t } from "./lang/helpers";
import { MigrationPrompt } from "./MigrationPrompt";
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
declare module "obsidian" {
interface App {
isMobile():boolean;
}
interface Vault {
getConfig(option:"attachmentFolderPath"): string;
}
interface Workspace {
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
}
}
export default class ExcalidrawPlugin extends Plugin {
public excalidrawFileModes: { [file: string]: string } = {};
private _loaded: boolean = false;
public settings: ExcalidrawSettings;
//public stencilLibrary: any = null;
private openDialog: OpenFileDialog;
private activeExcalidrawView: ExcalidrawView = null;
public lastActiveExcalidrawFilePath: string = null;
private hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
private observer: MutationObserver;
private fileExplorerObserver: MutationObserver;
public opencount:number = 0;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -79,8 +89,6 @@ export default class ExcalidrawPlugin extends Plugin {
addIcon(DISK_ICON_NAME,DISK_ICON);
addIcon(PNG_ICON_NAME,PNG_ICON);
addIcon(SVG_ICON_NAME,SVG_ICON);
addIcon(LOCK_ICON_NAME,LOCK_ICON);
addIcon(UNLOCK_ICON_NAME,UNLOCK_ICON);
await this.loadSettings();
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
@@ -102,7 +110,7 @@ export default class ExcalidrawPlugin extends Plugin {
//inspiration taken from kanban:
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
if(this.settings.loadCount<3) this.migrationNotice();
if(this.settings.loadCount<1) this.migrationNotice();
}
@@ -118,12 +126,14 @@ export default class ExcalidrawPlugin extends Plugin {
}
});
}
/**
* Displays a transcluded .excalidraw image in markdown preview mode
*/
private addMarkdownPostProcessor() {
interface imgElementAttributes {
file?: TFile,
fname: string, //Excalidraw filename
fwidth: string, //Display width of image
fheight: string, //Display height of image
@@ -131,14 +141,18 @@ export default class ExcalidrawPlugin extends Plugin {
}
/**
* Generates an img element with the .excalidraw drawing encoded as a base64 svg
* Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings)
* @param parts {imgElementAttributes} - display properties of the image
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the encoded SVG image
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the image
*/
const getIMG = async (parts:imgElementAttributes):Promise<HTMLElement> => {
const file = this.app.vault.getAbstractFileByPath(parts.fname);
if(!(file && file instanceof TFile)) {
return null;
const getIMG = async (imgAttributes:imgElementAttributes):Promise<HTMLElement> => {
let file = imgAttributes.file;
if(!imgAttributes.file) {
const f = this.app.vault.getAbstractFileByPath(imgAttributes.fname);
if(!(f && f instanceof TFile)) {
return null;
}
file = f;
}
const content = await this.app.vault.read(file);
@@ -146,17 +160,29 @@ export default class ExcalidrawPlugin extends Plugin {
withBackground: this.settings.exportWithBackground,
withTheme: this.settings.exportWithTheme
}
let svg = ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
const img = createEl("img");
img.setAttribute("width",imgAttributes.fwidth);
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
img.addClass(imgAttributes.style);
if(!this.settings.displaySVGInPreview) {
const width = parseInt(imgAttributes.fwidth);
let scale = 1;
if(width>=800) scale = 2;
if(width>=1600) scale = 3;
if(width>=2400) scale = 4;
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)),exportSettings, scale);
if(!png) return null;
img.src = URL.createObjectURL(png);
return img;
}
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
const img = createEl("img");
svg.removeAttribute('width');
svg.removeAttribute('height');
img.setAttribute("width",parts.fwidth);
if(parts.fheight) img.setAttribute("height",parts.fheight);
img.addClass(parts.style);
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML))));
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll("&nbsp;"," ")))));
return img;
}
@@ -166,12 +192,12 @@ export default class ExcalidrawPlugin extends Plugin {
* @param ctx
*/
const markdownPostProcessor = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => {
const drawings = el.querySelectorAll('.internal-embed');
if(drawings.length==0) return;
const embeddedItems = el.querySelectorAll('.internal-embed');
if(embeddedItems.length==0) return;
let attr:imgElementAttributes={fname:"",fheight:"",fwidth:"",style:""};
let alt:string, img:any, parts, div, file:TFile;
for (const drawing of drawings) {
let alt:string, parts, div, file:TFile;
for (const drawing of embeddedItems) {
attr.fname = drawing.getAttribute("src");
file = this.app.metadataCache.getFirstLinkpathDest(attr.fname, ctx.sourcePath);
if(file && file instanceof TFile && this.isExcalidrawFile(file)) {
@@ -181,19 +207,20 @@ export default class ExcalidrawPlugin extends Plugin {
if(alt == attr.fname) alt = ""; //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text
attr.style = "excalidraw-svg";
if(alt) {
//for some reason ![]() is rendered in a DIV and ![[]] in a span by Obsidian
//also the alt text of the DIV does not include the altext of the image
//thus need to add an additional "|" character when its a span
//for some reason Obsidian renders ![]() in a DIV and ![[]] in a SPAN
//also the alt-text of the DIV does not include the alt-text of the image
//thus need to add an additional "|" character when its a SPAN
if(drawing.tagName.toLowerCase()=="span") alt = "|"+alt;
parts = alt.match(/[^\|]*\|?(\d*)x?(\d*)\|?(.*)/);
//1:width, 2:height, 3:style 1 2 3
parts = alt.match(/[^\|]*\|?(\d*%?)x?(\d*%?)\|?(.*)/);
attr.fwidth = parts[1]? parts[1] : this.settings.width;
attr.fheight = parts[2];
if(parts[3]!=attr.fname) attr.style = "excalidraw-svg" + (parts[3] ? "-" + parts[3] : "");
}
attr.fname = file?.path;
img = await getIMG(attr);
div = createDiv(attr.style, (el)=>{
div = createDiv(attr.style, async (el)=>{
const img = await getIMG(attr);
el.append(img);
el.setAttribute("src",file.path);
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
@@ -236,18 +263,17 @@ export default class ExcalidrawPlugin extends Plugin {
this.hover.sourcePath = e.sourcePath;
};
this.registerEvent(
//@ts-ignore
this.app.workspace.on('hover-link',hoverEvent)
);
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
this.observer = new MutationObserver((m)=>{
this.observer = new MutationObserver(async (m)=>{
if(m.length == 0) return;
if(!this.hover.linkText) return;
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
if(!file || !(file instanceof TFile) || !this.isExcalidrawFile(file)) return
if((file as TFile).extension == "excalidraw") {
if(file.extension == "excalidraw") {
observerForLegacyFileFormat(m,file);
return;
}
@@ -265,12 +291,14 @@ export default class ExcalidrawPlugin extends Plugin {
if(m[i].addedNodes[0].firstElementChild?.firstElementChild?.className=="excalidraw-svg") return;
//@ts-ignore
if(!m[i].addedNodes[0].matchParent(".hover-popover")) return;
const hoverPopover = m[i].addedNodes[0].matchParent(".hover-popover");
if(!hoverPopover) return;
const node = m[i].addedNodes[0];
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
const div = createDiv("", async (el)=>{
const img = await getIMG({file:file,fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
@@ -279,12 +307,11 @@ export default class ExcalidrawPlugin extends Plugin {
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
});
m[i].addedNodes[0].insertBefore(div,m[i].addedNodes[0].firstChild)
node.insertBefore(div,node.firstChild)
});
//compatibility: .excalidraw file observer
let observerForLegacyFileFormat = (m:MutationRecord[], file:TFile) => {
let observerForLegacyFileFormat = async (m:MutationRecord[], file:TFile) => {
if(!this.hover.linkText) return;
if(m.length!=1) return;
if(m[0].addedNodes.length != 1) return;
@@ -295,8 +322,8 @@ export default class ExcalidrawPlugin extends Plugin {
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const img = await getIMG({file:file,fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
@@ -322,35 +349,32 @@ export default class ExcalidrawPlugin extends Plugin {
this.fileExplorerObserver = null;
}
/**
* Display characters configured in settings, in front of the filename, if the markdown file is an excalidraw drawing
*/
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))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
};
this.fileExplorerObserver = new MutationObserver((m)=>{
const mutationsWithNodes = m.filter((v)=>v.addedNodes.length > 0);
mutationsWithNodes.forEach((mu)=>{
mu.addedNodes.forEach((n)=>{
if(!(n instanceof Element)) return;
n.querySelectorAll(".nav-file-title").forEach((el)=>{
if(el.childElementCount == 1) {
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
}
});
n.querySelectorAll(".nav-file-title").forEach(insertFiletype);
});
});
});
const self = this;
this.app.workspace.onLayoutReady(()=>{
document.querySelectorAll(".nav-file-title").forEach((el)=>{
if(el.childElementCount == 1) {
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
}
});
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
this.fileExplorerObserver.observe(document.querySelector(".workspace"), {childList: true, subtree: true});
});
@@ -411,6 +435,27 @@ export default class ExcalidrawPlugin extends Plugin {
this.app.workspace.on("file-menu", fileMenuHandlerConvertReplaceExtension)
);
this.addCommand({
id: "excalidraw-download-lib",
name: t("DOWNLOAD_LIBRARY"),
callback: async () => {
if(this.app.isMobile) {
const prompt = new Prompt(this.app, "Please provide a filename",'my-library','filename, leave blank to cancel action');
prompt.openAndGetValue( async (filename:string)=> {
if(!filename) return;
filename = filename + ".excalidrawlib";
const folderpath = normalizePath(this.settings.folder);
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
this.app.vault.create(fname,this.settings.library);
new Notice("Exported library to " + fname,6000);
});
return;
}
download('data:text/plain;charset=utf-8',encodeURIComponent(this.settings.library), 'my-obsidian-library.excalidrawlib');
},
});
this.addCommand({
id: "excalidraw-open",
name: t("OPEN_EXISTING_NEW_PANE"),
@@ -469,6 +514,26 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
const insertDrawingToDoc = async (inNewPane:boolean) => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if(!activeView) return;
let folder = this.app.vault.getConfig("attachmentFolderPath");
// folder == null: save to vault root
// folder == "./" save to same folder as current file
// folder == "folder" save to specific folder in vault
// folder == "./folder" save to specific subfolder of current active folder
if(folder && folder.startsWith("./")) { // folder relative to current file
const activeFileFolder = splitFolderAndFilename(activeView.file.path).folderpath + "/";
folder = normalizePath(activeFileFolder + folder.substring(2));
}
if(!folder) folder = "";
await checkAndCreateFolder(this.app.vault,folder);
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
this.embedDrawing(normalizePath(folder + "/" + filename));
this.createDrawing(filename, inNewPane,folder==""?null:folder);
}
this.addCommand({
id: "excalidraw-autocreate-and-embed",
name: t("NEW_IN_NEW_PANE_EMBED"),
@@ -476,9 +541,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (checking) {
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown");
} else {
const filename = this.getNextDefaultFilename();
this.embedDrawing(filename);
this.createDrawing(filename, true);
insertDrawingToDoc(true);
return true;
}
},
@@ -491,9 +554,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (checking) {
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown");
} else {
const filename = this.getNextDefaultFilename();
this.embedDrawing(filename);
this.createDrawing(filename, false);
insertDrawingToDoc(false);
return true;
}
},
@@ -505,10 +566,6 @@ export default class ExcalidrawPlugin extends Plugin {
checkCallback: (checking: boolean) => {
if (checking) {
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
/* if(this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
return !(this.app.workspace.activeLeaf.view as ExcalidrawView).compatibilityMode;
}
return false;*/
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
@@ -550,7 +607,7 @@ export default class ExcalidrawPlugin extends Plugin {
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
view.lock(!view.isTextLocked);
view.changeTextMode((view.textMode==TextMode.parsed)?TextMode.raw:TextMode.parsed);
return true;
}
else return false;
@@ -586,7 +643,7 @@ export default class ExcalidrawPlugin extends Plugin {
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'');
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'','$\\theta$');
prompt.openAndGetValue( async (formula:string)=> {
if(!formula) return;
const el = createEl('p');
@@ -668,9 +725,19 @@ export default class ExcalidrawPlugin extends Plugin {
public async convertSingleExcalidrawToMD(file: TFile, replaceExtension:boolean = false, keepOriginal:boolean = false):Promise<TFile> {
const data = await this.app.vault.read(file);
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
const fname = this.getNewUniqueFilepath(filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
console.log(fname);
const result = await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
const result = await this.app.vault.create(fname,FRONTMATTER + this.exportSceneToMD(data));
if (this.settings.keepInSync) {
['.svg','.png'].forEach( (ext:string)=>{
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
const imgFile = this.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
if(imgFile && imgFile instanceof TFile) {
const newIMGpath = fname.substr(0,fname.lastIndexOf(".md")) + ext;
this.app.vault.rename(imgFile,newIMGpath);
}
});
}
if (!keepOriginal) this.app.vault.delete(file);
return result;
}
@@ -778,16 +845,18 @@ export default class ExcalidrawPlugin extends Plugin {
const self = this;
this.app.workspace.onLayoutReady(async () => {
//watch filename change to rename .svg, .png; to sync to .md; to update links
const renameEventHandler = async (file:TAbstractFile,oldPath:string) => {
if(!(file instanceof TFile)) return;
if (!self.isExcalidrawFile(file)) return;
if (!self.settings.keepInSync) return;
if(!self.isExcalidrawFile(file)) return;
if(!self.settings.keepInSync) return;
['.svg','.png','.excalidraw'].forEach(async (ext:string)=>{
const oldIMGpath = oldPath.substring(0,oldPath.lastIndexOf('.md')) + ext;
const oldIMGpath = getIMGPathFromExcalidrawFile(oldPath,ext);
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
if(imgFile && imgFile instanceof TFile) {
const newIMGpath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
const newIMGpath = getIMGPathFromExcalidrawFile(file.path,ext);
await self.app.vault.rename(imgFile,newIMGpath);
}
});
@@ -816,7 +885,9 @@ export default class ExcalidrawPlugin extends Plugin {
const deleteEventHandler = async (file:TFile) => {
if (!(file instanceof TFile)) return;
//@ts-ignore
if (file.unsaveCachedData && !file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)==-1) return;
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---[\r\n][\s\S]*excalidraw-plugin:\s*(locked|unlocked)[\r\n][\s\S]*---/gm)>-1)
|| (file.extension=="excalidraw");
if(!isExcalidarwFile) return;
//close excalidraw view where this file is open
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
@@ -829,7 +900,7 @@ export default class ExcalidrawPlugin extends Plugin {
//delete PNG and SVG files as well
if (self.settings.keepInSync) {
['.svg','.png','.excalidraw'].forEach(async (ext:string) => {
const imgPath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
const imgPath = getIMGPathFromExcalidrawFile(file.path,ext);
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(imgPath));
if(imgFile && imgFile instanceof TFile) {
await self.app.vault.delete(imgFile);
@@ -842,10 +913,10 @@ export default class ExcalidrawPlugin extends Plugin {
);
//save open drawings when user quits the application
const quitEventHandler = (tasks: Tasks) => {
const quitEventHandler = async (tasks: Tasks) => {
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for (let i=0;i<leaves.length;i++) {
(leaves[i].view as ExcalidrawView).save();
await (leaves[i].view as ExcalidrawView).save(true);
}
}
self.registerEvent(
@@ -881,7 +952,9 @@ export default class ExcalidrawPlugin extends Plugin {
excalidrawLeaves.forEach((leaf) => {
this.setMarkdownView(leaf);
});
this.settings.drawingOpenCount += this.opencount;
this.settings.loadCount++;
//this.saveSettings();
}
public embedDrawing(data:string) {
@@ -894,7 +967,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
private async loadSettings() {
public async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
@@ -902,6 +975,14 @@ export default class ExcalidrawPlugin extends Plugin {
await this.saveData(this.settings);
}
public getStencilLibrary():string {
return this.settings.library;
}
public setStencilLibrary(library:string) {
this.settings.library = library;
}
public triggerEmbedUpdates(filepath?:string){
const e = document.createEvent("Event")
e.initEvent(RERENDER_EVENT,true,false);
@@ -953,17 +1034,46 @@ export default class ExcalidrawPlugin extends Plugin {
if (this.settings.compatibilityMode) {
return BLANK_DRAWING;
}
return FRONTMATTER + '\n# Drawing\n'+ BLANK_DRAWING;
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(BLANK_DRAWING);
}
public getMarkdownDrawingSection(jsonString: string) {
return '%%\n# Drawing\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
+ jsonString + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + '%%';
}
/**
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
* @param {string} data - Excalidraw scene JSON string
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
*/
public exportSceneToMD(data:string): string {
if(!data) return "";
const excalidrawData = JSON_parse(data);
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
let outString = '# Text Elements\n';
let id:string;
for (const te of textElements) {
id = te.id;
//replacing Excalidraw text IDs with my own, because default IDs may contain
//characters not recognized by Obsidian block references
//also Excalidraw IDs are inconveniently long
if(te.id.length>8) {
id=nanoid();
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + this.getMarkdownDrawingSection(data);
}
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
const folder = this.app.vault.getAbstractFileByPath(folderpath);
if (!(folder && folder instanceof TFolder)) {
await this.app.vault.createFolder(folderpath);
}
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
const fname = this.getNewUniqueFilepath(filename,folderpath);
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
if(initData) {
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
@@ -992,27 +1102,10 @@ export default class ExcalidrawPlugin extends Plugin {
} as ViewState);
}
/**
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
* @param filename
* @param folderpath
* @returns
*/
getNewUniqueFilepath(filename:string, folderpath:string):string {
let fname = normalizePath(folderpath +'/'+ filename);
let file:TAbstractFile = this.app.vault.getAbstractFileByPath(fname);
let i = 0;
while(file) {
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
i++;
file = this.app.vault.getAbstractFileByPath(fname);
}
return fname;
}
isExcalidrawFile(f:TFile) {
public isExcalidrawFile(f:TFile) {
if(f.extension=="excalidraw") return true;
const fileCache = this.app.metadataCache.getFileCache(f);
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
}
}
}

View File

@@ -1,7 +1,8 @@
import {
App,
PluginSettingTab,
Setting
Setting,
TFile
} from 'obsidian';
import { VIEW_TYPE_EXCALIDRAW } from './constants';
import ExcalidrawView from './ExcalidrawView';
@@ -13,11 +14,13 @@ export interface ExcalidrawSettings {
templateFilePath: string,
drawingFilenamePrefix: string,
drawingFilenameDateTime: string,
displaySVGInPreview: boolean,
width: string,
showLinkBrackets: boolean,
linkPrefix: string,
autosave: boolean;
urlPrefix: string,
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
pngExportScale: number,
exportWithTheme: boolean,
exportWithBackground: boolean,
keepInSync: boolean,
@@ -25,12 +28,12 @@ export interface ExcalidrawSettings {
autoexportPNG: boolean,
autoexportExcalidraw: boolean,
syncExcalidraw: boolean,
library: string,
compatibilityMode: boolean,
experimentalFileType: boolean,
experimentalFileTag: string,
loadCount: number, //version 1.2 migration counter
drawingOpenCount: number,
library: string,
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
@@ -38,11 +41,13 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
templateFilePath: 'Excalidraw/Template.excalidraw',
drawingFilenamePrefix: 'Drawing ',
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
displaySVGInPreview: true,
width: '400',
linkPrefix: ">> ",
linkPrefix: "📍",
urlPrefix: "🌐",
showLinkBrackets: true,
autosave: false,
allowCtrlClick: true,
pngExportScale: 1,
exportWithTheme: true,
exportWithBackground: true,
keepInSync: false,
@@ -50,23 +55,41 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
autoexportPNG: false,
autoexportExcalidraw: false,
syncExcalidraw: false,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
experimentalFileType: false,
experimentalFileTag: "✏️",
compatibilityMode: false,
loadCount: 0,
drawingOpenCount: 0,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
}
export class ExcalidrawSettingTab extends PluginSettingTab {
plugin: ExcalidrawPlugin;
private requestEmbedUpdate:boolean = false;
private requestReloadDrawings:boolean = false;
constructor(app: App, plugin: ExcalidrawPlugin) {
super(app, plugin);
this.plugin = plugin;
}
async hide() {
if(this.requestReloadDrawings) {
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for(const v of exs) {
if(v.view instanceof ExcalidrawView) {
await v.view.save(false);
await v.view.reload(true);
}
}
this.requestEmbedUpdate = true;
}
if(this.requestEmbedUpdate) this.plugin.triggerEmbedUpdates();
}
display(): void {
this.requestEmbedUpdate = false;
this.requestReloadDrawings = false;
let {containerEl} = this;
this.containerEl.empty();
@@ -92,28 +115,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("AUTOSAVE_NAME"))
.setDesc(t("AUTOSAVE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autosave)
.onChange(async (value) => {
this.plugin.settings.autosave = value;
await this.plugin.saveSettings();
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for(const v of exs) {
if(v.view instanceof ExcalidrawView) {
if(v.view.autosaveTimer) {
clearInterval(v.view.autosaveTimer)
v.view.autosaveTimer = null;
}
if(value) {
v.view.setupAutosaveTimer();
}
}
}
}));
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
containerEl.createDiv('',(el) => {
el.innerHTML = t("FILENAME_DESC");
@@ -158,17 +159,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
this.containerEl.createEl('p',{
text: t("LINKS_DESC")});
const reloadDrawings = async () => {
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for(const v of exs) {
if(v.view instanceof ExcalidrawView) {
await v.view.save(false);
v.view.reload(true);
}
}
this.plugin.triggerEmbedUpdates();
}
new Setting(containerEl)
.setName(t("LINK_BRACKETS_NAME"))
@@ -178,19 +168,32 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.showLinkBrackets = value;
await this.plugin.saveSettings();
reloadDrawings();
this.requestReloadDrawings = true;
}));
new Setting(containerEl)
.setName(t("LINK_PREFIX_NAME"))
.setDesc(t("LINK_PREFIX_DESC"))
.addText(text => text
.setPlaceholder('>> ')
.setPlaceholder(t("INSERT_EMOJI"))
.setValue(this.plugin.settings.linkPrefix)
.onChange(async (value) => {
.onChange((value) => {
console.log(value);
this.plugin.settings.linkPrefix = value;
this.plugin.saveSettings();
this.requestReloadDrawings = true;
}));
new Setting(containerEl)
.setName(t("URL_PREFIX_NAME"))
.setDesc(t("URL_PREFIX_DESC"))
.addText(text => text
.setPlaceholder(t("INSERT_EMOJI"))
.setValue(this.plugin.settings.urlPrefix)
.onChange(async (value) => {
this.plugin.settings.urlPrefix = value;
await this.plugin.saveSettings();
reloadDrawings();
this.requestReloadDrawings = true;
}));
new Setting(containerEl)
@@ -205,6 +208,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
new Setting(containerEl)
.setName(t("EMBED_PREVIEW_SVG_NAME"))
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displaySVGInPreview)
.onChange(async (value) => {
this.plugin.settings.displaySVGInPreview = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("EMBED_WIDTH_NAME"))
.setDesc(t("EMBED_WIDTH_DESC"))
@@ -214,9 +229,29 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.width = value;
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
this.requestEmbedUpdate = true;
}));
let scaleText:HTMLDivElement;
new Setting(containerEl)
.setName(t("EXPORT_PNG_SCALE_NAME"))
.setDesc(t("EXPORT_PNG_SCALE_DESC"))
.addSlider(slider => slider
.setLimits(1,5,0.5)
.setValue(this.plugin.settings.pngExportScale)
.onChange(async (value)=> {
scaleText.innerText = " " + value.toString();
this.plugin.settings.pngExportScale = value;
await this.plugin.saveSettings();
}))
.settingEl.createDiv('',(el)=>{
scaleText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = " " + this.plugin.settings.pngExportScale.toString();
});
new Setting(containerEl)
.setName(t("EXPORT_BACKGROUND_NAME"))
.setDesc(t("EXPORT_BACKGROUND_DESC"))
@@ -225,7 +260,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.exportWithBackground = value;
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
this.requestEmbedUpdate = true;
}));
new Setting(containerEl)
@@ -236,7 +271,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.exportWithTheme = value;
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
this.requestEmbedUpdate = true;
}));
this.containerEl.createEl('h1', {text: t("EXPORT_HEAD")});
@@ -271,7 +306,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.autoexportPNG = value;
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
new Setting(containerEl)
@@ -322,7 +357,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setName(t("FILETAG_NAME"))
.setDesc(t("FILETAG_DESC"))
.addText(text => text
.setPlaceholder('✏️')
.setPlaceholder(t("INSERT_EMOJI"))
.setValue(this.plugin.settings.experimentalFileTag)
.onChange(async (value) => {
this.plugin.settings.experimentalFileTag = value;

View File

@@ -63,12 +63,30 @@ button.ToolIcon_type_button[title="Export"] {
flex-grow: 1;
}
li[data-testid] {
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
.excalidraw .context-menu-option-separator {
margin: 4px !important;
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
.excalidraw .popover {
padding: 0 !important;
border-color: transparent !important;
border: 0 !important;
box-shadow: 0 !important;
background-color: transparent !important;
}
.disable-zen-mode--visible {
color: var(--text-primary-color);
}
.disable-zen-mode {
width: 9em !important;
}

View File

@@ -16,10 +16,10 @@
"esnext",
"DOM.Iterable"
],
"jsx": "react",
"jsx": "react"
},
"include": [
"**/*.ts",
"**/*.tsx", "src/openDrawing.ts",
"**/*.tsx", "src/openDrawing.ts"
]
}

View File

@@ -1,3 +1,3 @@
{
"1.1.10": "0.11.13"
"1.2.22": "0.11.13"
}

View File

@@ -882,11 +882,6 @@
"@babel/helper-validator-identifier" "^7.14.5"
"to-fast-properties" "^2.0.0"
"@excalidraw/excalidraw@^0.8.0":
"integrity" "sha512-QSCorwl2NVZr1kv6+pSfMgxwb85v7qmjW37p0n+LMWrW+VgGWQxa3LtQqTIJ8OvdLNLNfO3kN7nHXNC4Z1h3ug=="
"resolved" "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.8.0.tgz"
"version" "0.8.0"
"@rollup/plugin-babel@^5.3.0":
"integrity" "sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw=="
"resolved" "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
@@ -1029,6 +1024,11 @@
dependencies:
"@types/estree" "*"
"@zsviczian/excalidraw@^0.9.0-onTextEditEvents-4":
"integrity" "sha512-4W/s1gbsOCpSerqgog6Nu38doy6n1h+aEy5q7DS/nodJVca5bc6wj+OcUJJeajw7NabkKofOI2RYOVw3oMgJcw=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-onTextEditEvents-4.tgz"
"version" "0.9.0-onTextEditEvents-4"
"abab@^1.0.3":
"integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
"resolved" "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz"
@@ -6897,14 +6897,13 @@
dependencies:
"isobject" "^3.0.1"
"obsidian@https://github.com/obsidianmd/obsidian-api/tarball/master":
"integrity" "sha512-vXWcdTZ6XFfbzBJUOJ8axQq58KoIxkXQln0yyloUO8DhWSe1xt0xQNZIa0RxOTfsGfK5bgKk/0nOex4pJiW8bw=="
"resolved" "https://github.com/obsidianmd/obsidian-api/tarball/master"
"version" "0.11.13"
"obsidian@^0.12.11":
"integrity" "sha512-Kv4m1n4nfd17FzpqHZfqFS2YZAyY+cxAUM7/5jqh1bmbPlmKoNd1XJZC7o9KvkXfTCxALiXfGRdrjHB+GUFAEA=="
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.12.11.tgz"
"version" "0.12.11"
dependencies:
"@types/codemirror" "0.0.108"
"moment" "2.29.1"
"yaml" "2.0.0-4"
"obuf@^1.0.0", "obuf@^1.1.1":
"integrity" "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
@@ -7106,6 +7105,11 @@
"resolved" "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz"
"version" "0.0.1"
"path-data-parser@^0.1.0", "path-data-parser@0.1.0":
"integrity" "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
"resolved" "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz"
"version" "0.1.0"
"path-dirname@^1.0.0":
"integrity" "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA="
"resolved" "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz"
@@ -7238,6 +7242,19 @@
"resolved" "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz"
"version" "7.0.0"
"points-on-curve@^0.2.0", "points-on-curve@0.2.0":
"integrity" "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
"resolved" "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz"
"version" "0.2.0"
"points-on-path@^0.2.1":
"integrity" "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="
"resolved" "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz"
"version" "0.2.1"
dependencies:
"path-data-parser" "0.1.0"
"points-on-curve" "0.2.0"
"portfinder@^1.0.9":
"integrity" "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA=="
"resolved" "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz"
@@ -7914,7 +7931,7 @@
"strip-ansi" "3.0.1"
"text-table" "0.2.0"
"react-dom@^17.0.1", "react-dom@^17.0.2":
"react-dom@^17.0.2":
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
"version" "17.0.2"
@@ -7979,7 +7996,7 @@
optionalDependencies:
"fsevents" "^1.1.3"
"react@^17.0.1", "react@^17.0.2", "react@17.0.2":
"react@^17.0.2", "react@17.0.2":
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
"version" "17.0.2"
@@ -8412,6 +8429,15 @@
optionalDependencies:
"fsevents" "~2.3.2"
"roughjs@4.4.1":
"integrity" "sha512-/RZvyVquID319VDc9HsF8wn8VPpbMBVdr4NMCi7mta9UeBBeqP6h5Hg4GZXG29DL6jwTkfMjyth/MF7Hn6Sq/w=="
"resolved" "https://registry.npmjs.org/roughjs/-/roughjs-4.4.1.tgz"
"version" "4.4.1"
dependencies:
"path-data-parser" "^0.1.0"
"points-on-curve" "^0.2.0"
"points-on-path" "^0.2.1"
"run-async@^2.2.0":
"integrity" "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="
"resolved" "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz"
@@ -9961,11 +9987,6 @@
"resolved" "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz"
"version" "2.1.2"
"yaml@2.0.0-4":
"integrity" "sha512-MoQoNhTFI400tkaeod+X0Vety1KD2L9dUa6pa1CVcyfcATjC/iDxoMLvqZ6U3D8c5KzxBrU2HnJH+PfaXOqI7w=="
"resolved" "https://registry.npmjs.org/yaml/-/yaml-2.0.0-4.tgz"
"version" "2.0.0-4"
"yargs-parser@^20.2.2":
"integrity" "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw=="
"resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz"