Compare commits

..

96 Commits

Author SHA1 Message Date
zsviczian
013279ab60 2.2.4-beta-4, markdown post processor, PDF frames, selectFrameElements 2024-08-13 22:53:03 +02:00
zsviczian
06193b6d49 2.4.0-beta-3 2024-08-11 09:22:13 +02:00
zsviczian
ac6f4af5d6 Merge pull request #1928 from mProjectsCode/patch-1
Fix `authorUrl` in manifest
2024-08-11 08:42:11 +02:00
zsviczian
bf148adc68 2.4.0-beta-2 2024-08-09 23:36:20 +02:00
Moritz Jung
0f9dafb01d Fix authorUrl in manifest 2024-08-09 22:16:07 +02:00
zsviczian
9fc0452b70 2.4.0-beta-1 2024-08-08 23:27:57 +02:00
zsviczian
83eda9b3f5 Update README.md 2024-08-04 11:01:12 +02:00
zsviczian
9bfbf47963 2.3.0 2024-08-04 10:37:06 +02:00
zsviczian
252bf411b1 2.3.0-beta-1 2024-08-03 21:26:47 +02:00
zsviczian
5622c019dd rebuild view DEVICE.isDesktop 2024-08-02 17:55:23 +02:00
zsviczian
b32fab7865 2.2.13-1 2024-08-01 22:18:16 +02:00
zsviczian
cafdad1f7a onWindowMigrated 2024-07-30 18:54:54 +02:00
zsviczian
9da40944ab 2.2.13 2024-07-29 18:13:13 +02:00
zsviczian
f678203a64 Merge pull request #1908 from dmscode/master
Update zh-cn.ts to 2.2.11
2024-07-29 17:32:14 +02:00
zsviczian
a9572e08e9 Merge branch 'master' into master 2024-07-29 17:31:58 +02:00
dmscode
ee9b042cdf Update zh-cn.ts to 2.2.11 2024-07-28 20:42:41 +08:00
zsviczian
bc138fa78a fixed storeAction, replaced "none" with "update" #1906 2024-07-28 10:06:31 +02:00
zsviczian
67dbe256f7 2.2.12 2024-07-28 06:55:27 +02:00
zsviczian
8b066d46e2 fix rename and skip inline fonts to cache key 2024-07-28 06:24:30 +02:00
zsviczian
1769a65a82 2.2.11 2024-07-27 21:25:23 +02:00
zsviczian
941eb56769 updated ExcaliAI 2024-07-27 07:09:19 +02:00
zsviczian
a317613ef4 Merge pull request #1859 from Saik0s/patch-1
Update ExcaliAI.md to fix incorrect request json for openai api
2024-07-27 07:05:58 +02:00
zsviczian
173571846f 2.2.10-2 2024-07-26 20:11:20 +02:00
zsviczian
ab1078d393 Fourth Font 2.2.10-1 2024-07-25 21:59:42 +02:00
zsviczian
681321a595 2.2.10-1 fontpicker beta 2024-07-25 21:52:47 +02:00
zsviczian
fcd50d4bc2 Merge pull request #1897 from dmscode/master
Updated zh-cn.ts to 2.2.10
2024-07-23 17:49:00 +02:00
dmscode
7e214e5aaa Updated zh-cn.ts to 2.2.10 2024-07-23 09:54:43 +08:00
zsviczian
de39053857 2.2.10 2024-07-22 22:01:36 +02:00
zsviczian
f543e3218e clippedframe, framesettings 2024-07-21 17:54:00 +02:00
zsviczian
e668aea214 Merge pull request #1890 from dmscode/master
Fixed link path
2024-07-20 08:33:09 +02:00
zsviczian
d810daa735 bump excalidraw version 2024-07-19 12:20:21 +02:00
zsviczian
b94b3118eb improved error message when getPNG fails 2024-07-19 12:19:48 +02:00
dmscode
06cbd0c92d Fixed link path 2024-07-19 12:44:50 +08:00
zsviczian
96ebbbf11d tolerate ! for embedded files 2024-07-17 21:05:01 +02:00
zsviczian
fc1467b05b Support for area, group, frame references in image embeds 2024-07-17 20:31:36 +02:00
zsviczian
53c27f2a59 2.2.9 2024-07-13 17:34:19 +02:00
zsviczian
db80f5c715 allows opacity 0% 2024-07-09 16:45:06 +02:00
zsviczian
405c98ca50 2.2.8 2024-07-05 07:00:42 +02:00
zsviczian
4892aed9e6 Merge pull request #1825 from dmscode/master
Updated and formated zh-cn.ts
2024-07-04 20:33:30 +02:00
zsviczian
1b04b94db2 Merge pull request #1843 from tobybaratta/patch-1
Documentation: Update Custom Pens documentation links
2024-07-04 20:33:01 +02:00
zsviczian
4a430f5fe7 pre-2.2.8 2024-07-04 20:15:33 +02:00
zsviczian
325bfd825f 2.2.7-5 beta 2024-07-04 09:23:49 +02:00
zsviczian
471553f913 2.2.7-4 2024-07-03 00:46:55 +02:00
zsviczian
a43d0689d8 2.2.7-3 without slob 2024-07-01 20:57:47 +02:00
zsviczian
afacb8a94b 2.2.7-2 2024-07-01 20:51:01 +02:00
zsviczian
afe5300e00 2.2.7-1 beta release after refactoring 2024-06-30 23:19:12 +02:00
zsviczian
ef85d0e323 select similar elements 2024-06-30 11:37:22 +02:00
Igor Tarasenko
d3aeedb9f6 Update ExcaliAI.md to fix incorrect request json for openai api 2024-06-28 21:03:58 +02:00
Toby Baratta
7718f69269 Fixing links to file names with spaces
commonmark spec thank you for working with spaces now
2024-06-20 19:17:00 -07:00
Toby Baratta
af50b0c5c6 Update Custom Pens documentation links
**Problem:**
- The README right now contains a broken link to a not-on-main file documenting how to add custom pens. (That document was archived -- I found the original via searching and tried to find the corresponding PRs for the details on the replacement functionality but couldn't find that PR.) 
- Pens are now handled in settings with a different YouTube video showing how to set them up (I think). 
- Adds links to the Hardware Eraser functionality & Auto Draw for Pen. 

Question:
- I think it might be useful to link to the freestyle docs here still, or have some sort of master "Alternative Pens" link that still shows how to do the JSON setup? Not sure if necessary.
2024-06-20 18:55:35 -07:00
dmscode
b576faad82 Updated zh-cn.ts to 2.2.7 2024-06-21 09:34:15 +08:00
稻米鼠
4583e603e9 Merge branch 'zsviczian:master' into master 2024-06-21 09:24:12 +08:00
zsviczian
7ee316a605 2.2.7 2024-06-19 20:56:34 +02:00
dmscode
7dca225691 Updated and formated zh-cn.ts
- Updated to 2.2.6
- Some punctuation to Chinese punctuation
- Add space between Chinese and English
- Change keyname to just first letter upper
- Remove space at line end
2024-06-13 08:23:22 +08:00
zsviczian
8a27d0240a 2.2.6 2024-06-12 22:23:32 +02:00
zsviczian
939bd6fd91 Merge pull request #1815 from dmscode/master
Update zh-cn.ts to 2.2.4
2024-06-12 22:20:20 +02:00
zsviczian
dfbd385de7 2.2.5 2024-06-09 16:34:48 +02:00
dmscode
0d791070dd Update zh-cn.ts to 2.2.4 2024-06-05 19:02:20 +08:00
zsviczian
94fbac38bf 2.2.4 2024-06-02 12:28:52 +02:00
zsviczian
73cf8e75d3 Merge pull request #1679 from evolutioned/patch-1
Update templater_mindmap.md grammar
2024-06-02 06:44:32 +02:00
zsviczian
395cbc104c Merge pull request #1805 from jmhammond/master
Remove autosaveInterval variable but preserve functionality
2024-06-02 06:43:31 +02:00
zsviczian
9a24db8379 Merge pull request #1810 from dmscode/master
Update zh-cn language to 2.2.3
2024-06-02 06:40:29 +02:00
zsviczian
934a5f4838 this to self 2024-06-02 06:39:55 +02:00
dmscode
9022500087 Update zh-cn language to 2.2.3 2024-06-01 12:03:23 +08:00
John Hammond
795807b6cf Remove autosaveInterval variable but preserve functionality 2024-05-29 17:15:14 -05:00
zsviczian
9eff79733c 2.2.3 2024-05-26 22:34:59 +02:00
zsviczian
c4e95d9207 replace commitToHistory with storeAction 2024-05-22 21:22:39 +02:00
zsviczian
32cd3a62b6 DEGUGGING in dev build with process.env.NODE_ENV, + this.lastSceneSnapshot 2024-05-22 20:08:49 +02:00
zsviczian
d05ccc0055 DEBUGGER improved 2024-05-20 19:28:33 +02:00
zsviczian
ff1d7b44b4 2.2.2 2024-05-20 12:25:53 +02:00
zsviczian
2b86ba2128 2.2.1 2024-05-20 09:37:14 +02:00
zsviczian
44a3b30e3b 2.2.0 2024-05-19 14:43:00 +02:00
zsviczian
bb9389c7dd cropping, fold everything, drag and drop fixes 2024-05-18 21:57:54 +02:00
zsviczian
ba4bfe9de7 embeddable theme 2024-05-18 14:33:25 +02:00
zsviczian
3a73b14ebb fixed select similar elements script 2024-05-18 09:35:42 +02:00
zsviczian
31f54db433 removed wrapAt, changed to refreshTextDimensions, updated onBeforeTextSubmit and Edit, replaced all != with !== plus additional type checks 2024-05-17 21:41:37 +02:00
zsviczian
26812dd297 force to open markdown command palette action, fixed decompress 2024-05-15 22:02:06 +02:00
zsviczian
ae4f4b4f08 2.1.8.1-beta-1 2024-05-14 21:07:42 +02:00
zsviczian
4ac0a4c565 2.1.8 2024-05-13 22:49:32 +02:00
zsviczian
69c9f824a0 Update README.md 2024-05-06 21:24:57 +02:00
zsviczian
6a2220c960 updated slideshow timestamp 2024-05-05 19:16:59 +02:00
zsviczian
aa501c2843 2.1.7 2024-05-05 19:13:30 +02:00
zsviczian
efce44f0a7 2.1.6.1-beta-1 2024-05-01 10:57:50 +02:00
zsviczian
491eb83d35 convert app.isMobile to DEVICE.isMobile, fix CropImage angle!==0 2024-04-30 19:31:00 +02:00
zsviczian
35cf0802d1 2.1.6 2024-04-23 22:19:49 +02:00
zsviczian
f768548f60 2.1.5 2024-04-22 20:10:21 +02:00
zsviczian
37789f9907 Update issue templates 2024-04-20 15:21:49 +02:00
zsviczian
131294464e 2.1.4 2024-04-13 09:44:15 +02:00
zsviczian
b7652a41f8 Update directory-info.json 2024-04-08 12:39:47 +02:00
zsviczian
91c5f85ec6 Update Deconstruct selected elements into new drawing.md 2024-04-08 12:38:40 +02:00
zsviczian
985983b31d updated slideshow script 2024-04-07 09:45:11 +02:00
zsviczian
9a27e38ce2 2.1.3 2024-04-06 15:52:34 +02:00
zsviczian
0017ed7c92 2.1.2 2024-04-06 13:32:56 +02:00
evolutioned
d5cf4ace21 Update templater_mindmap.md
Small grammar update
2024-04-02 06:40:55 +11:00
zsviczian
1c899746fd 2.1.1 2024-04-01 20:57:50 +02:00
zsviczian
9ea09c0fdd fixed excalidraw to PDF 2024-04-01 08:06:47 +02:00
78 changed files with 7171 additions and 3101 deletions

View File

@@ -1,12 +1,22 @@
---
name: Bug report
about: Create a report to help me improve Excalidraw
about: When something is clearly broken. Everything else is a feature request.
title: 'BUG: '
labels: ''
assignees: ''
---
Help me help you. I am a one man show doing this plugin as a part time hobby. There is no point in flooding me with issues, if there are too many, and they are poorly documented, I will just ignore them. Sorry...
Before creating a bug report, please
1. review recent release notes - maybe there is already an answer,
2. search issues (including closed ones) to see if there is anything similar.
⚠️ I will have to close all recorded bugs that do not provide this background information. Sorry, I need to control my workload/time. ⚠️
--------
**Your environment**
Please run `Command Palette/Show Debug info` in Obsidian and paste the result here.

View File

@@ -3,7 +3,7 @@
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
## Video Walkthrough
<a href="https://youtu.be/P_Q6avJGoWI" target="_blank"><img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/da34bb33-7610-45e6-b36f-cb7a02a9141b" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
@@ -63,6 +63,13 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Eraser, left-handed mode, improved filename configuration</a><br>
</details>
### Beta testing
The plugin follows a monthly release schedule. If you want to receive more frequent updates with new features (e.g. shiny new stuff available on excalidraw.com, but not yet in Obsidian) and minor bug fixes, then join the beta community.
[![Thumbnail - 20240803 Excalidraw Release Approach (Custom)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw Plugin Release Strategy (Phone)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## Features
@@ -243,11 +250,11 @@ Drag the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</
- In plugin settings, you can add a custom fourth font. For more details, see this [video](https://youtu.be/eKFmrSQhFA4)
- The plugin includes OCR support using Taskbone OCR. For more details, see this [video](https://youtu.be/7gu4ETx7zro)
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details, see this [video](https://youtu.be/vlC1-iBvIfo)
- You can define custom freedraw pens. See documentation [here].(https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md), [video](https://youtu.be/uZz5MgzWXiM)
- You can define custom pens and higlighters and pin them to the sidebar. For more details, see this [video](https://youtu.be/OjNhjaH2KjI). Using ExcalidrawAutomate, you can add support for [auto-toggling](<ea-scripts/Auto Draw for Pen.md>) pen & support for [hardware eraser buttons](<ea-scripts/Hardware Eraser Support.md>).
### Script Engine
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](ea-scripts/README.md).
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See the demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
### Other

View File

@@ -6,7 +6,7 @@ import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types";
import { ConnectionPoint, DeviceType } from "src/types/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -123,7 +123,7 @@ export declare class ExcalidrawAutomate {
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
* @returns
*/
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "LocalFont";
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "Local Font";
/**
* @param val //0:"light", 1:"dark"
* @returns
@@ -422,6 +422,7 @@ export declare class ExcalidrawAutomate {
appState?: AppState;
files?: BinaryFileData;
commitToHistory?: boolean;
storeAction?: "capture" | "none" | "update";
}, restore?: boolean): void;
/**
* connect an object to the selected element in the view

View File

@@ -1,6 +1,6 @@
# [◀ Excalidraw Automate How To](../readme.md)
## Generating a simple mindmap from a text outline
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
This is a slightly more elaborate example. This will generate a mindmap from a tabulated outline.
### Output
![Drawing 2021-05-05 20 52 34](https://user-images.githubusercontent.com/14358394/117194124-00a69d00-ade4-11eb-8b75-5e18a9cbc3cd.png)

View File

@@ -45,6 +45,16 @@ if(!settings["Templates"]) {
await ea.setScriptSettings(settings);
}
if(!settings["Default file name"]) {
settings["Default file name"] = {
value: "deconstructed",
description: "The default filename to use when deconstructing elements."
};
await ea.setScriptSettings(settings);
}
const DEFAULT_FILENAME = settings["Default file name"].value;
const templates = settings["Templates"]
.value
.split(",")
@@ -144,7 +154,7 @@ const customControls = (container) => {
const path = await utils.inputPrompt(
"Filename for new file",
"Filename",
await ea.getAttachmentFilepath("deconstructed"),
await ea.getAttachmentFilepath(DEFAULT_FILENAME),
actionButtons,
2,
false,
@@ -177,8 +187,14 @@ if(!f || !ea.isExcalidrawFile(f)) {
new Notice("Something went wrong");
return;
}
let padding = parseFloat(app.metadataCache.getCache(f.path)?.frontmatter["excalidraw-export-padding"]);
if(isNaN(padding)) {
padding = ea.plugin.settings.exportPaddingSVG;
}
ea.getElements().forEach(el=>el.isDeleted = true);
await ea.addImage(bb.topX,bb.topY,f,false, shouldAnchor);
await ea.addImage(bb.topX-padding,bb.topY-padding,f,false, shouldAnchor);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().history.clear();
if(!window.ExcalidrawDeconstructElements.openDeconstructedImage) {

View File

@@ -427,7 +427,7 @@ const run = async (text) => {
const requestObject = isImageEditRequest
? {
...imageDataURL ? {image: imageDataURL} : {},
...imageDataURL ? {image: {url: imageDataURL}} : {},
...(text && text.trim() !== "") ? {text} : {},
imageGenerationProperties: {
size: imageSize,
@@ -437,7 +437,7 @@ const run = async (text) => {
},
}
: {
...imageDataURL ? {image: imageDataURL} : {},
...imageDataURL ? {image: {url: imageDataURL}} : {},
...(text && text.trim() !== "") ? {text} : {},
systemPrompt: systemPrompt.prompt,
instruction: outputType.instruction,

View File

@@ -2,12 +2,12 @@
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png)
This script allows users to streamline their Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. Users can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. Users can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.
This script enables the selection of elements based on matching properties. Select the attributes (such as stroke color, fill style, font family, etc) that should match for selection. It's perfect for large scenes where manual selection of elements would be cumbersome. You can either run the script to select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria to.
```js */
let config = window.ExcalidrawSelectConfig;
config = config && (Date.now() - config.timestamp < 60000) ? config : null;
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
let elements = ea.getViewSelectedElements();
if(!config && (elements.length !==1)) {
@@ -27,14 +27,14 @@ const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerH
// RUN
//--------------------------
const run = () => {
selectedElements = ea.getViewElements().filter(el=>
selectedElements = elements.filter(el=>
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
@@ -56,12 +56,12 @@ const run = () => {
const showInstructions = () => {
const instructionsModal = new ea.obsidian.Modal(app);
instructionsModal.onOpen = () => {
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
instructionsModal.contentEl.createEl("p", {text: "Step 1: Choose the attributes that you want the selected elements to match."});
instructionsModal.contentEl.createEl("p", {text: "Step 2: Select an action:"});
instructionsModal.contentEl.createEl("ul", {}, el => {
el.createEl("li", {text: "Click 'RUN' to find matching elements throughout the entire scene."});
el.createEl("li", {text: "Click 'SELECT' to first choose a specific group of elements. Then run the 'Select Similar Elements' script once more on that group within 1 minute."});
el.createEl("li", {text: "Click 'SELECT' to 1) first choose a specific group of elements in the scene, then 2) run the 'Select Similar Elements' once more within 1 minute to apply the filter criteria only to that group of elements."});
});
instructionsModal.contentEl.createEl("p", {text: "Note: If you choose 'SELECT', make sure to click the 'Select Similar Elements' script again within 1 minute to apply your selection criteria to the group of elements you chose."});
};
@@ -71,14 +71,14 @@ const showInstructions = () => {
const selectAttributesToCopy = () => {
const configModal = new ea.obsidian.Modal(app);
configModal.onOpen = () => {
config = {};
config = {};
configModal.contentEl.createEl("h1", {text: "Select Similar Elements"});
new ea.obsidian.Setting(configModal.contentEl)
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
.addButton(button => button
.setButtonText("Instructions")
.onClick(showInstructions)
);
new ea.obsidian.Setting(configModal.contentEl)
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
.addButton(button => button
.setButtonText("Instructions")
.onClick(showInstructions)
);
// Add Toggles for the rest of the attributes
@@ -103,7 +103,7 @@ const selectAttributesToCopy = () => {
attributes.forEach(attr => {
const attrValue = elements[0][attr.key];
if(attrValue || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
if((typeof attrValue !== "undefined" && attrValue !== null) || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
let description = '';
switch(attr.key) {
@@ -144,8 +144,6 @@ const selectAttributesToCopy = () => {
description = `${attrValue}`;
break;
default:
console.log(attr.key);
console.log(attrValue);
description = `${attrValue.charAt(0).toUpperCase() + attrValue.slice(1)}`;
break;
}
@@ -192,7 +190,9 @@ const selectAttributesToCopy = () => {
configModal.onClose = () => {
setTimeout(()=>delete configModal);
setTimeout(()=>{
delete configModal
});
}
configModal.open();

View File

@@ -21,7 +21,7 @@ The script will convert your drawing into a slideshow presentation.
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.23")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -41,7 +41,7 @@ const TRANSITION_STEP_COUNT = 100;
const TRANSITION_DELAY = 1000; //maximum time for transition between slides in milliseconds
const FRAME_SLEEP = 1; //milliseconds
const EDIT_ZOOMOUT = 0.7; //70% of original slide zoom, set to a value between 1 and 0
const FADE_LEVEL = 0.15; //opacity of the slideshow controls after fade delay (value between 0 and 1)
const FADE_LEVEL = 0.1; //opacity of the slideshow controls after fade delay (value between 0 and 1)
//using outerHTML because the SVG object returned by Obsidin is in the main workspace window
//but excalidraw might be open in a popout window which has a different document object
const SVG_COG = ea.obsidian.getIcon("lucide-settings").outerHTML;
@@ -190,7 +190,7 @@ let preventFullscreenExit = true;
const gotoFullscreen = async () => {
if(isFullscreen) return;
preventFullscreenExit = true;
if(app.isMobile) {
if(ea.DEVICE.isMobile) {
ea.viewToggleFullScreen();
} else {
await contentEl.webkitRequestFullscreen();
@@ -206,8 +206,8 @@ const gotoFullscreen = async () => {
const exitFullscreen = async () => {
if(!isFullscreen) return;
preventFullscreenExit = true;
if(!app.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(app.isMobile) ea.viewToggleFullScreen();
if(!ea.DEVICE.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(ea.DEVICE.isMobile) ea.viewToggleFullScreen();
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MAXIMIZE;
await waitForExcalidrawResize();
resetControlPanelElPosition();
@@ -649,7 +649,7 @@ const initializeEventListners = () => {
controlPanelEl.removeEventListener('mouseenter', onMouseEnter, false);
controlPanelEl.removeEventListener('mouseleave', onMouseLeave, false);
controlPanelEl.parentElement?.removeChild(controlPanelEl);
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.removeEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.removeEventListener('fullscreenchange', fullscreenListener);
}
@@ -664,7 +664,7 @@ const initializeEventListners = () => {
return true;
};
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.addEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.addEventListener('fullscreenchange', fullscreenListener);
}
@@ -727,7 +727,7 @@ const exitPresentation = async (openForEdit = false) => {
//Resets pointer offsets. Ugly solution.
//During testing offsets were wrong after presentation, but don't know why.
//This should solve it even if they are wrong.
hostView.refresh();
hostView.refreshCanvasOffset();
excalidrawAPI.setActiveTool({type: "selection"});
})
}
@@ -738,6 +738,15 @@ const exitPresentation = async (openForEdit = false) => {
const start = async () => {
statusBarElement.style.display = "none";
ea.setViewModeEnabled(true);
const helpButton = ea.targetView.excalidrawContainer?.querySelector(".ToolIcon__icon.help-icon");
if(helpButton) {
helpButton.style.display = "none";
}
const zoomButton = ea.targetView.excalidrawContainer?.querySelector(".Stack.Stack_vertical.zoom-actions");
if(zoomButton) {
zoomButton.style.display = "none";
}
createPresentationNavigationPanel();
initializeEventListners();
if(startFullscreen) {
@@ -766,4 +775,4 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
slide: 0
};
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,7 @@ If you want to modify scripts, I recommend moving them to the `Excalidraw Automa
I would love to include your contribution in the script library. If you have a script of your own that you would like to share with the community, please open a [PR](https://github.com/zsviczian/obsidian-excalidraw-plugin/pulls) on GitHub. Be sure to include the following in your pull request
- The [script file](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts) with a self explanetory name. The name of the file will be the name of the script in the Command Palette.
- An [image](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/images) explaining the scripts purpose. Remember a picture speaks thousand words!
- An update to this file [ea-scripts/index.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index.md)
- An update to this file [ea-scripts/index-new.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index-new.md)
---
@@ -508,7 +508,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
## Set background color of unclosed line object by adding a shadow clone
```excalidraw-script-install

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.0.1-beta-2",
"version": "2.4.0-beta-4",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,12 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.1.0",
"version": "2.3.0",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",
"authorUrl": "https://www.zsolt.blog",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-excalidraw-plugin",
"version": "2.0.14",
"version": "2.2.5",
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-19",
"@zsviczian/excalidraw": "0.17.1-obsidian-39",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
@@ -32,9 +32,12 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roughjs": "^4.5.2",
"js-yaml": "^4.1.0"
"js-yaml": "^4.1.0",
"opentype.js": "^1.3.4",
"woff2sfnt-sfnt2woff": "^1.0.0"
},
"devDependencies": {
"dotenv": "^16.4.5",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
@@ -51,13 +54,14 @@
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/js-yaml": "^4.0.9",
"@types/opentype.js": "^1.3.8",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"cssnano": "^6.0.2",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"lz-string": "^1.5.0",
"obsidian": "^1.4.0",
"obsidian": "1.5.7-1",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.5.0",
@@ -67,7 +71,12 @@
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"@codemirror/commands": "^6.3.3",
"@codemirror/language": "^6.10.0",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -1,19 +1,22 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { env } from "process";
import babel from '@rollup/plugin-babel';
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import typescript2 from "rollup-plugin-typescript2";
import webWorker from "rollup-plugin-web-worker-loader";
import fs from'fs';
import fs from 'fs';
import LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
import cssnano from 'cssnano';
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production")
// Load environment variables
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);
@@ -26,14 +29,15 @@ const react_pkg = isLib ? "" : isProd
const reactdom_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
if(!isLib) {
if (!isLib) {
const excalidraw_styles = isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.production.css", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.development.css", "utf8");
const plugin_styles = fs.readFileSync("./styles.css", "utf8")
const plugin_styles = fs.readFileSync("./styles.css", "utf8");
const styles = plugin_styles + excalidraw_styles;
cssnano()
cssnano()
.process(styles) // Process the CSS
.then(result => {
fs.writeFileSync(`./${DIST_FOLDER}/styles.css`, result.css);
@@ -45,76 +49,88 @@ if(!isLib) {
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
const manifest = isLib ? {} : JSON.parse(manifestStr);
!isLib && console.log(manifest.version);
if (!isLib) console.log(manifest.version);
const packageString = isLib
? ""
const packageString = isLib
? ""
: ';' + lzstring_pkg +
'\nconst EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";\n' +
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
'const PLUGIN_VERSION="'+manifest.version+'";';
'\nlet EXCALIDRAW_PACKAGES = LZString.decompressFromBase64("' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '");\n' +
'let {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${EXCALIDRAW_PACKAGES};' +
'return {react: React, reactDOM: ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
'let PLUGIN_VERSION="' + manifest.version + '";';
const BASE_CONFIG = {
input: 'src/main.ts',
external: ['obsidian', '@zsviczian/excalidraw', 'react', 'react-dom'],
}
external: [
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
'obsidian',
'@zsviczian/excalidraw',
'react',
'react-dom'
],
};
const getRollupPlugins = (tsconfig, ...plugins) =>
[
typescript2(tsconfig),
nodeResolve({ browser: true }),
commonjs(),
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
].concat(plugins);
const getRollupPlugins = (tsconfig, ...plugins) => [
typescript2(tsconfig),
nodeResolve({ browser: true }),
commonjs(),
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
].concat(plugins);
const BUILD_CONFIG = {
...BASE_CONFIG,
output: {
dir: DIST_FOLDER,
entryFileNames: 'main.js',
//sourcemap: isProd?false:'inline',
format: 'cjs',
exports: 'default',
},
plugins: [
typescript2({
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
//inlineSources: !isProd
}),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
babel({
presets: [['@babel/preset-env', {
targets: {
esmodules: true,
ios: "15", // ios Compatibility //esmodules: true,
},
}]],
exclude: "node_modules/**"
exclude: "node_modules/**",
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
...isProd
? [
...(isProd ? [
terser({
toplevel: false,
compress: {passes: 2}
compress: { passes: 2 },
}),
//!postprocess - the version available on npmjs does not work, need this update:
// npm install brettz9/rollup-plugin-postprocess#update --save-dev
// https://github.com/developit/rollup-plugin-postprocess/issues/10
postprocess([
[/,React=require\("react"\);/, packageString],
])
]
: [
[/React=require\("react"\),state=require\("@codemirror\/state"\),view=require\("@codemirror\/view"\)/,
`state=require("@codemirror/state"),view=require("@codemirror/view")` + packageString],
]),
] : [
postprocess([
[/var React = require\('react'\);/, packageString],
])
],
]),
]),
copy({
targets: [
{ src: 'manifest.json', dest: DIST_FOLDER },
@@ -122,7 +138,7 @@ const BUILD_CONFIG = {
verbose: true, // Optional: To display copied files in the console
}),
],
}
};
const LIB_CONFIG = {
...BASE_CONFIG,
@@ -134,16 +150,16 @@ const LIB_CONFIG = {
name: "Excalidraw (Library)",
},
plugins: getRollupPlugins(
{ tsconfig: "tsconfig-lib.json"},
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
),
}
{ tsconfig: "tsconfig-lib.json" },
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
),
};
let config = [];
if(process.env.NODE_ENV === "lib") {
if (process.env.NODE_ENV === "lib") {
config.push(LIB_CONFIG);
} else {
config.push(BUILD_CONFIG);
}
export default config;
export default config;

View File

@@ -0,0 +1,49 @@
import { Extension } from "@codemirror/state";
import ExcalidrawPlugin from "src/main";
import { HideTextBetweenCommentsExtension } from "./Fadeout";
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
const editorExtensions: {[key:string]:Extension}= {
[EDITOR_FADEOUT]: HideTextBetweenCommentsExtension,
}
export class EditorHandler {
private activeEditorExtensions: Extension[] = [];
constructor(private plugin: ExcalidrawPlugin) {}
destroy(): void {
this.plugin = null;
}
setup(): void {
this.plugin.registerEditorExtension(this.activeEditorExtensions);
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
}
updateCMExtensionState(
extensionIdentifier: string,
extensionState: boolean,
) {
const extension = editorExtensions[extensionIdentifier];
if(!extension) return;
if (extensionState == true) {
this.activeEditorExtensions.push(extension);
// @ts-ignore
this.activeEditorExtensions[this.activeEditorExtensions.length - 1].exID = extensionIdentifier;
} else {
for (let i = 0; i < this.activeEditorExtensions.length; i++) {
const ext = this.activeEditorExtensions[i];
// @ts-ignore
if (ext.exID === extensionIdentifier) {
this.activeEditorExtensions.splice(i, 1);
break;
}
}
}
this.plugin.app.workspace.updateOptions();
}
update(): void {
this.plugin.app.workspace.updateOptions();
}
}

View File

@@ -0,0 +1,66 @@
import { RangeSetBuilder } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
const o30 = Decoration.line({ attributes: {class: "ex-opacity-30"} });
const o15 = Decoration.line({ attributes: {class: "ex-opacity-15"} });
const o8 = Decoration.line({ attributes: {class: "ex-opacity-8"} });
const o5 = Decoration.line({ attributes: {class: "ex-opacity-5"} });
const o0 = Decoration.line({ attributes: {class: "ex-opacity-0"} });
export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
class {
view: EditorView;
decorations: DecorationSet;
reExcalidrawData = /^%%(?:\r\n|\r|\n)# Excalidraw Data$/gm;
reTextElements = /^%%(?:\r\n|\r|\n)# Text Elements$/gm;
reDrawing = /^%%(?:\r\n|\r|\n)##? Drawing$/gm;
linecount = 0;
isExcalidraw = false;
constructor(view: EditorView) {
this.view = view;
this.isExcalidraw = view.state.doc.toString().search(/^excalidraw-plugin: /m) > 0;
if(!this.isExcalidraw) {
this.decorations = Decoration.none;
return;
}
this.decorations = this.updateDecorations(view);
}
updateDecorations (view: EditorView) {
const { state } = view;
const { doc } = state;
const text = doc.toString();
let start = text.search(this.reExcalidrawData);
if(start == -1) {
start = text.search(this.reTextElements);
}
if(start == -1) {
start = text.search(this.reDrawing);
}
if(start == -1) return Decoration.none;
const startLine = doc.lineAt(start).number;
const endLine = doc.lines;
let builder = new RangeSetBuilder<Decoration>()
for (let l = startLine; l <= endLine; l++) {
const line = doc.line(l);
const pos = l-startLine;
builder.add(line.from, line.from,
pos == 0 ? o30 : (pos == 1) ? o15 : (pos < 6) ? o8 : (pos < 12) ? o5 : o0);
}
return builder.finish()
}
update(update: ViewUpdate) {
if (this.isExcalidraw && update.docChanged) {
this.decorations = this.updateDecorations(update.view)
}
}
},
{
decorations: (x) => x.decorations,
}
);

View File

@@ -1,14 +1,9 @@
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "./constants/constFonts";
import {
DEFAULT_MD_EMBED_CSS,
fileid,
@@ -16,6 +11,7 @@ import {
nanoid,
THEME_FILTER,
FRONTMATTER_KEYS,
getFontDefinition,
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
@@ -23,7 +19,7 @@ import { ExportSettings } from "./ExcalidrawView";
import { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "./utils/FileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "./utils/FileUtils";
import {
errorlog,
getDataURL,
@@ -38,10 +34,13 @@ import {
LinkParts,
svgToBase64,
isMaskFile,
getEmbeddedFilenameParts,
} from "./utils/Utils";
import { ValueOf } from "./types";
import { ValueOf } from "./types/types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants/constants";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -142,8 +141,6 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
return svg;
}
export class EmbeddedFile {
public file: TFile = null;
public isSVGwithBitmap: boolean = false;
@@ -154,6 +151,7 @@ export class EmbeddedFile {
public mimeType: MimeType = "application/octet-stream";
public size: Size = { height: 0, width: 0 };
public linkParts: LinkParts;
public filenameparts: FILENAMEPARTS
private hostPath: string;
public attemptCounter: number = 0;
public isHyperLink: boolean = false;
@@ -164,7 +162,7 @@ export class EmbeddedFile {
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
this.plugin = plugin;
this.resetImage(hostPath, imgPath);
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
if(this.file && (this.plugin.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
try {
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
} catch (error) {
@@ -201,7 +199,7 @@ export class EmbeddedFile {
if (!this.linkParts.height) {
this.linkParts.height = this.plugin.settings.mdSVGmaxHeight;
}
this.file = app.metadataCache.getFirstLinkpathDest(
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
hostPath,
);
@@ -212,6 +210,9 @@ export class EmbeddedFile {
5000,
);
}
} else {
this.filenameparts = getEmbeddedFilenameParts(imgPath);
this.filenameparts.filepath = this.file.path;
}
}
@@ -229,7 +230,7 @@ export class EmbeddedFile {
return false;
}
}
return this.mtime != this.file.stat.mtime;
return this.mtime !== this.file.stat.mtime;
}
public setImage(
@@ -357,23 +358,68 @@ export class EmbeddedFilesLoader {
: false,
withTheme: !!forceTheme,
isMask,
skipInliningFonts: false,
};
const svg = replaceSVGColors(
await createSVG(
file?.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
const hasColorMap = Boolean(inFile instanceof EmbeddedFile ? inFile.colorMap : null);
const shouldUseCache = !hasColorMap && this.plugin.settings.allowImageCacheInScene && file && imageCache.isReady();
const hasFilenameParts = Boolean((inFile instanceof EmbeddedFile) && inFile.filenameparts);
const filenameParts = hasFilenameParts ? (inFile as EmbeddedFile).filenameparts : null;
const cacheKey:ImageKey = {
...hasFilenameParts? {
...filenameParts,
inlineFonts: !exportSettings.skipInliningFonts,
}: {
filepath: file.path,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
hasSectionref: false,
inlineFonts: !exportSettings.skipInliningFonts,
blockref: null,
sectionref: null,
linkpartReference: null,
linkpartAlias: null,
},
isDark,
previewImageType: PreviewImageType.SVG,
scale: 1,
isTransparent: !exportSettings.withBackground,
}
const maybeSVG = shouldUseCache
? await imageCache.getImageFromCache(cacheKey)
: undefined;
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: replaceSVGColors(
await createSVG(
hasFilenameParts
? (filenameParts.hasGroupref || filenameParts.hasBlockref ||
filenameParts.hasSectionref || filenameParts.hasFrameref ||
filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path)
: file?.path,
false, //false
hasFilenameParts && filenameParts.hasClippedFrameref
? {...exportSettings, frameRendering: {enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
@@ -382,7 +428,8 @@ export class EmbeddedFilesLoader {
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark) {
if (hasSVGwithBitmap && isDark && !Boolean(maybeSVG)) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
@@ -393,6 +440,20 @@ export class EmbeddedFilesLoader {
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
if(shouldUseCache && !Boolean(maybeSVG)) {
//cache SVG should have the width and height parameters and not the embedded font
//see svgWithFont below
imageCache.addImageToCache(cacheKey,"", svg);
}
if(!svg.hasAttribute("width") && svg.hasAttribute("viewBox")){
//2024.06.09
//this addresses backward compatibility issues where the cache does not have the width and height attributes
//this should be removed in the future
const vb = svg.getAttr("viewBox").split(" ");
Boolean(vb[2]) && svg.setAttribute("width", vb[2]);
Boolean(vb[3]) && svg.setAttribute("height", vb[3]);
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
@@ -511,7 +572,8 @@ export class EmbeddedFilesLoader {
return {
mimeType,
fileId: await generateIdFromFile(
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab,
inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined
),
dataURL,
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
@@ -526,7 +588,8 @@ export class EmbeddedFilesLoader {
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number
depth:number,
isThemeChange:boolean = false,
) {
if(depth > 7) {
@@ -563,7 +626,8 @@ export class EmbeddedFilesLoader {
}
//files.push(fileData);
}
} /*else if (embeddedFile.isSVGwithBitmap) {
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
//this will reload the image in light/dark mode when switching themes
const fileData = {
mimeType: embeddedFile.mimeType,
id: entry.value[0],
@@ -580,7 +644,7 @@ export class EmbeddedFilesLoader {
catch(e) {
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
}
}*/
}
}
let equation;
@@ -752,13 +816,29 @@ export class EmbeddedFilesLoader {
}
switch (fontName) {
case "Virgil":
fontDef = VIRGIL_FONT;
fontDef = await getFontDefinition(1);
break;
case "Cascadia":
fontDef = CASCADIA_FONT;
fontDef = await getFontDefinition(3);
break;
case "Assistant":
fontDef = ASSISTANT_FONT;
case "Assistant":
case "Helvetica":
fontDef = await getFontDefinition(2);
break;
case "Excalifont":
fontDef = await getFontDefinition(5);
break;
case "Nunito":
fontDef = await getFontDefinition(6);
break;
case "Lilita One":
fontDef = await getFontDefinition(7);
break;
case "Comic Shanns":
fontDef = await getFontDefinition(8);
break;
case "Liberation Sans":
fontDef = await getFontDefinition(9);
break;
case "":
fontDef = "";
@@ -785,7 +865,7 @@ export class EmbeddedFilesLoader {
? fileCache.frontmatter[FRONTMATTER_KEYS["md-css"].name] ?? ""
: "";
let frontmatterCSSisAfile = false;
if (style && style != "") {
if (style && style !== "") {
const f = plugin.app.metadataCache.getFirstLinkpathDest(style, file.path);
if (f) {
style = await plugin.app.vault.read(f);
@@ -958,7 +1038,7 @@ const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Pro
return svgToBase64(svg) as DataURL;
};
export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
let id: FileId;
try {
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
@@ -973,4 +1053,37 @@ export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> =>
id = fileid() as FileId;
}
return id;
};*/
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
let id: FileId;
try {
// Convert the file ArrayBuffer to a Uint8Array
const fileArray = new Uint8Array(file);
// If a key is provided, concatenate it to the file data
let dataToHash: Uint8Array;
if (key) {
const encoder = new TextEncoder();
const keyArray = encoder.encode(key);
dataToHash = new Uint8Array(fileArray.length + keyArray.length);
dataToHash.set(fileArray);
dataToHash.set(keyArray, fileArray.length);
} else {
dataToHash = fileArray;
}
// Hash the combined data (file and key, if provided)
const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash);
id =
// Convert buffer to byte array
Array.from(new Uint8Array(hashBuffer))
// Convert to hex string
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("") as FileId;
} catch (error) {
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
id = fileid() as FileId;
}
return id;
};

View File

@@ -11,7 +11,9 @@ import {
StrokeRoundness,
RoundnessType,
ExcalidrawFrameElement,
ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { MimeType } from "./EmbeddedFileLoader";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
@@ -26,7 +28,7 @@ import {
GITHUB_RELEASES,
determineFocusDistance,
getCommonBoundingBox,
getDefaultLineHeight,
getLineHeight,
getMaximumGroups,
intersectElementWithLine,
measureText,
@@ -35,14 +37,12 @@ import {
REG_LINKINDEX_INVALIDCHARS,
THEME_FILTER,
mermaidToExcalidraw,
MD_TEXTELEMENTS,
MD_DRAWING,
refreshTextDimensions,
getFontFamilyString,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getListOfTemplateFiles, getNewUniqueFilepath, } from "src/utils/FileUtils";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
import {
arrayToMap,
//debug,
embedFontsInSVG,
errorlog,
getEmbeddedFilenameParts,
getImageSize,
@@ -51,18 +51,18 @@ import {
getSVG,
isMaskFile,
isVersionNewerThanOther,
log,
scaleLoadedImage,
wrapTextAtCharLength,
arrayToMap,
} from "src/utils/Utils";
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/ObsidianUtils";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
import { tex2dataURL } from "src/LaTeX";
import { GenericInputPrompt, NewFileActions, Prompt } from "src/dialogs/Prompt";
import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
import { t } from "src/lang/helpers";
import { ScriptEngine } from "src/Scripts";
import { ConnectionPoint, DeviceType } from "src/types";
import { ConnectionPoint, DeviceType } from "src/types/types";
import CM, { ColorMaster, extendPlugins } from "colormaster";
import HarmonyPlugin from "colormaster/plugins/harmony";
import MixPlugin from "colormaster/plugins/mix"
@@ -81,7 +81,7 @@ import { TInput } from "colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
import { emulateKeysForLinkClick, PaneTarget } from "src/utils/ModifierkeyHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import PolyBool from "polybooljs";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
@@ -91,9 +91,9 @@ import {
extractCodeBlocks as _extractCodeBlocks,
} from "./utils/AIUtils";
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
import { CropImage } from "./utils/CropImage";
import { has } from "./svgToExcalidraw/attributes";
import { getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { log } from "./utils/DebugHelper";
import { ExcalidrawLib } from "./ExcalidrawLib";
extendPlugins([
HarmonyPlugin,
@@ -113,6 +113,7 @@ extendPlugins([
declare const PLUGIN_VERSION:string;
declare var LZString: any;
declare const excalidrawLib: typeof ExcalidrawLib;
const GAP = 4;
@@ -134,7 +135,7 @@ export class ExcalidrawAutomate {
public help(target: Function | string) {
if (!target) {
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
return;
}
@@ -153,14 +154,14 @@ export class ExcalidrawAutomate {
}
if(!funcInfo) {
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
return;
}
let isMissing = true;
if (funcInfo.code) {
isMissing = false;
console.log(`Declaration: ${funcInfo.code}`);
log(`Declaration: ${funcInfo.code}`);
}
if (funcInfo.desc) {
isMissing = false;
@@ -171,10 +172,10 @@ export class ExcalidrawAutomate {
.replace(/<a onclick='window\.open\("(.*?)"\)'>(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
console.log(`Description: ${formattedDesc}`, ...styles);
log(`Description: ${formattedDesc}`, ...styles);
}
if (isMissing) {
console.log("Description not available for this function.");
log("Description not available for this function.");
}
}
@@ -252,6 +253,22 @@ export class ExcalidrawAutomate {
return getListOfTemplateFiles(this.plugin);
}
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
if(!excalidrawFile && this.targetView && this.targetView.file) {
excalidrawFile = this.targetView.file;
}
if(!excalidrawFile) {
return [];
}
return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
}
public async getAttachmentFilepath(filename: string): Promise<string> {
if (!this.targetView || !this.targetView?.file) {
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
@@ -372,7 +389,7 @@ export class ExcalidrawAutomate {
opacity: number;
strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
roundness: null | { type: RoundnessType; value?: number };
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font
fontSize: number;
textAlign: string; //"left"|"right"|"center"
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
@@ -601,6 +618,7 @@ export class ExcalidrawAutomate {
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-export-embed-scene"?: boolean;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
@@ -705,7 +723,7 @@ export class ExcalidrawAutomate {
const generateMD = ():string => {
const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
let outString = `${MD_TEXTELEMENTS}\n`;
let outString = `# Excalidraw Data\n## Text Elements\n`;
textElements.forEach(te=> {
outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
});
@@ -716,7 +734,7 @@ export class ExcalidrawAutomate {
})
outString += Object.keys(this.imagesDict).length > 0
? "\n# Embedded files\n"
? `\n## Embedded Files\n`
: "";
Object.keys(this.imagesDict).forEach((key: FileId)=> {
@@ -1009,6 +1027,22 @@ export class ExcalidrawAutomate {
return id;
};
/**
* Add elements to frame
* @param frameId
* @param elementIDs to add
* @returns void
*/
addElementsToFrame(frameId: string, elementIDs: string[]):void {
if(!this.getElement(frameId)) return;
elementIDs.forEach(elID => {
const el = this.getElement(elID);
if(el) {
el.frameId = frameId;
}
});
}
/**
*
* @param topX
@@ -1172,14 +1206,14 @@ export class ExcalidrawAutomate {
if (element.type !== "text") {
return;
}
const { w, h, baseline } = _measureText(
const { w, h } = _measureText(
element.text,
element.fontSize,
element.fontFamily,
getDefaultLineHeight(element.fontFamily)
getLineHeight(element.fontFamily)
);
// @ts-ignore
element.width = w; element.height = h; element.baseline = baseline;
element.width = w;
element.height = h;
}
@@ -1198,7 +1232,8 @@ export class ExcalidrawAutomate {
topY: number,
text: string,
formatting?: {
wrapAt?: number;
autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well.
wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
@@ -1211,20 +1246,24 @@ export class ExcalidrawAutomate {
): string {
id = id ?? nanoid();
const originalText = text;
text = formatting?.wrapAt ? this.wrapText(text, formatting.wrapAt) : text;
const { w, h, baseline } = _measureText(
const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box)
? true
: (formatting?.autoResize ?? true)
text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text;
const { w, h } = _measureText(
text,
this.style.fontSize,
this.style.fontFamily,
getDefaultLineHeight(this.style.fontFamily)
getLineHeight(this.style.fontFamily)
);
const width = formatting?.width ? formatting.width : w;
const height = formatting?.height ? formatting.height : h;
let boxId: string = null;
const boxPadding = formatting?.boxPadding ?? 30;
const strokeColor = this.style.strokeColor;
this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor;
const boxPadding = formatting?.boxPadding ?? 30;
if (formatting?.box) {
switch (formatting.box) {
case "ellipse":
@@ -1270,12 +1309,12 @@ export class ExcalidrawAutomate {
? formatting.textAlign
: this.style.textAlign ?? "left",
verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign,
baseline,
...this.boxedElement(id, "text", topX, topY, width, height),
containerId: isContainerBound ? boxId : null,
originalText: isContainerBound ? originalText : text,
rawText: isContainerBound ? originalText : text,
lineHeight: getDefaultLineHeight(this.style.fontFamily),
lineHeight: getLineHeight(this.style.fontFamily),
autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
};
if (boxId && formatting?.box === "blob") {
this.addToGroup([id, boxId]);
@@ -1287,6 +1326,25 @@ export class ExcalidrawAutomate {
}
box.boundElements.push({ type: "text", id });
}
const textElement = this.getElement(id) as Mutable<ExcalidrawTextElement>;
const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable<ExcalidrawTextContainer>: undefined;
const dimensions = refreshTextDimensions(
textElement,
container,
arrayToMap(this.getElements()),
originalText,
);
if(dimensions) {
textElement.width = dimensions.width;
textElement.height = dimensions.height;
textElement.x = dimensions.x;
textElement.y = dimensions.y;
textElement.text = dimensions.text;
if(container) {
container.width = dimensions.width + 2 * boxPadding;
container.height = dimensions.height + 2 * boxPadding;
}
}
return boxId ?? id;
};
@@ -1530,6 +1588,26 @@ export class ExcalidrawAutomate {
return id;
};
/**
* returns the base64 dataURL of the LaTeX equation rendered as an SVG
* @param tex The LaTeX equation as string
* @param scale of the image, default value is 4
* @returns
*/
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
async tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
};
/**
*
* @param objectA
@@ -1820,12 +1898,30 @@ export class ExcalidrawAutomate {
this.targetView.updateScene({
elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)),
appState: st,
commitToHistory: true,
storeAction: "capture",
});
//this.targetView.save();
return true;
};
/**
* Adds a back of the note card to the current active view
* @param sectionTitle: string
* @param activate:boolean = true; if true, the new Embedded Element will be activated after creation
* @param sectionBody?: string;
* @param embeddableCustomData?: EmbeddableMDCustomProps; formatting of the embeddable element
* @returns embeddable element id
*/
async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise<string> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "addBackOfTheCardNoteToView()");
return null;
}
await this.targetView.forceSave(true);
return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData);
}
/**
* get the selected element in the view, if more are selected, get the first
* @returns
@@ -1837,15 +1933,16 @@ export class ExcalidrawAutomate {
/**
*
* @param includeFrameChildren
* @returns
*/
getViewSelectedElements(): any[] {
getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getViewSelectedElements()");
return [];
}
return this.targetView.getViewSelectedElements();
return this.targetView.getViewSelectedElements(includeFrameChildren);
};
/**
@@ -1938,7 +2035,7 @@ export class ExcalidrawAutomate {
appState: {
viewModeEnabled: !isFullscreen,
},
commitToHistory: false,
storeAction: "update",
});
this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
}
@@ -1957,7 +2054,7 @@ export class ExcalidrawAutomate {
return;
}
const view = this.targetView as ExcalidrawView;
view.updateScene({appState:{viewModeEnabled: enabled}});
view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"});
view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled);
}
@@ -1973,6 +2070,7 @@ export class ExcalidrawAutomate {
appState?: AppState,
files?: BinaryFileData,
commitToHistory?: boolean,
storeAction?: "capture" | "none" | "update",
},
restore: boolean = false,
):void {
@@ -1981,7 +2079,16 @@ export class ExcalidrawAutomate {
errorMessage("targetView not set", "viewToggleFullScreen()");
return;
}
this.targetView.updateScene(scene,restore);
if (!Boolean(scene.storeAction)) {
scene.storeAction = scene.commitToHistory ? "capture" : "update";
}
this.targetView.updateScene({
elements: scene.elements,
appState: scene.appState,
files: scene.files,
storeAction: scene.storeAction,
},restore);
}
/**
@@ -2327,35 +2434,61 @@ export class ExcalidrawAutomate {
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInTheSameGroupWithElement(
element: ExcalidrawElement,
elements: ExcalidrawElement[],
includeFrameElements: boolean = false,
): ExcalidrawElement[] {
if(!element || !elements) return [];
const container = (element.type === "text" && element.containerId)
? elements.filter(el=>el.id === element.containerId)
: [];
if(element.groupIds.length === 0) {
if(includeFrameElements && element.type === "frame") {
return this.getElementsInFrame(element,elements,true);
}
if(container.length === 1) return [element,container[0]];
return [element];
}
if(container.length === 1) {
return elements.filter(el=>
el.groupIds.some(id=>element.groupIds.includes(id)) ||
el === container[0]
);
const conditionFN = container.length === 1
? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
: (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
if(!includeFrameElements) {
return elements.filter(el=>conditionFN(el));
} else {
//I use the set and the filter at the end to preserve scene layer seqeuence
//adding frames could potentially mess up the sequence otherwise
const elementIDs = new Set<string>();
elements
.filter(el=>conditionFN(el))
.forEach(el=>{
if(el.type === "frame") {
this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
} else {
elementIDs.add(el.id);
}
});
return elements.filter(el=>elementIDs.has(el.id));
}
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
}
/**
* Gets all the elements from elements[] that are contained in the frame.
* @param element
* @param elements - typically all the non-deleted elements in the scene
* @param frameElement - the frame element for which to get the elements
* @param elements - typically all the non-deleted elements in the scene
* @param shouldIncludeFrame - if true, the frame element will be included in the returned array
* this is useful when generating an image in which you want the frame to be clipped
* @returns
*/
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInFrame(
frameElement: ExcalidrawElement,
elements: ExcalidrawElement[],
shouldIncludeFrame: boolean = false,
): ExcalidrawElement[] {
if(!frameElement || !elements || frameElement.type !== "frame") return [];
return elements.filter(el=>el.frameId === frameElement.id);
return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
}
/**
@@ -2421,7 +2554,7 @@ export class ExcalidrawAutomate {
text,
this.style.fontSize,
this.style.fontFamily,
getDefaultLineHeight(this.style.fontFamily),
getLineHeight(this.style.fontFamily),
);
return { width: size.w ?? 0, height: size.h ?? 0 };
};
@@ -2542,7 +2675,7 @@ export class ExcalidrawAutomate {
elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]);
this.targetView.updateScene({
elements,
commitToHistory: true,
storeAction: "capture",
});
};
@@ -2628,12 +2761,26 @@ export class ExcalidrawAutomate {
importSVG(svgString:string):boolean {
const res:ConversionResult = svgToExcalidraw(svgString);
if(res.hasErrors) {
new Notice (`There were errors while parsing the given SVG:\n${[...res.errors].map((el) => el.innerHTML)}`);
new Notice (`There were errors while parsing the given SVG:\n${res.errors}`);
return false;
}
this.copyViewElementsToEAforEditing(res.content);
return true;
}
destroy(): void {
this.targetView = null;
this.plugin = null;
this.elementsDict = {};
this.imagesDict = {};
this.mostRecentMarkdownSVG = null;
this.activeScript = null;
//@ts-ignore
this.style = {};
//@ts-ignore
this.canvas = {};
this.colorPalette = {};
}
};
export async function initExcalidrawAutomate(
@@ -2646,14 +2793,10 @@ export async function initExcalidrawAutomate(
return ea;
}
export function destroyExcalidrawAutomate() {
delete window.ExcalidrawAutomate;
}
function normalizeLinePoints(
points: [x: number, y: number][],
//box: { x: number; y: number; w: number; h: number },
) {
): number[][] {
const p = [];
const [x, y] = points[0];
for (let i = 0; i < points.length; i++) {
@@ -2662,7 +2805,9 @@ function normalizeLinePoints(
return p;
}
function getLineBox(points: [x: number, y: number][]) {
function getLineBox(
points: [x: number, y: number][]
):{x:number, y:number, w: number, h:number} {
const [x1, y1, x2, y2] = estimateLineBound(points);
return {
x: x1,
@@ -2672,23 +2817,16 @@ function getLineBox(points: [x: number, y: number][]) {
};
}
function getFontFamily(id: number) {
switch (id) {
case 1:
return "Virgil, Segoe UI Emoji";
case 2:
return "Helvetica, Segoe UI Emoji";
case 3:
return "Cascadia, Segoe UI Emoji";
case 4:
return "LocalFont";
}
function getFontFamily(id: number):string {
return getFontFamilyString({fontFamily:id})
}
async function initFonts() {
for (let i = 1; i <= 3; i++) {
await (document as any).fonts.load(`20px ${getFontFamily(i)}`);
}
export async function initFonts():Promise<void> {
await excalidrawLib.registerFontsInCSS();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
};
}
export function _measureText(
@@ -2696,21 +2834,21 @@ export function _measureText(
fontSize: number,
fontFamily: number,
lineHeight: number,
) {
): {w: number, h:number} {
//following odd error with mindmap on iPad while synchornizing with desktop.
if (!fontSize) {
fontSize = 20;
}
if (!fontFamily) {
fontFamily = 1;
lineHeight = getDefaultLineHeight(fontFamily);
lineHeight = getLineHeight(fontFamily);
}
const metrics = measureText(
newText,
`${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
lineHeight
);
return { w: metrics.width, h: metrics.height, baseline: metrics.baseline };
return { w: metrics.width, h: metrics.height };
}
async function getTemplate(
@@ -2761,9 +2899,9 @@ async function getTemplate(
textMode,
);
let trimLocation = data.search(new RegExp(`^${MD_TEXTELEMENTS}$`,"m"));
let trimLocation = data.search(/^##? Text Elements$/m);
if (trimLocation == -1) {
trimLocation = data.search(`${MD_DRAWING}\n`);
trimLocation = data.search(/##? Drawing\n/);
}
let scene = excalidrawData.scene;
@@ -2795,18 +2933,17 @@ async function getTemplate(
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
if(el.length > 0) {
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
}
}
if(filenameParts.hasFrameref) {
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
if(el) {
groupElements = plugin.ea.getElementsInFrame(el,scene.elements)
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
}
}
if(filenameParts.hasTaskbone) {
groupElements = groupElements.filter( el =>
el.type==="freedraw" ||
@@ -2815,6 +2952,7 @@ async function getTemplate(
));
}
excalidrawData.destroy();
return {
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
@@ -2854,7 +2992,7 @@ export async function createPNG(
depth: number,
padding?: number,
imagesDict?: any,
) {
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
@@ -2883,6 +3021,7 @@ export async function createPNG(
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
@@ -2937,7 +3076,7 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
})
}
function addFilterToForeignObjects(svg:SVGSVGElement) {
function addFilterToForeignObjects(svg:SVGSVGElement):void {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
@@ -2962,6 +3101,9 @@ export async function createSVG(
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
if(typeof exportSettings.skipInliningFonts === "undefined") {
exportSettings.skipInliningFonts = !embedFont;
}
const template = templatePath
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
: null;
@@ -2978,6 +3120,7 @@ export async function createSVG(
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
const filenameParts = getEmbeddedFilenameParts(templatePath);
const svg = await getSVG(
{
//createAndOpenDrawing
@@ -2989,6 +3132,7 @@ export async function createSVG(
theme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
@@ -2997,6 +3141,9 @@ export async function createSVG(
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme,
isMask: exportSettings?.isMask ?? false,
...filenameParts?.hasClippedFrameref
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
: {},
},
padding,
null,
@@ -3004,9 +3151,8 @@ export async function createSVG(
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
const filenameParts = getEmbeddedFilenameParts(templatePath);
if(
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
(filenameParts.hasBlockref || filenameParts.hasSectionref)
) {
let el = filenameParts.hasSectionref
@@ -3028,7 +3174,7 @@ export async function createSVG(
if (template?.hasSVGwithBitmap) {
svg.setAttribute("hasbitmap", "true");
}
return embedFont ? embedFontsInSVG(svg, plugin) : svg;
return svg;
}
function estimateLineBound(points: any): [number, number, number, number] {
@@ -3079,7 +3225,7 @@ export function repositionElementsToCursor(
return restore({elements}, null, null).elements;
}
function errorMessage(message: string, source: string) {
function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({

View File

@@ -11,28 +11,22 @@ import {
fileid,
DEVICE,
EMBEDDABLE_THEME_FRONTMATTER_VALUES,
getBoundTextMaxWidth,
getDefaultLineHeight,
getFontString,
wrapText,
getLineHeight,
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
FRONTMATTER_KEYS,
MD_TEXTELEMENTS,
MD_DRAWING,
MD_ELEMENTLINKS,
refreshTextDimensions,
getContainerElement,
} from "./constants/constants";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { TextMode } from "./ExcalidrawView";
import {
addAppendUpdateCustomData,
compress,
debug,
decompress,
//getBakPath,
getBinaryFileFromDataURL,
getContainerElement,
_getContainerElement,
getExportTheme,
getLinkParts,
hasExportTheme,
@@ -40,17 +34,21 @@ import {
LinkParts,
updateFrontmatterInString,
wrapTextAtCharLength,
arrayToMap,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawTextElement,
FileId,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { DEBUGGING, debug } from "./utils/DebugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -117,12 +115,12 @@ export const REGEX_LINK = {
};
//added \n at and of DRAWING_REG: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/357
const DRAWING_REG = /\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
const DRAWING_REG = /\n##? Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n##? Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
export const DRAWING_COMPRESSED_REG =
/(\n# Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
/(\n##? Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_COMPRESSED_REG_FALLBACK =
/(\n# Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
/(\n##? Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const isCompressedMD = (data: string): boolean => {
@@ -204,10 +202,10 @@ export function getMarkdownDrawingSection(
compressed: boolean,
) {
return compressed
? `# Drawing\n\x60\x60\x60compressed-json\n${compress(
? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
jsonString,
)}\n\x60\x60\x60\n%%`
: `# Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
}
/**
@@ -240,32 +238,161 @@ const estimateMaxLineLen = (text: string, originalText: string): number => {
const wrap = (text: string, lineLen: number) =>
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
const RE_TEXTELEMENTS = new RegExp(`^(%%\n)?${MD_TEXTELEMENTS}(?:\n|$)`, "m");
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_EXCALIDRAWDATA_WITHSECTION_OK = /^(#\n+)%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_WITHSECTION_NOTOK = /#\n+%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_NOSECTION_OK = /^(%%\n+)?# Excalidraw Data(?:\n|$)/m;
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = /^#\n+%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = /#\n+%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_NOSECTION_OK = /^(%%\n+)?##? Text Elements(?:\n|$)/m;
//The issue is that when editing in markdown embeds the user can delete the last enter causing two sections
//to collide. This is particularly problematic when the user is editing the lest section before # Text Elements
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)${MD_TEXTELEMENTS}(?:\n|$)`, "m");
//to collide. This is particularly problematic when the user is editing the last section before # Text Elements
const RE_EXCALIDRAWDATA_FALLBACK_1 = /(.*)%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_FALLBACK_2 = /(.*)# Excalidraw Data(?:\n|$)/m;
const RE_TEXTELEMENTS_FALLBACK_1 = /(.*)%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_FALLBACK_2 = /(.*)##? Text Elements(?:\n|$)/m;
const RE_DRAWING = new RegExp(`(%%\n)?${MD_DRAWING}\n`);
const RE_DRAWING = /^(%%\n+)?##? Drawing\n/m;
export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,string][]):string => {
let trimLocation = data.search(RE_TEXTELEMENTS);
//The base case scenario is at the top, continued with fallbacks in order of likelihood and file structure
//change history for sake of backward compatibility
/* Expected markdown structure:
bla bla bla
#
%%
# Excalidraw Data
*/
//trimming the json because in legacy excalidraw files the JSON was a single string resulting in very slow regexp parsing
const drawingTrimLocation = data.search(RE_DRAWING);
if(drawingTrimLocation>0) {
data = data.substring(0, drawingTrimLocation);
}
const m1 = data.match(RE_EXCALIDRAWDATA_WITHSECTION_OK);
let trimLocation = m1?.index ?? -1; //data.search(RE_EXCALIDRAWDATA_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += m1[1].length; //accounts for the "(#\n\s*)" which I want to leave there untouched
}
/* Expected markdown structure (this happens when the user deletes the last empty line of the last back-of-the-card note):
bla bla bla#
%%
# Excalidraw Data
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_NOTOK);
if(trimLocation > 0) {
shouldFixTrailingHashtag = true;
}
}
/* Expected markdown structure
a)
bla bla bla
%%
# Excalidraw Data
b)
bla bla bla
# Excalidraw Data
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
}
/* Expected markdown structure:
bla bla bla%%
# Excalidraw Data
*/
if(trimLocation === -1) {
const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_1);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla# Excalidraw Data
*/
if(trimLocation === -1) {
const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_2);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla
#
%%
# Text Elements
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
if(trimLocation > 0) {
trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
}
}
/* Expected markdown structure:
bla bla bla#
%%
# Text Elements
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_NOTOK);
if(trimLocation > 0) {
shouldFixTrailingHashtag = true;
}
}
/* Expected markdown structure
a)
bla bla bla
%%
# Text Elements
b)
bla bla bla
# Text Elements
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
}
/* Expected markdown structure:
bla bla bla%%
# Text Elements
*/
if(trimLocation === -1) {
const res = data.match(RE_TEXTELEMENTS_FALLBACK_1);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla# Text Elements
*/
if(trimLocation === -1) {
const res = data.match(RE_TEXTELEMENTS_FALLBACK_2);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
}
/* Expected markdown structure:
a)
bla bla bla
# Drawing
b)
bla bla bla
%%
# Drawing
*/
if (trimLocation === -1) {
trimLocation = data.search(RE_DRAWING);
if (drawingTrimLocation > 0) {
trimLocation = drawingTrimLocation;
}
}
if (trimLocation === -1) {
return data.endsWith("\n") ? data : (data + "\n");
@@ -273,21 +400,22 @@ export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,st
let header = updateFrontmatterInString(data.substring(0, trimLocation),keys);
//this should be removed at a later time. Left it here to remediate 1.4.9 mistake
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
/*const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
if (header.match(REG_IMG)) {
header = header.replace(REG_IMG, "$1");
}
}*/
//end of remove
return header.endsWith("\n") ? header : (header + "\n");
return shouldFixTrailingHashtag
? header + "\n#\n"
: header.endsWith("\n") ? header : (header + "\n");
}
export class ExcalidrawData {
public textElements: Map<
string,
{ raw: string; parsed: string; wrapAt: number | null }
{ raw: string; parsed: string}
> = null;
public elementLinks: Map<string, string> = null;
public scene: any = null;
public deletedElements: ExcalidrawElement[] = [];
public file: TFile = null;
@@ -299,6 +427,7 @@ export class ExcalidrawData {
public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit;
private textMode: TextMode = TextMode.raw;
public loaded: boolean = false;
public elementLinks: Map<string, string> = null;
public files: Map<FileId, EmbeddedFile> = null; //fileId, path
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
private mermaids: Map<FileId, { mermaid: string; isLoaded: boolean }> = null; //fileId, path
@@ -309,12 +438,34 @@ export class ExcalidrawData {
constructor(
private plugin: ExcalidrawPlugin,
) {
this.app = plugin.app;
this.app = this.plugin.app;
this.files = new Map<FileId, EmbeddedFile>();
this.equations = new Map<FileId, { latex: string; isLoaded: boolean }>();
this.mermaids = new Map<FileId, { mermaid: string; isLoaded: boolean }>();
}
public destroy() {
this.textElements = null;
this.scene = null;
this.deletedElements = [];
this.file = null;
this.app = null;
this.showLinkBrackets = null;
this.linkPrefix = null;
this.embeddableTheme = null;
this.urlPrefix = null;
this.autoexportPreference = null;
this.textMode = null;
this.loaded = false;
this.elementLinks = null;
this.files = null;
this.equations = null;
this.mermaids = null;
this.compatibilityMode = null;
this.textElementCommentedOut = null;
this.selectedElementIds = null;
}
/**
* 1.5.4: for backward compatibility following the release of container bound text elements and the depreciation boundElementIds field
*/
@@ -392,6 +543,18 @@ export class ExcalidrawData {
if (el.fontSize === null) {
el.fontSize = 20;
}
if (el.type === "text" && !el.hasOwnProperty("autoResize")) {
el.autoResize = true;
}
if (el.type === "text" && !el.hasOwnProperty("lineHeight")) {
el.lineHeight = getLineHeight(el.fontFamily);
}
if (el.type === "image" && !el.hasOwnProperty("roundness")) {
el.roundness = null;
}
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/569
@@ -486,10 +649,10 @@ export class ExcalidrawData {
this.selectedElementIds = {};
this.textElements = new Map<
string,
{ raw: string; parsed: string; wrapAt: number }
{ raw: string; parsed: string}
>();
this.elementLinks = new Map<string, string>();
if (this.file != file) {
if (this.file !== file) {
//this is a reload - files, equations and mermaids will take care of reloading when needed
this.files.clear();
this.equations.clear();
@@ -580,28 +743,72 @@ export class ExcalidrawData {
data = data.substring(0, sceneJSONandPOS.pos);
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
//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(RE_TEXTELEMENTS);
let position = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
// ....# Excalidraw Data
// ....
// --------------
// instead of
// --------------
// # back of note section
// ....
// # Excalidraw Data
position = data.search(RE_EXCALIDRAWDATA_FALLBACK_2);
}
if(position === -1) {
// # back of note section
// ....
// # Text Elements
position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
}
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
// ....# Text Elements
// ....
// --------------
// instead of
// --------------
// # back of note section
// ....
// # Text Elements
position = data.search(RE_TEXTELEMENTS_FALLBACK_2);
}
if (position === -1) {
await this.setTextMode(textMode, false);
this.loaded = true;
return true; //Text Elements header does not exist
}
const textElementsMatch = data.match(new RegExp(`^((%%\n)?${MD_TEXTELEMENTS}(?:\n|$))`, "m"))[0]
position += textElementsMatch.length;
data = data.slice(position);
const normalMatch = data.match(/^((%%\n*)?# Excalidraw Data\n## Text Elements(?:\n|$))/m)
?? data.match(/^((%%\n*)?##? Text Elements(?:\n|$))/m);
const textElementsMatch = normalMatch
? normalMatch[0]
: data.match(/(.*##? Text Elements(?:\n|$))/m)[0];
data = data.slice(textElementsMatch.length);
this.textElementCommentedOut = textElementsMatch.startsWith("%%\n");
position = 0;
let parts;
//load element links
const elementLinkMap = new Map<string,string>();
const elementLinksData = data.substring(
data.indexOf(`${MD_ELEMENTLINKS}\n`) + `${MD_ELEMENTLINKS}\n`.length,
);
const indexOfNewElementLinks = data.indexOf("## Element Links\n");
const lengthOfNewElementLinks = 17; //`## Element Links\n`.length
const indexOfOldElementLinks = data.indexOf("# Element Links\n");
const lengthOfOldElementLinks = 16; //`# Element Links\n`.length
const elementLinksData = indexOfNewElementLinks>-1
? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
: data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
//Load Embedded files
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
@@ -611,7 +818,7 @@ export class ExcalidrawData {
//iterating through all the text elements in .md
//Text elements always contain the raw value
const BLOCKREF_LEN: number = " ^12345678\n\n".length;
const BLOCKREF_LEN: number = 12; // " ^12345678\n\n".length;
const RE_TEXT_ELEMENT_LINK = /^%%\*\*\*>>>text element-link:(\[\[[^<*\]]*]])<<<\*\*\*%%/gm;
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
while (!(parts = res.next()).done) {
@@ -629,9 +836,8 @@ export class ExcalidrawData {
}
this.elementLinks.set(id, text);
} else {
const wrapAt = estimateMaxLineLen(textEl.text, textEl.originalText);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
const elementLinkRes = text.matchAll(RE_TEXT_ELEMENT_LINK);
const elementLinkRes = text.matchAll(RE_TEXT_ELEMENT_LINK);
const elementLink = elementLinkRes.next();
if(!elementLink.done) {
text = text.replace(RE_TEXT_ELEMENT_LINK,"");
@@ -646,7 +852,6 @@ export class ExcalidrawData {
this.textElements.set(id, {
raw: text,
parsed: parseRes.parsed,
wrapAt,
});
if (parseRes.link) {
textEl.link = parseRes.link;
@@ -672,13 +877,17 @@ export class ExcalidrawData {
}
}
const indexOfEmbeddedFiles = data.indexOf("# Embedded files\n");
if(indexOfEmbeddedFiles>-1) {
data = data.substring(
indexOfEmbeddedFiles + "# Embedded files\n".length,
);
const indexOfNewEmbeddedFiles = data.indexOf("## Embedded Files\n");
const embeddedFilesNewLength = 18; //"## Embedded Files\n".length
const indexOfOldEmbeddedFiles = data.indexOf("# Embedded files\n");
const embeddedFilesOldLength = 17; //"# Embedded files\n".length
if(indexOfNewEmbeddedFiles>-1 || indexOfOldEmbeddedFiles>-1) {
data = indexOfNewEmbeddedFiles>-1
? data.substring(indexOfNewEmbeddedFiles + embeddedFilesNewLength)
: data.substring(indexOfOldEmbeddedFiles + embeddedFilesOldLength);
//Load Embedded files
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\!?\[\[([^\]]*)]]\s*(\{[^}]*})?\n/gm;
res = data.matchAll(REG_FILEID_FILEPATH);
while (!(parts = res.next()).done) {
const embeddedFile = new EmbeddedFile(
@@ -741,7 +950,7 @@ export class ExcalidrawData {
this.file = file;
this.textElements = new Map<
string,
{ raw: string; parsed: string; wrapAt: number }
{ raw: string; parsed: string}
>();
this.elementLinks = new Map<string, string>();
this.setShowLinkBrackets();
@@ -772,34 +981,6 @@ export class ExcalidrawData {
await this.updateSceneTextElements(forceupdate);
}
//update a single text element in the scene if the newText is different
public updateTextElement(
sceneTextElement: any,
newText: string,
newOriginalText: string,
forceUpdate: boolean = false,
containerType?: string,
) {
if (forceUpdate || newText != sceneTextElement.text) {
const measure = _measureText(
newText,
sceneTextElement.fontSize,
sceneTextElement.fontFamily,
sceneTextElement.lineHeight??getDefaultLineHeight(sceneTextElement.fontFamily),
);
sceneTextElement.text = newText;
sceneTextElement.originalText = newOriginalText;
if (!sceneTextElement.containerId || containerType==="arrow") {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/376
//I leave the setting of text width to excalidraw, when text is in a container
//because text width is fixed to the container width
sceneTextElement.width = measure.w;
}
sceneTextElement.height = measure.h;
sceneTextElement.baseline = measure.baseline;
}
}
/**
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
@@ -810,26 +991,27 @@ export class ExcalidrawData {
private async updateSceneTextElements(forceupdate: boolean = false) {
//update text in scene based on textElements Map
//first get scene text elements
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
const elementsMap = arrayToMap(this.scene.elements);
const texts = this.scene.elements?.filter((el: any) => el.type === "text" && !el.isDeleted) as Mutable<ExcalidrawTextElement>[];
for (const te of texts) {
const container = getContainerElement(te,this.scene);
const container = getContainerElement(te, elementsMap);
const originalText =
(await this.getText(te.id)) ?? te.originalText ?? te.text;
const wrapAt = this.textElements.get(te.id)?.wrapAt;
const {text, x, y, width, height} = refreshTextDimensions(
te,
container,
elementsMap,
originalText,
)
try { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1062
this.updateTextElement(
te,
wrapAt ? wrapText(
originalText,
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
getBoundTextMaxWidth(container as any)
) : originalText,
originalText,
forceupdate,
container?.type,
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
te.originalText = originalText;
te.text = text;
te.x = x;
te.y = y;
te.width = width;
te.height = height;
} catch(e) {
debug({where: "ExcalidrawData.updateSceneTextElements", fn: this.updateSceneTextElements, textElement: te});
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateSceneTextElements, `ExcalidrawData.updateSceneTextElements, textElement:${te?.id}`, te, this.updateSceneTextElements);
}
}
}
@@ -846,7 +1028,6 @@ export class ExcalidrawData {
this.textElements.set(id, {
raw: text.raw,
parsed: (await this.parse(text.raw)).parsed,
wrapAt: text.wrapAt,
});
}
//console.log("parsed",this.textElements.get(id).parsed);
@@ -923,21 +1104,18 @@ export class ExcalidrawData {
this.textElements.set(id, {
raw: text.raw,
parsed: text.parsed,
wrapAt: text.wrapAt,
});
this.textElements.delete(te.id); //delete the old ID from the Map
}
if (!this.textElements.has(id)) {
const raw = te.rawText && te.rawText !== "" ? te.rawText : te.text; //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
const wrapAt = estimateMaxLineLen(te.text, te.originalText);
this.textElements.set(id, { raw, parsed: null, wrapAt });
this.parseasync(id, raw, wrapAt);
this.textElements.set(id, { raw, parsed: null});
this.parseasync(id, raw);
}
} else if (!this.textElements.has(te.id)) {
const raw = te.rawText && te.rawText !== "" ? te.rawText : te.text; //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
const wrapAt = estimateMaxLineLen(te.text, te.originalText);
this.textElements.set(id, { raw, parsed: null, wrapAt });
this.parseasync(id, raw, wrapAt);
this.textElements.set(id, { raw, parsed: null});
this.parseasync(id, raw);
}
}
@@ -985,22 +1163,19 @@ export class ExcalidrawData {
? el[0].rawText
: (el[0].originalText ?? el[0].text);
if (text !== (el[0].originalText ?? el[0].text)) {
const wrapAt = estimateMaxLineLen(el[0].text, el[0].originalText);
this.textElements.set(key, {
raw,
parsed: (await this.parse(raw)).parsed,
wrapAt,
});
}
}
}
}
private async parseasync(key: string, raw: string, wrapAt: number) {
private async parseasync(key: string, raw: string) {
this.textElements.set(key, {
raw,
parsed: (await this.parse(raw)).parsed,
wrapAt,
});
}
@@ -1191,7 +1366,10 @@ export class ExcalidrawData {
disableCompression: boolean = false;
generateMD(deletedElements: ExcalidrawElement[] = []): string {
let outString = this.textElementCommentedOut ? "%%\n" : "";
outString += `${MD_TEXTELEMENTS}\n`;
outString += `# Excalidraw Data\n## Text Elements\n`;
if (this.plugin.settings.addDummyTextElement) {
outString += `\n^_dummy!_\n\n`;
}
const textElementLinks = new Map<string, string>();
for (const key of this.textElements.keys()) {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
@@ -1207,7 +1385,7 @@ export class ExcalidrawData {
}
if (this.elementLinks.size > 0 || textElementLinks.size > 0) {
outString += `${MD_ELEMENTLINKS}\n`;
outString += `## Element Links\n`;
for (const key of this.elementLinks.keys()) {
outString += `${key}: ${this.elementLinks.get(key)}\n`;
}
@@ -1220,7 +1398,7 @@ export class ExcalidrawData {
// deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
outString +=
this.equations.size > 0 || this.files.size > 0
? "# Embedded files\n"
? "## Embedded Files\n"
: "";
if (this.equations.size > 0) {
for (const key of this.equations.keys()) {
@@ -1360,14 +1538,18 @@ export class ExcalidrawData {
const processedIds = new Set<string>();
fileIds.forEach((fileId,idx)=>{
if(processedIds.has(fileId)) {
const file = this.getFile(fileId);
const embeddedFile = this.getFile(fileId);
const equation = this.getEquation(fileId);
const mermaid = this.getMermaid(fileId);
//images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene
if(file && (file.isHyperLink || file.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
if (embeddedFile &&
(embeddedFile.isHyperLink || embeddedFile.isLocalLink ||
(embeddedFile.file &&
(embeddedFile.file.extension !== "md" || this.plugin.isExcalidrawFile(embeddedFile.file))
)
)
) {
return;
}
if(mermaid) {
@@ -1379,6 +1561,11 @@ export class ExcalidrawData {
return;
}
if(!embeddedFile && !equation && !mermaid) {
//processing freshly pasted images from likely anotehr instance of excalidraw (e.g. Excalidraw.com, or another Obsidian instance)
return;
}
const newId = fileid();
(scene
.elements
@@ -1387,8 +1574,8 @@ export class ExcalidrawData {
.fileId = newId;
dirty = true;
processedIds.add(newId);
if(file) {
this.setFile(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,file.linkParts.original));
if(embeddedFile) {
this.setFile(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,embeddedFile.linkParts.original));
}
if(equation) {
this.setEquation(newId as FileId, {latex:equation.latex, isLoaded:false});
@@ -1458,12 +1645,12 @@ export class ExcalidrawData {
* @param id
* @returns
*/
public getParsedText(id: string): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
public getParsedText(id: string): string {
const t = this.textElements.get(id);
if (!t) {
return [null, null, null];
return null;
}
return [wrap(t.parsed, t.wrapAt), t.parsed, null];
return t.parsed;
}
/**
@@ -1480,24 +1667,23 @@ export class ExcalidrawData {
* @param rawText
* @param rawOriginalText
* @param updateSceneCallback
* @returns [parseResultWrapped: string, parseResultOriginal: string, link: string]
* @returns [parseResultOriginal: string, link: string]
*/
public setTextElement(
elementID: string,
rawText: string,
rawOriginalText: string,
updateSceneCallback: Function,
): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
): [parseResultOriginal: string, link: string] {
//const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
const [parseResult, link] = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion
if (parseResult) {
//No transclusion
this.textElements.set(elementID, {
raw: rawOriginalText,
parsed: parseResult,
wrapAt: maxLineLen,
});
return [wrap(parseResult, maxLineLen), parseResult, link];
return [parseResult, link];
}
//transclusion needs to be resolved asynchornously
this.parse(rawOriginalText).then((parseRes) => {
@@ -1505,35 +1691,28 @@ export class ExcalidrawData {
this.textElements.set(elementID, {
raw: rawOriginalText,
parsed: parsedText,
wrapAt: maxLineLen,
});
if (parsedText) {
updateSceneCallback(wrap(parsedText, maxLineLen), parsedText);
updateSceneCallback(parsedText);
}
});
return [null, null, null];
return [null, null];
}
public async addTextElement(
elementID: string,
rawText: string,
rawOriginalText: string,
): Promise<[string, string, string]> {
let wrapAt: number = estimateMaxLineLen(rawText, rawOriginalText);
if (this.textElements.has(elementID)) {
wrapAt = this.textElements.get(elementID).wrapAt;
}
): Promise<{parseResult: string, link:string}> {
const parseResult = await this.parse(rawOriginalText);
this.textElements.set(elementID, {
raw: rawOriginalText,
parsed: parseResult.parsed,
wrapAt,
});
return [
wrap(parseResult.parsed, wrapAt),
parseResult.parsed,
parseResult.link,
];
return {
parseResult: parseResult.parsed,
link: parseResult.link,
};
}
public deleteTextElement(id: string) {
@@ -1547,7 +1726,8 @@ export class ExcalidrawData {
: this.plugin.settings.defaultMode;
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] !== "undefined")
) {
mode = fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name];
}
@@ -1567,7 +1747,8 @@ export class ExcalidrawData {
let opacity = this.plugin.settings.linkOpacity;
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] !== "undefined")
) {
opacity = fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name];
}
@@ -1578,7 +1759,8 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] !== "undefined")
) {
return fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name];
}
@@ -1590,13 +1772,13 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name] != null
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name] !== "undefined")
) {
this.linkPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name];
this.linkPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name]??"";
} else {
this.linkPrefix = this.plugin.settings.linkPrefix;
}
return linkPrefix != this.linkPrefix;
return linkPrefix !== this.linkPrefix;
}
private setUrlPrefix(): boolean {
@@ -1604,20 +1786,21 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name] != null
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name] !== "undefined")
) {
this.urlPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name];
this.urlPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name]??"";
} else {
this.urlPrefix = this.plugin.settings.urlPrefix;
}
return urlPrefix != this.urlPrefix;
return urlPrefix !== this.urlPrefix;
}
private setAutoexportPreferences() {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name] !== "undefined")
) {
switch ((fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name]).toLowerCase()) {
case "none": this.autoexportPreference = AutoexportPreference.none; break;
@@ -1636,16 +1819,28 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["embeddable-theme"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["embeddable-theme"].name] !== "undefined")
) {
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name].toLowerCase();
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEYS["embeddable-theme"].name].toLowerCase();
if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
this.embeddableTheme = "default";
}
} else {
this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
if ( //backwards compatibility
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] !== "undefined")
) {
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name].toLowerCase();
if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
this.embeddableTheme = "default";
}
} else {
this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
}
}
return embeddableTheme != this.embeddableTheme;
return embeddableTheme !== this.embeddableTheme;
}
private setShowLinkBrackets(): boolean {
@@ -1653,14 +1848,15 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== "undefined")
) {
this.showLinkBrackets =
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] != false;
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== false;
} else {
this.showLinkBrackets = this.plugin.settings.showLinkBrackets;
}
return showLinkBrackets != this.showLinkBrackets;
return showLinkBrackets !== this.showLinkBrackets;
}
/**
@@ -1893,7 +2089,7 @@ export const getTransclusion = async (
{ isCancelled: () => false },
file,
)
).blocks.filter((block: any) => block.node.type != "comment");
).blocks.filter((block: any) => block.node.type !== "comment");
if (!blocks) {
return { contents: linkParts.original.trim(), lineNum: 0 };
}

View File

@@ -1,8 +1,9 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { AppState, BinaryFiles, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
@@ -46,6 +47,7 @@ declare namespace ExcalidrawLib {
exportPadding?: number;
exportingFrame: ExcalidrawFrameElement | null | undefined;
renderEmbeddables?: boolean;
skipInliningFonts?: boolean;
}): Promise<SVGSVGElement>;
function sceneCoordsToViewportCoords(
@@ -87,6 +89,24 @@ declare namespace ExcalidrawLib {
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): BoundingBox;
function getContainerElement(
element: ExcalidrawTextElement | null,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null;
function refreshTextDimensions(
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
text: string,
): {
text: string,
x: number,
y: number,
width: number,
height: number,
};
function getMaximumGroups(
elements: ExcalidrawElement[],
elementsMap: ElementsMap,
@@ -96,10 +116,9 @@ declare namespace ExcalidrawLib {
text: string,
font: FontString,
lineHeight: number,
): { width: number; height: number; baseline: number };
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
): { width: number; height: number; };
function getLineHeight (fontFamily: FontFamilyValues):number;
function wrapText(text: string, font: FontString, maxWidth: number): string;
function getFontString({
@@ -110,6 +129,13 @@ declare namespace ExcalidrawLib {
fontFamily: FontFamilyValues;
}): FontString;
function getFontFamilyString ({
fontFamily,
}: {
fontFamily: number;
}): string;
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
function exportToBlob(
@@ -137,4 +163,18 @@ declare namespace ExcalidrawLib {
files?: any;
error?: string;
} | undefined>;
}
var getSceneVersion: any;
var Excalidraw: any;
var MainMenu: any;
var WelcomeScreen: any;
var TTDDialogTrigger: any;
var TTDDialog: any;
function destroyObsidianUtils(): void;
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
function getFontFamilies(): string[];
function registerFontsInCSS(): Promise<void>;
function getFontDefinition(fontFamily: number): Promise<string>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,21 +7,18 @@ import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getImageSize, svgToBase64 } from "./utils/Utils";
import { fileid } from "./constants/constants";
import { TFile } from "obsidian";
import { MathDocument } from "mathjax-full/js/core/MathDocument";
import { stripVTControlCharacters } from "util";
export const updateEquation = async (
equation: string,
fileId: string,
view: ExcalidrawView,
addFiles: Function,
plugin: ExcalidrawPlugin,
) => {
const data = await tex2dataURL(equation);
if (data) {
@@ -40,11 +37,15 @@ export const updateEquation = async (
};
let adaptor: LiteAdaptor;
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
let html: MathDocument<any, any, any>;
let preamble: string;
export const clearMathJaxVariables = () => {
adaptor = null;
html = null;
preamble = null;
};
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
const loadPreamble = async () => {
const file = app.vault.getAbstractFileByPath("preamble.sty");
@@ -63,6 +64,9 @@ export async function tex2dataURL(
created: number;
size: { height: number; width: number };
}> {
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
if(!adaptor) {
await loadPreamble();
adaptor = liteAdaptor();

View File

@@ -1,4 +1,5 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
@@ -12,7 +13,6 @@ import { ExportSettings } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {getIMGFilename,} from "./utils/FileUtils";
import {
embedFontsInSVG,
getEmbeddedFilenameParts,
getExportTheme,
getQuickImagePreview,
@@ -26,7 +26,7 @@ import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./util
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
@@ -39,8 +39,11 @@ interface imgElementAttributes {
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = true;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
@@ -61,8 +64,9 @@ const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
app = plugin.app;
vault = app.vault;
metadataCache = app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -75,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
@@ -86,7 +91,14 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale};
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.PNG,
scale,
isTransparent: !exportSettings.withBackground,
inlineFonts: true, //though for PNG this makes no difference, but the key requires it
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
@@ -105,11 +117,13 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
const png =
quickPNG ??
(await createPNG(
(filenameParts.hasGroupref || filenameParts.hasFrameref)
(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref)
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
filenameParts.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -132,6 +146,7 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
onCanvas: boolean,
}
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
let style = "";
if(imgAttributes.fwidth) {
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
@@ -163,7 +178,17 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVGIMG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
@@ -182,13 +207,15 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
}
}
let svg = convertSVGStringToElement((
const svg = convertSVGStringToElement((
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
true,
exportSettings,
filenameParts?.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -204,7 +231,6 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
return null;
}
svg = embedFontsInSVG(svg, plugin, false);
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
@@ -220,20 +246,31 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
let svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
false,
exportSettings,
filenameParts.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -250,11 +287,14 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
return null;
}
svg = embedFontsInSVG(svg, plugin, true);
//cache SVG should have the width and height parameters and not the embedded font
if(!Boolean(maybeSVG)) {
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
}
svg.removeAttribute("width");
svg.removeAttribute("height");
containerElement.append(svg);
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
return containerElement;
}
@@ -269,6 +309,7 @@ const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLImageElement | HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -316,22 +357,23 @@ const getIMG = async (
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
};
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
@@ -344,6 +386,7 @@ const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
@@ -358,7 +401,7 @@ const createImgElement = async (
imgOrDiv.setAttribute("draggable","false");
imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false");
let timer:NodeJS.Timeout;
let timer:number;
const clickEvent = (ev:PointerEvent) => {
if(!(ev.target instanceof Element)) {
return;
@@ -411,7 +454,7 @@ const createImgElement = async (
eventElement.addEventListener("pointermove",(ev)=>{
if(!timer) return;
if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) {
clearTimeout(timer);
window.clearTimeout(timer);
timer = null;
}
});
@@ -420,11 +463,11 @@ const createImgElement = async (
//@ts-ignore
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin;
const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile;
timer = setTimeout(()=>clickEvent(ev),timeoutValue);
timer = window.setTimeout(()=>clickEvent(ev),timeoutValue);
pointerDownEvent = ev;
});
eventElement.addEventListener("pointerup",()=>{
if(timer) clearTimeout(timer);
if(timer) window.clearTimeout(timer);
timer = null;
})
eventElement.addEventListener("dblclick",clickEvent);
@@ -471,6 +514,7 @@ const createImageDiv = async (
attr: imgElementAttributes,
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
@@ -479,6 +523,7 @@ const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
@@ -510,6 +555,7 @@ const processReadingMode = async (
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
const attr: imgElementAttributes = {
fname: "",
fheight: "",
@@ -546,6 +592,7 @@ const processAltText = (
alt:string,
attr: imgElementAttributes
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
@@ -565,10 +612,11 @@ const processAltText = (
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
(fnameParts.hasBlockref || fnameParts.hasSectionref)
}
@@ -576,6 +624,7 @@ const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
@@ -592,8 +641,18 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
return;
}
let internalEmbedDiv: HTMLElement = containerEl;
while (
!internalEmbedDiv.hasClass("print") &&
!internalEmbedDiv.hasClass("dataview") &&
!internalEmbedDiv.hasClass("cm-preview-code-block") &&
!internalEmbedDiv.hasClass("cm-embed-block") &&
@@ -604,7 +663,7 @@ const tmpObsidianWYSIWYG = async (
) {
internalEmbedDiv = internalEmbedDiv.parentElement;
}
if(
internalEmbedDiv.hasClass("dataview") ||
internalEmbedDiv.hasClass("cm-preview-code-block") ||
@@ -613,18 +672,32 @@ const tmpObsidianWYSIWYG = async (
return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
}
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
if(isHoverPopover && shouldOpenMD) {
return;
}
}
const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
fheight: getDefaultHeight(plugin),
fwidth: getDefaultWidth(plugin),
fheight: isPrinting ? "100%" : getDefaultHeight(plugin),
fwidth: isPrinting ? "100%" : getDefaultWidth(plugin),
style: ["excalidraw-svg"],
};
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
}
//We are processing the markdown preview of an actual Excalidraw file
//the excalidraw file in markdown preview mode
const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter"));
@@ -634,7 +707,7 @@ const tmpObsidianWYSIWYG = async (
} else {
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
if(warningEl) {
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=)[^ ]*)/)?.[1];
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
if(ref) {
attr.fname = file.path + ref;
areaPreview = true;
@@ -684,22 +757,22 @@ const tmpObsidianWYSIWYG = async (
internalEmbedDiv.appendChild(imgDiv);
//timer to avoid the image flickering when the user is typing
let timer: NodeJS.Timeout = null;
let timer: number = null;
const markdownObserverFn: MutationCallback = (m) => {
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
return;
}
if (timer) {
clearTimeout(timer);
window.clearTimeout(timer);
}
timer = setTimeout(async () => {
timer = window.setTimeout(async () => {
timer = null;
internalEmbedDiv.empty();
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
internalEmbedDiv.appendChild(imgDiv);
}, 500);
}
const observer = isDebugMode
const observer = DEBUGGING
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
: new MutationObserver(markdownObserverFn);
observer.observe(internalEmbedDiv, {
@@ -707,6 +780,7 @@ const tmpObsidianWYSIWYG = async (
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -716,12 +790,29 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el);
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
//if yes, then there should be no .internal-embed containers
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
const isPreview = isPrinting ||
//@ts-ignore
Boolean(ctx.containerEl && getParentOfClass(ctx.containerEl, "markdown-reading-view")) ||
//@ts-ignore
(Boolean(ctx.containerEl && getParentOfClass(ctx.containerEl, "hover-popover")) &&
Boolean(ctx?.frontmatter?.["excalidraw-open-md"])) ;
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if (!isPreview && embeddedItems.length === 0) {
if(el.hasClass("mod-frontmatter")) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId)) {
if(!el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
return;
}
}
await tmpObsidianWYSIWYG(el, ctx);
return;
}
@@ -730,11 +821,14 @@ export const markdownPostProcessor = async (
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
if (excalidrawFile) {
if (!isPreview && excalidrawFile) {
el.style.display = "none";
return;
}
if(isPrinting && el.hasClass("mod-frontmatter")) {
return;
}
await processReadingMode(embeddedItems, ctx);
};
@@ -792,10 +886,10 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
if (!plugin.hover.linkText) {
return;
}
if (m.length != 1) {
if (m.length !== 1) {
return;
}
if (m[0].addedNodes.length != 1) {
if (m[0].addedNodes.length !== 1) {
return;
}
if (
@@ -832,7 +926,7 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
node.appendChild(div);
};
export const legacyExcalidrawPopoverObserver = isDebugMode
export const legacyExcalidrawPopoverObserver = DEBUGGING
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
: new MutationObserver(legacyExcalidrawPopoverObserverFn);

View File

@@ -12,6 +12,8 @@ import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialog
import { getIMGFilename } from "./utils/FileUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { getEA } from "src";
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
import { WeakArray } from "./utils/WeakArray";
export type ScriptIconMap = {
[key: string]: { name: string; group: string; svgString: string };
@@ -22,6 +24,7 @@ export class ScriptEngine {
private scriptPath: string;
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
public scriptIconMap: ScriptIconMap;
eaInstances = new WeakArray<ExcalidrawAutomate>();
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
@@ -30,65 +33,95 @@ export class ScriptEngine {
this.registerEventHandlers();
}
registerEventHandlers() {
const handleSvgFileChange = (path: string) => {
if (!path.endsWith(".svg")) {
return;
public removeViewEAs(view: ExcalidrawView) {
const eas = new Set<ExcalidrawAutomate>();
this.eaInstances.forEach((ea) => {
if (ea.targetView === view) {
eas.add(ea);
ea.destroy();
}
const scriptFile = app.vault.getAbstractFileByPath(
getIMGFilename(path, "md"),
);
if (scriptFile && scriptFile instanceof TFile) {
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
this.loadScript(scriptFile);
}
};
});
this.eaInstances.removeObjects(eas);
}
const deleteEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.unloadScript(this.getScriptName(file), file.path);
handleSvgFileChange(file.path);
};
this.plugin.registerEvent(
app.vault.on("delete", deleteEventHandler),
public destroy() {
this.eaInstances.forEach((ea) => ea.destroy());
this.eaInstances.clear();
this.eaInstances = null;
this.scriptIconMap = null;
this.plugin = null;
this.scriptPath = null;
}
private handleSvgFileChange (path: string) {
if (!path.endsWith(".svg")) {
return;
}
const scriptFile = app.vault.getAbstractFileByPath(
getIMGFilename(path, "md"),
);
if (scriptFile && scriptFile instanceof TFile) {
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
this.loadScript(scriptFile);
}
}
const createEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
private async deleteEventHandler (file: TFile) {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.unloadScript(this.getScriptName(file), file.path);
this.handleSvgFileChange(file.path);
};
private async createEventHandler (file: TFile) {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.loadScript(file);
this.handleSvgFileChange(file.path);
};
private async renameEventHandler (file: TAbstractFile, oldPath: string) {
if (!(file instanceof TFile)) {
return;
}
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
const newFileIsScript = file.path.startsWith(this.scriptPath);
if (oldFileIsScript) {
this.unloadScript(this.getScriptName(oldPath), oldPath);
this.handleSvgFileChange(oldPath);
}
if (newFileIsScript) {
this.loadScript(file);
handleSvgFileChange(file.path);
};
this.plugin.registerEvent(
app.vault.on("create", createEventHandler),
);
this.handleSvgFileChange(file.path);
}
}
const renameEventHandler = async (file: TAbstractFile, oldPath: string) => {
if (!(file instanceof TFile)) {
return;
}
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
const newFileIsScript = file.path.startsWith(this.scriptPath);
if (oldFileIsScript) {
this.unloadScript(this.getScriptName(oldPath), oldPath);
handleSvgFileChange(oldPath);
}
if (newFileIsScript) {
this.loadScript(file);
handleSvgFileChange(file.path);
}
};
registerEventHandlers() {
this.plugin.registerEvent(
app.vault.on("rename", renameEventHandler),
this.plugin.app.vault.on(
"delete",
(file: TFile)=>this.deleteEventHandler(file)
),
);
this.plugin.registerEvent(
this.plugin.app.vault.on(
"create",
(file: TFile)=>this.createEventHandler(file)
),
);
this.plugin.registerEvent(
this.plugin.app.vault.on(
"rename",
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
),
);
}
@@ -172,6 +205,7 @@ export class ScriptEngine {
(async()=>{
const script = await app.vault.read(f);
if(script) {
//remove YAML frontmatter if present
this.executeScript(view, script, scriptName,f);
}
})()
@@ -212,7 +246,9 @@ export class ScriptEngine {
if (!view || !script || !title) {
return;
}
script = script.replace(/^---.*?---\n/gs, "");
const ea = getEA(view);
this.eaInstances.push(ea);
ea.activeScript = title;
//https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
@@ -251,7 +287,7 @@ export class ScriptEngine {
instructions?: Instruction[],
) =>
ScriptEngine.suggester(
app,
this.plugin.app,
displayItems,
items,
hint,
@@ -263,13 +299,12 @@ export class ScriptEngine {
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
errorlog({ script: this.plugin.ea.activeScript, error: e });
}*/
//ea.activeScript = null;
return result;
return result;
}
private updateToolPannels() {
const leaves =
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf: WorkspaceLeaf) => {
const excalidrawView = leaf.view as ExcalidrawView;
excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,21 @@
import { customAlphabet } from "nanoid";
import { DeviceType } from "../types";
import { DeviceType } from "../types/types";
import { ExcalidrawLib } from "../ExcalidrawLib";
import { moment } from "obsidian";
import ExcalidrawPlugin from "src/main";
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
declare const PLUGIN_VERSION:string;
export const MD_TEXTELEMENTS = "# Text Elements";
export const MD_JSON_START = "```json\n";
export const MD_JSON_END = "```";
export const MD_DRAWING = "# Drawing";
export const MD_ELEMENTLINKS = "# Element Links";
export const MD_EMBEDFILES = "# Embed files";
export const MD_EX_SECTIONS = [MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_ELEMENTLINKS = "## Element Links";
const MD_EMBEDFILES = "## Embedded Files";
const MD_DRAWING = "## Drawing";
export const MD_EX_SECTIONS = [MD_EXCALIDRAW, MD_TEXTELEMENTS, MD_ELEMENTLINKS, MD_EMBEDFILES, MD_DRAWING];
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
@@ -80,7 +85,7 @@ export const {
getCommonBoundingBox,
getMaximumGroups,
measureText,
getDefaultLineHeight,
getLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
@@ -89,8 +94,14 @@ export const {
mutateElement,
restore,
mermaidToExcalidraw,
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getFontDefinition,
} = excalidrawLib;
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
export function JSON_parse(x: string): any {
return JSON.parse(x.replaceAll("&#91;", "["));
}
@@ -159,6 +170,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
"export-svgpadding": {name: "excalidraw-export-svgpadding", type: "number", depricated: true},
"export-padding": {name: "excalidraw-export-padding", type: "number"},
"export-pngscale": {name: "excalidraw-export-pngscale", type: "number"},
"export-embed-scene": {name: "excalidraw-export-embed-scene", type: "checkbox"},
"link-prefix": {name: "excalidraw-link-prefix", type: "text"},
"url-prefix": {name: "excalidraw-url-prefix", type: "text"},
"link-brackets": {name: "excalidraw-link-brackets", type: "checkbox"},
@@ -170,7 +182,8 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
"border-color": {name: "excalidraw-border-color", type: "text"},
"md-css": {name: "excalidraw-css", type: "text"},
"autoexport": {name: "excalidraw-autoexport", type: "text"},
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text"},
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text", depricated: true},
"embeddable-theme": {name: "excalidraw-embeddable-theme", type: "text"},
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
};
@@ -191,7 +204,7 @@ export const FRONTMATTER = [
"tags: [excalidraw]",
"",
"---",
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==",
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== You can decompress Drawing data with the command palette: 'Decompress current Excalidraw file'. For more info check in plugin settings under 'Saving'",
"",
"",
].join("\n");

View File

@@ -19,20 +19,22 @@ declare module "obsidian" {
}
}
const getTheme = (view: ExcalidrawView, theme:string): string => view.excalidrawData.embeddableTheme === "dark"
function getTheme (view: ExcalidrawView, theme:string): string {
return view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
}
//--------------------------------------------------------------------------------
//Render webview for anything other than Vimeo and Youtube
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
//required to control the video
//--------------------------------------------------------------------------------
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
export function renderWebView (src: string, view: ExcalidrawView, id: string, _: UIAppState):JSX.Element {
const isDataURL = src.startsWith("data:");
if(DEVICE.isDesktop && !isDataURL) {
return (
@@ -86,36 +88,36 @@ function RenderObsidianView(
if (!file) {
return null;
}
const react = view.plugin.getPackage(view.ownerWindow).react;
const React = view.packages.react;
//@ts-ignore
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode, editNode?: Function} | null>(null);
const isEditingRef = react.useRef(false);
const isActiveRef = react.useRef(false);
const themeRef = react.useRef(theme);
const elementRef = react.useRef(element);
const leafRef = React.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode, editNode?: Function} | null>(null);
const isEditingRef = React.useRef(false);
const isActiveRef = React.useRef(false);
const themeRef = React.useRef(theme);
const elementRef = React.useRef(element);
// Update themeRef when theme changes
react.useEffect(() => {
React.useEffect(() => {
themeRef.current = theme;
}, [theme]);
// Update elementRef when element changes
react.useEffect(() => {
React.useEffect(() => {
elementRef.current = element;
}, [element]);
//--------------------------------------------------------------------------------
//block propagation of events to the parent if the iframe element is active
//--------------------------------------------------------------------------------
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
const stopPropagation = React.useCallback((event:React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation(); // Stop the event from propagating up the DOM tree
}
}, [isActiveRef.current]);
//runs once after mounting of the component and when the component is unmounted
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef?.current) {
return;
}
@@ -134,7 +136,7 @@ function RenderObsidianView(
}, []);
//blocking or not the propagation of events to the parent if the iframe is active
react.useEffect(() => {
React.useEffect(() => {
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
if(!containerRef?.current) {
return;
@@ -156,7 +158,7 @@ function RenderObsidianView(
//--------------------------------------------------------------------------------
//Mount the workspace leaf or the canvas node depending on subpath
//--------------------------------------------------------------------------------
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef?.current) {
return;
}
@@ -175,7 +177,7 @@ function RenderObsidianView(
rootSplit.containerEl.style.height = '100%';
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
leafRef.current = {
leaf: app.workspace.createLeafInParent(rootSplit, 0),
leaf: view.app.workspace.createLeafInParent(rootSplit, 0),
node: null,
editNode: null,
};
@@ -221,20 +223,29 @@ function RenderObsidianView(
} else {
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
rootSplit.containerEl.addClass("mod-visible");
containerRef.current.appendChild(rootSplit.containerEl);
setColors(containerRef.current, element, mdProps, canvasColor);
}
patchMobileView(view);
view.updateEmbeddableLeafRef(element.id, leafRef.current);
})();
}
return () => {}; //cleanup on unmount
return () => {
if(!leafRef.current) {
return;
}
view.canvasNodeFactory.removeNode(leafRef.current.node);
leafRef.current.leaf?.detach();
leafRef.current = null;
}; //cleanup on unmount
}, [linkText, subpath, containerRef]);
//--------------------------------------------------------------------------------
//Set colors of the canvas node
//--------------------------------------------------------------------------------
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
function setColors (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) {
if(!mdProps) return;
if (!leafRef.current?.hasOwnProperty("node")) return;
@@ -296,7 +307,7 @@ function RenderObsidianView(
//--------------------------------------------------------------------------------
//Set colors of the canvas node
//--------------------------------------------------------------------------------
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef.current) {
return;
}
@@ -314,7 +325,7 @@ function RenderObsidianView(
//--------------------------------------------------------------------------------
//Switch to preview mode when the iframe is not active
//--------------------------------------------------------------------------------
react.useEffect(() => {
React.useEffect(() => {
if(isEditingRef.current) {
if(leafRef.current?.node) {
containerRef.current?.addClasses(["is-editing", "is-focused"]);
@@ -324,11 +335,10 @@ function RenderObsidianView(
}
}, [isEditingRef.current, leafRef]);
//--------------------------------------------------------------------------------
//Switch to edit mode when markdown view is clicked
//--------------------------------------------------------------------------------
const handleClick = react.useCallback((event?: React.PointerEvent<HTMLElement>) => {
const handleClick = React.useCallback((event?: React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event?.stopPropagation();
}
@@ -361,7 +371,7 @@ function RenderObsidianView(
if(leafRef.current) leafRef.current.editNode = handleClick;
// Event listener for key press
react.useEffect(() => {
React.useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === "Enter") {
handleClick(event); // Call handleClick function when Enter key is pressed
@@ -378,7 +388,7 @@ function RenderObsidianView(
//--------------------------------------------------------------------------------
// Set isActiveRef and switch to preview mode when the iframe is not active
//--------------------------------------------------------------------------------
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef?.current || !leafRef?.current) {
return;
}
@@ -426,8 +436,8 @@ function RenderObsidianView(
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
const react = view.plugin.getPackage(view.ownerWindow).react;
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
const React = view.packages.react;
const containerRef: React.RefObject<HTMLDivElement> = React.useRef(null);
const theme = getTheme(view, appState.theme);
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
return (

View File

@@ -62,7 +62,6 @@ export class EmbeddableSettings extends Modal {
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
this.mdCustomData.borderOpacity = element.opacity;
}
}
onOpen(): void {
@@ -73,8 +72,16 @@ export class EmbeddableSettings extends Modal {
onClose() {
this.containerEl.removeEventListener("keydown",this.onKeyDown);
this.plugin = null;
this.view = null;
this.file = null;
this.element = null;
this.ea.destroy();
this.ea = null;
this.mdCustomData = null;
}
async createForm() {
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
@@ -140,16 +147,14 @@ export class EmbeddableSettings extends Modal {
button
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
.setTooltip("ESC")
.onClick(() => {
this.close();
})
.onClick(this.close.bind(this))
)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_OK"))
.setTooltip("CTRL/Opt+Enter")
.setCta()
.onClick(()=>this.applySettings())
.onClick(this.applySettings.bind(this))
)
@@ -163,8 +168,6 @@ export class EmbeddableSettings extends Modal {
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
}
private async applySettings() {
let dirty = false;
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
@@ -174,16 +177,24 @@ export class EmbeddableSettings extends Modal {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.folderpath,
fnparts.filename,
fnparts.folderpath,
);
await this.app.vault.rename(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
if(this.app.vault.getAbstractFileByPath(newPath)) {
new Notice("File rename failed. A file with this name already exists.\n"+newPath,10000);
} else {
try {
await this.app.fileManager.renameFile(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
} catch(e) {
new Notice("File rename failed. "+e,10000);
}
}
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
@@ -215,11 +226,12 @@ export class EmbeddableSettings extends Modal {
(async() => {
await this.ea.addElementsToView();
//@ts-ignore
this.ea.viewUpdateScene({appState: {}});
this.ea.viewUpdateScene({appState: {}, storeAction: "update"});
this.close(); //close should only run once update scene is done
})();
} else {
this.close();
}
this.close();
};
}

View File

@@ -5,7 +5,7 @@ import { DEVICE } from "src/constants/constants";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground } from "src/utils/Utils";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/Utils";
export class ExportDialog extends Modal {
private ea: ExcalidrawAutomate;
@@ -33,20 +33,33 @@ export class ExportDialog extends Modal {
private view: ExcalidrawView,
private file: TFile,
) {
super(app);
super(plugin.app);
this.ea = getEA(this.view);
this.api = this.ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
this.padding = getExportPadding(this.plugin,this.file);
this.scale = getPNGScale(this.plugin,this.file)
this.theme = getExportTheme(this.plugin, this.file, (this.api).getAppState().theme)
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
this.embedScene = false;
this.embedScene = shouldEmbedScene(this.plugin, this.file);
this.exportSelectedOnly = false;
this.saveToVault = true;
this.transparent = !getWithBackground(this.plugin, this.file);
this.saveSettings = false;
}
destroy() {
this.app = null;
this.plugin = null;
this.ea.destroy();
this.ea = null;
this.view = null;
this.file = null;
this.api = null;
this.theme = null;
this.selectedOnlySetting = null;
this.containerEl.remove();
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Export Image`);

View File

@@ -11,7 +11,6 @@ import {
SuggestModal,
Scope,
} from "obsidian";
import { t } from "../lang/helpers";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
class Suggester<T> {
@@ -132,7 +131,7 @@ class Suggester<T> {
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
items: T[] = [];
suggestions: HTMLDivElement[];
popper: PopperInstance;
popper: WeakRef<PopperInstance>;
//@ts-ignore
scope: Scope = new Scope(this.app.scope);
suggester: Suggester<FuzzyMatch<T>>;
@@ -197,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
this.app.keymap.pushScope(this.scope);
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
this.popper = createPopper(this.inputEl, this.suggestEl, {
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
placement: "bottom-start",
modifiers: [
{
@@ -213,7 +212,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
},
},
],
});
}));
}
onEscape(): void {
@@ -225,11 +224,15 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
this.app.keymap.popScope(this.scope);
this.suggester.setSuggestions([]);
if (this.popper) {
this.popper.destroy();
if (this.popper?.deref()) {
this.popper.deref().destroy();
}
this.suggestEl.detach();
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
this.inputEl.removeEventListener("blur", this.close.bind(this));
this.suggestEl.detach();
}
createPrompt(prompts: HTMLSpanElement[]) {
if (!this.promptEl) {

View File

@@ -0,0 +1,83 @@
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { t } from "src/lang/helpers";
export const showFrameSettings = (ea: ExcalidrawAutomate) => {
const {enabled, clip, name, outline} = ea.getExcalidrawAPI().getAppState().frameRendering;
// Create modal dialog
const frameSettingsModal = new ea.obsidian.Modal(app);
frameSettingsModal.onOpen = () => {
const {contentEl} = frameSettingsModal;
contentEl.createEl("h1", {text: t("FRAME_SETTINGS_TITLE")});
const settings = { enabled, clip, name, outline };
// Add toggles
const enableFramesSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTINGS_ENABLE"))
.addToggle(toggle => toggle
.setValue(settings.enabled)
.onChange(value => {
settings.enabled = value;
hideComponent(displayFrameNameSetting, !value);
hideComponent(displayFrameOutlineSetting, !value);
hideComponent(enableFrameClippingSetting, !value);
})
);
const displayFrameNameSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTIGNS_NAME"))
.addToggle(toggle => toggle
.setValue(settings.name)
.onChange(value => settings.name = value)
);
const displayFrameOutlineSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTINGS_OUTLINE"))
.addToggle(toggle => toggle
.setValue(settings.outline)
.onChange(value => settings.outline = value)
);
const enableFrameClippingSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTINGS_CLIP"))
.addToggle(toggle => toggle
.setValue(settings.clip)
.onChange(value => settings.clip = value)
);
// Hide or show components based on initial state
hideComponent(displayFrameNameSetting, !settings.enabled);
hideComponent(displayFrameOutlineSetting, !settings.enabled);
hideComponent(enableFrameClippingSetting, !settings.enabled);
// Add OK button
new ea.obsidian.Setting(contentEl)
.addButton(button => button
.setButtonText("OK")
.onClick(() => {
// Update appState with new settings
ea.viewUpdateScene({
// @ts-ignore
appState: {
frameRendering: settings
},
storeAction: "update",
});
frameSettingsModal.close();
})
);
};
frameSettingsModal.onClose = () => {
ea.destroy();
}
frameSettingsModal.open();
};
// Function to hide or show a component
function hideComponent(comp:any, value:any) {
comp.settingEl.style.display = value ? "none" : "";
}

View File

@@ -11,6 +11,12 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
destroy() {
this.app = null;
this.plugin = null;
this.view = null;
}
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
@@ -38,14 +44,23 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
return item.path;
}
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
async onChooseItem(item: TFile, _: KeyboardEvent): Promise<void> {
if(!item) return;
const ea = getEA(this.view) as ExcalidrawAutomate;
const svg = await app.vault.read(item);
const svg = await this.app.vault.read(item);
if(!svg || svg === "") return;
ea.importSVG(svg);
ea.addToGroup(ea.getElements().map(el=>el.id));
ea.addElementsToView(true, true, true,true);
await ea.addElementsToView(true, true, true,true);
ea.destroy();
}
onClose(): void {
//deley this.view destruction until onChooseItem is called
window.setTimeout(() => {
this.view = null;
});
super.onClose();
}
public start(view: ExcalidrawView) {

View File

@@ -6,6 +6,11 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
destroy() {
this.app = null;
this.addText = null;
}
constructor(app: App) {
super(app);
this.app = app;
@@ -32,10 +37,18 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
onChooseItem(item: any): void {
const cmdId = item?.id;
this.addText(`⚙️[${item.name}](cmd://${item.id})`);
this.addText = null;
}
public start(addText: Function) {
this.addText = addText;
this.open();
}
onClose(): void {
window.setTimeout(()=>{
this.addText = null;
}) //onChooseItem must run first
super.onClose();
}
}

View File

@@ -4,12 +4,28 @@ import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/co
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
destroy() {
this.app = null;
this.plugin = null;
this.view = null;
this.inputEl.onkeyup = null;
}
onClose(): void {
//deley this.view destruction until onChooseItem is called
window.setTimeout(() => {
this.view = null;
});
super.onClose();
}
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
@@ -55,13 +71,14 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
}
onChooseItem(item: TFile, event: KeyboardEvent): void {
const ea = this.plugin.ea.getAPI(this.view);
const ea = getEA(this.view);
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
const scaleToFullsize = scaleToFullsizeModifier(event);
(async () => {
//this.view.currentPosition = this.position;
await ea.addImage(0, 0, item, !scaleToFullsize);
ea.addElementsToView(true, true, true);
await ea.addElementsToView(true, true, true);
ea.destroy();
})();
}

View File

@@ -9,6 +9,12 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
private addText: Function;
private drawingPath: string;
destroy() {
this.app = null;
this.addText = null;
this.drawingPath = null;
}
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
this.app = plugin.app;
@@ -51,6 +57,13 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
}
onClose(): void {
window.setTimeout(()=>{
this.addText = null
}); //make sure this happens after onChooseItem runs
super.onClose();
}
public start(drawingPath: string, addText: Function) {
this.addText = addText;
this.drawingPath = drawingPath;

View File

@@ -2,12 +2,19 @@ import { App, FuzzySuggestModal, TFile } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
destroy() {
this.app = null;
this.plugin = null;
this.view = null;
}
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
@@ -34,12 +41,11 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
}
onChooseItem(item: TFile): void {
const ea = this.plugin.ea;
ea.reset();
ea.setView(this.view);
const ea = getEA(this.view);
(async () => {
await ea.addImage(0, 0, item);
ea.addElementsToView(true, false, true);
await ea.addElementsToView(true, false, true);
ea.destroy();
})();
}
@@ -47,4 +53,12 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
this.view = view;
this.open();
}
onClose(): void {
//deley this.view destruction until onChooseItem is called
window.setTimeout(() => {
this.view = null;
});
super.onClose();
}
}

View File

@@ -1,4 +1,4 @@
import { ButtonComponent, TFile } from "obsidian";
import { ButtonComponent, TFile, ToggleComponent } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
@@ -7,9 +7,11 @@ import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { t } from "src/lang/helpers";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;
private frame: boolean = false;
private gapSize:number = 20;
private groupPages: boolean = false;
private direction: "down" | "right" = "right";
@@ -28,7 +30,7 @@ export class InsertPDFModal extends Modal {
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
) {
super(app);
super(plugin.app);
}
open (file?: TFile) {
@@ -48,18 +50,24 @@ export class InsertPDFModal extends Modal {
if(this.dirty) {
this.plugin.settings.pdfImportScale = this.importScale;
this.plugin.settings.pdfBorderBox = this.borderBox;
this.plugin.settings.pdfFrame = this.frame;
this.plugin.settings.pdfGapSize = this.gapSize;
this.plugin.settings.pdfGroupPages = this.groupPages;
this.plugin.settings.pdfNumColumns = this.numColumns;
this.plugin.settings.pdfNumRows = this.numRows;
this.plugin.settings.pdfDirection = this.direction;
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
this.plugin.saveSettings();
await this.plugin.saveSettings();
}
if(this.pdfDoc) {
this.pdfDoc.destroy();
this.pdfDoc = null;
}
this.plugin = null;
this.view = null;
this.app = null;
this.imageSizeMessage.remove();
this.setImageSizeMessage = null;
}
private async getPageDimensions (pdfDoc: any) {
@@ -115,6 +123,7 @@ export class InsertPDFModal extends Modal {
async createForm() {
await this.plugin.loadSettings();
this.borderBox = this.plugin.settings.pdfBorderBox;
this.frame = this.plugin.settings.pdfFrame;
this.gapSize = this.plugin.settings.pdfGapSize;
this.groupPages = this.plugin.settings.pdfGroupPages;
this.numColumns = this.plugin.settings.pdfNumColumns;
@@ -133,13 +142,13 @@ export class InsertPDFModal extends Modal {
const importButtonMessages = () => {
if(!this.pdfDoc) {
importMessage.innerText = "Please select a PDF file";
importMessage.innerText = t("IPM_SELECT_PDF");
importButton.buttonEl.style.display="none";
return;
}
if(this.pagesToImport.length === 0) {
importButton.buttonEl.style.display="none";
importMessage.innerText = "Please select pages to import";
importMessage.innerText = t("IPM_SELECT_PAGES_TO_IMPORT");
return
}
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
@@ -156,7 +165,7 @@ export class InsertPDFModal extends Modal {
const numPagesMessages = () => {
if(numPages === 0) {
numPagesMessage.innerText = "Please select a PDF file";
numPagesMessage.innerText = t("IPM_SELECT_PDF");
return;
}
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
@@ -206,7 +215,7 @@ export class InsertPDFModal extends Modal {
numPagesMessage = ce.createEl("p", {text: ""});
numPagesMessages();
new Setting(ce)
.setName("Pages to import")
.setName(t("IPM_PAGES_TO_IMPORT_NAME"))
.setDesc("e.g.: 1,3-5,7,9-10")
.addText(text => {
pageRangesTextComponent = text;
@@ -217,18 +226,52 @@ export class InsertPDFModal extends Modal {
})
importPagesMessage = ce.createEl("p", {text: ""});
new Setting(ce)
.setName("Add border box")
.addToggle(toggle => toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
this.dirty = true;
}))
let bbToggle: ToggleComponent;
let fToggle: ToggleComponent;
let laiToggle: ToggleComponent;
this.frame = this.borderBox ? false : this.frame;
new Setting(ce)
.setName("Group pages")
.setDesc("This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.")
.setName(t("IPM_ADD_BORDER_BOX_NAME"))
.addToggle(toggle => {
bbToggle = toggle;
toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
if(value) {
this.frame = false;
fToggle.setValue(false);
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_ADD_FRAME_NAME"))
.setDesc(t("IPM_ADD_FRAME_DESC"))
.addToggle(toggle => {
fToggle = toggle;
toggle
.setValue(this.frame)
.onChange((value) => {
this.frame = value;
if(value) {
this.borderBox = false;
bbToggle.setValue(false);
if(!this.lockAfterImport) {
this.lockAfterImport = true;
laiToggle.setValue(true);
}
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_GROUP_PAGES_NAME"))
.setDesc(t("IPM_GROUP_PAGES_DESC"))
.addToggle(toggle => toggle
.setValue(this.groupPages)
.onChange((value) => {
@@ -239,12 +282,15 @@ export class InsertPDFModal extends Modal {
new Setting(ce)
.setName("Lock pages on canvas after import")
.addToggle(toggle => toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
}))
.addToggle(toggle => {
laiToggle = toggle;
toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
})
})
let numColumnsSetting: Setting;
let numRowsSetting: Setting;
@@ -386,6 +432,12 @@ export class InsertPDFModal extends Modal {
if(this.lockAfterImport) imgEl.locked = true;
ea.addToGroup([boxID,imageID]);
if(this.frame) {
const frameID = ea.addFrame(topX, topY,imgWidth,imgHeight,`${page}`);
ea.addElementsToFrame(frameID, [boxID,imageID]);
ea.getElement(frameID).link = this.pdfFile.path + `#page=${page}`;
}
switch(this.direction) {
case "right":
@@ -399,7 +451,9 @@ export class InsertPDFModal extends Modal {
}
}
if(this.groupPages) {
const ids = ea.getElements().map(el => el.id);
const ids = ea.getElements()
.filter(el=>!this.frame || (el.type === "frame"))
.map(el => el.id);
ea.addToGroup(ids);
}
await ea.addElementsToView(true,true,false);
@@ -408,6 +462,7 @@ export class InsertPDFModal extends Modal {
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
api.selectElements(viewElements);
api.zoomToFit(viewElements);
ea.destroy();
this.close();
})
importButton = button;

View File

@@ -6,7 +6,7 @@ If you'd like to learn more, please subscribe to my YouTube channel: [Visual PKM
Thank you & Enjoy!
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/o0exK-xFP3k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`;
@@ -17,6 +17,351 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.3.0": `
I am moving to a new release approach aiming to publish one update per month to the Obsidian script store. If you want to continue to receive more frequent updates with new features and minor bug fixes, then join the beta testing team. [#1912](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1912)
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/2poSS-Z91lY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Elbow connectors: https://x.com/excalidraw/status/1819084086222393554
## Fixed
- Convert Markdown to Excalidraw did not work correctly when there was ${String.fromCharCode(96)}---${String.fromCharCode(96)} anywhere in the file, but no frontmatter (e.g. a table)
- Fixed Obsidian move tab to new window
- Fixed duplicating bound arrows without its bound elements throwing error [#8315](https://github.com/excalidraw/excalidraw/issues/8315)
`,
"2.2.13": `
## Fixed
- Could not undo element after pasting [#1906](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1906)
- Links broke after renaming an Excalidraw file using the F2 shortcut [#1907](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1907)
- Unable to open or convert very large ${String.fromCharCode(96)}.excalidraw${String.fromCharCode(96)} file, e.g. BoaPs you can download from [here](https://ko-fi.com/zsolt/shop)
`,
"2.2.12": `
## Fixed
- Rename moved files to root folder [#1905](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1905)
- Fonts not displaying correctly in cached image previews
`,
"2.2.11": `
<img alt="badges" src="https://github.com/user-attachments/assets/7591b523-6bc6-46ff-b552-5c3492139e4c" referrerpolicy="no-referrer" style="width: 100%;">
## New
- Font picker with additional fonts (not yet fully configurable, but that will come in due time) [#8012](https://github.com/excalidraw/excalidraw/pull/8012)
- Introducing Visual Thinking Badges. The more you use Excalidraw the higher your rank will be. Levels are: Bronze, Silver, Gold and Platinum.
## Fixed
- Embedded PDF was not visible on phones [#1904](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1904)
- F2 does not rename files in Excalidraw View [#1900](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1900)
- Wireframe to Code now honors the GPT model settings in plugin settings. [#1901](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1901)
- Updated ExcaliAI to support gpt-4o for vision. [#1859](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1859) 🙏@Saik0s
- Minor fixes from excalidraw.com [#8287](https://github.com/excalidraw/excalidraw/pull/8287), [#8285](https://github.com/excalidraw/excalidraw/pull/8285)
`,
"2.2.10": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/sjZfdqpxqsg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Drastically degraded rendering performance when zoomed in and when arrows with labels are used. [#8267](https://github.com/excalidraw/excalidraw/pull/8267), [#8266](https://github.com/excalidraw/excalidraw/pull/8266), [#1893](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1893)
- Frame title font in exports.
## New
- Area, Group, Frame, and Clipped-Frame references to images now also work when pasting images to Excalidraw.
- The new reference type ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} works in the same way as ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} but will display the elements clipped by the frame. ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} will always display the image with zero padding.
- New command palette action: ${String.fromCharCode(96)}Frame Settings${String.fromCharCode(96)} gives you fine-grained control over how frames are rendered. Frame settings will also be reflected in image exports. For example, if you hide the frame name or outline, then in exports they will not be visible.
`,
"2.2.9": `
## New
- Improved the "Open the back-of-the-note of the selected Excalidraw image" action. It now works with grouped elements and keeps the popout window within the visible screen area when elements are close to the top of the canvas. Note: Due to an Obsidian bug, I do not recommend using this feature with versions 1.6.0 - 1.6.6, if you have Obsidian Sync enabled, because Obsidian may freeze when closing the popout window. It functions properly in Obsidian versions before 1.6.0 and from 1.6.7 onwards.
## Fixed
- Drag and drop from a local folder (outside Obsidian) resulted in duplicate images.
- Insert Link Action did not work [#1873](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1873)
- Insert Obsidian Command Action did not work
- Element link for text element got deleted when editing the text. [#1878](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1878)
- When back-of-the-drawing Section Headings have spaces in them, clicking the link opens the drawing side not the markdown side. [#1877](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1877)
- obsidian:// links did not work as expected. [#1872](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1872)
- copying and moving a rectangle with text, moves the text unexpectedly. The issue should now be resolved (at least much less likely to occur) [#1867](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1867)
`,
"2.2.8": `
While this release may appear modest with no new features, it represents nearly 50 hours of dedicated work. Here's what's been improved:
- **Enhanced Memory Management**: Significant improvements to optimize memory usage.
- Bug Fixes:
- Support for multi-file drag and drop from the operating system.
- Correct theming of animated GIFs as Embeddables.
- Several other minor bug fixes.
Please note that due to extensive refactoring of the codebase, there may be some unexpected issues. Thanks for your understanding and patience.
`,
"2.2.7": `
## New
- In Miscellaneous Settings: added **Load Excalidraw Properties into Obsidian Suggester**. This setting toggles the automatic loading of Excalidraw properties at startup. Enabled by default for easy use of front matter properties. Disabling it prevents auto-loading, but you'll need to manually remove unwanted properties using Obsidian properties view. A plugin restart is required after enabling auto-loading.
## Fixed
- Zotero support [1835](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1835)
- Lines binding to elements and selections [#8146](https://github.com/excalidraw/excalidraw/issues/8146), and plugin getting stuck with dragging an element [#8131](https://github.com/excalidraw/excalidraw/issues/8131)`
,
"2.2.6": `
## Fixed
- CTRL+F search for text elements in drawing, the result did not get selected. This was a regression in 2.2.5 [#1822](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1822)
## New
- Zotero compatibility support for back-of-the-side markdown notes. This needs to be enabled in plugin settings under Compatibility [#1820](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1820)
## New from Excalidraw.com
- ${String.fromCharCode(96)}Stats & Element Properties${String.fromCharCode(96)}, accessible via the context menu, is now editable, e.g. you can type in the exact position and size of objects, change font size and set element angle.
- Pasting mermaid diagrams from chatGPT will embed a diagram instead of the text
`,
"2.2.5": `
## Fixed
- Cursor visibility in dark mode [#1812](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1812)
- SVG to Excalidraw now...
- converts elements inside the ${String.fromCharCode(96)}<switch>${String.fromCharCode(96)} tag, improving compatibility with SVGs from [The Noun Project](https://thenounproject.com/)
- sets visibility for all elements, preventing invisible converted images.
- Cached images sometimes lost their font face and natural size when nested in an Excalidraw scene. This issue occurred when drawings were embedded in a markdown note (native SVG) and nested in a drawing simultaneously. Depending on the update and render sequence, these drawings sometimes appeared incorrectly in the Excalidraw scene.
`,
"2.2.4":`
<div style="text-align: center;">
<a data-tooltip-position="top" aria-label="https://youtube.com/shorts/zF1p2yfk4f4" rel="noopener" class="external-link" href="https://youtube.com/shorts/zF1p2yfk4f4" target="_blank">
<img src="https://private-user-images.githubusercontent.com/14358394/335857018-c4f5c4c7-9b8f-427f-aa6f-8c1b189610af.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTczMjQxNDksIm5iZiI6MTcxNzMyMzg0OSwicGF0aCI6Ii8xNDM1ODM5NC8zMzU4NTcwMTgtYzRmNWM0YzctOWI4Zi00MjdmLWFhNmYtOGMxYjE4OTYxMGFmLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA2MDIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNjAyVDEwMjQwOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdhZGUwNDRkZmM2NmJjNTNiYjUwNjMxMmU2MGEyZTQwZGQwNmUyZmI5ZDFhNzMwMzg2OThjMjhmZmJkNzNiZDkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.wZfkXmBRcXwz0pN6q0EEvmwtxVAB9ymPk9a9upmGXYE" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
</a>
</div>
## New from Excalidraw.com
- You can now set the text width even during creation. Simply drag with the text tool. Note, there's a minimum distance before the text enters the wrapped mode so there aren't false positives. [See example here](https://x.com/excalidraw/status/1795468201335075000)
## New
- Updated zh-cn translation. Thank you @dmscode
- New context menu and command palette action: "Move back-of-note card to File". This is only active when an eligible embeddable element is selected.
## Fixed
- Setting different autosaveIntervals on Desktop and Mobile will no longer trigger unnecessary commits to settings each time you open Excalidraw on a different device. Thanks @jmhammond for the contribution! [#1805](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1805), [#1652](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1652), [#888](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/888)
## Fixed in ExcalidrawAutomate
- ${String.fromCharCode(96)}getCM(color)${String.fromCharCode(96)} was missing from ${String.fromCharCode(96)}ea.help()${String.fromCharCode(96)}. It is now added. getCM returns a ColorMaster object. ColorMaster is a powerful library should you want to create scripts to manipulate colors. Check out my [Scripting Colors](https://youtu.be/7gJDwNgQ6NU) video should you want to learn more. [#1806](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1806)
`,
"2.2.3":`
## Fixed
- Undo history was not properly initialized [#1791](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1785)
- Excalidraw did not save edits when switching to markdown view mode with a hotkey or terminating the popout window
- SVG export did not maintain the aspect ratio of manually distorted images [#1780](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1780)
## New
- In pen mode, double tapping the screen will toggle the eraser tool when using freedraw tool, or one of the other tools in locked mode.
- New setting under "Excalidraw appearance and behavior" to disable rendering of Excalidraw drawings in hover previews, in case the file has the ${String.fromCharCode(96)}excalidraw-open-md: true${String.fromCharCode(96)} frontmatter property [#1795](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1795)
- Additional foolproofing of ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)}. The file is now more resilient to automated linting and other changes. There is also a new setting under "Compatibility Features" to add a dummy first text element to ${String.fromCharCode(96)}## Text Elements${String.fromCharCode(96)}. You can use this feature if your auto-linter adds empty lines after section headings.
- Pasting markdown code blocks will create a back-of-the-note card with the code block. CTRL+SHIFT+V will paste the text as a normal text element. When copying code from Chat GPT the markdown code fence (triple backtick) is missing. In this case, you may use the new context menu action "Paste code block" to create a back of the note card with the code block.
- Pasting long text will be wrapped in the text element.
## New in ExcalidrawAutomate
- Updated viewUpdateScene: This now implements the [new Excalidraw API](https://github.com/excalidraw/excalidraw/pull/7898)
${String.fromCharCode(96, 96, 96)}ts
viewUpdateScene (
scene: {
elements?: ExcalidrawElement[],
appState?: AppState,
files?: BinaryFileData,
commitToHistory?: boolean,
storeAction?: "capture" | "none" | "update",
},
restore: boolean = false,
):void ;
${String.fromCharCode(96, 96, 96)}
- Updated addText. The function now supports the new text-wrapping feature
${String.fromCharCode(96, 96, 96)}ts
addText(
topX: number,
topY: number,
text: string,
formatting?: {
autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the container. If set to false, you must set a width value as well.
wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
},
id?: string,
): string
${String.fromCharCode(96, 96, 96)}
`,
"2.2.2":`
## Fixed
- ExcaliBrain stopped working with 2.2.0
![I apologize](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/3b05aa28-788d-4329-9721-798ad58a6ca2)
`,
"2.2.1":`
## Fixed
- Text height becomes unreadable after 2.2.0 update [#1784](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1784)
- Images are loaded with a rounded border when loading old Excalidraw files
- Embedded Excalidraw images cache gets stuck with old version of the image
- Extremely long loading times with legacy (3+ years old) Excalidraw files
`,
"2.2.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/dV0NEOwn5NM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
⚠️⚠️⚠️ BREAKING CHANGE ⚠️⚠️⚠️
Files you save with 2.2.0 are not backward compatible with earlier plugin versions!
## New from excalidraw.com
- Wrapable text elements (without the need for transparent sticky notes!)
## New
- File format. I nested all Excalidraw markup under ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)}. Here's the new structure.
${String.fromCharCode(96,96,96)}markdown
---
excalidraw-plugin: parsed
other-frontmatter-properties: values
---
back of the note bla bla bla
# Excalidraw Data
## Text Element
## Element Links
## Embedded Files
%%
## Drawing
%%
${String.fromCharCode(96,96,96)}
- When opening Excalidraw in Markdown ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)} will be folded.
- New command palette action: ${String.fromCharCode(96)}Open the back-of-the-note of the selected Excalidraw image${String.fromCharCode(96)}. The action is only visible when selecting an embedded Excalidraw drawing in the Scene. On a desktop, the command will open the back of the selected card in a popout window, and on a mobile, in a new tab.
## Fixed
- Drag and drop from Finder/Explorer (OS external). Images will retain their filenames. PDFs will be imported to the Vault. [#1779](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1779)
`,
"2.1.8":`
## Fixed
- Fixing issues that surfaced after upgrading to Obsidian 1.6.0
- Fixed Excalidraw hover previews
- Cursor for editing links, text elements, and frame names became virtually invisible if Obsidian was in dark mode and Excalidraw in light mode and vica versa.
- Rendering Excalidraw drawings in Markdown views, right after Obsidian loaded did not work.
- I implemented more graceful fail if you submitted a malformed SVG for SVG to Excalidraw conversation. [#1768](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1768)
## New
- New setting under "Save" in plugin settings to NOT decompress JSON when switching to Markdown view mode. For details see description under "Save" settings. The benefit is smaller file size and fewer results in the Obsidian search. If you want to edit the JSON, you can always manually decompress JSON in markdown view mode with the command palette action "Excalidraw: Decompress JSON".
`,
"2.1.7:":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Updates from Excalidraw.com
- Improved undo management.
- Improved handle to scale images from the side.
- Changed arrow binding behavior.
- Many other minor fixes and improvements.
## New
- Introduced image caching for nested (embedded) Excalidraw drawings on the scene. This enhancement should lead to improved scene loading times, especially when dealing with numerous embedded Excalidraw drawings.
- Added new OCR Command Palette actions. Users can now re-run OCR and run OCR for selected elements.
## Fixed
- Fixed an issue where cropping an embeddable PDF frame in the Excalidraw Scene caused distortion based on the embeddable element's aspect ratio. ([#1756](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1756))
- Removed the listing of ${String.fromCharCode(96)}# Embedded files${String.fromCharCode(96)} section when adding a "Back of the note card". ([#1754](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1754))
- Resolved the issue where closing the on-screen keyboard with the keyboard hide button of your phone, instead of tapping somewhere else on the Excalidraw scene, did not resize the scene correctly. ([#1729](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729))
- Fixed the problem where pasting a text element as text into markdown incorrectly pasted the text to the end of the MD note, with line breaks as rendered on screen in Excalidraw. Also addressed the issue where pasting an image element as an image resulted in it being pasted to the end of the document. ([#1749](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1749))
- Corrected the color inversion of embedded images when changing the theme from light to dark, then back from dark to light, and again from light to dark on the third change.
- Addressed the problem where cropping an image while unlocking and rotating it in the cropper did not reflect the rotation. Note that rotating the image in Cropper required switching to markdown view mode, changing the "locked": true property to false, then switching back to Excalidraw mode. This issue likely impacted only a very few power users. ([#1745](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1745))
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}ts
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[];
${String.fromCharCode(96,96,96)}
`,
"2.1.6":`
## Two minor fixes
- Scaling of LaTeX formulas when the formula is changed
- If the back of the note card only contains a block embed ${String.fromCharCode(96)}![[embed]]${String.fromCharCode(96)} this got removed when saving the Excalidraw file. This issue has been present since November, 2021 (v1.4.9).
`,
"2.1.5":`
## New
- Save "Snap to objects" with the scene state. If this is the only change you make to the scene, force save it using CTRL+S (note, use CTRL on Mac as well).
- Added "Copy markdown link" to the context menu.
## Fixed
- Paste operation occasionally duplicated text elements.
- Pasting multiple instances of the same image from excalidraw.com or another instance of Obsidian, or pasting an image from anywhere and making copies with ALT/OPT + drag immediately after pasting (before autosave triggered) led to broken images when reopening the drawing.
- CTRL/CMD+Click on a Text Element with an element link did not work (previously, you had to click the top right link indicator). Now, you can click anywhere on the element.
- Hover preview for elements with a link only worked when hovering over the element link. Now, you can hover anywhere. If there are multiple elements with links, the top-level element will take precedence.
- Link navigation within drawing when the "Focus on Existing Tab" feature is enabled under "Links, transclusion and TODOs" in settings works again.
- If a link points to a back-of-the-card section or block the drawing will automatically switch to markdown view mode and navigate to the block or section.
- DynamicSytle, dark mode when canvas background is set to transparent.
- Scale to maintain the aspect ratio of a markdown notes embedded as images.
- You can now borrow interactive markdown embeds to tables, blockquotes, list elements and callouts - not just paragraphs.
- Back of the drawing cards:
- Leaving the Section Name empty when creating the first back of the card note resulted in an error.
- If you add the markdown comment (${String.fromCharCode(96)}%%${String.fromCharCode(96)}) directly before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, a trailing ${String.fromCharCode(96)}#${String.fromCharCode(96)} will be added to your document, when adding a back of the card note. This is to hide the markdown comment from the card. The trailing (empty) ${String.fromCharCode(96)}#${String.fromCharCode(96)} will not be visible in reading mode, pdf exports, and when publishing with Obsidian Publish.
Here's a sample markdown structure of your document:
${String.fromCharCode(96,96,96)}markdown
---
excalidraw-plugin: parsed
---
# Your back of the card section
bla bla bla
#
%%
# Text Elements
... the rest of the Excalidraw file
${String.fromCharCode(96,96,96)}
`,
"2.1.4":`
## Fixed
- Fixed the **aspect ratio** of an Excalidraw embedded within another Excalidraw **not updating**. [#1707](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1707)
- Some plugins automatically add document properties to all files in the Vault. Users with this configuration were **unable to run Excalidraw scripts**. Excalidraw now removes document properties from the script before execution.
- The very last markdown edit sometimes **wasn't saved when immediately switching from Markdown to Excalidraw View**. I now force a save before switching views.
- The setting to disable/enable ${String.fromCharCode(96)}CTRL/CMD + CLICK on text with [[links]] or [](links) to open them${String.fromCharCode(96)} works again. [#1704](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1704)
- **Annotation and cropping** of images in Markdown notes now also work **with Markdown links that have encoded characters** e.g.: ${String.fromCharCode(96)}![images with](markdown%20links)${String.fromCharCode(96)}.
- Solved compatibility issue of **Taskbone OCR on Android**.
## New
- New settings:
- Under "Appearance and Behavior": Option to **render Excalidraw file as an image in Markdown reading mode**. This setting is disabled by default. [#1706](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1706), [#1705](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1705)
- Under "Embedding Excalidraw ... and Exporting"/"Export Settings": Option to **render Excalidraw file as an image when exporting to PDF** in Markdown mode. This option is disabled by default. When enabled, exporting an Excalidraw drawing in markdown view mode to PDF will render the image on the page.
- **Enhanced annotation and cropping** of images in Markdown documents:
- Newly embedded **links will now follow the style of the original link**. If the original format was a ${String.fromCharCode(96)}![markdown](link)${String.fromCharCode(96)}, the annotated file will follow this format. For ${String.fromCharCode(96)}[[wiki links]]${String.fromCharCode(96)}, it will follow that style. Additionally, if an alias was specified like ${String.fromCharCode(96)}[[link|alias]]${String.fromCharCode(96)}, the annotated or cropped image will retain the alias.
- Introduced a new setting under "Saving" titled **"Preserve image size when annotating"**. This setting is disabled by default. When enabled, the embed link replacing the annotated image will maintain the size of the original image.
- Option to **automatically embed the scene in exported PNG and SVG image files**. Including the scene will allow users to open the picture on Excalidraw.com or in another Obsidian Vault as an editable Excalidraw file.New setting is under the Export category. The new frontmatter tag is: ${String.fromCharCode(96)}excalidraw-export-embed-scene: true/false${String.fromCharCode(96)}.
`,
"2.1.3":`
This is a republish of 2.1.2 with a minor change. Sorry about the frequent releases. I will hold back for a few weeks now.
`,
"2.1.2":`
## Quality of Life Improvements
- The "Insert Any File" option that disappeared from the Command Palette is now restored. [#1690](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1690)
- Improved two-finger pan speed.
- Fixed text wrapping issue that caused text to jump around when editing text in a sticky note when the Obsidian zoom level was not set to 100%.
- Mask Generation in [ExcaliAI](https://youtu.be/3G8hsV-V-gQ) Edit Image now works properly again. [#1684](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1684)
- Fixed aspect ratio change for .jpg, .png, .bmp, .webp, .SVG (non-Excalidraw) images. Previously, if the image was distorted (i.e. you held SHIFT while resizing it), it would revert to the original aspect ratio upon saving the drawing. Resetting the aspect ratio is the desired behavior for nested Excalidraw drawings since you might have changed the source image and want it to still display with the correct aspect ratio, however for other image files, the behavior is not desired. [#1698](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1698)
- The command palette action "Set selected image element size to 100% of original" now works even on freshly pasted images, not just after saving the drawing. ([#1695](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1698))
- If a text element has an element link (CTRL/CMD+K), but the link was not reflected in the Element Text, then CTRL/CMD+clicking the text element did not navigate to the link, only clicking the link indicator did. Now you can also CTRL/CMD click anywhere on the text element and it will navigate. Note, however, that links in the text element text take precedence over element links.
`,
"2.1.1":`
## Fixed
- Printing a markdown page that has an Excalidraw drawing on the back side, resulted in an empty PDF. This is now resolved.
## New
- Reduce the visual clutter by fading out the Excalidraw markup in markdown view mode. This feature needs to be enabled in plugin settings. You'll find the setting under ${String.fromCharCode(96)}Miscellaneous features${String.fromCharCode(96)}. Look for ${String.fromCharCode(96)}Fade out Excalidraw markup${String.fromCharCode(96)}. Depending on the location of the markdown comment ${String.fromCharCode(96)}%%${String.fromCharCode(96)}, if the comment starts before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} then the fading will start from ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, if the comment is before ${String.fromCharCode(96)}# Drawing${String.fromCharCode(96)} then the fading will only start with "drawing". If you delete the opening ${String.fromCharCode(96)}%%${String.fromCharCode(96)} the markup will be visible. Note, that if you place the comment before ${String.fromCharCode(96)}#Text Elements${String.fromCharCode(96)}, you will not be able to reference blocks in the ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} section, because Obsidian does not index blocks within comment blocks. Image references are not effective, they will work.
<img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/bb96cdb4-8c5f-4dc5-ad39-7fccee6d5cac" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
<img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/e627fdb7-6820-4d7d-97f9-a030016be9aa" referrerpolicy="no-referrer" style="width: 100%; margin: 0 auto;">
`,
"2.1.0":`
Bumping the version to 2.1.0 due to minor file format changes that aren't backward compatible. Essentially, 2.0.26 is already not backward compatible, but I forgot to update the version number.

View File

@@ -14,6 +14,13 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
private action: openDialogAction;
private onNewPane: boolean;
destroy() {
this.app = null;
this.plugin = null;
this.action = null;
this.inputEl.onkeyup = null;
}
constructor(app: App, plugin: ExcalidrawPlugin) {
super(app);
this.app = app;

View File

@@ -22,7 +22,7 @@ import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { REGEX_LINK } from "src/ExcalidrawData";
import { ScriptEngine } from "src/Scripts";
import { openExternalLink, openTagSearch } from "src/utils/ExcalidrawViewUtils";
import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/ExcalidrawViewUtils";
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
@@ -527,6 +527,11 @@ export class NewFileActions extends Modal {
onClose() {
super.onClose();
this.resolvePromise(this.newFile);
this.app = null;
this.plugin = null;
this.view = null;
this.parentFile = null;
this.sourceElement = null;
}
createForm(): void {
@@ -588,7 +593,8 @@ export class NewFileActions extends Modal {
ea.copyViewElementsToEAforEditing([this.sourceElement]);
ea.getElement(this.sourceElement.id).isDeleted = true;
ea.addEmbeddable(this.sourceElement.x, this.sourceElement.y,MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, undefined,f);
ea.addElementsToView();
await ea.addElementsToView();
ea.destroy();
}
this.close();
};
@@ -702,24 +708,32 @@ export class ConfirmationPrompt extends Modal {
}
}
export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawView, message: string = "Select link to open"):Promise<[file:TFile, linkText:string, subpath: string]> => {
export async function linkPrompt (
linkText:string,
app: App,
view?: ExcalidrawView,
message: string = "Select link to open",
):Promise<[file:TFile, linkText:string, subpath: string]> {
const partsArray = REGEX_LINK.getResList(linkText);
let subpath: string = null;
let file: TFile = null;
let parts = partsArray[0];
if (partsArray.length > 1) {
parts = await ScriptEngine.suggester(
app,
partsArray.filter(p=>Boolean(p.value)).map(p => {
const alias = REGEX_LINK.getAliasOrLink(p);
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
}),
partsArray.filter(p=>Boolean(p.value)),
message,
);
if(!parts) return;
}
if(!parts) return;
if (partsArray.length > 1) {
parts = await ScriptEngine.suggester(
app,
partsArray.filter(p=>Boolean(p.value)).map(p => {
const alias = REGEX_LINK.getAliasOrLink(p);
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
}),
partsArray.filter(p=>Boolean(p.value)),
message,
);
if(!parts) return;
}
if(!parts) {
return;
}
if (!parts.value) {
openTagSearch(linkText, app);
@@ -728,6 +742,9 @@ export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawVie
linkText = REGEX_LINK.getLink(parts);
if(openExternalLink(linkText, app)) return;
const maybeObsidianLink = parseObsidianLink(linkText, app, false);
if (typeof maybeObsidianLink === "boolean" && maybeObsidianLink) return;
if (typeof maybeObsidianLink === "string") linkText = maybeObsidianLink;
if (linkText.search("#") > -1) {
const linkParts = getLinkParts(linkText, view ? view.file : undefined);
@@ -745,8 +762,6 @@ export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawVie
return [file, linkText, subpath];
}
export const templatePromt = async (files: TFile[], app: App): Promise<TFile> => {
if(files.length === 1) return files[0];
return ((await linkPrompt(

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
import ExcalidrawPlugin from "../main";
import { errorlog, escapeRegExp, log } from "../utils/Utils";
import { errorlog, escapeRegExp } from "../utils/Utils";
import { log } from "src/utils/DebugHelper";
const URL =
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";

View File

@@ -3,11 +3,8 @@ import { t } from "../lang/helpers";
import ExcalidrawView from "src/ExcalidrawView";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
import { MD_EX_SECTIONS } from "src/constants/constants";
import { ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
import { addBackOfTheNoteCard } from "src/utils/ExcalidrawViewUtils";
export class SelectCard extends FuzzySuggestModal<string> {
@@ -29,46 +26,12 @@ export class SelectCard extends FuzzySuggestModal<string> {
if (e.key == "Enter") {
if (this.containerEl.innerText.includes(t("EMPTY_SECTION_MESSAGE"))) {
const item = this.inputEl.value;
if(MD_EX_SECTIONS.includes(item)) {
if(item === "" || MD_EX_SECTIONS.includes(item)) {
new Notice(t("INVALID_SECTION_NAME"));
this.close();
return;
}
(async () => {
const data = view.data;
const header = getExcalidrawMarkdownHeaderSection(data);
view.data = data.replace(header, header + `\n# ${item}\n\n`);
await view.forceSave(true);
let watchdog = 0;
await sleep(200);
let found:string;
while (watchdog++ < 10 && !(found=(await this.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },view.file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
.map((b: any) => cleanSectionHeading(b.display))
.find((b: any) => b === item))) {
await sleep(200);
}
const ea = getEA(this.view) as ExcalidrawAutomate;
const id = ea.addEmbeddable(
0,0,400,500,
`[[${this.view.file.path}#${item}]]`
);
await ea.addElementsToView(true, false, true);
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
const el = ea.getViewElements().find(el=>el.id === id);
api.selectElements([el]);
setTimeout(()=>{
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}});
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
});
})();
//create new section
//`# ${this.inputEl.value}\n\n`;
//Do not allow MD_EX_SECTIONS
}
addBackOfTheNoteCard(this.view, item);
this.close();
}
}
@@ -92,6 +55,7 @@ export class SelectCard extends FuzzySuggestModal<string> {
(async () => {
await ea.addElementsToView(true, false, true);
ea.selectElementsInView([id]);
ea.destroy();
})();
}

View File

@@ -9,6 +9,9 @@ const hyperlink = (url: string, text: string) => {
return `<a onclick='window.open("${url}")'>${text}</a>`;
}
const EMBEDDABLE_MDCUSTOMPROPS = `type EmbeddableMDCustomProps = {<br>useObsidianDefaults: boolean;<br>backgroundMatchCanvas: boolean;<br>backgroundMatchElement: boolean;<br>backgroundColor: string;<br>backgroundOpacity: number;<br>borderMatchElement: boolean;<br>borderColor: string;<br>borderOpacity: number;<br>filenameVisible: boolean;<br>};<br>`;
export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "help",
@@ -97,7 +100,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "style.fontFamily",
code: "[number]",
desc: "1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont",
desc: "1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font, 5: Excalifont, 6: Nunito, 7: Lilita One, 8: Comic Shanns, 9: Liberation Sans",
after: "",
},
{
@@ -197,6 +200,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
' "excalidraw-export-dark"?: boolean;\n' +
' "excalidraw-export-padding"?: number;\n' +
' "excalidraw-export-pngscale"?: number;\n' +
' "excalidraw-export-embed-scene"?: boolean;\n' +
' "excalidraw-default-mode"?: "view" | "zen";\n' +
' "excalidraw-onload-script"?: string;\n' +
' "excalidraw-linkbutton-opacity"?: number;\n' +
@@ -229,6 +233,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addElementsToFrame",
code: "addElementsToFrame(frameId: string, elementIDs: string[]):void;",
desc: null,
after: "",
},
{
field: "addFrame",
code: "addFrame(topX: number, topY: number, width: number, height: number, name?: string): string;",
desc: null,
after: "",
},
{
field: "addRect",
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
@@ -261,8 +277,10 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addText",
code: 'addText(topX: number, topY: number, text: string, formatting?: {wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
desc: "If box is !null, then text will be boxed\nThe function returns the id of the TextElement. If the text element is boxed i.e. it is a sticky note, then the id of the container object",
code: 'addText(topX: number, topY: number, text: string, formatting?: {autoResize?: boolean; wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
desc: "If box is !null, then text will be boxed\nThe function returns the id of the TextElement. If the text element is boxed i.e. it is a sticky note, then the id of the container object.\n"+
"Default value for autoResize is true. Setting autoResize to false will wrap the text in the text element without the need for the container. If set to false, you must provide a width value as well.\n" +
"wrapAt will be ignored if autoResize is set to false (and a width is also provided)",
after: "",
},
{
@@ -285,8 +303,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addEmbeddable",
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe/webview will be loaded from the file. Both the url and the file may not be null.",
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile, embeddableCustomData?: EmbeddableMDCustomProps): string;",
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. " +
"If url is null then the iframe/webview will be loaded from the file. Both the url and the file may not be null.<br>" + EMBEDDABLE_MDCUSTOMPROPS,
after: "",
},
{
@@ -304,6 +323,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
field: "tex2dataURL",
code: "async tex2dataURL(tex: string, scale: number = 4): Promise<{mimeType: MimeType;fileId: FileId;dataURL: DataURL;created: number;size: { height: number; width: number };}> ",
desc: "returns the base64 dataURL of the LaTeX equation rendered as an SVG. tex is the LaTeX equation string",
after: "",
},
{
field: "connectObjects",
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
@@ -364,6 +389,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addBackOfTheCardNoteToView",
code: "async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise<string>",
desc: "Adds a back of the note card to the current active view. If <b>body</b> is provided the note will be created with the body text, otherwise the note will be created with the title only.<br>Returns the id of the created element.<br>" +
"If <b>activate</b> is true, the embedded note will be activated for editing.<br>" +
"This is an async function, if you need the element ID of the created element, the function should be awaited.<br>" + EMBEDDABLE_MDCUSTOMPROPS,
after: "",
},
{
field: "getViewSelectedElement",
code: "getViewSelectedElement(): ExcalidrawElement;",
@@ -372,8 +405,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getViewSelectedElements",
code: "getViewSelectedElements(): ExcalidrawElement[];",
desc: null,
code: "getViewSelectedElements(includeFrameChildren: boolean = true): ExcalidrawElement[];",
desc: "If a frame is selected this function will return the frame and all its elements unless includeFrameChildren is set to false",
after: "",
},
{
@@ -427,7 +460,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "getExportSettings",
code: "getExportSettings(withBackground: boolean, withTheme: boolean,): ExportSettings;",
desc: "Utility function to generate ExportSettings object",
desc: "Utility function to generate ExportSettings object\n" +
"export interface ExportSettings {\n" +
" withBackground: boolean;\n" +
" withTheme: boolean;\n" +
" isMask: boolean; //if true elements will be processed as mask, clipping, etc.\n" +
" frameRendering?: { //optional, overrides relevant appState settings for rendering the frame\n" +
" enabled: boolean;\n" +
" name: boolean;\n" +
" outline: boolean;\n" +
" clip: boolean;\n" +
" };\n" +
" skipInliningFonts?: boolean;\n" +
"}",
after: "",
},
{
@@ -462,10 +507,17 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getElementsInTheSameGroupWithElement",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[], includeFrameElements: boolean = false): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.<br>" +
"If includeFrameElements is true, then if the frame is part of the group all the elements that are in the frame will also be included in the result set",
after: ""
},
{
field: "getElementsInFrame",
code: " getElementsInFrame(frameElement: ExcalidrawElement,elements: ExcalidrawElement[],shouldIncludeFrame: boolean = false,): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that are inside the frameElement. If shouldIncludeFrame is true, the frameElement will also be included in the result.",
after: "",
},
{
field: "activeScript",
code: "activeScript: string;",
@@ -496,6 +548,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Measures text size based on current style settings",
after: "",
},
{
field: "getOriginalImageSize",
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>",
desc: "Returns the size of the image element at 100% (i.e. the original size). This is an async function, you need to await the result.",
after: "",
},
{
field: "verifyMinimumPluginVersion",
code: "verifyMinimumPluginVersion(requiredVersion: string): boolean;",
@@ -556,6 +614,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Converts a CSS color name to its HEX color equivalent. 'White' to #FFFFFF",
after: "",
},
{
field: "getCM",
code: "getCM(color:TInput): ColorMaster;",
desc: `Returns a ${hyperlink("https://github.com/lbragile/ColorMaster", "ColorMaster")} object. ` +
"The function also accepts css color names. Under the hood, before calling ColorMaster it uses " +
"colorNameToHex to convert the color name to a HEX color.",
after: "",
},
{
field: "obsidian",
code: "obsidian",
@@ -570,6 +636,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
"If no template is set, it returns null.",
after: "",
},
{
field: "getEmbeddedImagesFiletree",
code: "getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[]",
desc: "Retruns the embedded images in the scene recursively. If excalidrawFile is not provided, " +
"the function will use ea.targetView.file",
after: "",
},
{
field: "getAttachmentFilepath",
code: "async getAttachmentFilepath(filename: string): Promise<string>",
@@ -678,8 +751,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "viewUpdateScene",
code: "viewUpdateScene(scene:{elements?:ExcalidrawElement[],appState?: AppState,files?: BinaryFileData,commitToHistory?: boolean,},restore:boolean=false):void",
desc: "Calls the ExcalidrawAPI updateScene function for the targetView. When restore=true, excalidraw will try to correct errors in the scene such as setting default values to missing element properties.",
code: "viewUpdateScene(scene:{elements?:ExcalidrawElement[],appState?: AppState,files?: BinaryFileData,commitToHistory?: boolean,storeAction?: 'capture' | 'none' | 'update'},restore:boolean=false):void",
desc: "Calls the ExcalidrawAPI updateScene function for the targetView. When restore=true, excalidraw will try to correct errors in the scene such as setting default values to missing element properties. " +
`Note that commitToHistory has been deprecated in Excalidraw and is no longer used. You should use storeAction instead. See ${hyperlink("https://github.com/excalidraw/excalidraw/pull/7898", "ExcalidrawAPI")} documentation for more information.`,
after: "",
},
{
@@ -821,6 +895,12 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
desc: "If this key is present it will override the default excalidraw embed and export setting. This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.",
after: ": 1",
},
{
field: "excalidraw-export-embed-scene",
code: null,
desc: "If this key is present it will override the default excalidraw embed and export setting.",
after: ": false",
},
{
field: "open-md",
code: null,
@@ -834,9 +914,9 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
after: ": png",
},
{
field: "iframe-theme",
field: "embeddable-theme",
code: null,
desc: "Override iFrame theme plugin-settings for this file. 'match' will match the Excalidraw theme, 'default' will match the obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
desc: "Override embeddable's theme plugin-settings for this file. 'auto' will match the Excalidraw theme, 'default' will match the Obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
after: ": auto",
},
{

View File

@@ -19,7 +19,7 @@ export class UniversalInsertFileModal extends Modal {
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
) {
super(app);
super(plugin.app);
const appState = (view.excalidrawAPI as ExcalidrawImperativeAPI).getAppState();
const containerRect = view.containerEl.getBoundingClientRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
@@ -86,7 +86,7 @@ export class UniversalInsertFileModal extends Modal {
const isPDF = file && file.extension === "pdf";
const isExcalidraw = file && ea.isExcalidrawFile(file);
const sections = file && file.extension === "md"
const sections = (file && file.extension === "md")
? (await this.plugin.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
@@ -189,6 +189,7 @@ export class UniversalInsertFileModal extends Modal {
`[[${path}${sectionPicker.selectEl.value}]]`,
)]
);
ea.destroy();
this.close();
})
actionIFrame = button;
@@ -220,6 +221,7 @@ export class UniversalInsertFileModal extends Modal {
ea.isExcalidrawFile(file) ? !anchorTo100 : undefined,
)]
);
ea.destroy();
this.close();
})
actionImage = button;
@@ -271,5 +273,8 @@ export class UniversalInsertFileModal extends Modal {
onClose(): void {
this.view.ownerWindow.removeEventListener("keydown", this.onKeyDown);
this.view = null;
this.file = null;
this.plugin = null;
}
}

View File

@@ -9,6 +9,7 @@ export default {
// main.ts
CONVERT_URL_TO_FILE: "Save image from URL to local file",
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
ZIP_CURRENT_FILE: "Compress current Excalidraw file",
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
@@ -35,6 +36,7 @@ export default {
TRANSCLUDE: "Embed a drawing",
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image",
NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW",
NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB",
NEW_IN_ACTIVE_PANE: "Create new drawing - IN THE CURRENT ACTIVE WINDOW",
@@ -48,16 +50,20 @@ export default {
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)",
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
INSERT_LINK_TO_ELEMENT:
`Copy markdown link for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
INSERT_LINK_TO_ELEMENT_GROUP:
"Copy 'group=' markdown link for selected element to clipboard.",
"Copy 'group=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_AREA:
"Copy 'area=' markdown link for selected element to clipboard.",
"Copy 'area=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_FRAME:
"Copy 'frame=' markdown link for selected element to clipboard.",
"Copy 'frame=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
"Copy 'clippedframe=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_NORMAL:
"Copy markdown link for selected element to clipboard.",
"Copy [[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
INSERT_LINK: "Insert link to file",
@@ -69,13 +75,19 @@ export default {
INSERT_PDF: "Insert PDF file from vault",
UNIVERSAL_ADD_FILE: "Insert ANY file",
INSERT_CARD: "Add back-of-note card",
CONVERT_CARD_TO_FILE: "Move back-of-note card to File",
ERROR_TRY_AGAIN: "Please try again.",
PASTE_CODEBLOCK: "Paste code block",
INSERT_LATEX:
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
ENTER_LATEX: "Enter a valid LaTeX expression",
READ_RELEASE_NOTES: "Read latest release notes",
RUN_OCR: "OCR: Grab text from freedraw scribble and pictures to clipboard",
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
RERUN_OCR: "OCR full drawing re-run: Grab text from freedraw + images to clipboard and doc.props",
RUN_OCR_ELEMENTS: "OCR selected elements: Grab text from freedraw + images to clipboard",
TRAY_MODE: "Toggle property-panel tray-mode",
SEARCH: "Search for text in drawing",
CROP_PAGE: "Crop and mask selected page",
CROP_IMAGE: "Crop and mask image",
ANNOTATE_IMAGE : "Annotate image in Excalidraw",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert active PDF page as image",
@@ -84,6 +96,23 @@ export default {
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
//ExcalidrawView.ts
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
LINKLIST_SECOND_ORDER_LINK: "Second Order Link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>",
FRAME_CLIPPING_ENABLED: "Frame Rendering: Enabled",
FRAME_CLIPPING_DISABLED: "Frame Rendering: Disabled",
ARROW_BINDING_INVERSE_MODE: "Inverted Mode: Default arrow binding is now disabled. Use CTRL/CMD to temporarily enable binding when needed.",
ARROW_BINDING_NORMAL_MODE: "Normal Mode: Arrow binding is now enabled. Use CTRL/CMD to temporarily disable binding when needed.",
EXPORT_FILENAME_PROMPT: "Please provide filename",
EXPORT_FILENAME_PROMPT_PLACEHOLDER: "filename, leave blank to cancel action",
WARNING_SERIOUS_ERROR: "WARNING: Excalidraw ran into an unknown problem!\n\n" +
"There is a risk that your most recent changes cannot be saved.\n\n" +
"To be on the safe side...\n" +
"1) Please select your drawing using CTRL/CMD+A and make a copy with CTRL/CMD+C.\n" +
"2) Then create an empty drawing in a new pane by CTRL/CMD+clicking the Excalidraw ribbon button,\n" +
"3) and paste your work to the new document with CTRL/CMD+V.",
ARIA_LABEL_TRAY_MODE: "Tray-mode offers an alternative, more spacious canvas",
MASK_FILE_NOTICE: "This is a mask file. It is used to crop images and mask out parts of the image. Press and hold notice to open the help video.",
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
OPEN_AS_MD: "Open as Markdown",
@@ -110,6 +139,7 @@ export default {
ERROR_SAVING_IMAGE: "Unknown error occurred while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
RECURSIVE_INSERT_ERROR: "You may not recursively insert part of an image into the same image as it would create an infinite loop",
CONVERT_TO_MARKDOWN: "Convert to file...",
SELECT_TEXTELEMENT_ONLY: "Select text element only (not container)",
REMOVE_LINK: "Remove text element link",
@@ -139,6 +169,9 @@ export default {
ANNOTATE_PREFIX_DESC:
"The first part of the filename for new drawings created when annotating an image. " +
"If empty the default 'annotated_' will be used.",
ANNOTATE_PRESERVE_SIZE_NAME: "Preserve image size when annotating",
ANNOTATE_PRESERVE_SIZE_DESC:
"When annotating an image in markdown the replacment image link will include the width of the original image.",
CROP_FOLDER_NAME: "Crop file folder",
CROP_FOLDER_DESC:
"Default location for new drawings created when cropping an image. If empty, drawings will be created following the Vault attachments settings.",
@@ -197,7 +230,7 @@ export default {
"The default OpenAI API URL. This is a freetext field, so you can enter any valid OpenAI API compatible URL. " +
"Excalidraw will use this URL when posting API requests to OpenAI. I am not doing any error handling on this field, so make sure you enter a valid URL and only change this if you know what you are doing. ",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4-vision-preview",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4o",
SAVING_HEAD: "Saving",
SAVING_DESC: "In the 'Saving' section of Excalidraw Settings, you can configure how your drawings are saved. This includes options for compressing Excalidraw JSON in Markdown, setting autosave intervals for both desktop and mobile, defining filename formats, and choosing whether to use the .excalidraw.md or .md file extension. ",
COMPRESS_NAME: "Compress Excalidraw JSON in Markdown",
@@ -211,6 +244,14 @@ export default {
"once you switch back to Excalidraw view. " +
"The setting only has effect 'point forward', meaning, existing drawings will not be affected by the setting " +
"until you open them and save them.<br><b><u>Toggle ON:</u></b> Compress drawing JSON<br><b><u>Toggle OFF:</u></b> Leave drawing JSON uncompressed",
DECOMPRESS_FOR_MD_NAME: "Decompress Excalidraw JSON in Markdown View",
DECOMPRESS_FOR_MD_DESC:
"By enabling this feature Excalidraw will automatically decompress the drawing JSON when you switch to Markdown view. " +
"This will allow you to easily read and edit the JSON string. The drawing will be compressed again " +
"once you switch back to Excalidraw view and save the drawing (CTRL+S).<br>" +
"I recommend switching this feature off as it will result in smaller file sizes and avoiding unnecessary results in Obsidian search. " +
"You can always use the 'Excalidraw: Decompress current Excalidraw file' command from the command palette "+
"to manually decompress the drawing JSON when you need to read or edit it.",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "Interval for autosave on Desktop",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"The time interval between saves. Autosave will skip if there are no changes in the drawing. " +
@@ -283,7 +324,26 @@ FILENAME_HEAD: "Filename",
"Should pen mode be automatically enabled when opening Excalidraw?",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b>SHOW <b><u>Toggle OFF:</u></b>HIDE",
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
"The effect depends on the device. Crosshair is typically visible on drawing tablets, MS Surface, but not on iOS.",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render Excalidraw file as an image in hover preview...",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
"markdown side of the document.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"Must close the active excalidraw/markdown file and reopen it for this change to take effect.<br>When you are in markdown reading mode (aka. reading the back side of the drawing), should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode, when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <b>PDF Export</b> under 'Embedding and Exporting' further below.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render Excalidraw as an image when EXPORTING TO PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"This setting controls the behavior of Excalidraw when exporting a file to PDF in markdown mode using Obsidian's Export to PDF... feature.<br>" +
"When <b>enabled</b> the PDF will show the Excalidraw drawing only; when <b>disabled</b> the PDF will show the markdown side of the document.<br><ul>" +
"<li>See other related setting for <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul><br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -380,7 +440,7 @@ FILENAME_HEAD: "Filename",
`${labelCTRL()}+CLICK on text with [[links]] or [](links) to open them`,
LINK_CTRL_CLICK_DESC:
"You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
`this is turned off, you can either use ${labelCTRL()} + ${labelMETA()} or the link indicator in the top right of the element to open links.`,
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
TRANSCLUSION_WRAP_DESC:
"Number specifies the character count where the text should be wrapped. " +
@@ -460,6 +520,10 @@ FILENAME_HEAD: "Filename",
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
EMBED_IMAGE_CACHE_DESC: "Cache images for embedding in markdown. This will speed up the embedding process, but in case you compose images of several sub-component drawings, " +
"the embedded image in Markdown won't update until you open the drawing and save it to trigger an update of the cache.",
SCENE_IMAGE_CACHE_NAME: "Cache nested Excalidraws in Scene",
SCENE_IMAGE_CACHE_DESC: "Cache nested Excalidraws in the Scene for faster scene rendering. This will speed up the rendering process, especially if you have deeply nested Excalidraws in your scene. " +
"Excalidraw will try to intelligently identify if any children of a nested Excalidraw have changed and will update the cache accordingly. " +
"You may want to turn this off, in case you are suspecting that the cache is not updating properly.",
EMBED_IMAGE_CACHE_CLEAR: "Purge Cache",
BACKUP_CACHE_CLEAR: "Purge Backups",
BACKUP_CACHE_CLEAR_CONFIRMATION: "This action will delete all Excalidraw drawing backups. Backups are used as a safety measure in case your drawing file gets damaged. Each time you open Obsidian the plugin automatically deletes backups for files that no longer exist in your Vault. Are you sure you want to clear all backups?",
@@ -515,13 +579,17 @@ FILENAME_HEAD: "Filename",
"If turned off, the exported image will be transparent.",
EXPORT_PADDING_NAME: "Image Padding",
EXPORT_PADDING_DESC:
"The padding (in pixels) around the exported SVG or PNG image. " +
"The padding (in pixels) around the exported SVG or PNG image. Padding is set to 0 for clippedFrame references." +
"If you have curved lines close to the edge of the image they might get cropped during image export. You can increase this value to avoid cropping. " +
"You can also override this setting at a file level by adding the <code>excalidraw-export-padding: 5<code> frontmatter key.",
EXPORT_THEME_NAME: "Export image with theme",
EXPORT_THEME_DESC:
"Export the image matching the dark/light theme of your drawing. If turned off, " +
"drawings created in dark mode will appear as they would in light mode.",
EXPORT_EMBED_SCENE_NAME: "Embed scene in exported image",
EXPORT_EMBED_SCENE_DESC:
"Embed Excalidraw scene in exported image. Can be overridden at a file level by adding the <code>excalidraw-export-embed-scene: true/false<code> frontmatter key. " +
"The setting only takes effect the next time you (re)open drawings.",
EXPORT_HEAD: "Auto-export Settings",
EXPORT_SYNC_NAME:
"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
@@ -543,6 +611,18 @@ FILENAME_HEAD: "Filename",
"Double files will be exported both if auto-export SVG or PNG (or both) are enabled, as well as when clicking export on a single image.",
COMPATIBILITY_HEAD: "Compatibility features",
COMPATIBILITY_DESC: "You should only enable these features if you have a strong reason for wanting to work with excalidraw.com files instead of markdown files. Many of the plugin features are not supported on legacy files. Typical usecase would be if you use set your vault up on top of a Visual Studio Code project folder and you have .excalidraw drawings you want to access from Visual Studio Code as well. Another usecase might be using Excalidraw in Logseq and Obsidian in parallel.",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "Linter compatibility",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw is sensitive to the file structure below <code># Excalidraw Data</code>. Automatic linting of documents can create errors in Excalidraw Data. " +
"While I've made some effort to make the data loading resilient to " +
"lint changes, this solution is not foolproof.<br><mark>The best is to avoid liniting or otherwise automatically changing Excalidraw documents using different plugins.</mark><br>" +
"Use this setting if for good reasons you have decided to ignore my recommendation and configured linting of Excalidraw files.<br> " +
"The <code>## Text Elements</code> section is sensitive to empty lines. A common linting approach is to add an empty line after section headings. In case of Excalidraw this will break/change the first text element in your drawing. " +
"To overcome this, you can enable this setting. When enabled, Excalidraw will add a dummy element to the beginning of <code>## Text Elements</code> that the linter can safely modify." ,
PRESERVE_TEXT_AFTER_DRAWING_NAME: "Zotero compatibility",
PRESERVE_TEXT_AFTER_DRAWING_DESC: "Preserve text after the ## Drawing section of the markdown file. This may have a very slight performance impact when saving very large drawings.",
DEBUGMODE_NAME: "Enable debug messages",
DEBUGMODE_DESC: "I recommend restarting Obsidian after enabling/disabling this setting. This enable debug messages in the console. This is useful for troubleshooting issues. " +
"If you are experiencing problems with the plugin, please enable this setting, reproduce the issue, and include the console log in the issue you raise on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/issues'>GitHub</a>",
SLIDING_PANES_NAME: "Sliding panes plugin support",
SLIDING_PANES_DESC:
"Need to restart Obsidian for this change to take effect.<br>" +
@@ -571,7 +651,7 @@ FILENAME_HEAD: "Filename",
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a fourth font option, which adds a fourth font button to the properties panel for text elements. `,
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a local font option, which adds a local font to the list of fonts on the element properties panel for text elements. `,
RENDER_TWEAK_HEAD: "Rendering tweaks",
MAX_IMAGE_ZOOM_IN_NAME: "Maximum image zoom in resolution",
MAX_IMAGE_ZOOM_IN_DESC: "To save on memory and because Apple Safari (Obsidian on iOS) has some hard-coded limitations, Excalidraw.com limits the max resolution of images and large objects when zooming in. You can override this limitation using a multiplicator. " +
@@ -611,16 +691,26 @@ FILENAME_HEAD: "Filename",
"Turn this on to support image embedding styles such as ![[drawing|width|style]] in live preview editing mode. " +
"The setting will not affect the currently open documents. You need close the open documents and re-open them for the change " +
"to take effect.",
CUSTOM_FONT_HEAD: "Fourth font",
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
FADE_OUT_EXCALIDRAW_MARKUP_NAME: "Fade out Excalidraw markup",
FADE_OUT_EXCALIDRAW_MARKUP_DESC: "In Markdown view mode, the section after the markdown comment %% " +
"fades out. The text is still there, but the visual clutter is reduced. Note, you can place the %% in the line right above # Text Elements, " +
"in this case the entire drawing markdown will fade out including # Text Elements. The side effect is you won't be able to block reference text in other markdown notes, that is after the %% comment section. This is seldom an issue. " +
"Should you want to edit the Excalidraw markdown script, simply switch to markdown view mode and temporarily remove the %% comment.",
EXCALIDRAW_PROPERTIES_NAME: "Load Excalidraw Properties into Obsidian Suggester",
EXCALIDRAW_PROPERTIES_DESC: "Toggle this setting to load Excalidraw document properties into Obsidian's property suggester at plugin startup. "+
"Enabling this feature simplifies the use of Excalidraw front matter properties, allowing you to leverage many powerful settings. If you prefer not to load these properties automatically, " +
"you can disable this feature, but you will need to manually remove any unwanted properties from the suggester. " +
"Note that turning on this setting requires restarting the plugin as properties are loaded at startup.",
CUSTOM_FONT_HEAD: "Local font",
ENABLE_FOURTH_FONT_NAME: "Enable local font option",
ENABLE_FOURTH_FONT_DESC:
"By turning this on, you will see a fourth font button on the properties panel for text elements. " +
"Files that use this fourth font will (partly) lose their platform independence. " +
"By turning this on, you will see a local font in the font list on the properties panel for text elements. " +
"Files that use this local font will (partly) lose their platform independence. " +
"Depending on the custom font set in settings, they will look differently when loaded in another vault, or at a later time. " +
"Also the 4th font will display as system default font on excalidraw.com, or other Excalidraw versions.",
FOURTH_FONT_NAME: "Fourth font file",
FOURTH_FONT_NAME: "Local font file",
FOURTH_FONT_DESC:
"Select a .ttf, .woff or .woff2 font file from your vault to use as the fourth font. " +
"Select a .ttf, .woff or .woff2 font file from your vault to use as the local font. " +
"If no file is selected, Excalidraw will use the Virgil font by default.",
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
@@ -660,7 +750,7 @@ FILENAME_HEAD: "Filename",
SELECT_SECTION_OR_TYPE_NEW:
"Select existing section or type name of a new section then press Enter.",
INVALID_SECTION_NAME: "Invalid section name.",
EMPTY_SECTION_MESSAGE: "Hit enter to create a new Section",
EMPTY_SECTION_MESSAGE: "Type the Section Name and hit enter to create a new Section",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING:
@@ -742,4 +832,23 @@ FILENAME_HEAD: "Filename",
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
PANE_TARGET: "Link click behavior",
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
//FrameSettings.ts
FRAME_SETTINGS_TITLE: "Frame Settings",
FRAME_SETTINGS_ENABLE: "Enable Frames",
FRAME_SETTIGNS_NAME: "Display Frame Name",
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "Pages to import",
IPM_SELECT_PAGES_TO_IMPORT: "Please select pages to import",
IPM_ADD_BORDER_BOX_NAME: "Add border box",
IPM_ADD_FRAME_NAME: "Add page to frame",
IPM_ADD_FRAME_DESC: "For easier handling I recommend to lock the page inside the frame. " +
"If, however, you do lock the page inside the frame then the only way to unlock it is to right-click the frame, select remove elements from frame, then unlock the page.",
IPM_GROUP_PAGES_NAME: "Group pages",
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
IPM_SELECT_PDF: "Please select a PDF file",
};

View File

@@ -9,7 +9,8 @@ export default {
// main.ts
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
PUBLISH_SVG_CHECK: "Obsidian Publish: 搜索过期的 SVG 和 PNG 导出文件",
ZIP_CURRENT_FILE: "压缩当前 Excalidraw 文件",
PUBLISH_SVG_CHECK: "Obsidian Publish搜索过期的 SVG 和 PNG 导出文件",
EMBEDDABLE_PROPERTIES: "Embeddable 元素设置",
EMBEDDABLE_RELATIVE_ZOOM: "使元素的缩放等级等于当前画布的缩放等级",
OPEN_IMAGE_SOURCE: "打开 Excalidraw 绘图文件",
@@ -35,6 +36,7 @@ export default {
TRANSCLUDE: "嵌入绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LEFTHANDED_MODE: "切换为左手模式",
FLIP_IMAGE: "打开当前所选 excalidraw 图像的“背景笔记”",
NEW_IN_NEW_PANE: "新建绘图 - 于新面板",
NEW_IN_NEW_TAB: "新建绘图 - 于新页签",
NEW_IN_ACTIVE_PANE: "新建绘图 - 于当前面板",
@@ -48,14 +50,18 @@ export default {
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LOCK: "文本元素原文模式RAW⟺ 预览模式PREVIEW",
DELETE_FILE: "从库中删除所选图像(或以图像形式嵌入绘图中的 Markdown的源文件",
COPY_ELEMENT_LINK: "复制所选元素的链接(形如 [[file#^id]]]",
COPY_DRAWING_LINK: "复制绘图的嵌入链接(形如 ![[darwing]]",
INSERT_LINK_TO_ELEMENT:
`复制所选元素为内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组为内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )。\n按住 ${labelALT()} 可观看视频演示。`,
INSERT_LINK_TO_ELEMENT_GROUP:
"复制所选元素所在分组为内部链接(形如 [[file#^group=id]] ",
"复制所选元素所在分组为嵌入链接(形如 ![[file#^group=id]] ",
INSERT_LINK_TO_ELEMENT_AREA:
"复制所选元素所在区域为内部链接(形如 [[file#^area=id]] ",
"复制所选元素所在区域为嵌入链接(形如 ![[file#^area=id]] ",
INSERT_LINK_TO_ELEMENT_FRAME:
"复制所选框架为内部链接(形如 [[file#^frame=id]] ",
"复制所选框架为嵌入链接(形如 ![[file#^frame=id]] ",
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
"复制所选框架(内容)为嵌入链接(形如 ![[file#^clippedframe=id]] ",
INSERT_LINK_TO_ELEMENT_NORMAL:
"复制所选元素为内部链接(形如 [[file#^id]] ",
INSERT_LINK_TO_ELEMENT_ERROR: "未选择画布里的单个元素",
@@ -64,27 +70,54 @@ export default {
INSERT_COMMAND: "插入 Obsidian 命令(以内部链接形式嵌入)到当前绘图中",
INSERT_IMAGE: "插入图像或 Excalidraw 绘图(以图像形式嵌入)到当前绘图中",
IMPORT_SVG: "从 SVG 文件导入图形元素到当前绘图中(暂不支持文本元素)",
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
INSERT_CARD: "插入“背景笔记”卡片",
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
ERROR_TRY_AGAIN: "请重试。",
PASTE_CODEBLOCK: "粘贴代码块",
INSERT_LATEX:
`插入 LaTeX 公式到当前绘图`,
ENTER_LATEX: "输入 LaTeX 表达式",
READ_RELEASE_NOTES: "阅读本插件的更新说明",
RUN_OCR: "OCR识别涂鸦和图片里的文本并复制到剪贴板",
RUN_OCR: "OCR 完整画布:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
RERUN_OCR: "重新运行 OCR 完整画笔:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
RUN_OCR_ELEMENTS: "OCR 选中的元素:识别涂鸦和图片里的文本并复制到剪贴板",
TRAY_MODE: "绘图工具属性页:面板模式 ⟺ 托盘模式",
SEARCH: "搜索文本",
CROP_IMAGE: "裁剪与蒙版",
CROP_PAGE: "对所选页面裁剪并添加蒙版",
CROP_IMAGE: "对图片裁剪并添加蒙版",
ANNOTATE_IMAGE : "在 Excalidraw 中标注图像",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "将当前激活的的 PDF 页面作为图片插入",
RESET_IMG_TO_100: "重设图像元素的尺寸为 100%",
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
//ExcalidrawView.ts
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",
LINKLIST_SECOND_ORDER_LINK: "二级链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "自定义链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "不要在文件名周围添加[[方括号Wiki 格式链接)]]<br>编辑链接时请遵循以下格式:<br><mark>文件名#^块引用|宽度x最大高度</mark>",
FRAME_CLIPPING_ENABLED: "渲染框架:已启用",
FRAME_CLIPPING_DISABLED: "渲染框架:已禁用",
ARROW_BINDING_INVERSE_MODE: "反转模式:默认方向按键已禁用。需要时请使用 Ctrl/CMD 临时启用。",
ARROW_BINDING_NORMAL_MODE: "正常模式:方向键已启用。需要时请使用 Ctrl/CMD 临时禁用。",
EXPORT_FILENAME_PROMPT: "请提供文件名",
EXPORT_FILENAME_PROMPT_PLACEHOLDER: "请输入文件名,留空以取消操作",
WARNING_SERIOUS_ERROR: "警告Excalidraw 遇到了未知的问题!\n\n" +
"您最近的更改可能无法保存。\n\n" +
"为了安全起见,请按以下步骤操作:\n" +
"1) 使用 Ctrl/CMD+A 选择您的绘图,然后使用 Ctrl/CMD+C 进行复制。\n" +
"2) 然后在新窗格中,通过 Ctrl/CMD 点击 Excalidraw 功能区按钮创建一个空白绘图。\n" +
"3) 最后,使用 Ctrl/CMD+V 将您的作品粘贴到新文档中。",
ARIA_LABEL_TRAY_MODE: "托盘模式提供了一个更宽敞的画布空间",
MASK_FILE_NOTICE: "这是一个蒙版图像。长按本提示来观看视频讲解。",
INSTALL_SCRIPT_BUTTON: "安装或更新 Excalidraw 脚本",
OPEN_AS_MD: "打开为 Markdown 文档",
EXPORT_IMAGE: `导出为图像`,
OPEN_LINK: "打开所选元素里的链接 \n按住 SHIFT 在新面板打开)",
OPEN_LINK: "打开所选元素里的链接 \n按住 Shift 在新面板打开)",
EXPORT_EXCALIDRAW: "导出为 .excalidraw 文件(旧版绘图文件格式)",
LINK_BUTTON_CLICK_NO_TEXT:
"请选择一个含有链接的图形或文本元素。",
@@ -103,10 +136,15 @@ export default {
BACKUP_RESTORED: "已恢复备份",
CACHE_NOT_READY: "抱歉,加载绘图文件时出错。<br><br><mark>现在有耐心,将来更省心。</mark><br><br>该插件有备份机制,但您似乎刚刚打开 Obsidian需要等待一分钟或更长的时间来读取缓存。缓存读取完毕时您将会在右上角收到提示。<br><br>请点击 OK 并耐心等待缓存,或者选择点击取消后手动修复你的文件。<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误。可能是由于某种原因,图像不可用或拒绝了 Obsidian 的获取请求。",
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
USE_INSERT_FILE_MODAL: "使用“插入任意文件”功能来嵌入 Markdown 文档",
RECURSIVE_INSERT_ERROR: "你不能将图像的一部分嵌入到此图像中,因为这可能导致无限循环。",
CONVERT_TO_MARKDOWN: "转存为 Markdown 文档(并嵌入为 MD-Embeddable",
SELECT_TEXTELEMENT_ONLY: "只选择文本元素(非容器)",
REMOVE_LINK: "移除文字元素链接",
LASER_ON: "启用激光笔",
LASER_OFF: "关闭激光笔",
//settings.ts
RELEASE_NOTES_NAME: "显示更新说明",
@@ -123,6 +161,23 @@ export default {
FOLDER_NAME: "Excalidraw 文件夹",
FOLDER_DESC:
"新绘图的默认存储路径。若为空,将在库的根目录中创建新绘图。",
CROP_PREFIX_NAME: "剪贴文件的前缀",
CROP_PREFIX_DESC:
"当剪贴图片进来时保存的文件名的前缀。 " +
"留空则使用 'cropped_'",
ANNOTATE_PREFIX_NAME: "标注文件的前缀",
ANNOTATE_PREFIX_DESC:
"在标注图像时创建新绘图的文件名的第一部分。" +
"留空则使用'annotated_'",
ANNOTATE_PRESERVE_SIZE_NAME: "在标注时保留图像尺寸",
ANNOTATE_PRESERVE_SIZE_DESC:
"当在 Markdown 中标注图像时,替换后的图像链接将包含原始图像的宽度。",
CROP_FOLDER_NAME: "剪贴文件文件夹",
CROP_FOLDER_DESC:
"剪贴图像时创建新绘图的默认存储路径。如果留空,将按照 Vault 附件设置创建。",
ANNOTATE_FOLDER_NAME: "图片标注文件文件夹",
ANNOTATE_FOLDER_DESC:
"创建图片标注是的默认存储路径。如果留空,将按照 Vault 附件设置创建。",
FOLDER_EMBED_NAME:
"将 Excalidraw 文件夹用于“新建绘图”系列命令",
FOLDER_EMBED_DESC:
@@ -131,11 +186,14 @@ export default {
"<b>开启:</b>使用上面的 Excalidraw 文件夹。 <br><b>关闭:</b>使用 Obsidian 设置的新附件默认位置。",
TEMPLATE_NAME: "Excalidraw 模板文件",
TEMPLATE_DESC:
"Excalidraw 模板文件的存储路径。<br>" +
"如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
"Template.md则此项应设为 Excalidraw/Template.md也可省略 .md 扩展名,即 Excalidraw/Template<br>" +
"Excalidraw 模板文件(文件夹)的存储路径。<br>" +
"<b>模板文件:</b>比如:如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
"Template.md则此项应设为 Excalidraw/Template.md也可省略 .md 扩展名,即 Excalidraw/Template。" +
"如果您在兼容模式下使用 Excalidraw那么您的模板文件也必须是旧的 *.excalidraw 格式," +
"例如 Excalidraw/Template.excalidraw。",
"例如 Excalidraw/Template.excalidraw。<br><b>模板文件夹:</b> 你还可以将文件夹设置为模板。 " +
"在这种情况下,创建新绘图时将提示您选择使用哪个模板。<br>" +
"<b>专业提示:</b> 如果您正在使用 Obsidian Templater 插件,您可以将 Templater 代码添加到不同的" +
"Excalidraw 模板中,以自动配置您的绘图",
SCRIPT_FOLDER_NAME: "Excalidraw 自动化脚本的文件夹(大小写敏感!)",
SCRIPT_FOLDER_DESC:
"此文件夹用于存放 Excalidraw 自动化脚本。" +
@@ -171,8 +229,8 @@ export default {
AI_OPENAI_DEFAULT_API_URL_DESC:
"默认的 OpenAI API URL。请填写有效的 OpenAI API URL。" +
"Excalidraw 会通过该 URL 发送 API 请求给 OpenAI。我没有对此选项做任何错误处理请谨慎修改。",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4-vision-preview",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI 图像生成 API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "输入你的默认 AI 模型名称例如gpt-4o",
SAVING_HEAD: "保存",
SAVING_DESC: "包括:压缩,自动保存的时间间隔,文件的命名格式和扩展名等的设置。",
COMPRESS_NAME: "压缩 Excalidraw JSON",
@@ -186,6 +244,14 @@ export default {
"而当您切换回 Excalidraw 模式时,数据就会被再次编码。<br>" +
"开启此项后,对于之前已存在但未压缩的绘图文件," +
"需要重新打开并保存才能生效。",
DECOMPRESS_FOR_MD_NAME: "在 Markdown 视图中解压缩 Excalidraw JSON",
DECOMPRESS_FOR_MD_DESC:
"通过启用此功能Excalidraw 将在切换到 Markdown 视图时自动解压缩绘图 JSON。" +
"这将使您能够轻松阅读和编辑 JSON 字符串。" +
"一旦您切换回 Excalidraw 视图并保存绘图Ctrl+S绘图将再次被压缩。<br>" +
"我建议关闭此功能,因为这可以获得更小的文件尺寸,并避免在 Obsidian 搜索中出现不必要的结果。 " +
"您始终可以使用命令面板中的“Excalidraw: 解压缩当前 Excalidraw 文件”命令"+
"在需要阅读或编辑时手动解压缩绘图 JSON。",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "桌面端自动保存时间间隔",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"每隔多长时间自动保存一次(如果绘图文件没有发生改变,将不会保存)。" +
@@ -235,7 +301,7 @@ FILENAME_HEAD: "文件名",
IFRAME_MATCH_THEME_DESC:
"<b>开启:</b>当 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
"开启此项后以交互形式嵌入到绘图中的元素Embeddable 将会匹配 Excalidraw 主题。<br>" +
"<b>关闭:</b>如果您想要 Embeddable 匹配 Obsidian 主题,请关闭此项。",
"<b>关闭:</b>如果您想要 Embeddable 匹配 Obsidian 主题,请关闭此项。",
MATCH_THEME_NAME: "使新建的绘图匹配 Obsidian 主题",
MATCH_THEME_DESC:
"如果 Obsidian 使用黑暗主题,新建的绘图文件也将使用黑暗主题。<br>" +
@@ -256,12 +322,30 @@ FILENAME_HEAD: "文件名",
DEFAULT_PEN_MODE_NAME: "触控笔模式Pen mode",
DEFAULT_PEN_MODE_DESC:
"打开绘图时,是否自动开启触控笔模式?",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在 Markdown 文件的悬停预览中渲染为图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"这个设置影响 frontmatter 中具有 <b>excalidraw-open-md: true</b> 的文件。",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "在 Markdown 文件阅读模式下渲染为图片",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时 Excalidraw 绘图是否应该呈现为图像? " +
"此设置不会影响您处于 Excalidraw 模式时的绘图显示,也不会影响将绘图嵌入到 Markdown 文档中或在渲染悬停预览时的显示。<br><ul>" +
"<li>看下面的“嵌入和导出”中的 <b>PDF 导出</b>的其他相关设置。</li>" +
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在 Markdown 模式下导出为 PDF 时渲染为图片",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时将笔记导出为 PDFExcalidraw 绘图是否应该呈现为图像? <br><ul>" +
"<li>查看上面“外观和行为”下的 <b>Markdown 阅读模式</b>的其他相关设置。</li>" +
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
THEME_HEAD: "主题和样式",
ZOOM_HEAD: "缩放",
DEFAULT_PINCHZOOM_NAME: "允许在触控笔模式下进行双指缩放",
DEFAULT_PINCHZOOM_DESC:
"在触控笔模式下使用自由画笔工具时,双指缩放可能造成干扰。<br>" +
"<b>开启: </b>允许在触控笔模式下进行双指缩放<br><b>关闭: </b>禁止在触控笔模式下进行双指缩放",
"<b>开启:</b>允许在触控笔模式下进行双指缩放<br><b>关闭: </b>禁止在触控笔模式下进行双指缩放",
DEFAULT_WHEELZOOM_NAME: "鼠标滚轮缩放页面",
DEFAULT_WHEELZOOM_DESC:
@@ -287,7 +371,7 @@ FILENAME_HEAD: "文件名",
LINKS_DESC:
`按住 ${labelCTRL()} 并点击包含 <code>[[链接]]</code> 的文本元素可以打开其中的链接。` +
"如果所选文本元素包含多个 <code>[[有效的内部链接]]</code> ,只会打开第一个链接;" +
"如果所选文本元素包含有效的 URL 链接 (如 <code>https://</code> 或 <code>http://</code>)" +
"如果所选文本元素包含有效的 URL 链接如 <code>https://</code> 或 <code>http://</code>" +
"插件会在浏览器中打开链接。<br>" +
"链接的源文件被重命名时,绘图中相应的 <code>[[内部链接]]</code> 也会同步更新。" +
"若您不愿绘图中的链接外观因此而变化,可使用 <code>[[内部链接|别名]]</code>。",
@@ -296,7 +380,19 @@ FILENAME_HEAD: "文件名",
"Excalidraw 不会检查您的设置是否合理,因此请谨慎设置,避免冲突。" +
"以下选项在苹果和非苹果设备上区别很大,如果您在多个硬件平台上使用 Obsidian需要分别进行设置。"+
"选项里的 4 个开关依次代表 " +
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Win 键)。"),
(DEVICE.isIOS || DEVICE.isMacOS ? "Shift, CMD, OPT, CONTROL." : "Shift, Ctrl, Alt, META (Win 键)。"),
LONG_PRESS_DESKTOP_NAME: "长按打开(电脑端)",
LONG_PRESS_DESKTOP_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
LONG_PRESS_MOBILE_NAME: "长按打开(移动端)",
LONG_PRESS_MOBILE_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
FOCUS_ON_EXISTING_TAB_NAME: "聚焦于当前标签页",
FOCUS_ON_EXISTING_TAB_DESC: "当打开一个链接时如果该文件已经打开Excalidraw 将会聚焦到现有的标签页上 " +
"启用这个设置会在文件已经打开的情况下覆盖“重用相邻窗格”的设置。",
SECOND_ORDER_LINKS_NAME: "显示二级链接",
SECOND_ORDER_LINKS_DESC: "在 Excalidraw 中点击链接时显示链接。二级链接是指指向被点击链接的反向链接" +
"当使用图标连接相似的笔记时,二级链接可以让你直接到达相关笔记,而不需要两次点击。" +
"请观看 <a href='https://youtube.com/shorts/O_1ls9c6wBY?feature=share'>这个 YouTube Shorts 视频</a> 以了解更多信息。",
ADJACENT_PANE_NAME: "在相邻面板中打开",
ADJACENT_PANE_DESC:
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在新面板中打开该链接。<br>` +
@@ -306,7 +402,7 @@ FILENAME_HEAD: "文件名",
MAINWORKSPACE_PANE_NAME: "在主工作区中打开",
MAINWORKSPACE_PANE_DESC:
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在当前窗口的新面板中打开该链接。<br>` +
"若开启此项Excalidraw 会在主工作区的面板中打开该链接。",
"若开启此项Excalidraw 会在主工作区的面板中打开该链接。",
LINK_BRACKETS_NAME: "在链接的两侧显示 <code>[[中括号]]</code>",
LINK_BRACKETS_DESC: `${
"文本元素处于预览PREVIEW模式时在内部链接的两侧显示中括号。<br>" +
@@ -331,7 +427,7 @@ FILENAME_HEAD: "文件名",
HOVERPREVIEW_NAME: "鼠标悬停预览内部链接",
HOVERPREVIEW_DESC:
`<b>开启:</b>在 Excalidraw <u>阅读模式View</u>下,鼠标悬停在 <code>[[内部链接]]</code> 上即可预览;` +
"而在<u>普通模式Normal</u>下, 鼠标悬停在内部链接右上角的蓝色标识上即可预览。<br> " +
"而在<u>普通模式Normal</u>下鼠标悬停在内部链接右上角的蓝色标识上即可预览。<br> " +
`<b>关闭:</b>鼠标悬停在 <code>[[内部链接]]</code> 上,并且按住 ${labelCTRL()} 才能预览。`,
LINKOPACITY_NAME: "链接标识的透明度",
LINKOPACITY_DESC:
@@ -340,7 +436,7 @@ FILENAME_HEAD: "文件名",
`按住 ${labelCTRL()} 并点击含有 [[链接]] 或 [别名](链接) 的文本来打开链接`,
LINK_CTRL_CLICK_DESC:
"如果此功能影响到您使用某些原版 Excalidraw 功能,可将其关闭。" +
"关闭后,您只能通过绘图面板标题栏中的链接按钮来打开链接。",
"关闭后,您可以使用 ${labelCTRL()} + ${labelMETA()} 或者元素右上角的链接指示器来打开链接。",
TRANSCLUSION_WRAP_NAME: "MD-Transclusion 的折行方式",
TRANSCLUSION_WRAP_DESC:
"中的 number 表示嵌入的文本溢出时,在第几个字符处进行折行。<br>" +
@@ -374,12 +470,12 @@ FILENAME_HEAD: "文件名",
MD_TRANSCLUDE_WIDTH_DESC:
"MD-Embed 的宽度。该选项会影响到折行,以及图像元素的宽度。<br>" +
"您可为绘图中的某个 MD-Embed 单独设置此项,方法是将绘图切换至 Markdown 模式," +
"并修改相应的 <code>[[Embed文件名#标题|宽度x最大高度]]</code>。",
"并修改相应的 <code>[[Embed 文件名#标题|宽度x最大高度]]</code>。",
MD_TRANSCLUDE_HEIGHT_NAME:
"MD-Embed 的默认最大高度",
MD_TRANSCLUDE_HEIGHT_DESC:
"MD-Embed 的高度取决于 Markdown 文档内容的多少,但最大不会超过该值。<br>" +
"您可为绘图中的某个 MD-Embed 单独设置此项,方法是将绘图切换至 Markdown 模式,并修改相应的 <code>[[Embed文件名#^块引ID|宽度x最大高度]]</code>。",
"您可为绘图中的某个 MD-Embed 单独设置此项,方法是将绘图切换至 Markdown 模式,并修改相应的 <code>[[Embed 文件名#^块引ID|宽度x最大高度]]</code>。",
MD_DEFAULT_FONT_NAME:
"MD-Embed 的默认字体",
MD_DEFAULT_FONT_DESC:
@@ -400,11 +496,11 @@ FILENAME_HEAD: "文件名",
MD_CSS_DESC:
"MD-Embed 图像所采用的 CSS 样式表文件名。需包含扩展名,例如 md-embed.css。" +
"允许使用 Markdown 文件(如 md-embed-css.md但其内容应符合 CSS 语法。<br>" +
"如果您要查询 CSS 所作用的 HTML 节点,请在 Obsidian 开发者控制台CTRL+SHIFT+i)中键入命令:" +
"如果您要查询 CSS 所作用的 HTML 节点,请在 Obsidian 开发者控制台Ctrl+Shift+I)中键入命令:" +
"<code>ExcalidrawAutomate.mostRecentMarkdownSVG</code> —— 这将显示 Excalidraw 最近生成的 SVG。<br>" +
"此外,在 CSS 中不能任意地设置字体,您一般只能使用系统默认的标准字体(详见 README" +
"但可以通过上面的设置来额外添加一个自定义字体。<br>" +
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的CSS文件或CSS片段</code> 的键值对。",
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的 CSS 文件或 CSS 片段</code> 的键值对。",
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图",
EMBED_DESC: `包括:嵌入到 Markdown 文档中的绘图的预览图类型SVG、PNG、源文件类型Excalidraw 绘图文件、SVG、PNG、缓存、图像大小、图像主题以及嵌入的语法等。
此外,还有自动导出 SVG 或 PNG 文件并保持与绘图文件状态同步的设置。`,
@@ -420,6 +516,10 @@ FILENAME_HEAD: "文件名",
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图缓存",
EMBED_IMAGE_CACHE_DESC: "可提高下次嵌入的速度。" +
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
SCENE_IMAGE_CACHE_NAME: "缓存场景中嵌套的 Excalidraw",
SCENE_IMAGE_CACHE_DESC: "缓存场景中嵌套的 Excalidraw 以加快场景渲染速度。这将加快渲染过程,特别是在您的场景中有深度嵌套的 Excalidraw 时。" +
"Excalidraw 将智能地尝试识别嵌套 Excalidraw 的子元素是否发生变化,并更新缓存。 " +
"如果您怀疑缓存未能正确更新,您可能需要关闭此功能。",
EMBED_IMAGE_CACHE_CLEAR: "清除缓存",
BACKUP_CACHE_CLEAR: "清除备份",
BACKUP_CACHE_CLEAR_CONFIRMATION: "该操作将删除所有绘图文件的备份。备份是绘图文件损坏时的一种补救手段。每次您打开 Obsidian 时,本插件会自动清理无用的备份。您确定要现在删除所有备份吗?",
@@ -439,7 +539,7 @@ FILENAME_HEAD: "文件名",
EMBED_PREVIEW_IMAGETYPE_DESC:
"<b>Native SVG</b>高品质、可交互。<br>" +
"<b>SVG</b>高品质、不可交互。<br>" +
"<b>PNG</b>高性能、<a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>不可交互</a>。",
"<b>PNG</b>高性能、<a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>不可交互</a>。",
PREVIEW_MATCH_OBSIDIAN_NAME: "预览图匹配 Obsidian 主题",
PREVIEW_MATCH_OBSIDIAN_DESC:
"开启此项,则当 Obsidian 处于黑暗模式时,嵌入到 Markdown 文档中的绘图的预览图也会以黑暗模式渲染;当 Obsidian 处于明亮模式时,预览图也会以明亮模式渲染。<br>" +
@@ -449,17 +549,22 @@ FILENAME_HEAD: "文件名",
"嵌入到 Markdown 文档中的绘图的预览图的默认宽度。该选项也适用于鼠标悬停时浮现的预览图。<br>" +
"您可为某个要嵌入到 Markdown 文档中的绘图文件单独设置此项," +
"方法是修改相应的内部链接格式为形如 <code>![[drawing.excalidraw|100]]</code> 或 <code>[[drawing.excalidraw|100x100]]</code>。",
EMBED_HEIGHT_NAME: "预览图的默认高度",
EMBED_HEIGHT_DESC:
"嵌入到 Markdown 文档中的绘图的预览图得默认高度。该选项也适用于实时预览编辑和阅读模式,以及悬停预览。" +
"您可以在使用 <code>![[drawing.excalidraw|100]]</code> 或者 <code>[[drawing.excalidraw|100x100]]</code>" +
"格式在嵌入图像时指定自定义高度。",
EMBED_TYPE_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令的源文件类型",
EMBED_TYPE_DESC:
"在命令面板中执行“嵌入绘图到当前 Markdown 文档中”系列命令时,要嵌入绘图文件本身,还是嵌入其 PNG 或 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本”。<br>" +
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
EMBED_MARKDOWN_COMMENT_DESC:
"Embed the link to the original Excalidraw file as a markdown link under the image, e.g.:<code>%%[[drawing.excalidraw]]%%</code>.<br>" +
"Instead of adding a markdown comment you may also select the embedded SVG or PNG line and use the command palette action: " +
"'<code>Excalidraw: Open Excalidraw drawing</code>' to open the drawing.",
"在图像下方以 Markdown 链接的形式嵌入原始 Excalidraw 文件的链接,例如:<code>%%[[drawing.excalidraw]]%%</code><br>" +
"除了添加 Markdown 注释之外,您还可以选择嵌入的 SVG PNG,并使用命令面板:" +
"'<code>Excalidraw: 打开 Excalidraw 绘图</code>'来打开该绘图",
EMBED_WIKILINK_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令产生的内部链接类型",
EMBED_WIKILINK_DESC:
"<b>开启:</b>将产生 <code>![[Wiki 链接]]</code>。<b>关闭:</b>将产生 <code>![](Markdown 链接)</code>。",
@@ -470,13 +575,17 @@ FILENAME_HEAD: "文件名",
"如果关闭,将导出透明背景的图像。",
EXPORT_PADDING_NAME: "导出的图像的空白边距",
EXPORT_PADDING_DESC:
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。<br>" +
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。对于裁剪框架引用,间距被设置为 0。<br>" +
"增加该值,可以避免在导出图像时,靠近图像边缘的图形被裁掉。<br>" +
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>excalidraw-export-padding: 5<code> 的键值对。",
EXPORT_THEME_NAME: "导出的图像匹配主题",
EXPORT_THEME_DESC:
"导出与绘图的黑暗/明亮主题匹配的图像。" +
"如果关闭,在黑暗主题下导出的图像将和明亮主题一样。",
EXPORT_EMBED_SCENE_NAME: "在导出的图片中嵌入场景",
EXPORT_EMBED_SCENE_DESC:
"在导出的图像中嵌入 Excalidraw 场景。可以通过在文件级别添加 <code>excalidraw-export-embed-scene: true/false</code> frontmatter 元数据键来覆盖此设置。" +
"此设置仅在您下次(重新)打开绘图时生效。",
EXPORT_HEAD: "导出设置",
EXPORT_SYNC_NAME:
"保持 SVG/PNG 文件名与绘图文件同步",
@@ -498,6 +607,18 @@ FILENAME_HEAD: "文件名",
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
COMPATIBILITY_HEAD: "兼容性设置",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "代码格式化Linting兼容性",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw 对 <code># Excalidraw Data</code> 下的文件结构非常敏感。文档的自动代码格式化linting可能会在 Excalidraw 数据中造成错误。" +
"虽然我已经努力使数据加载对自动代码格式化linting变更具有一定的抗性但这种解决方案并非万无一失。<br>"+
"<mark>最好的方法是避免使用不同的插件对 Excalidraw 文档进行自动更改。</mark><br>" +
"如果出于某些合理的原因,您决定忽略我的建议并配置了 Excalidraw 文件的自动代码格式化,那么可以使用这个设置<br> " +
"<code>## Text Elements</code> 部分对空行很敏感。一种常见的代码格式化是在章节标题后添加一个空行。但对于 Excalidraw 来说,这将破坏/改变您绘图中的第一个文本元素。" +
"为了解决这个问题,您可以启用这个设置。启用后 Excalidraw 将在 <code>## Text Elements</code> 的开头添加一个虚拟元素,供自动代码格式化工具修改。" ,
PRESERVE_TEXT_AFTER_DRAWING_NAME: "Zotero 兼容性",
PRESERVE_TEXT_AFTER_DRAWING_DESC: "保留 Markdown 文件中 <code>## Drawing</code> 部分之后的文本内容。保存非常大的绘图时,这可能会造成微小的性能影响。",
DEBUGMODE_NAME: "开启 debug 信息",
DEBUGMODE_DESC: "我建议在启用/禁用此设置后重新启动 Obsidian。这将在控制台中启用调试消息。这对于排查问题很有帮助。" +
"如果您在使用插件时遇到问题,请启用此设置,重现问题,并在 <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/issues'>GitHub</a> 上提出的问题中包含控制台日志。",
SLIDING_PANES_NAME: "Sliding panes 插件支持",
SLIDING_PANES_DESC:
"设置此项后需要重启 Obsidian 才能生效。<br>" +
@@ -525,8 +646,13 @@ FILENAME_HEAD: "文件名",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
LATEX_DEFAULT_DESC: "允许留空。允许使用类似 <code>\\color{white}</code> 的格式化表达式。",
NONSTANDARD_HEAD: "非 Excalidraw.com 官方支持的特性",
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果 Excalidraw.com 格式导出绘图,这些特性将会发生不可预知的变化。
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果 Excalidraw.com 导入绘图,这些特性将会发生不可预知的变化。
包括:自定义画笔工具的数量,自定义字体等。`,
RENDER_TWEAK_HEAD: "渲染优化",
MAX_IMAGE_ZOOM_IN_NAME: "最大图像放大倍数",
MAX_IMAGE_ZOOM_IN_DESC: "为了节省内存,并且因为 Apple Safari (Obsidian on iOS) 有一些硬编码的限制Excalidraw.com 在放大时会限制图像和大型对象的最大分辨率。您可以使用乘数来覆盖这个限制。" +
"这意味着将乘以 Excalidraw 默认设置的限制,乘数越大,图像放大分辨率就越高,但也会消耗更多内存。" +
"我建议尝试多个值来设置这个参数。当您放大一个较大的 PNG 图像时,如果图像突然从视图中消失,那就说明您已经达到了极限。默认值为 1。此设置对 iOS 无效。",
CUSTOM_PEN_HEAD: "自定义画笔",
CUSTOM_PEN_NAME: "自定义画笔工具的数量",
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单按钮旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
@@ -561,7 +687,17 @@ FILENAME_HEAD: "文件名",
"开启此项,则可在 Obsidian 实时预览模式的编辑视图下,用形如 <code>![[绘图|宽度|样式]]</code> 的语法来嵌入绘图。<br>" +
"该选项不会在已打开的文档中立刻生效 —— " +
"你需要重新打开此文档来使其生效。",
CUSTOM_FONT_HEAD: "自定义字体",
FADE_OUT_EXCALIDRAW_MARKUP_NAME: "淡化 Excalidraw 标记",
FADE_OUT_EXCALIDRAW_MARKUP_DESC: "在 Markdown 视图模式下,在 Markdown 注释 %% " +
"之后的部分会淡化。文本仍然存在,但视觉杂乱感会减少。请注意,您可以将 %% 放在 # Text Elements 行的上一行," +
"这样,整个 Excalidraw Markdown 都会淡化,包括 # Text Elements。 副作用是您将无法在其他 Markdown 笔记中引用文本块,即 %% 注释部分之后的内容。这应该不是大问题。" +
"如果您想编辑 Excalidraw Markdown 脚本,只需切换到 Markdown 视图模式并暂时删除 %% 注释。",
EXCALIDRAW_PROPERTIES_NAME: "将 Excalidraw 属性加载到 Obsidian 的自动提示中",
EXCALIDRAW_PROPERTIES_DESC: "切换此设置以在插件启动时将 Excalidraw 文档属性加载到 Obsidian 的属性自动提示中。"+
"启用此功能简化了 Excalidraw 前置属性的使用,使您能够利用许多强大的设置。如果您不希望自动加载这些属性," +
"您可以禁用此功能,但您将需要手动从自动提示中移除任何不需要的属性。" +
"请注意,启用此设置需要重启插件,因为属性是在启动时加载的。",
CUSTOM_FONT_HEAD: "本地字体",
ENABLE_FOURTH_FONT_NAME: "为文本元素启用本地字体",
ENABLE_FOURTH_FONT_DESC:
"开启此项后,文本元素的属性面板里会多出一个本地字体按钮。<br>" +
@@ -570,7 +706,7 @@ FILENAME_HEAD: "文件名",
"若在 excalidraw.com 或者其他版本的 Excalidraw 中打开,使用本地字体的文本会变回系统默认字体。",
FOURTH_FONT_NAME: "本地字体文件",
FOURTH_FONT_DESC:
"选择库文件夹中的一个 .ttf, .woff 或 .woff2 字体文件作为本地字体文件。" +
"选择库文件夹中的一个 .ttf.woff 或 .woff2 字体文件作为本地字体文件。" +
"若未选择文件,则使用默认的 Virgil 字体。",
SCRIPT_SETTINGS_HEAD: "已安装脚本的设置",
SCRIPT_SETTINGS_DESC: "有些 Excalidraw 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
@@ -581,7 +717,7 @@ FILENAME_HEAD: "文件名",
"注意,识别的过程不是在本地进行的,而是通过在线 API图像会被上传到 taskbone 的服务器(仅用于识别目的)。如果您介意,请不要使用这个功能。",
TASKBONE_ENABLE_NAME: "启用 Taskbone",
TASKBONE_ENABLE_DESC: "启用意味着您同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>.",
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>",
TASKBONE_APIKEY_NAME: "Taskbone API Key",
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
"Taskbone 的开发者您懂的没有人能用爱发电Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
@@ -590,7 +726,7 @@ FILENAME_HEAD: "文件名",
//openDrawings.ts
SELECT_FILE: "选择一个文件后按回车。",
SELECT_COMMAND: "选择一个命令后按回车。",
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+ENTER 以 100% 尺寸插入。`,
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+Enter 以 100% 尺寸插入。`,
NO_MATCH: "查询不到匹配的文件。",
NO_MATCHING_COMMAND: "查询不到匹配的命令。",
SELECT_FILE_TO_LINK: "选择要插入(以内部链接形式嵌入)到当前绘图中的文件。",
@@ -605,6 +741,13 @@ FILENAME_HEAD: "文件名",
PDF_PAGES_HEADER: "页码范围",
PDF_PAGES_DESC: "示例1, 3-5, 7, 9-11",
//SelectCard.ts
TYPE_SECTION: "输入章节名称(标题)进行选择",
SELECT_SECTION_OR_TYPE_NEW:
"选择现有章节(标题)或输入新章节(标题)的名称,然后按 Enter。",
INVALID_SECTION_NAME: "无效的章节名称(标题)",
EMPTY_SECTION_MESSAGE: "输入章节(标题)名称以创建",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING:
"EXCALIDRAW 警告\n停止加载嵌入的图像因为此文件中存在死循环\n",
@@ -677,11 +820,19 @@ FILENAME_HEAD: "文件名",
PROMPT_BUTTON_INSERT_SPACE: "插入空格",
PROMPT_BUTTON_INSERT_LINK: "插入内部链接",
PROMPT_BUTTON_UPPERCASE: "大写",
PROMPT_SELECT_TEMPLATE: "选择一个模板",
//ModifierKeySettings
WEB_BROWSER_DRAG_ACTION: "从浏览器拖进来时",
LOCAL_FILE_DRAG_ACTION: "从本地文件系统拖进来时",
INTERNAL_DRAG_ACTION: "在 Obsidian 内部拖放时",
PANE_TARGET: "点击链接时",
DEFAULT_ACTION_DESC: "无修饰键时的行为:",
//FrameSettings.ts
FRAME_SETTINGS_TITLE: "框架设置",
FRAME_SETTINGS_ENABLE: "启用框架",
FRAME_SETTIGNS_NAME: "显示框架名称",
FRAME_SETTINGS_OUTLINE: "显示框架外边框",
FRAME_SETTINGS_CLIP: "启用框架裁剪",
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
import * as React from "react";
import ExcalidrawView from "../ExcalidrawView";
import { Notice } from "obsidian";
type ButtonProps = {
title: string;
action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
longpress?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
icon: JSX.Element;
view: ExcalidrawView;
};
type ButtonState = {
@@ -24,6 +23,10 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
};
}
componentWillUnmount(): void {
this.render = () => null;
}
render() {
return (
<button
@@ -48,7 +51,8 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
onPointerDown={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
this.toastMessageTimeout = window.setTimeout(
() => {
this.props.view.excalidrawAPI?.setToast({message:this.props.title, duration: 3000, closable: true});
new Notice(this.props.title, 3000);
//this.props.view.excalidrawAPI?.setToast({message:this.props.title, duration: 3000, closable: true});
this.toastMessageTimeout = 0;
},
400,
@@ -58,7 +62,8 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
if(this.props.longpress) {
this.props.longpress(event);
} else {
this.props.view.excalidrawAPI?.setToast({message:"Cannot pin this action", duration: 3000, closable: true});
new Notice("Cannot pin this action", 3000);
//this.props.view.excalidrawAPI?.setToast({message:"Cannot pin this action", duration: 3000, closable: true});
}
this.longpressTimeout = 0;
},

File diff suppressed because one or more lines are too long

View File

@@ -17,6 +17,8 @@ import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
export class EmbeddableMenu {
private menuFadeTimeout: number = 0;
private menuElementId: string = null;
constructor(
private view:ExcalidrawView,
@@ -24,6 +26,19 @@ export class EmbeddableMenu {
) {
}
public destroy() {
if(this.menuFadeTimeout) {
clearTimeout(this.menuFadeTimeout);
this.menuFadeTimeout = null;
}
this.view = null;
this.containerRef = null;
this.updateElement = null;
this.handleMouseEnter = null;
this.handleMouseLeave = null;
this.renderButtons = null;
}
private updateElement = (subpath: string, element: ExcalidrawEmbeddableElement, file: TFile) => {
if(!element) return;
const view = this.view;
@@ -39,26 +54,119 @@ export class EmbeddableMenu {
const ea = getEA(view) as ExcalidrawAutomate;
ea.copyViewElementsToEAforEditing([element]);
ea.getElement(element.id).link = link;
//mutateElement (element,{link});
//view.setDirty(99);
view.excalidrawData.elementLinks.set(element.id, link);
ea.addElementsToView(false, true, true);
ea.addElementsToView(false, true, true).then(() => ea.destroy());
}
private menuFadeTimeout: number = 0;
private menuElementId: string = null;
private handleMouseEnter () {
clearTimeout(this.menuFadeTimeout);
this.containerRef.current?.style.setProperty("opacity", "1");
};
private handleMouseLeave () {
const self = this;
this.menuFadeTimeout = window.setTimeout(() => {
self.containerRef.current?.style.setProperty("opacity", "0.2");
this.containerRef.current?.style.setProperty("opacity", "0.2");
}, 5000);
};
private async actionMarkdownSelection (file: TFile, isExcalidrawFile: boolean, subpath: string, element: ExcalidrawEmbeddableElement) {
this.view.updateScene({appState: {activeEmbeddable: null}, storeAction: "update"});
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
let values, display;
if(isExcalidrawFile) {
values = sections.map((b: any) => `#${cleanSectionHeading(b.display)}`);
display = sections.map((b: any) => b.display);
} else {
values = [""].concat(
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
);
display = [t("SHOW_ENTIRE_FILE")].concat(
sections.map((b: any) => b.display)
);
}
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
this.updateElement(newSubpath, element, file);
}
}
private async actionMarkdownBlock (file: TFile, subpath: string, element: ExcalidrawEmbeddableElement) {
if(!file) return;
this.view.updateScene({appState: {activeEmbeddable: null}, storeAction: "update"});
const paragraphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node &&
(b.node.type === "paragraph" || b.node.type === "blockquote" || b.node.type === "listItem" || b.node.type === "table" || b.node.type === "callout")
);
const values = ["entire-file"].concat(paragraphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!selectedBlock) return;
if(selectedBlock==="entire-file") {
if(subpath==="") return;
this.updateElement("", element, file);
return;
}
let blockID = selectedBlock.node.id;
if(blockID && (`#^${blockID}` === subpath)) return;
if (!blockID) {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
if(!fileContents) return;
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
}
private actionZoomToElement (element: ExcalidrawEmbeddableElement, maxLevel?: number) {
if(!element) return;
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
api.zoomToFit([element], maxLevel ?? this.view.plugin.settings.zoomToFitMaxLevel, 0.1);
}
private actionProperties (element: ExcalidrawEmbeddableElement, file: TFile) {
if(!element) return;
new EmbeddableSettings(this.view.plugin,this.view,file,element).open();
}
private actionCrop (element: ExcalidrawEmbeddableElement) {
if(!element) return;
//@ts-ignore
this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
}
private actionReload (iframe: HTMLIFrameElement, link: string) {
iframe.src = link;
}
private actionOpen (iframe: HTMLIFrameElement, element: ExcalidrawEmbeddableElement) {
openExternalLink(
!iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
? iframe.src
: element.link,
this.view.app
);
}
private actionCopyCode (element: ExcalidrawEmbeddableElement, link: string) {
if(!element) return;
navigator.clipboard.writeText(atob(link.split(",")[1]));
}
renderButtons(appState: AppState) {
const view = this.view;
@@ -105,6 +213,7 @@ export class EmbeddableMenu {
if(!file) return;
const isMD = file.extension==="md";
const isExcalidrawFile = view.plugin.isExcalidrawFile(file);
const isPDF = file.extension==="pdf";
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
const left = `${x-appState.offsetLeft}px`;
@@ -133,98 +242,38 @@ export class EmbeddableMenu {
<ActionButton
key={"MarkdownSection"}
title={t("NARROW_TO_HEADING")}
action={async () => {
view.updateScene({appState: {activeEmbeddable: null}});
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
let values, display;
if(isExcalidrawFile) {
values = sections.map((b: any) => `#${cleanSectionHeading(b.display)}`);
display = sections.map((b: any) => b.display);
} else {
values = [""].concat(
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
);
display = [t("SHOW_ENTIRE_FILE")].concat(
sections.map((b: any) => b.display)
);
}
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
this.updateElement(newSubpath, element, file);
}
}}
action={async () => this.actionMarkdownSelection(file, isExcalidrawFile, subpath, element)}
icon={ICONS.ZoomToSection}
view={view}
/>
)}
{isMD && !isExcalidrawFile && (
<ActionButton
key={"MarkdownBlock"}
title={t("NARROW_TO_BLOCK")}
action={async () => {
if(!file) return;
view.updateScene({appState: {activeEmbeddable: null}});
const paragraphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
const values = ["entire-file"].concat(paragraphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!selectedBlock) return;
if(selectedBlock==="entire-file") {
if(subpath==="") return;
this.updateElement("", element, file);
return;
}
let blockID = selectedBlock.node.id;
if(blockID && (`#^${blockID}` === subpath)) return;
if (!blockID) {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
if(!fileContents) return;
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
}}
action={async () => this.actionMarkdownBlock(file, subpath, element)}
icon={ICONS.ZoomToBlock}
view={view}
/>
)}
<ActionButton
key={"ZoomToElement"}
title={t("ZOOM_TO_FIT")}
action={() => {
if(!element) return;
api.zoomToFit([element], 30, 0.1);
}}
action={() => this.actionZoomToElement(element,30)}
icon={ICONS.ZoomToSelectedElement}
view={view}
/>
<ActionButton
key={"Properties"}
title={t("PROPERTIES")}
action={() => {
if(!element) return;
new EmbeddableSettings(view.plugin,view,file,element).open();
}}
action={() => this.actionProperties(element, file)}
icon={ICONS.Properties}
view={view}
/>
{isPDF && (
<ActionButton
key={"Crop"}
title={t("CROP_PAGE")}
action={() => this.actionCrop(element)}
icon={ICONS.Crop}
/>
)}
</div>
</div>
);
@@ -262,57 +311,34 @@ export class EmbeddableMenu {
<ActionButton
key={"Reload"}
title={t("RELOAD")}
action={()=>{
iframe.src = link;
}}
action={()=> this.actionReload(iframe, link)}
icon={ICONS.Reload}
view={view}
/>
)}
<ActionButton
key={"Open"}
title={t("OPEN_IN_BROWSER")}
action={() => {
openExternalLink(
!iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
? iframe.src
: element.link,
view.app
);
}}
action={() => this.actionOpen(iframe, element)}
icon={ICONS.Globe}
view={view}
/>
<ActionButton
key={"ZoomToElement"}
title={t("ZOOM_TO_FIT")}
action={() => {
if(!element) return;
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
}}
action={() => this.actionZoomToElement(element)}
icon={ICONS.ZoomToSelectedElement}
view={view}
/>
<ActionButton
key={"Properties"}
title={t("PROPERTIES")}
action={() => {
if(!element) return;
new EmbeddableSettings(view.plugin,view,null,element).open();
}}
action={() => this.actionProperties(element, null)}
icon={ICONS.Properties}
view={view}
/>
{link?.startsWith("data:text/html") && (
<ActionButton
key={"CopyCode"}
title={t("COPYCODE")}
action={() => {
if(!element) return;
navigator.clipboard.writeText(atob(link.split(",")[1]));
}}
action={() => this.actionCopyCode(element, link)}
icon={ICONS.Copy}
view={view}
/>
)}
</div>

View File

@@ -14,7 +14,7 @@ import { t } from "src/lang/helpers";
declare const PLUGIN_VERSION:string;
export const setPen = (pen: PenStyle, api: any) => {
export function setPen (pen: PenStyle, api: any) {
const st = api.getAppState();
api.updateScene({
appState: {
@@ -34,11 +34,12 @@ export const setPen = (pen: PenStyle, api: any) => {
currentItemRoughness: st.currentItemRoughness,
}}
: null,
}
},
storeAction: "update",
})
}
export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) => {
export function resetStrokeOptions (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) {
api.updateScene({
appState: {
...resetCustomPen ? {
@@ -50,13 +51,16 @@ export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperative
}: null,
resetCustomPen: null,
...clearCurrentStrokeOptions ? {currentStrokeOptions: null} : null,
}
},
storeAction: "update",
});
}
export class ObsidianMenu {
private clickTimestamp:number[];
private activePen: PenStyle;
private longpressTimeout : { [key: number]: number } = {};
private prevClickTimestamp: number = 0;
constructor(
private plugin: ExcalidrawPlugin,
private toolsRef: React.MutableRefObject<any>,
@@ -65,9 +69,98 @@ export class ObsidianMenu {
this.clickTimestamp = Array.from({length: Object.keys(PENS).length}, () => 0);
}
renderCustomPens = (isMobile: boolean, appState: AppState) => {
private actionCustomPenLabelClick(index: number, pen: PenStyle) {
const now = Date.now();
const dblClick = now-this.clickTimestamp[index] < 500;
//open pen settings on double click
if(dblClick) {
const penSettings = new PenSettingsModal(this.plugin,this.view,index);
(async () => {
await this.plugin.loadSettings();
penSettings.open();
})();
return;
}
this.clickTimestamp[index] = now;
const api = this.view.excalidrawAPI;
const st = api.getAppState();
//single second click to reset freedraw to default
if(st.currentStrokeOptions === pen.penOptions && st.activeTool.type === "freedraw") {
resetStrokeOptions(st.resetCustomPen, api, true);
return;
}
//apply pen settings to canvas
this.activePen = {...pen};
setPen(pen,api);
api.setActiveTool({type:"freedraw"});
}
private actionScriptButtonPonterUp(index: number, key: string) {
if(this.longpressTimeout[index]) {
this.view.ownerWindow.clearTimeout(this.longpressTimeout[index]);
this.longpressTimeout[index] = 0;
(async ()=>{
const f = this.view.app.vault.getAbstractFileByPath(key);
if (f && f instanceof TFile) {
this.plugin.scriptEngine.executeScript(
this.view,
await this.view.app.vault.read(f),
this.plugin.scriptEngine.getScriptName(f),
f
);
}
})()
}
}
private actionScriptButtonPointerDown(index: number, key: string) {
const now = Date.now();
if(this.longpressTimeout[index]>0) {
this.view.ownerWindow.clearTimeout(this.longpressTimeout[index]);
this.longpressTimeout[index] = 0;
}
if(now-this.prevClickTimestamp >= 500) {
this.longpressTimeout[index] = this.view.ownerWindow.setTimeout(
() => {
this.longpressTimeout[index] = 0;
(async () =>{
await this.plugin.loadSettings();
const index = this.plugin.settings.pinnedScripts.indexOf(key)
if(index > -1) {
this.plugin.settings.pinnedScripts.splice(index,1);
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
}
await this.plugin.saveSettings();
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
})
})()
},
1500
)
}
this.prevClickTimestamp = now;
}
private actionShowHideMenu (isMobile: boolean, appState: AppState) {
this.toolsRef.current.setTheme(appState.theme);
this.toolsRef.current.toggleVisibility(
appState.zenModeEnabled || isMobile,
);
}
private actionInsertAnyFile() {
this.view.setCurrentPositionToCenter();
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
insertFileModal.open();
}
public renderCustomPens (isMobile: boolean, appState: AppState) {
return(
appState.customPens?.map((key,index)=>{
appState.customPens?.map((_,index)=>{
const pen = this.plugin.settings.customPens[index]
//Reset stroke setting when changing to a different tool
if(
@@ -75,7 +168,7 @@ export class ObsidianMenu {
appState.activeTool.type !== "freedraw" &&
appState.currentStrokeOptions === pen.penOptions
) {
setTimeout(()=> resetStrokeOptions(appState.resetCustomPen, this.view.excalidrawAPI, false))
setTimeout(()=> resetStrokeOptions(appState.resetCustomPen, this.view.excalidrawAPI, false));
}
//if Pen settings are loaded, select custom pen when activating the freedraw element
if (
@@ -111,34 +204,7 @@ export class ObsidianMenu {
"is-mobile": isMobile,
},
)}
onClick={() => {
const now = Date.now();
const dblClick = now-this.clickTimestamp[index] < 500;
//open pen settings on double click
if(dblClick) {
const penSettings = new PenSettingsModal(this.plugin,this.view,index);
(async () => {
await this.plugin.loadSettings();
penSettings.open();
})();
return;
}
this.clickTimestamp[index] = now;
const api = this.view.excalidrawAPI;
const st = api.getAppState();
//single second click to reset freedraw to default
if(st.currentStrokeOptions === pen.penOptions && st.activeTool.type === "freedraw") {
resetStrokeOptions(st.resetCustomPen, api, true);
return;
}
//apply pen settings to canvas
this.activePen = {...pen};
setPen(pen,api);
api.setActiveTool({type:"freedraw"});
}}
onClick={ this.actionCustomPenLabelClick.bind(this,index, pen) }
>
<div
className="ToolIcon__icon"
@@ -157,10 +223,7 @@ export class ObsidianMenu {
)
}
private longpressTimeout : { [key: number]: number } = {};
renderPinnedScriptButtons = (isMobile: boolean, appState: AppState) => {
let prevClickTimestamp = 0;
public renderPinnedScriptButtons (isMobile: boolean, appState: AppState) {
return (
appState?.pinnedScripts?.map((key,index)=>{ //pinned scripts
const scriptProp = this.plugin.scriptEngine.scriptIconMap[key];
@@ -179,51 +242,8 @@ export class ObsidianMenu {
"is-mobile": isMobile,
},
)}
onPointerUp={() => {
if(this.longpressTimeout[index]) {
window.clearTimeout(this.longpressTimeout[index]);
this.longpressTimeout[index] = 0;
(async ()=>{
const f = app.vault.getAbstractFileByPath(key);
if (f && f instanceof TFile) {
this.plugin.scriptEngine.executeScript(
this.view,
await app.vault.read(f),
this.plugin.scriptEngine.getScriptName(f),
f
);
}
})()
}
}}
onPointerDown={()=>{
const now = Date.now();
if(this.longpressTimeout[index]>0) {
window.clearTimeout(this.longpressTimeout[index]);
this.longpressTimeout[index] = 0;
}
if(now-prevClickTimestamp >= 500) {
this.longpressTimeout[index] = window.setTimeout(
() => {
this.longpressTimeout[index] = 0;
(async () =>{
await this.plugin.loadSettings();
const index = this.plugin.settings.pinnedScripts.indexOf(key)
if(index > -1) {
this.plugin.settings.pinnedScripts.splice(index,1);
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
}
await this.plugin.saveSettings();
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
})
})()
},
1500
)
}
prevClickTimestamp = now;
}}
onPointerUp={this.actionScriptButtonPonterUp.bind(this,index,key)}
onPointerDown={this.actionScriptButtonPointerDown.bind(this,index,key)}
>
<div
className="ToolIcon__icon"
@@ -237,7 +257,7 @@ export class ObsidianMenu {
)
}
renderButton = (isMobile: boolean, appState: AppState) => {
public renderButton (isMobile: boolean, appState: AppState) {
return (
<>
<label
@@ -248,12 +268,7 @@ export class ObsidianMenu {
"is-mobile": isMobile,
},
)}
onClick={() => {
this.toolsRef.current.setTheme(appState.theme);
this.toolsRef.current.toggleVisibility(
appState.zenModeEnabled || isMobile,
);
}}
onClick={this.actionShowHideMenu.bind(this,isMobile,appState)}
>
<div className="ToolIcon__icon" aria-label={t("OBSIDIAN_TOOLS_PANEL")}>
{ICONS.obsidian}
@@ -267,11 +282,7 @@ export class ObsidianMenu {
"is-mobile": isMobile,
},
)}
onClick={() => {
this.view.setCurrentPositionToCenter();
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
insertFileModal.open();
}}
onClick={this.actionInsertAnyFile.bind(this)}
>
<div className="ToolIcon__icon" aria-label={t("UNIVERSAL_ADD_FILE")}>
{ICONS["add-file"]}
@@ -282,4 +293,19 @@ export class ObsidianMenu {
</>
);
};
destroy() {
Object.values(this.longpressTimeout).forEach(
t=>this.view.ownerWindow.clearTimeout(t)
);
this.longpressTimeout = {};
this.activePen = null;
this.plugin = null;
this.toolsRef = null;
this.view = null;
this.clickTimestamp = null;
this.renderButton = null;
this.renderCustomPens = null;
this.renderPinnedScriptButtons = null;
}
}

View File

@@ -16,6 +16,7 @@ import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
import { ExportDialog } from "src/dialogs/ExportDialog";
import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal";
import { DEBUGGING, debug } from "src/utils/DebugHelper";
declare const PLUGIN_VERSION:string;
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
@@ -23,8 +24,9 @@ const light = '<svg style="stroke:#212529;color:#212529;fill:#212529" ';
type PanelProps = {
visible: boolean;
view: ExcalidrawView;
view: WeakRef<ExcalidrawView>;
centerPointer: Function;
observer: WeakRef<ResizeObserver>;
};
export type PanelState = {
@@ -37,7 +39,7 @@ export type PanelState = {
isDirty: boolean;
isFullscreen: boolean;
isPreviewMode: boolean;
scriptIconMap: ScriptIconMap;
scriptIconMap: ScriptIconMap | null;
};
const TOOLS_PANEL_WIDTH = 228;
@@ -54,10 +56,22 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
onRightEdge: boolean = false;
onBottomEdge: boolean = false;
public containerRef: React.RefObject<HTMLDivElement>;
private view: ExcalidrawView;
componentWillUnmount(): void {
if (this.containerRef.current) {
this.props.observer.deref()?.unobserve(this.containerRef.current);
}
this.setState({ scriptIconMap: null });
this.containerRef = null;
this.view = null;
}
constructor(props: PanelProps) {
super(props);
const react = props.view.plugin.getPackage(props.view.ownerWindow).react;
this.view = props.view.deref();
const react = this.view.packages.react;
this.containerRef = react.createRef();
this.state = {
visible: props.visible,
@@ -74,12 +88,14 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
updateScriptIconMap(scriptIconMap: ScriptIconMap) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()");
this.setState(() => {
return { scriptIconMap };
});
}
setPreviewMode(isPreviewMode: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()");
this.setState(() => {
return {
isPreviewMode,
@@ -88,6 +104,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
setFullscreen(isFullscreen: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()");
this.setState(() => {
return {
isFullscreen,
@@ -96,6 +113,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
setDirty(isDirty: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()");
this.setState(()=> {
return {
isDirty,
@@ -104,6 +122,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
setExcalidrawViewMode(isViewModeEnabled: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()");
this.setState(() => {
return {
excalidrawViewMode: isViewModeEnabled,
@@ -112,6 +131,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
toggleVisibility(isMobileOrZen: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()");
this.setTopCenter(isMobileOrZen);
this.setState((prevState: PanelState) => {
return {
@@ -121,6 +141,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
setTheme(theme: "dark" | "light") {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()");
this.setState((prevState: PanelState) => {
return {
theme,
@@ -129,6 +150,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
setTopCenter(isMobileOrZen: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()");
this.setState(() => {
return {
left:
@@ -144,6 +166,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
updatePosition(deltaY: number = 0, deltaX: number = 0) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()");
this.setState(() => {
const {
offsetTop,
@@ -184,7 +207,234 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
});
}
actionOpenScriptInstallDialog() {
new ScriptInstallPrompt(this.view.plugin).open();
}
actionOpenReleaseNotes() {
new ReleaseNotes(
this.view.app,
this.view.plugin,
PLUGIN_VERSION,
).open();
}
actionConvertExcalidrawToMD() {
this.view.convertExcalidrawToMD();
}
actionToggleViewMode() {
if (this.state.isPreviewMode) {
this.view.changeTextMode(TextMode.raw);
} else {
this.view.changeTextMode(TextMode.parsed);
}
}
actionToggleTrayMode() {
this.view.toggleTrayMode();
}
actionToggleFullscreen() {
if (this.state.isFullscreen) {
this.view.exitFullscreen();
} else {
this.view.gotoFullscreen();
}
}
actionSearch() {
search(this.view);
}
actionOCR(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if(!this.view.plugin.settings.taskboneEnabled) {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return;
}
this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)});
}
actionOpenLink(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
const event = new MouseEvent("click", {
ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS),
metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
shiftKey: e.shiftKey,
altKey: e.altKey,
});
this.view.handleLinkClick(event);
}
actionOpenLinkProperties() {
const event = new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
shiftKey: false,
altKey: false,
});
this.view.handleLinkClick(event);
}
actionForceSave() {
this.view.forceSave();
}
actionExportLibrary() {
this.view.plugin.exportLibrary();
}
actionExportImage() {
const view = this.view;
if(!view.exportDialog) {
view.exportDialog = new ExportDialog(view.plugin, view,view.file);
view.exportDialog.createForm();
}
view.exportDialog.open();
}
actionOpenAsMarkdown() {
this.view.openAsMarkdown();
}
actionLinkToElement(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if(isWinALTorMacOPT(e)) {
openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app);
return;
}
this.view.copyLinkToSelectedElementToClipboard(
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
);
}
actionAddAnyFile() {
this.props.centerPointer();
const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view);
insertFileModal.open();
}
actionInsertImage() {
this.props.centerPointer();
this.view.plugin.insertImageDialog.start(
this.view,
);
}
actionInsertPDF() {
this.props.centerPointer();
const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view);
insertPDFModal.open();
}
actionInsertMarkdown() {
this.props.centerPointer();
this.view.plugin.insertMDDialog.start(
this.view,
);
}
actionInsertBackOfNote() {
this.props.centerPointer();
this.view.insertBackOfTheNoteCard();
}
actionInsertLaTeX(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if(isWinALTorMacOPT(e)) {
openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app);
return;
}
this.props.centerPointer();
insertLaTeXToView(this.view);
}
actionInsertLink() {
this.props.centerPointer();
this.view.plugin.insertLinkDialog.start(
this.view.file.path,
(text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save),
);
}
actionImportSVG(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
this.view.plugin.importSVGDialog.start(this.view);
}
actionCropImage(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
// @ts-ignore
this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
}
async actionRunScript(key: string) {
const view = this.view;
const plugin = view.plugin;
const f = app.vault.getAbstractFileByPath(key);
if (f && f instanceof TFile) {
plugin.scriptEngine.executeScript(
view,
await app.vault.read(f),
plugin.scriptEngine.getScriptName(f),
f
);
}
}
async actionPinScript(key: string, scriptName: string) {
const view = this.view;
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
const plugin = view.plugin;
await plugin.loadSettings();
const index = plugin.settings.pinnedScripts.indexOf(key)
if(index > -1) {
plugin.settings.pinnedScripts.splice(index,1);
api?.setToast({message:`Pin removed: ${scriptName}`, duration: 3000, closable: true});
} else {
plugin.settings.pinnedScripts.push(key);
api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true})
}
await plugin.saveSettings();
plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
})
}
private islandOnClick(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
event.preventDefault();
if (
Math.abs(this.penDownX - this.pos3) > 5 ||
Math.abs(this.penDownY - this.pos4) > 5
) {
return;
}
this.setState((prevState: PanelState) => {
return {
minimized: !prevState.minimized,
};
});
}
private islandOnPointerDown(event: React.PointerEvent) {
const onDrag = (e: PointerEvent) => {
e.preventDefault();
this.pos1 = this.pos3 - e.clientX;
this.pos2 = this.pos4 - e.clientY;
this.pos3 = e.clientX;
this.pos4 = e.clientY;
this.updatePosition(this.pos2, this.pos1);
};
const onPointerUp = () => {
this.view.ownerDocument?.removeEventListener("pointerup", onPointerUp);
this.view.ownerDocument?.removeEventListener("pointermove", onDrag);
};
event.preventDefault();
this.penDownX = this.pos3 = event.clientX;
this.penDownY = this.pos4 = event.clientY;
this.view.ownerDocument.addEventListener("pointerup", onPointerUp);
this.view.ownerDocument.addEventListener("pointermove", onDrag);
};
render() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()");
return (
<div
ref={this.containerRef}
@@ -219,41 +469,8 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
width: "100%",
cursor: "move",
}}
onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault();
if (
Math.abs(this.penDownX - this.pos3) > 5 ||
Math.abs(this.penDownY - this.pos4) > 5
) {
return;
}
this.setState((prevState: PanelState) => {
return {
minimized: !prevState.minimized,
};
});
}}
onPointerDown={(event: React.PointerEvent) => {
const onDrag = (e: PointerEvent) => {
e.preventDefault();
this.pos1 = this.pos3 - e.clientX;
this.pos2 = this.pos4 - e.clientY;
this.pos3 = e.clientX;
this.pos4 = e.clientY;
this.updatePosition(this.pos2, this.pos1);
};
const onPointerUp = () => {
this.props.view.ownerDocument?.removeEventListener("pointerup", onPointerUp);
this.props.view.ownerDocument?.removeEventListener("pointermove", onDrag);
};
event.preventDefault();
this.penDownX = this.pos3 = event.clientX;
this.penDownY = this.pos4 = event.clientY;
this.props.view.ownerDocument.addEventListener("pointerup", onPointerUp);
this.props.view.ownerDocument.addEventListener("pointermove", onDrag);
}}
onClick={this.islandOnClick.bind(this)}
onPointerDown={this.islandOnPointerDown.bind(this)}
>
<svg
aria-hidden="true"
@@ -286,62 +503,39 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
<ActionButton
key={"scriptEngine"}
title={t("INSTALL_SCRIPT_BUTTON")}
action={() => {
new ScriptInstallPrompt(this.props.view.plugin).open();
}}
action={this.actionOpenScriptInstallDialog.bind(this)}
icon={ICONS.scriptEngine}
view={this.props.view}
/>
<ActionButton
key={"release-notes"}
title={t("READ_RELEASE_NOTES")}
action={() => {
new ReleaseNotes(
this.props.view.app,
this.props.view.plugin,
PLUGIN_VERSION,
).open();
}}
action={this.actionOpenReleaseNotes.bind(this)}
icon={ICONS.releaseNotes}
view={this.props.view}
/>
{this.state.isPreviewMode === null ? (
<ActionButton
key={"convert"}
title={t("CONVERT_FILE")}
action={() => {
this.props.view.convertExcalidrawToMD();
}}
action={(this.actionConvertExcalidrawToMD.bind(this))}
icon={ICONS.convertFile}
view={this.props.view}
/>
) : (
<ActionButton
key={"viewmode"}
title={this.state.isPreviewMode ? t("PARSED") : t("RAW")}
action={() => {
if (this.state.isPreviewMode) {
this.props.view.changeTextMode(TextMode.raw);
} else {
this.props.view.changeTextMode(TextMode.parsed);
}
}}
action={this.actionToggleViewMode.bind(this)}
icon={
this.state.isPreviewMode
? ICONS.rawMode
: ICONS.parsedMode
}
view={this.props.view}
/>
)}
<ActionButton
key={"tray-mode"}
title={t("TRAY_MODE")}
action={() => {
this.props.view.toggleTrayMode();
}}
action={this.actionToggleTrayMode.bind(this)}
icon={ICONS.trayMode}
view={this.props.view}
/>
<ActionButton
key={"fullscreen"}
@@ -350,80 +544,42 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
? t("EXIT_FULLSCREEN")
: t("GOTO_FULLSCREEN")
}
action={() => {
if (this.state.isFullscreen) {
this.props.view.exitFullscreen();
} else {
this.props.view.gotoFullscreen();
}
}}
action={this.actionToggleFullscreen.bind(this)}
icon={
this.state.isFullscreen
? ICONS.exitFullScreen
: ICONS.gotoFullScreen
}
view={this.props.view}
/>
<ActionButton
key={"search"}
title={t("SEARCH")}
action={() => {
search(this.props.view);
}}
action={this.actionSearch.bind(this)}
icon={ICONS.search}
view={this.props.view}
/>
<ActionButton
key={"ocr"}
title={t("RUN_OCR")}
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if(!this.props.view.plugin.settings.taskboneEnabled) {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return;
}
this.props.view.plugin.taskbone.getTextForView(this.props.view, isWinCTRLorMacCMD(e));
}}
action={this.actionOCR.bind(this)}
icon={ICONS.ocr}
view={this.props.view}
/>
<ActionButton
key={"openLink"}
title={t("OPEN_LINK_CLICK")}
action={(e) => {
const event = new MouseEvent("click", {
ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS),
metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
shiftKey: e.shiftKey,
altKey: e.altKey,
});
this.props.view.handleLinkClick(event);
}}
action={this.actionOpenLink.bind(this)}
icon={ICONS.openLink}
view={this.props.view}
/>
<ActionButton
key={"openLinkProperties"}
title={t("OPEN_LINK_PROPS")}
action={() => {
const event = new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
shiftKey: false,
altKey: false,
});
this.props.view.handleLinkClick(event);
}}
action={this.actionOpenLinkProperties.bind(this)}
icon={ICONS.openLinkProperties}
view={this.props.view}
/>
<ActionButton
key={"save"}
title={t("FORCE_SAVE")}
action={() => {
this.props.view.forceSave();
}}
action={this.actionForceSave.bind(this)}
icon={saveIcon(this.state.isDirty)}
view={this.props.view}
/>
</div>
</fieldset>
@@ -433,49 +589,26 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
<ActionButton
key={"lib"}
title={t("DOWNLOAD_LIBRARY")}
action={() => {
this.props.view.plugin.exportLibrary();
}}
action={this.actionExportLibrary.bind(this)}
icon={ICONS.exportLibrary}
view={this.props.view}
/>
<ActionButton
key={"exportIMG"}
title={t("EXPORT_IMAGE")}
action={() => {
const view = this.props.view;
if(!view.exportDialog) {
view.exportDialog = new ExportDialog(view.plugin, view,view.file);
view.exportDialog.createForm();
}
view.exportDialog.open();
}}
action={this.actionExportImage.bind(this)}
icon={ICONS.ExportImage}
view={this.props.view}
/>
<ActionButton
key={"md"}
title={t("OPEN_AS_MD")}
action={() => {
this.props.view.openAsMarkdown();
}}
action={this.actionOpenAsMarkdown.bind(this)}
icon={ICONS.switchToMarkdown}
view={this.props.view}
/>
<ActionButton
key={"link-to-element"}
title={t("INSERT_LINK_TO_ELEMENT")}
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if(isWinALTorMacOPT(e)) {
openExternalLink("https://youtu.be/yZQoJg2RCKI", this.props.view.app);
return;
}
this.props.view.copyLinkToSelectedElementToClipboard(
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
);
}}
action={this.actionLinkToElement.bind(this)}
icon={ICONS.copyElementLink}
view={this.props.view}
/>
</div>
</fieldset>
@@ -485,104 +618,56 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
<ActionButton
key={"anyfile"}
title={t("UNIVERSAL_ADD_FILE")}
action={() => {
this.props.centerPointer();
const insertFileModal = new UniversalInsertFileModal(this.props.view.plugin, this.props.view);
insertFileModal.open();
}}
action={this.actionAddAnyFile.bind(this)}
icon={ICONS["add-file"]}
view={this.props.view}
/>
<ActionButton
key={"image"}
title={t("INSERT_IMAGE")}
action={() => {
this.props.centerPointer();
this.props.view.plugin.insertImageDialog.start(
this.props.view,
);
}}
action={this.actionInsertImage.bind(this)}
icon={ICONS.insertImage}
view={this.props.view}
/>
<ActionButton
key={"pdf"}
title={t("INSERT_PDF")}
action={() => {
this.props.centerPointer();
const insertPDFModal = new InsertPDFModal(this.props.view.plugin, this.props.view);
insertPDFModal.open();
}}
action={this.actionInsertPDF.bind(this)}
icon={ICONS.insertPDF}
view={this.props.view}
/>
<ActionButton
key={"insertMD"}
title={t("INSERT_MD")}
action={() => {
this.props.centerPointer();
this.props.view.plugin.insertMDDialog.start(
this.props.view,
);
}}
action={this.actionInsertMarkdown.bind(this)}
icon={ICONS.insertMD}
view={this.props.view}
/>
<ActionButton
key={"insertBackOfNote"}
title={t("INSERT_CARD")}
action={() => {
this.props.centerPointer();
this.props.view.insertBackOfTheNoteCard();
}}
action={this.actionInsertBackOfNote.bind(this)}
icon={ICONS.BackOfNote}
view={this.props.view}
/>
<ActionButton
key={"latex"}
title={t("INSERT_LATEX")}
action={(e) => {
if(isWinALTorMacOPT(e)) {
openExternalLink("https://youtu.be/r08wk-58DPk", this.props.view.app);
return;
}
this.props.centerPointer();
insertLaTeXToView(this.props.view);
}}
action={this.actionInsertLaTeX.bind(this)}
icon={ICONS.insertLaTeX}
view={this.props.view}
/>
<ActionButton
key={"link"}
title={t("INSERT_LINK")}
action={() => {
this.props.centerPointer();
this.props.view.plugin.insertLinkDialog.start(
this.props.view.file.path,
(text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.props.view.addText (text, fontFamily, save),
);
}}
action={this.actionInsertLink.bind(this)}
icon={ICONS.insertLink}
view={this.props.view}
/>
<ActionButton
key={"import-svg"}
title={t("IMPORT_SVG")}
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
this.props.view.plugin.importSVGDialog.start(this.props.view);
}}
action={this.actionImportSVG.bind(this)}
icon={ICONS.importSVG}
view={this.props.view}
/>
<ActionButton
key={"crop-image"}
title={t("CROP_IMAGE")}
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// @ts-ignore
this.props.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image")
}}
action={this.actionCropImage.bind(this)}
icon={ICONS.Crop}
view={this.props.view}
/>
</div>
</fieldset>
@@ -596,11 +681,12 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
}
private renderScriptButtons(isDownloaded: boolean) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()");
if (Object.keys(this.state.scriptIconMap).length === 0) {
return "";
}
const downloadedScriptsRoot = `${this.props.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
const downloadedScriptsRoot = `${this.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
const filterCondition = (key: string): boolean =>
isDownloaded
@@ -635,45 +721,15 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
<ActionButton
key={key}
title={value.name}
action={async () => {
const view = this.props.view;
const plugin = view.plugin;
const f = app.vault.getAbstractFileByPath(key);
if (f && f instanceof TFile) {
plugin.scriptEngine.executeScript(
view,
await app.vault.read(f),
plugin.scriptEngine.getScriptName(f),
f
);
}
}}
longpress={async () => {
const view = this.props.view;
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
const plugin = view.plugin;
await plugin.loadSettings();
const index = plugin.settings.pinnedScripts.indexOf(key)
if(index > -1) {
plugin.settings.pinnedScripts.splice(index,1);
api?.setToast({message:`Pin removed: ${value.name}`, duration: 3000, closable: true});
} else {
plugin.settings.pinnedScripts.push(key);
api?.setToast({message:`Pinned: ${value.name}`, duration: 3000, closable: true})
}
await plugin.saveSettings();
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
})
}}
action={this.actionRunScript.bind(this,key)}
longpress={this.actionPinScript.bind(this,key, value.name)}
icon={
value.svgString
new WeakRef(value.svgString
? stringToSVG(value.svgString)
: (
ICONS.cog
)
)).deref()
}
view={this.props.view}
/>
))}
</div>

View File

@@ -1,12 +1,13 @@
import { createPNG, ExcalidrawAutomate } from "../ExcalidrawAutomate";
import { ExcalidrawAutomate, createPNG } from "../ExcalidrawAutomate";
import {Notice, requestUrl} from "obsidian"
import ExcalidrawPlugin from "../main"
import {log} from "../utils/Utils"
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
import FrontmatterEditor from "src/utils/Frontmatter";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { blobToBase64 } from "src/utils/FileUtils";
import { getEA } from "src";
import { log } from "src/utils/DebugHelper";
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
@@ -21,6 +22,10 @@ export default class Taskbone {
) {
}
public destroy() {
this.plugin = null;
}
public async initialize(save:boolean = true):Promise<string> {
if(this.plugin.settings.taskboneAPIkey !== "") return;
const response = await requestUrl({
@@ -39,23 +44,9 @@ export default class Taskbone {
return apiKey;
}
public async getTextForView(view: ExcalidrawView, forceReScan: boolean) {
await view.forceSave(true);
const viewElements = view.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement) =>
el.type==="freedraw" ||
( el.type==="image" &&
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
));
if(viewElements.length === 0) {
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
return;
}
const fe = new FrontmatterEditor(view.data);
if(fe.hasKey("taskbone-ocr") && !forceReScan) {
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
return;
}
const bb = this.plugin.ea.getBoundingBox(viewElements);
public async getTextForElements(elements: ExcalidrawElement[], ea: ExcalidrawAutomate): Promise<string> {
ea.copyViewElementsToEAforEditing(elements, true);
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
@@ -79,27 +70,56 @@ export default class Taskbone {
};
const img =
await createPNG(
view.file.path + "#^taskbone",
await ea.createPNG(
null,
scale,
exportSettings,
loader,
"light",
null,
null,
[],
this.plugin,
0
);
)
return await this.getTextForImage(img);
}
const text = await this.getTextForImage(img);
if(text) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
window.navigator.clipboard.writeText(text);
new Notice("I placed the recognized in the drawing's frontmatter and onto the system clipboard.");
public async getTextForView(view: ExcalidrawView, {
forceReScan,
selectedElementsOnly = false,
addToFrontmatter = true,
}: {
forceReScan: boolean,
selectedElementsOnly?: boolean,
addToFrontmatter?: boolean,
}) {
await view.forceSave(true);
const ea = getEA(view) as ExcalidrawAutomate;
const viewElements = (selectedElementsOnly ? ea.getViewSelectedElements() : ea.getViewElements())
.filter((el:ExcalidrawElement) =>
el.type==="freedraw" ||
( el.type==="image" &&
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
));
if(viewElements.length === 0) {
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
ea.destroy();
return;
}
const fe = new FrontmatterEditor(view.data);
if(addToFrontmatter && fe.hasKey("taskbone-ocr") && !forceReScan) {
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
ea.destroy();
return;
}
const text = await this.getTextForElements(viewElements, ea);
if(text) {
if(addToFrontmatter) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
}
window.navigator.clipboard.writeText(text);
new Notice(`I placed the recognized text onto the system clipboard${addToFrontmatter?" and to document properties":""}.`);
}
ea.destroy();
}
private async getTextForImage(image: Blob):Promise<string> {
@@ -114,7 +134,18 @@ export default class Taskbone {
}]
};
const apiResponse = await requestUrl ({
const apiResponse = await fetch(url,{
method: "post",
//@ts-ignore
contentType: "application/json",
body: JSON.stringify(input),
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${this.apiKey}`
}});
const content = await apiResponse?.json();
/*const apiResponse = await requestUrl ({
url: url,
method: "post",
contentType: "application/json",
@@ -124,7 +155,7 @@ export default class Taskbone {
},
throw: false
});
const content = apiResponse?.json;
const content = apiResponse?.json;*/
if(!content || apiResponse.status !== 200) {
new Notice("Something went wrong while processing your request. Please check developer console for more information");

View File

@@ -2,6 +2,7 @@ import {
App,
ButtonComponent,
DropdownComponent,
getIcon,
normalizePath,
PluginSettingTab,
Setting,
@@ -13,7 +14,7 @@ import ExcalidrawView from "./ExcalidrawView";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
import { PenStyle } from "./PenTypes";
import { DynamicStyle } from "./types";
import { DynamicStyle } from "./types/types";
import { PreviewImageType } from "./utils/UtilTypes";
import { setDynamicStyle } from "./utils/DynamicStyling";
import {
@@ -34,6 +35,10 @@ import { startupScript } from "./constants/starutpscript";
import { ModifierKeySet, ModifierSetType } from "./utils/ModifierkeyHelper";
import { ModifierKeySettingsComponent } from "./dialogs/ModifierKeySettings";
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./utils/CarveOut";
import { EDITOR_FADEOUT } from "./CodeMirrorExtension/EditorHandler";
import { setDebugging } from "./utils/DebugHelper";
import { link } from "fs";
import { Rank } from "./menu/ActionIcons";
export interface ExcalidrawSettings {
folder: string;
@@ -43,8 +48,10 @@ export interface ExcalidrawSettings {
templateFilePath: string;
scriptFolderPath: string;
compress: boolean;
decompressForMDView: boolean;
onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0
onceOffGPTVersionReset: boolean; //used to reset GPT version in 2.2.11
autosave: boolean;
autosaveInterval: number;
autosaveIntervalDesktop: number;
autosaveIntervalMobile: number;
drawingFilenamePrefix: string;
@@ -54,9 +61,11 @@ export interface ExcalidrawSettings {
useExcalidrawExtension: boolean;
cropPrefix: string;
annotatePrefix: string;
annotatePreserveSize: boolean;
displaySVGInPreview: boolean; //No longer used since 1.9.13
previewImageType: PreviewImageType; //Introduced with 1.9.13
allowImageCache: boolean;
allowImageCacheInScene: boolean;
displayExportedImageIfAvailable: boolean;
previewMatchObsidianTheme: boolean;
width: string;
@@ -70,6 +79,9 @@ export interface ExcalidrawSettings {
defaultMode: string;
defaultPenMode: "never" | "mobile" | "always";
penModeCrosshairVisible: boolean;
renderImageInMarkdownReadingMode: boolean,
renderImageInHoverPreviewForMDNotes: boolean,
renderImageInMarkdownToPDF: boolean,
allowPinchZoom: boolean;
allowWheelZoom: boolean;
zoomToFitOnOpen: boolean;
@@ -97,6 +109,7 @@ export interface ExcalidrawSettings {
exportWithTheme: boolean;
exportWithBackground: boolean;
exportPaddingSVG: number;
exportEmbedScene: boolean;
keepInSync: boolean;
autoexportSVG: boolean;
autoexportPNG: boolean;
@@ -110,8 +123,12 @@ export interface ExcalidrawSettings {
experimentalFileType: boolean;
experimentalFileTag: string;
experimentalLivePreview: boolean;
fadeOutExcalidrawMarkup: boolean;
loadPropertySuggestions: boolean;
experimentalEnableFourthFont: boolean;
experimantalFourthFont: string;
addDummyTextElement: boolean;
zoteroCompatibility: boolean;
fieldSuggester: boolean;
//loadCount: number; //version 1.2 migration counter
drawingOpenCount: number;
@@ -151,6 +168,7 @@ export interface ExcalidrawSettings {
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfFrame: boolean;
pdfGapSize: number;
pdfGroupPages: boolean;
pdfLockAfterImport: boolean;
@@ -182,6 +200,8 @@ export interface ExcalidrawSettings {
areaZoomLimit: number;
longPressDesktop: number;
longPressMobile: number;
isDebugMode: boolean;
rank: Rank;
}
declare const PLUGIN_VERSION:string;
@@ -193,9 +213,11 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
embedUseExcalidrawFolder: false,
templateFilePath: "Excalidraw/Template.excalidraw",
scriptFolderPath: "Excalidraw/Scripts",
compress: false,
compress: true,
decompressForMDView: false,
onceOffCompressFlagReset: false,
onceOffGPTVersionReset: false,
autosave: true,
autosaveInterval: 15000,
autosaveIntervalDesktop: 15000,
autosaveIntervalMobile: 10000,
drawingFilenamePrefix: "Drawing ",
@@ -205,9 +227,11 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
useExcalidrawExtension: true,
cropPrefix: CROPPED_PREFIX,
annotatePrefix: ANNOTATED_PREFIX,
annotatePreserveSize: false,
displaySVGInPreview: undefined,
previewImageType: undefined,
allowImageCache: true,
allowImageCacheInScene: true,
displayExportedImageIfAvailable: false,
previewMatchObsidianTheme: false,
width: "400",
@@ -220,7 +244,10 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
matchThemeTrigger: false,
defaultMode: "normal",
defaultPenMode: "never",
penModeCrosshairVisible: false,
penModeCrosshairVisible: true,
renderImageInMarkdownReadingMode: false,
renderImageInHoverPreviewForMDNotes: false,
renderImageInMarkdownToPDF: false,
allowPinchZoom: false,
allowWheelZoom: false,
zoomToFitOnOpen: true,
@@ -248,6 +275,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
exportWithTheme: true,
exportWithBackground: true,
exportPaddingSVG: 10, //since 1.6.17, not only SVG but also PNG
exportEmbedScene: false,
keepInSync: false,
autoexportSVG: false,
autoexportPNG: false,
@@ -260,8 +288,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
experimentalFileType: false,
experimentalFileTag: "✏️",
experimentalLivePreview: true,
fadeOutExcalidrawMarkup: false,
loadPropertySuggestions: true,
experimentalEnableFourthFont: false,
experimantalFourthFont: "Virgil",
addDummyTextElement: false,
zoteroCompatibility: false,
fieldSuggester: true,
compatibilityMode: false,
//loadCount: 0,
@@ -308,6 +340,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfFrame: false,
pdfGapSize: 20,
pdfGroupPages: false,
pdfLockAfterImport: true,
@@ -335,7 +368,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
startupScriptPath: "",
openAIAPIToken: "",
openAIDefaultTextModel: "gpt-3.5-turbo-1106",
openAIDefaultVisionModel: "gpt-4-vision-preview",
openAIDefaultVisionModel: "gpt-4o",
openAIDefaultImageGenerationModel: "dall-e-3",
openAIURL: "https://api.openai.com/v1/chat/completions",
openAIImageGenerationURL: "https://api.openai.com/v1/images/generations",
@@ -425,6 +458,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
areaZoomLimit: 1,
longPressDesktop: 500,
longPressMobile: 500,
isDebugMode: false,
rank: "Bronze",
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -513,6 +548,46 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
},
});
coffeeImg.height = 45;
const iconLinks = [
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>`,
href: "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues",
aria: "Report bugs and raise feature requsts on the plugin's GitHub page",
text: "Bugs and Feature Requests",
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19c-2.3 0-6.4-.2-8.1-.6-.7-.2-1.2-.7-1.4-1.4-.3-1.1-.5-3.4-.5-5s.2-3.9.5-5c.2-.7.7-1.2 1.4-1.4C5.6 5.2 9.7 5 12 5s6.4.2 8.1.6c.7.2 1.2.7 1.4 1.4.3 1.1.5 3.4.5 5s-.2 3.9-.5 5c-.2.7-.7 1.2-1.4 1.4-1.7.4-5.8.6-8.1.6 0 0 0 0 0 0z"></path><polygon points="10 15 15 12 10 9"></polygon></svg>`,
href: "https://www.youtube.com/@VisualPKM",
aria: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
text: "Visual PKM on YouTube",
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" strokeWidth="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
href: "https://discord.gg/DyfAXFwUHc",
aria: "Join the Visual Thinking Workshop Discord Server",
text: "Community on Discord",
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>`,
href: "https://twitter.com/zsviczian",
aria: "Follow me on Twitter",
text: "Follow me on Twitter",
},
{
icon: getIcon("graduation-cap").outerHTML,
href: "https://visual-thinking-workshop.com",
aria: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
text: "Join the Visual Thinking Workshop",
}
];
const linksEl = containerEl.createDiv("setting-item-description excalidraw-settings-links-container");
iconLinks.forEach(({ icon, href, aria, text }) => {
linksEl.createEl("a",{href, attr: { "aria-label": aria }}, (a)=> {
a.innerHTML = icon + text;
});
});
// ------------------------------------------------
// Saving
@@ -651,6 +726,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DECOMPRESS_FOR_MD_NAME"))
.setDesc(fragWithHTML(t("DECOMPRESS_FOR_MD_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.decompressForMDView)
.onChange(async (value) => {
this.plugin.settings.decompressForMDView = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("AUTOSAVE_INTERVAL_DESKTOP_NAME"))
.setDesc(fragWithHTML(t("AUTOSAVE_INTERVAL_DESKTOP_DESC")))
@@ -663,9 +750,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.autosaveIntervalDesktop.toString())
.onChange(async (value) => {
this.plugin.settings.autosaveIntervalDesktop = parseInt(value);
this.plugin.settings.autosaveInterval = app.isMobile
? this.plugin.settings.autosaveIntervalMobile
: this.plugin.settings.autosaveIntervalDesktop;
this.applySettingsUpdate();
}),
);
@@ -682,9 +766,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.autosaveIntervalMobile.toString())
.onChange(async (value) => {
this.plugin.settings.autosaveIntervalMobile = parseInt(value);
this.plugin.settings.autosaveInterval = app.isMobile
? this.plugin.settings.autosaveIntervalMobile
: this.plugin.settings.autosaveIntervalDesktop;
this.applySettingsUpdate();
}),
);
@@ -828,6 +909,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("ANNOTATE_PRESERVE_SIZE_NAME"))
.setDesc(fragWithHTML(t("ANNOTATE_PRESERVE_SIZE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.annotatePreserveSize)
.onChange(async (value) => {
this.plugin.settings.annotatePreserveSize = value;
this.applySettingsUpdate();
}),
);
//------------------------------------------------
// AI Settings
//------------------------------------------------
@@ -944,6 +1038,30 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.renderImageInMarkdownReadingMode)
.onChange(async (value) => {
this.plugin.settings.renderImageInMarkdownReadingMode = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.renderImageInHoverPreviewForMDNotes)
.onChange(async (value) => {
this.plugin.settings.renderImageInHoverPreviewForMDNotes = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("LEFTHANDED_MODE_NAME"))
.setDesc(fragWithHTML(t("LEFTHANDED_MODE_DESC")))
@@ -1666,6 +1784,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
})
)
new Setting(detailsEl)
.setName(t("SCENE_IMAGE_CACHE_NAME"))
.setDesc(fragWithHTML(t("SCENE_IMAGE_CACHE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.allowImageCacheInScene)
.onChange((value) => {
this.plugin.settings.allowImageCacheInScene = value;
this.applySettingsUpdate();
})
)
new Setting(detailsEl)
.setName(t("EMBED_IMAGE_CACHE_CLEAR"))
.addButton((button) =>
button
.setButtonText(t("EMBED_IMAGE_CACHE_CLEAR"))
@@ -1673,6 +1804,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
imageCache.clearImageCache();
})
)
new Setting(detailsEl)
.setName(t("BACKUP_CACHE_CLEAR"))
.addButton((button) =>
button
.setButtonText(t("BACKUP_CACHE_CLEAR"))
@@ -1705,12 +1838,36 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h3",
});
addIframe(detailsEl, "wTtaXmRJ7wg",171);
new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.renderImageInMarkdownToPDF)
.onChange(async (value) => {
this.plugin.settings.renderImageInMarkdownToPDF = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("EXPORT_EMBED_SCENE_NAME"))
.setDesc(fragWithHTML(t("EXPORT_EMBED_SCENE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.exportEmbedScene)
.onChange(async (value) => {
this.plugin.settings.exportEmbedScene = value;
this.applySettingsUpdate();
}),
);
detailsEl = exportDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("EMBED_SIZING"),
cls: "excalidraw-setting-h4",
});
new Setting(detailsEl)
.setName(t("EMBED_WIDTH_NAME"))
.setDesc(fragWithHTML(t("EMBED_WIDTH_DESC")))
@@ -2029,7 +2186,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
d.addOption("Assistant", "Assistant");
this.app.vault
.getFiles()
.filter((f) => ["ttf", "woff", "woff2"].contains(f.extension))
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension))
.forEach((f: TFile) => {
d.addOption(f.path, f.name);
});
@@ -2115,7 +2272,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
areaZoomText.innerText = ` ${value.toString()}`;
this.plugin.settings.areaZoomLimit = value;
this.applySettingsUpdate();
this.plugin.excalidrawConfig.updateValues();
this.plugin.excalidrawConfig.updateValues(this.plugin);
}),
)
.settingEl.createDiv("", (el) => {
@@ -2260,6 +2417,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("FADE_OUT_EXCALIDRAW_MARKUP_NAME"))
.setDesc(fragWithHTML(t("FADE_OUT_EXCALIDRAW_MARKUP_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.fadeOutExcalidrawMarkup)
.onChange(async (value) => {
this.plugin.settings.fadeOutExcalidrawMarkup = value;
this.plugin.editorHandler.updateCMExtensionState(EDITOR_FADEOUT, value)
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("EXCALIDRAW_PROPERTIES_NAME"))
.setDesc(fragWithHTML(t("EXCALIDRAW_PROPERTIES_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.loadPropertySuggestions)
.onChange(async (value) => {
this.plugin.settings.loadPropertySuggestions = value;
this.applySettingsUpdate();
}),
);
detailsEl = experimentalDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("TASKBONE_HEAD"),
@@ -2388,6 +2570,47 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME"))
.setDesc(fragWithHTML(t("DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.addDummyTextElement)
.onChange((value) => {
this.plugin.settings.addDummyTextElement = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("PRESERVE_TEXT_AFTER_DRAWING_NAME"))
.setDesc(fragWithHTML(t("PRESERVE_TEXT_AFTER_DRAWING_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.zoteroCompatibility)
.onChange((value) => {
this.plugin.settings.zoteroCompatibility = value;
this.applySettingsUpdate();
}),
);
if (process.env.NODE_ENV === 'development') {
new Setting(detailsEl)
.setName(t("DEBUGMODE_NAME"))
.setDesc(fragWithHTML(t("DEBUGMODE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.isDebugMode)
.onChange((value) => {
this.plugin.settings.isDebugMode = value;
setDebugging(value);
this.applySettingsUpdate();
}),
);
}
new Setting(detailsEl)
.setName(t("SLIDING_PANES_NAME"))
.setDesc(fragWithHTML(t("SLIDING_PANES_DESC")))

View File

@@ -4,37 +4,55 @@ import { createTreeWalker, walk } from "./walker";
export type ConversionResult = {
hasErrors: boolean;
errors: NodeListOf<Element> | null;
errors: string;
content: any; // Serialized Excalidraw JSON
};
export const svgToExcalidraw = (svgString: string): ConversionResult => {
const parser = new DOMParser();
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
try {
const parser = new DOMParser();
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
// was there a parsing error?
const errorsElements = svgDOM.querySelectorAll("parsererror");
const hasErrors = errorsElements.length > 0;
let content = null;
// was there a parsing error?
const errorsElements = svgDOM.querySelectorAll("parsererror");
const hasErrors = errorsElements.length > 0;
let content = null;
if (hasErrors) {
console.error(
"There were errors while parsing the given SVG: ",
[...errorsElements].map((el) => el.innerHTML),
);
} else {
const tw = createTreeWalker(svgDOM);
const scene = new ExcalidrawScene();
const groups: Group[] = [];
if (hasErrors) {
console.error(
"There were errors while parsing the given SVG: ",
[...errorsElements].map((el) => el.innerHTML),
);
} else {
const tw = createTreeWalker(svgDOM);
const scene = new ExcalidrawScene();
const groups: Group[] = [];
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
content = scene.elements; //scene.toExJSON();
const hasVisibleElements = Boolean(scene.elements.find((el)=>el.opacity !== 0));
if (!hasVisibleElements) {
scene.elements.forEach((el) => {
el.opacity = 100;
});
}
scene.elements.forEach((el) => {
if(el.opacity <= 1) el.opacity = 100;
});
content = scene.elements; //scene.toExJSON();
}
return {
hasErrors,
errors: hasErrors ? `${[...errorsElements].map((el) => el.innerHTML)}` : "",
content,
};
} catch (error) {
console.log(error);
return {
hasErrors: true,
errors: `${error}`,
content:[],
};
}
return {
hasErrors,
errors: hasErrors ? errorsElements : null,
content,
};
};

View File

@@ -37,6 +37,7 @@ const SUPPORTED_TAGS = [
"rect",
"polyline",
"polygon",
"switch",
];
const nodeValidator = (node: Element): number => {
@@ -120,6 +121,18 @@ const walkers = {
walk(args, args.tw.nextNode());
},
switch: (args: WalkerArgs) => {
const nextArgs = {
...args,
tw: createTreeWalker(args.tw.currentNode),
groups: [...args.groups, new Group(args.tw.currentNode as Element)],
};
walk(nextArgs, nextArgs.tw.nextNode());
walk(args, args.tw.nextSibling());
},
g: (args: WalkerArgs) => {
const nextArgs = {
...args,

View File

@@ -1,68 +1,83 @@
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null;
export type Packages = {
react: any,
reactDOM: any,
excalidrawLib: any,
}
export type ValueOf<T> = T[keyof T];
export type DynamicStyle = "none" | "gray" | "colorful";
export type DeviceType = {
isDesktop: boolean,
isPhone: boolean,
isTablet: boolean,
isMobile: boolean,
isLinux: boolean,
isMacOS: boolean,
isWindows: boolean,
isIOS: boolean,
isAndroid: boolean
};
declare global {
interface Window {
ExcalidrawAutomate: ExcalidrawAutomate;
}
}
declare module "obsidian" {
interface App {
internalPlugins: any;
isMobile(): boolean;
getObsidianUrl(file:TFile): string;
metadataTypeManager: {
setType(name:string, type:string): void;
};
}
interface Keymap {
getRootScope(): Scope;
}
interface Scope {
keys: any[];
}
interface Workspace {
on(
name: "hover-link",
callback: (e: MouseEvent) => any,
ctx?: any,
): EventRef;
}
interface DataAdapter {
url: {
pathToFileURL(path: string): URL;
},
basePath: string;
}
interface Editor {
insertText(data: string): void;
}
interface MetadataCache {
getBacklinksForFile(file: TFile): any;
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };
}
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
import { ExcalidrawLib } from "../ExcalidrawLib";
export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null;
export type Packages = {
react: any,
reactDOM: any,
excalidrawLib: typeof ExcalidrawLib,
}
export type ValueOf<T> = T[keyof T];
export type DynamicStyle = "none" | "gray" | "colorful";
export type DeviceType = {
isDesktop: boolean,
isPhone: boolean,
isTablet: boolean,
isMobile: boolean,
isLinux: boolean,
isMacOS: boolean,
isWindows: boolean,
isIOS: boolean,
isAndroid: boolean
};
declare global {
interface Window {
ExcalidrawAutomate: ExcalidrawAutomate;
}
}
declare module "obsidian" {
interface App {
internalPlugins: any;
isMobile(): boolean;
getObsidianUrl(file:TFile): string;
metadataTypeManager: {
setType(name:string, type:string): void;
};
}
interface Keymap {
getRootScope(): Scope;
}
interface Scope {
keys: any[];
}
interface Workspace {
on(
name: "hover-link",
callback: (e: MouseEvent) => any,
ctx?: any,
): EventRef;
}
interface DataAdapter {
url: {
pathToFileURL(path: string): URL;
},
basePath: string;
}
interface FoldPosition {
from: number;
to: number;
}
interface FoldInfo {
folds: FoldPosition[];
lines: number;
}
interface MarkdownSubView {
applyFoldInfo(foldInfo: FoldInfo): void;
getFoldInfo(): FoldInfo | null;
}
/*interface Editor {
insertText(data: string): void;
}*/
interface MetadataCache {
getBacklinksForFile(file: TFile): any;
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };
}
}

View File

@@ -11,7 +11,7 @@ container.appendChild(node.contentEl)
import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import ExcalidrawView from "src/ExcalidrawView";
import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils";
import { CustomMutationObserver, isDebugMode } from "./DebugHelper";
import { CustomMutationObserver, DEBUGGING } from "./DebugHelper";
declare module "obsidian" {
interface Workspace {
@@ -33,6 +33,7 @@ export interface ObsidianCanvasNode {
child: any;
isEditing: boolean;
file: TFile;
detach: Function;
}
export class CanvasNodeFactory {
@@ -41,6 +42,7 @@ export class CanvasNodeFactory {
nodes = new Map<string, ObsidianCanvasNode>();
initialized: boolean = false;
public isInitialized = () => this.initialized;
private observer: CustomMutationObserver | MutationObserver;
constructor(
private view: ExcalidrawView,
@@ -110,11 +112,11 @@ export class CanvasNodeFactory {
}
}
};
const observer = isDebugMode
this.observer = DEBUGGING
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);
observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
this.observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
})();
}
@@ -127,12 +129,29 @@ export class CanvasNodeFactory {
node.child.showPreview();
}
removeNode(node: ObsidianCanvasNode) {
if(!this.initialized || !node) return;
this.nodes.delete(node.file.path);
this.canvas.removeNode(node);
node.detach();
}
public purgeNodes() {
if(!this.initialized) return;
this.nodes.forEach(node => {
this.canvas.removeNode(node);
this.canvas.removeNode(node);
node.detach();
});
this.nodes.clear();
}
destroy() {
this.purgeNodes();
this.initialized = false; //calling after purgeNodes becaues purge nodes checks for initialized
this.observer?.disconnect();
this.view = null;
this.canvas = null;
this.leaf = null;
}
}

View File

@@ -17,7 +17,10 @@ export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: E
targetEA.copyViewElementsToEAforEditing([viewImageEl],true);
const {height, width} = await sourceEA.getOriginalImageSize(viewImageEl);
if(!height || !width || height === 0 || width === 0) return;
if(!height || !width || height === 0 || width === 0) {
targetEA.destroy();
return;
}
const newImage = targetEA.getElement(viewImageEl.id) as Mutable<ExcalidrawImageElement>;
newImage.x = 0;
@@ -47,7 +50,10 @@ export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: E
const {folderpath, filename} = await getCropFileNameAndFolder(sourceEA.plugin,sourceEA.targetView.file.path,fname);
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, folderpath, filename);
if(!file) return;
if(!file) {
targetEA.destroy();
return;
}
//console.log(await app.vault.read(file));
sourceEA.clear();
@@ -61,7 +67,8 @@ export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: E
replacingImage.height = sourceImageEl.height;
replacingImage.scale = scale;
replacingImage.angle = angle;
sourceEA.addElementsToView(false, true, true);
await sourceEA.addElementsToView(false, true, true);
targetEA.destroy();
}
export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: ExcalidrawEmbeddableElement, pdfPathWithPage: string, pdfFile: TFile) => {
@@ -69,16 +76,17 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
const targetEA = getEA(sourceEA.targetView) as ExcalidrawAutomate;
const {height, width} = embeddableEl;
let {height, width} = embeddableEl;
if(!height || !width || height === 0 || width === 0) return;
if(!height || !width || height === 0 || width === 0) {
targetEA.destroy();
return;
}
const imageId = await targetEA.addImage(0,0, pdfPathWithPage);
const newImage = targetEA.getElement(imageId) as Mutable<ExcalidrawImageElement>;
newImage.x = 0;
newImage.y = 0;
newImage.width = width;
newImage.height = height;
const angle = embeddableEl.angle;
const fname = pdfFile.basename;
@@ -87,7 +95,10 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
const {folderpath, filename} = await getCropFileNameAndFolder(sourceEA.plugin,sourceEA.targetView.file.path,fname);
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, folderpath, filename);
if(!file) return;
if(!file) {
targetEA.destroy();
return;
}
//console.log(await app.vault.read(file));
sourceEA.clear();
@@ -102,7 +113,8 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
replacingImage.width = replacingImage.height * imageAspectRatio;
}
replacingImage.angle = angle;
sourceEA.addElementsToView(false, true, true);
await sourceEA.addElementsToView(false, true, true);
targetEA.destroy();
}
@@ -171,41 +183,5 @@ export const createImageCropperFile = async (targetEA: ExcalidrawAutomate, image
return;
}
/*
//wait for the new ExcalidrawView to open and initialize
counter = 0;
let newView = workspace.getActiveViewOfType(ExcalidrawView) as ExcalidrawView;
while(
(workspace.getActiveFile() !== file ||
newView?.file !== file ||
!newView?.isLoaded ||
!Boolean(newView?.excalidrawAPI)) &&
counter < 100
) {
await sleep(100);
newView = workspace.getActiveViewOfType(ExcalidrawView) as ExcalidrawView;
counter++;
}
//console.log({counter});
if(newView?.file !== file || !newView?.isLoaded ||!Boolean(newView?.excalidrawAPI)) {
new Notice("View did not initialize. NewExcalidraw Drawing is taking too long to open. Please try again.");
return;
}
//wait for the image to load to the new view
const api = newView.excalidrawAPI as ExcalidrawImperativeAPI;
counter = 0;
while(Object.keys(api.getFiles()).length === 0 && counter < 100) {
await sleep(100);
counter++;
}
if(Object.keys(api.getFiles()).length === 0) {
new Notice("Image did not load to the view. NewExcalidraw Drawing is taking too long to load. Please try again.");
return;
}
*/
//console.log({counter, path: workspace.getActiveFile()?.path, newView, files: api.getFiles()});
return file;
}

View File

@@ -6,7 +6,6 @@ import { Notice } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate, cloneElement } from "src/ExcalidrawAutomate";
import { ExportSettings } from "src/ExcalidrawView";
import { embedFontsInSVG } from "./Utils";
import { nanoid } from "src/constants/constants";
export class CropImage {
@@ -45,6 +44,15 @@ export class CropImage {
})
}
public destroy() {
this.imageEA.destroy();
this.maskEA.destroy();
this.imageEA = null;
this.maskEA = null;
this.elements = null;
this.bbox = null;
}
private setBoundingEl(ea: ExcalidrawAutomate, bgColor: string) {
const {topX, topY, width, height} = this.bbox;
ea.style.backgroundColor = bgColor;
@@ -83,7 +91,7 @@ export class CropImage {
isMask: false,
}
const maskSVG = await this.maskEA.createSVG(null,false,exportSettings,null,null,0);
const maskSVG = await this.maskEA.createSVG(null,true,exportSettings,null,null,0);
const defs = maskSVG.querySelector("defs");
const styleEl = maskSVG.querySelector("style");
const style = styleEl ? styleEl.outerHTML : "";
@@ -97,16 +105,12 @@ export class CropImage {
withTheme: false,
isMask: false,
}
const isRotated = this.imageEA.getElements().some(el=>el.type === "image" && el.angle !== 0);
const images = Object.values(this.imageEA.imagesDict);
if(images.length === 1) {
if(!isRotated && (images.length === 1)) {
return images[0].dataURL;
}
return await this.imageEA.createPNGBase64(null,1,exportSettings,null,null,0);
const imageSVG = await this.imageEA.createSVG(null,false,exportSettings,null,null,0);
const svgData = new XMLSerializer().serializeToString(imageSVG);
return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`;
// const blob = new Blob([svgString], { type: 'image/svg+xml' });
// return `data:image/svg+xml;base64,${await blobToBase64(blob)}`;
}
private async buildSVG(): Promise<SVGSVGElement> {
@@ -133,7 +137,7 @@ export class CropImage {
async getCroppedPNG(): Promise<Blob> {
//@ts-ignore
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"];
const svg = embedFontsInSVG(await this.buildSVG(), PLUGIN);
const svg = await this.buildSVG();
return new Promise((resolve, reject) => {
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement('canvas');

View File

@@ -1,6 +1,19 @@
export const isDebugMode = false;
export const durationTreshold = 0; //0.05; //ms
export function setDebugging(value: boolean) {
DEBUGGING = (process.env.NODE_ENV === 'development')
? value
: false;
}
export let DEBUGGING = false;
export const log = console.log.bind(window.console);
export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => {
//console.log(fnName,fn,...messages);
console.log(fnName, ...messages);
};
export class CustomMutationObserver {
private originalCallback: MutationCallback;
private observer: MutationObserver | null;
@@ -19,7 +32,7 @@ export class CustomMutationObserver {
const endTime = performance.now(); // Get end time
const executionTime = endTime - startTime;
if (executionTime > durationTreshold) {
console.log(`Excalidraw ${this.name} MutationObserver callback took ${executionTime}ms to execute`);
console.log(`Excalidraw ${this.name} MutationObserver callback took ${executionTime}ms to execute`, observer);
}
};

View File

@@ -2,7 +2,7 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/
import { ColorMaster } from "colormaster";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { DynamicStyle } from "src/types";
import { DynamicStyle } from "src/types/types";
import { cloneElement } from "src/ExcalidrawAutomate";
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { addAppendUpdateCustomData } from "./Utils";
@@ -16,22 +16,24 @@ export const setDynamicStyle = (
) => {
if(dynamicStyle === "none") {
view.excalidrawContainer?.removeAttribute("style");
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}}));
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}, storeAction: "update"}));
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
toolspanel.setAttribute("style",toolsStyle);
}
return;
}
const doc = view.ownerDocument;
//const doc = view.ownerDocument;
const isLightTheme =
view?.excalidrawAPI?.getAppState?.()?.theme === "light" ||
view?.excalidrawData?.scene?.appState?.theme === "light";
if (color==="transparent") {
color = "#ffffff";
}
const darker = "#101010";
const lighter = "#f0f0f0";
const step = 10;
@@ -110,7 +112,8 @@ export const setDynamicStyle = (
[`--h2-color`]: str(text),
[`--h3-color`]: str(text),
[`--h4-color`]: str(text),
[`color`]: str(text),
[`color`]: str(text),
['--excalidraw-caret-color']: str(isLightTheme ? text : cmBG()),
[`--select-highlight-color`]: str(gray1()),
[`--color-gray-80`]: str(isDark?text.darkerBy(40):text.lighterBy(40)), //frame
};
@@ -124,9 +127,22 @@ export const setDynamicStyle = (
styleString
)*/
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
toolspanel.setAttribute("style",toolsStyle+styleString);
}
setTimeout(()=>{
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
if(!api) return;
if(!api) {
view = null;
ea = null;
color = null;
dynamicStyle = null;
return;
}
const frameColor = {
stroke: str(isDark?gray2().lighterBy(15):gray2().darkerBy(15)),
fill: str((isDark?gray2().lighterBy(30):gray2().darkerBy(30)).alphaTo(0.2)),
@@ -151,13 +167,12 @@ export const setDynamicStyle = (
appState:{
frameColor,
dynamicStyle: styleObject
}
},
storeAction: "update",
});
view = null;
ea = null;
color = null;
dynamicStyle = null;
});
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
toolspanel.setAttribute("style",toolsStyle+styleString);
}
}

View File

@@ -5,13 +5,13 @@ export class ExcalidrawConfig {
public areaLimit: number = 16777216;
public widthHeightLimit: number = 32767;
constructor(private plugin: ExcalidrawPlugin) {
this.updateValues();
constructor(plugin: ExcalidrawPlugin) {
this.updateValues(plugin);
}
updateValues() {
updateValues(plugin: ExcalidrawPlugin) {
if(DEVICE.isIOS) return;
this.areaLimit = 16777216*this.plugin.settings.areaZoomLimit; //this.plugin.settings.areaLimit;
this.widthHeightLimit = 32767*this.plugin.settings.areaZoomLimit; //his.plugin.settings.widthHeightLimit;
this.areaLimit = 16777216*plugin.settings.areaZoomLimit; //this.plugin.settings.areaLimit;
this.widthHeightLimit = 32767*plugin.settings.areaZoomLimit; //his.plugin.settings.widthHeightLimit;
}
}

View File

@@ -1,18 +1,24 @@
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
import { App, TFile } from "obsidian";
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
import { App, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
import ExcalidrawView from "src/ExcalidrawView";
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getLinkParts } from "./Utils";
import { cleanSectionHeading } from "./ObsidianUtils";
import { getEA } from "src";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddableMDCustomProps } from "src/dialogs/EmbeddableSettings";
import { nanoid } from "nanoid";
import { t } from "src/lang/helpers";
export const insertImageToView = async (
export async function insertImageToView(
ea: ExcalidrawAutomate,
position: { x: number, y: number },
file: TFile | string,
scale?: boolean,
):Promise<string> => {
):Promise<string> {
ea.clear();
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
@@ -28,17 +34,17 @@ export const insertImageToView = async (
return id;
}
export const insertEmbeddableToView = async (
export async function insertEmbeddableToView (
ea: ExcalidrawAutomate,
position: { x: number, y: number },
file?: TFile,
link?: string,
):Promise<string> => {
):Promise<string> {
ea.clear();
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
return await insertImageToView(ea, position, file);
return await insertImageToView(ea, position, link??file);
} else {
const id = ea.addEmbeddable(
position.x,
@@ -53,7 +59,7 @@ export const insertEmbeddableToView = async (
}
}
export const getLinkTextFromLink = (text: string): string => {
export function getLinkTextFromLink (text: string): string {
if (!text) return;
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
@@ -66,7 +72,7 @@ export const getLinkTextFromLink = (text: string): string => {
return linktext;
}
export const openTagSearch = (link:string, app: App, view?: ExcalidrawView) => {
export function openTagSearch (link:string, app: App, view?: ExcalidrawView) {
const tags = link
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
.next();
@@ -87,21 +93,70 @@ export const openTagSearch = (link:string, app: App, view?: ExcalidrawView) => {
return;
}
export const openExternalLink = (link:string, app: App, element?: ExcalidrawElement):boolean => {
function getLinkFromMarkdownLink(link: string): string {
const result = /^\[[^\]]*]\(([^\)]*)\)/.exec(link);
return result ? result[1] : link;
}
export function openExternalLink (link:string, app: App, element?: ExcalidrawElement):boolean {
link = getLinkFromMarkdownLink(link);
if (link.match(/^cmd:\/\/.*/)) {
const cmd = link.replace("cmd://", "");
//@ts-ignore
app.commands.executeCommandById(cmd);
return true;
}
if (link.match(REG_LINKINDEX_HYPERLINK)) {
window.open(link, "_blank");
if (!link.startsWith("obsidian://") && link.match(REG_LINKINDEX_HYPERLINK)) {
window.open(link, "_blank");
return true;
}
return false;
}
export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, secondOrderLinksSet: Set<string>):string => {
/**
*
* @param link
* @param app
* @param returnWikiLink
* @returns
* false if the link is not an obsidian link,
* true if the link is an obsidian link and it was opened (i.e. it is a link to another Vault or not a file link e.g. plugin link), or
* the link to the file path. By default as a wiki link, or as a file path if returnWikiLink is false.
*/
export function parseObsidianLink(link: string, app: App, returnWikiLink: boolean = true): boolean | string {
link = getLinkFromMarkdownLink(link);
if (!link.startsWith("obsidian://")) {
return false;
}
const url = new URL(link);
const action = url.pathname.slice(2); // Remove leading '//'
const props: {[key: string]: string} = {};
url.searchParams.forEach((value, key) => {
props[key] = decodeURIComponent(value);
});
if (action === "open" && props.vault === app.vault.getName()) {
const file = props.file;
const f = app.metadataCache.getFirstLinkpathDest(file, "");
if (f && f instanceof TFile) {
if (returnWikiLink) {
return `[[${f.path}]]`;
} else {
return f.path;
}
}
}
window.open(link, "_blank");
return true;
}
export function getExcalidrawFileForwardLinks (
app: App, excalidrawFile: TFile,
secondOrderLinksSet: Set<string>,
):string {
let secondOrderLinks = "";
const forwardLinks = app.metadataCache.getLinks()[excalidrawFile.path];
if(forwardLinks && forwardLinks.length > 0) {
@@ -120,7 +175,10 @@ export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, s
return secondOrderLinks;
}
export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: ExcalidrawElement[]): ExcalidrawFrameElement | null => {
export function getFrameBasedOnFrameNameOrId(
frameName: string,
elements: ExcalidrawElement[],
): ExcalidrawFrameElement | null {
const frames = elements
.filter((el: ExcalidrawElement)=>el.type==="frame")
.map((el: ExcalidrawFrameElement, idx: number)=>{
@@ -129,4 +187,197 @@ export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: Excali
.filter((item:any) => item.id === frameName || item.name === frameName)
.map((item:any)=>item.el as ExcalidrawFrameElement);
return frames.length === 1 ? frames[0] : null;
}
export async function addBackOfTheNoteCard(
view: ExcalidrawView,
title: string,
activate: boolean = true,
cardBody?: string,
embeddableCustomData?: EmbeddableMDCustomProps,
):Promise<string> {
const data = view.data;
const header = getExcalidrawMarkdownHeaderSection(data);
const body = data.split(header)[1];
const shouldAddHashtag = body && body.startsWith("%%");
const hastag = header.match(/#\n+$/m);
const shouldRemoveTrailingHashtag = Boolean(hastag);
view.data = data.replace(
header,
(shouldRemoveTrailingHashtag
? header.substring(0,header.length-hastag[0].length)
: header) +
`\n# ${title}\n\n${cardBody ? cardBody+"\n\n" : ""}${
shouldAddHashtag || shouldRemoveTrailingHashtag ? "#\n" : ""}`);
await view.forceSave(true);
let watchdog = 0;
await sleep(200);
let found:string;
while (watchdog++ < 10 && !(found=(await view.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },view.file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
.map((b: any) => cleanSectionHeading(b.display))
.find((b: any) => b === title))) {
await sleep(200);
}
const ea = getEA(view) as ExcalidrawAutomate;
const id = ea.addEmbeddable(
0,0,400,500,
`[[${view.file.path}#${title}]]`,
undefined,
embeddableCustomData
);
await ea.addElementsToView(true, false, true);
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
const el = ea.getViewElements().find(el=>el.id === id);
api.selectElements([el]);
if(activate) {
window.setTimeout(()=>{
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}, storeAction: "update"});
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
});
}
ea.destroy();
return el.id;
}
export function renderContextMenuAction(
React: any,
label: string,
action: Function,
onClose: (callback?: () => void) => void,
) {
return React.createElement (
"li",
{
key: nanoid(),
onClick: () => {
onClose(()=>action())
},
},
React.createElement(
"button",
{
className: "context-menu-item",
},
React.createElement(
"div",
{
className: "context-menu-item__label",
},
label,
),
React.createElement(
"kbd",
{
className: "context-menu-item__shortcut",
},
"", //this is where the shortcut may go in the future
),
)
);
}
export function tmpBruteForceCleanup (view: ExcalidrawView) {
window.setTimeout(()=>{
if(!view) return;
// const cleanupHTMLElement = (el: Element) => {
// //console.log(el);
// while(el.firstElementChild) {
// cleanupHTMLElement(el.firstElementChild);
// el.removeChild(el.firstElementChild);
// }
// Object.keys(el).forEach((key) => {
// //@ts-ignore
// delete el[key];
// });
// el.empty();
// }
// const cleanupLeaf = (l:any) => {
// l.detach?.();
// l.resizeObserver?.disconnect?.();
// l.view?.unload?.();
// l.component?.unload?.();
// Object.keys(l).forEach((key) => {
// const obj = l[key];
// if (obj instanceof Element) {
// // Recursively empty the DOM element's children
// while (obj.firstChild) {
// cleanupHTMLElement(obj.firstElementChild);
// obj.removeChild(obj.firstElementChild);
// }
// obj.empty();
// delete l[key];
// return;
// }
// //@ts-ignore
// delete l[key];
// });
// }
// //@ts-ignore
// if(view.leaf && !view.leaf.parent) {
// if(view.containerEl) {
// cleanupHTMLElement(view.containerEl);
// }
// const leaves = new Set();
// leaves.add(view.leaf);
// while(leaves.has(view.leaf.getContainer())) {
// leaves.add(view.leaf.getContainer());
// }
// const roots = new Set();
// roots.add(view.leaf.getRoot());
// leaves.forEach((leaf:WorkspaceLeaf) => {
// roots.add(leaf.getRoot());
// });
// leaves.forEach((l:any) => cleanupLeaf(l));
// leaves.clear();
// roots.forEach((root:any) => cleanupLeaf(root));
// roots.clear();
// }
Object.keys(view).forEach((key) => {
//@ts-ignore
delete view[key];
});
}, 500);
}
/**
* Check if the text matches the transclusion pattern and if so,
* check if the link in the transclusion can be resolved to a file in the vault.
* if yes, call the callback function with the link and the file.
* @param text
* @param callback
* @returns true if text is a transclusion and the link can be resolved to a file in the vault, false otherwise.
*/
export function isTextImageTransclusion (
text: string,
view: ExcalidrawView,
callback: (link: string, file: TFile)=>void
): boolean {
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
const match = text.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
if(match?.value?.[0]) {
const link = match.value[1] ?? match.value[2];
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
if(view.file === file) {
new Notice(t("RECURSIVE_INSERT_ERROR"));
return false;
}
if(file && file instanceof TFile) {
if (file.extension !== "md" || view.plugin.isExcalidrawFile(file)) {
callback(link, file);
return true;
} else {
new Notice(t("USE_INSERT_FILE_MODAL"),5000);
}
}
}
return false;
}

View File

@@ -11,6 +11,7 @@ import { getAttachmentsFolderAndFilePath } from "./ObsidianUtils";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
* @param filepath
* @returns folderpath will be normalized. This means "/" for root folder and no trailing "/" for other folders
*/
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;
@@ -334,6 +335,7 @@ export const getPathWithoutExtension = (f:TFile): string => {
const VAULT_BASE_URL = DEVICE.isDesktop
? app.vault.adapter.url.pathToFileURL(app.vault.adapter.basePath).toString()
: "";
export const getInternalLinkOrFileURLLink = (
path: string, plugin:ExcalidrawPlugin, alias?: string, sourceFile?: TFile
):{link: string, isInternal: boolean, file?: TFile, url?: string} => {
@@ -345,7 +347,7 @@ export const getInternalLinkOrFileURLLink = (
const vault = plugin.app.vault;
const fileURLString = vault.adapter.url.pathToFileURL(path).toString();
if (fileURLString.startsWith(VAULT_BASE_URL)) {
const internalPath = normalizePath(fileURLString.substring(VAULT_BASE_URL.length));
const internalPath = normalizePath(unescape(fileURLString.substring(VAULT_BASE_URL.length)));
const file = vault.getAbstractFileByPath(internalPath);
if(file && file instanceof TFile) {
const link = plugin.app.metadataCache.fileToLinktext(
@@ -367,13 +369,24 @@ export const getInternalLinkOrFileURLLink = (
*/
export const getLink = (
plugin: ExcalidrawPlugin,
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string }
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string },
wikilinkOverride?: boolean
):string => {
return plugin.settings.embedWikiLink
const isWikiLink = (typeof wikilinkOverride !== "undefined")
? wikilinkOverride
: plugin.settings.embedWikiLink;
return isWikiLink
? `${embed ? "!" : ""}[[${path}${alias ? `|${alias}` : ""}]]`
: `${embed ? "!" : ""}[${alias ?? ""}](${encodeURI(path)})`
}
export const getAliasWithSize = (alias: string, size: string): string => {
if(alias && alias !== "") {
return `${alias}${size?`|${size}`:""}`;
}
return size;
}
export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => {
let prefix = plugin.settings.cropPrefix;
if(!prefix || prefix.trim() === "") prefix = CROPPED_PREFIX;
@@ -429,3 +442,38 @@ export const fileShouldDefaultAsExcalidraw = (path:string, app:App):boolean => {
cache.frontmatter[FRONTMATTER_KEYS["plugin"].name] &&
!Boolean(cache.frontmatter[FRONTMATTER_KEYS["open-as-markdown"].name]);
}
export const getExcalidrawEmbeddedFilesFiletree = (sourceFile: TFile, plugin: ExcalidrawPlugin):TFile[] => {
if(!sourceFile || !plugin.isExcalidrawFile(sourceFile)) {
return [];
}
const fileList = new Set<TFile>();
const app = plugin.app;
const addAttachedImages = (f:TFile) => Object
.keys(app.metadataCache.resolvedLinks[f.path])
.forEach(path => {
const file = app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) return;
const isExcalidraw = plugin.isExcalidrawFile(file);
if (
(file.extension === "md" && !isExcalidraw) ||
fileList.has(file) //avoid infinite loops
) {
return;
}
fileList.add(file);
if (isExcalidraw) {
addAttachedImages(file);
}
});
addAttachedImages(sourceFile);
return Array.from(fileList);
}
export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:number, plugin: ExcalidrawPlugin):boolean => {
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
return fileList.some(f=>f.stat.mtime > mtime);
}

View File

@@ -22,7 +22,7 @@ export const getElementsAtPointer = (
y <= pointer.y &&
y + h >= pointer.y
);
});
}).reverse();
};
export const getTextElementAtPointer = (pointer: any, view: ExcalidrawView) => {

View File

@@ -2,6 +2,7 @@ import { App, Notice, TFile } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { convertSVGStringToElement } from "./Utils";
import { FILENAMEPARTS, PreviewImageType } from "./UtilTypes";
import { hasExcalidrawEmbeddedImagesTreeChanged } from "./FileUtils";
//@ts-ignore
const DB_NAME = "Excalidraw " + app.appId;
@@ -19,17 +20,20 @@ export type ImageKey = {
isDark: boolean;
previewImageType: PreviewImageType;
scale: number;
isTransparent: boolean;
inlineFonts: boolean;
} & FILENAMEPARTS;
const getKey = (key: ImageKey): string =>
`${key.filepath}#${key.blockref??""}#${key.sectionref??""}#${key.isDark ? 1 : 0}#${
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasSectionref}#${
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasClippedFrameref}#${
key.hasSectionref}#${key.inlineFonts}#${
key.previewImageType === PreviewImageType.SVGIMG
? 1
: key.previewImageType === PreviewImageType.PNG
? 0
: 2
}#${key.scale}`; //key.isSVG ? 1 : 0
}#${key.scale}${key.isTransparent?"#t":""}`; //key.isSVG ? 1 : 0
class ImageCache {
private dbName: string;
@@ -41,6 +45,19 @@ class ImageCache {
private app: App;
public initializationNotice: boolean = false;
private obsidanURLCache = new Map<string, string>();
private purgeInvalidCacheTimer: number = null;
private purgeInvalidBackupTimer: number = null;
public destroy(): void {
this.isInitializing = true;
if(this.purgeInvalidCacheTimer) clearTimeout(this.purgeInvalidCacheTimer);
if(this.purgeInvalidBackupTimer) clearTimeout(this.purgeInvalidBackupTimer);
this.db = null;
this.plugin = null;
this.app = null;
this.obsidanURLCache.clear();
this.obsidanURLCache = null;
}
constructor(dbName: string, cacheStoreName: string, backupStoreName: string) {
this.dbName = dbName;
@@ -127,8 +144,15 @@ class ImageCache {
});
}
setTimeout(async ()=>this.purgeInvalidCacheFiles(), 60000);
setTimeout(async ()=>this.purgeInvalidBackupFiles(), 120000);
this.purgeInvalidCacheTimer = window.setTimeout(async ()=>{
this.purgeInvalidCacheTimer = null;
this.purgeInvalidCacheFiles();
}, 60000);
this.purgeInvalidBackupTimer = window.setTimeout(async ()=>{
this.purgeInvalidBackupTimer = null;
this.purgeInvalidBackupFiles();
}, 120000);
} finally {
this.isInitializing = false;
if(this.initializationNotice) {
@@ -150,11 +174,11 @@ class ImageCache {
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
if(cursor) {
const key = cursor.key as string;
const isLegacyKey = key.replaceAll(/[^#]/g,"").length < 9; // introduced hasGroupref, etc. in 1.9.28
const isLegacyKey = key.split("#").length-1 < 12; // introduced hasGroupref, etc. in 1.9.28 // introduced hasClippedFrameref in 2.2.10 //introduced inlineFonts 2.2.11
const filepath = key.split("#")[0];
const fileExists = files.some((f: TFile) => f.path === filepath);
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
if (isLegacyKey || !file || (file && file.stat.mtime > cursor.value.mtime) || (!cursor.value.blob && !cursor.value.svg)) {
if (isLegacyKey || !file || (file && (file.stat.mtime > cursor.value.mtime)) || (!cursor.value.blob && !cursor.value.svg)) {
deletePromises.push(
new Promise<void>((innerResolve, innerReject) => {
const deleteRequest = store.delete(cursor.primaryKey);
@@ -229,16 +253,16 @@ class ImageCache {
});
}
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
private getObjectStore(mode: IDBTransactionMode, storeName: string): IDBObjectStore {
const transaction = this.db!.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly", this.cacheStoreName);
const store = this.getObjectStore("readonly", this.cacheStoreName);
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
return await new Promise<FileCacheData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as FileCacheData;
resolve(result || null);
@@ -251,7 +275,7 @@ class ImageCache {
}
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
const store = await this.getObjectStore("readonly", this.backupStoreName);
const store = this.getObjectStore("readonly", this.backupStoreName);
const request = store.get(key);
return new Promise<BackupData | null>((resolve, reject) => {
@@ -284,14 +308,19 @@ class ImageCache {
? await this.getCacheData(key)
: await Promise.race([
this.getCacheData(key),
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
new Promise<undefined>((_,reject) => setTimeout(() => {
reject(undefined);
}, 100))
]);
this.fullyInitialized = true;
if(!cachedData) return undefined;
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
if (cachedData && (cachedData.mtime >= file.stat.mtime)) {
if(hasExcalidrawEmbeddedImagesTreeChanged(file, cachedData.mtime, this.plugin)) {
return undefined;
}
if(cachedData.svg) {
return convertSVGStringToElement(cachedData.svg);
}
@@ -316,6 +345,7 @@ class ImageCache {
return this.getBackupData(filepath);
}
//cache SVG should have the width and height parameters and not the embedded font
public addImageToCache(key_: ImageKey, obsidianURL: string, image: Blob|SVGSVGElement): void {
if (!this.isReady()) {
return; // Database not initialized yet
@@ -332,7 +362,7 @@ class ImageCache {
} else {
blob = image as Blob;
}
const data: FileCacheData = { mtime: file.stat.mtime, blob, svg};
const data: FileCacheData = { mtime: Date.now(), blob, svg};
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
const store = transaction.objectStore(this.cacheStoreName);
const key = getKey(key_);

View File

@@ -1,14 +1,18 @@
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { requireApiVersion } from "obsidian";
export const getMermaidImageElements = (elements: ExcalidrawElement[]):ExcalidrawImageElement[] =>
elements
export function getMermaidImageElements (elements: ExcalidrawElement[]):ExcalidrawImageElement[] {
return elements
? elements.filter((element) =>
element.type === "image" && element.customData?.mermaidText
) as ExcalidrawImageElement[]
: [];
}
export const getMermaidText = (element: ExcalidrawElement):string =>
element.customData?.mermaidText;
export function getMermaidText (element: ExcalidrawElement):string {
return element.customData?.mermaidText;
}
export const shouldRenderMermaid = ():boolean => requireApiVersion("1.4.14");
export function shouldRenderMermaid():boolean {
return requireApiVersion("1.4.14");
}

View File

@@ -37,7 +37,7 @@ export const modifierKeyTooltipMessages = ():ModifierKeyTooltipMessages => {
// Add more messages for WebBrowserDragAction as needed
},
LocalFileDragAction: {
"image-import": "Insert Image: import external or reuse existing if path in Vault",
"image-import": "Import external file or reuse existing file if path is from the Vault",
"image-url": `Insert Image: with local URI or internal-link if from Vault`,
"link": "Insert Link: local URI or internal-link if from Vault",
"embeddable": "Insert Interactive-Frame: local URI or internal-link if from Vault",

View File

@@ -1,13 +1,16 @@
import {
App,
Editor,
FrontMatterCache,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
import yaml from "js-yaml";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import yaml, { Mark } from "js-yaml";
import { debug, DEBUGGING } from "./DebugHelper";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
@@ -29,11 +32,11 @@ export const getLeaf = (
const newTab = ():WorkspaceLeaf => {
if(!plugin.settings.openInMainWorkspace) return app.workspace.getLeaf('tab');
const [leafLoc, mainLeavesIds] = getLeafLoc(origo);
if(leafLoc === 'main') return app.workspace.getLeaf('tab');
if(leafLoc === 'main') return plugin.app.workspace.getLeaf('tab');
return getNewOrAdjacentLeaf(plugin,origo);
}
const newTabGroup = ():WorkspaceLeaf => getNewOrAdjacentLeaf(plugin,origo);
const newWindow = ():WorkspaceLeaf => app.workspace.openPopoutLeaf();
const newWindow = ():WorkspaceLeaf => plugin.app.workspace.openPopoutLeaf();
switch(linkClickModifierType(ev)) {
case "active-pane": return origo;
@@ -285,14 +288,20 @@ export const openLeaf = ({
leaf = l;
}
});
if(leaf) return {leaf, promise: Promise.resolve()};
if(leaf) {
if(openState) {
const promise = leaf.openFile(file, openState);
return {leaf, promise};
}
return {leaf, promise: Promise.resolve()};
}
}
leaf = fnGetLeaf();
const promise = leaf.openFile(file, openState);
return {leaf, promise};
}
export const mergeMarkdownFiles = (template: string, target: string): string => {
export function mergeMarkdownFiles (template: string, target: string): string {
// Extract frontmatter from the template
const templateFrontmatterEnd = template.indexOf('---', 4); // Find end of frontmatter
const templateFrontmatter = template.substring(4, templateFrontmatterEnd).trim();
@@ -304,8 +313,8 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
// Extract frontmatter from the target if it exists
let targetFrontmatterObj: FrontMatterCache = {};
let targetContent = '';
if (target.includes('---')) {
const targetFrontmatterEnd = target.indexOf('---', 4); // Find end of frontmatter
if (target.startsWith('---\n') && target.indexOf('---\n', 4) > 0) {
const targetFrontmatterEnd = target.indexOf('---\n', 4); // Find end of frontmatter
const targetFrontmatter = target.substring(4, targetFrontmatterEnd).trim();
targetContent = target.substring(targetFrontmatterEnd + 3); // Skip frontmatter and ---
@@ -336,4 +345,68 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
const mergedMarkdown = `---\n${mergedFrontmatterYaml}---\n${targetContent}\n\n${templateContent.trim()}\n`;
return mergedMarkdown;
};
};
export const editorInsertText = (editor: Editor, text: string)=> {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
const updatedLine = line.slice(0, cursor.ch) + text + line.slice(cursor.ch);
editor.setLine(cursor.line, updatedLine);
}
export const foldExcalidrawSection = (view: MarkdownView) => {
if (!view || !(view instanceof MarkdownView)) return;
const foldStart = {
ed: -1, // # Excalidraw Data
te: -1, // ## Text Elements
el: -1, // ## Element Links
ef: -1, // ## Embedded Files
d: -1, // ## Drawing
};
const existingFolds = view.currentMode.getFoldInfo()?.folds ?? [];
const lineCount = view.editor.lineCount();
for (let i = 0; i < lineCount; i++) {
const line = view.editor.getLine(i);
switch (line) {
case "# Excalidraw Data": foldStart.ed = i; break;
case "## Text Elements": foldStart.te = i; break;
case "## Element Links": foldStart.el = i; break;
case "## Embedded Files": foldStart.ef = i; break;
case "## Drawing": foldStart.d = i; break;
}
if (line === "## Drawing") break;
}
if (foldStart.ed > -1 && foldStart.d > -1) {
const foldPositions = [
...existingFolds,
...(foldStart.te > -1 ? [{ from: foldStart.te, to: (foldStart.el > -1 ? foldStart.el : (foldStart.ef > -1 ? foldStart.ef : foldStart.d)) - 1 }] : []),
...(foldStart.el > -1 ? [{ from: foldStart.el, to: (foldStart.ef > -1 ? foldStart.ef : foldStart.d) - 1 }] : []),
...(foldStart.ef > -1 ? [{ from: foldStart.ef, to: foldStart.d - 1 }] : []),
{ from: foldStart.d, to: lineCount - 1 },
{ from: foldStart.ed, to: lineCount - 1 },
];
view.currentMode.applyFoldInfo({ folds: foldPositions, lines: lineCount });
}
};
export async function setExcalidrawView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
export async function closeLeafView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: "empty",
state: {},
});
}

View File

@@ -1,8 +1,32 @@
import { WorkspaceWindow } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { getAllWindowDocuments } from "./ObsidianUtils";
import { DEBUGGING, debug } from "./DebugHelper";
const STYLE_VARIABLES = ["--background-modifier-cover","--background-primary-alt","--background-secondary","--background-secondary-alt","--background-modifier-border","--text-normal","--text-muted","--text-accent","--text-accent-hover","--text-faint","--text-highlight-bg","--text-highlight-bg-active","--text-selection","--interactive-normal","--interactive-hover","--interactive-accent","--interactive-accent-hover","--scrollbar-bg","--scrollbar-thumb-bg","--scrollbar-active-thumb-bg"];
const STYLE_VARIABLES = [
"--background-modifier-cover",
"--background-primary-alt",
"--background-secondary",
"--background-secondary-alt",
"--background-modifier-border",
"--text-normal",
"--text-muted",
"--text-accent",
"--text-accent-hover",
"--text-faint",
"--text-highlight-bg",
"--text-highlight-bg-active",
"--text-selection",
"--interactive-normal",
"--interactive-hover",
"--interactive-accent",
"--interactive-accent-hover",
"--scrollbar-bg",
"--scrollbar-thumb-bg",
"--scrollbar-active-thumb-bg",
"--tab-container-background",
"--titlebar-background-focused",
];
const EXCALIDRAW_CONTAINER_CLASS = "excalidraw__embeddable__outer";
export class StylesManager {
@@ -11,49 +35,45 @@ export class StylesManager {
private styleDark: string;
private plugin: ExcalidrawPlugin;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
plugin.app.workspace.onLayoutReady(async () => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(undefined, "StylesManager.constructor > app.workspace.onLayoutReady", this);
await this.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => {
this.copyPropertiesToTheme(doc);
})
getAllWindowDocuments(plugin.app).forEach(doc => this.copyPropertiesToTheme(doc));
//initialize
plugin.registerEvent(
plugin.app.workspace.on("css-change", async () => {
await this.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => {
this.copyPropertiesToTheme(doc);
})
}),
plugin.app.workspace.on("css-change", ()=>this.onCSSChange()),
)
plugin.registerEvent(
plugin.app.workspace.on("window-open", (win: WorkspaceWindow, window: Window) => {
this.stylesMap.set(win.doc, {
light: document.head.querySelector(`style[id="excalidraw-embedded-light"]`),
dark: document.head.querySelector(`style[id="excalidraw-embedded-dark"]`)
});
//this.copyPropertiesToTheme(win.doc);
}),
plugin.app.workspace.on("window-open", (win)=>this.onWindowOpen(win)),
)
plugin.registerEvent(
plugin.app.workspace.on("window-open", (win: WorkspaceWindow, window: Window) => {
this.stylesMap.delete(win.doc);
}),
plugin.app.workspace.on("window-close", (win)=>this.onWindowClose(win)),
)
});
}
public unload() {
for (const [doc, styleTags] of this.stylesMap) {
doc.head.removeChild(styleTags.light);
doc.head.removeChild(styleTags.dark);
}
private async onCSSChange () {
await this.harvestStyles();
getAllWindowDocuments(this.plugin.app).forEach(doc => {
this.copyPropertiesToTheme(doc);
})
}
private onWindowOpen (win: WorkspaceWindow) {
this.stylesMap.set(win.doc, {
light: document.head.querySelector(`style[id="excalidraw-embedded-light"]`),
dark: document.head.querySelector(`style[id="excalidraw-embedded-dark"]`)
});
//this.copyPropertiesToTheme(win.doc);
}
private onWindowClose (win: WorkspaceWindow) {
this.stylesMap.delete(win.doc);
}
private async harvestStyles() {
@@ -126,4 +146,13 @@ export class StylesManager {
this.stylesMap.set(doc, {light: lightStyleTag, dark: darkStyleTag});
}
}
public destroy() {
for (const [doc, styleTags] of this.stylesMap) {
doc.head.removeChild(styleTags.light);
doc.head.removeChild(styleTags.dark);
}
this.plugin = null;
}
}

View File

@@ -5,8 +5,9 @@ export type FILENAMEPARTS = {
hasTaskbone: boolean,
hasArearef: boolean,
hasFrameref: boolean,
blockref: string,
hasClippedFrameref: boolean,
hasSectionref: boolean,
blockref: string,
sectionref: string,
linkpartReference: string,
linkpartAlias: string

View File

@@ -1,35 +1,33 @@
import {
App,
Notice,
request,
requestUrl,
request,requestUrl,
TFile,
TFolder,
} from "obsidian";
import { Random } from "roughjs/bin/math";
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/excalidraw/types";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "src/constants/constFonts";
import {
exportToSvg,
exportToBlob,
IMAGE_TYPES,
FRONTMATTER_KEYS,
EXCALIDRAW_PLUGIN,
getCommonBoundingBox,
DEVICE,
getContainerElement,
} from "../constants/constants";
import ExcalidrawPlugin from "../main";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExportSettings } from "../ExcalidrawView";
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
import { FILENAMEPARTS } from "./UtilTypes";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
import { CropImage } from "./CropImage";
import opentype from 'opentype.js';
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -46,8 +44,9 @@ declare module "obsidian" {
}
}
export let versionUpdateCheckTimer: number = null;
let versionUpdateChecked = false;
export const checkExcalidrawVersion = async (app: App) => {
export async function checkExcalidrawVersion() {
if (versionUpdateChecked) {
return;
}
@@ -57,12 +56,13 @@ export const checkExcalidrawVersion = async (app: App) => {
const gitAPIrequest = async () => {
return JSON.parse(
await request({
url: `https://api.github.com/repos/zsviczian/obsidian-excalidraw-plugin/releases?per_page=5&page=1`,
url: `https://api.github.com/repos/zsviczian/obsidian-excalidraw-plugin/releases?per_page=15&page=1`,
}),
);
};
const latestVersion = (await gitAPIrequest())
.filter((el: any) => !el.draft && !el.prerelease)
.map((el: any) => {
return {
version: el.tag_name,
@@ -80,12 +80,17 @@ export const checkExcalidrawVersion = async (app: App) => {
} catch (e) {
errorlog({ where: "Utils/checkExcalidrawVersion", error: e });
}
setTimeout(() => (versionUpdateChecked = false), 28800000); //reset after 8 hours
versionUpdateCheckTimer = window.setTimeout(() => {
versionUpdateChecked = false;
versionUpdateCheckTimer = null;
}, 28800000); //reset after 8 hours
};
const random = new Random(Date.now());
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
export function randomInteger () {
return Math.floor(random.next() * 2 ** 31)
};
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
export function wrapTextAtCharLength(
@@ -150,40 +155,17 @@ const rotate = (
centerY,
];
export const rotatedDimensions = (
export function rotatedDimensions (
element: ExcalidrawElement,
): [number, number, number, number] => {
if (element.angle === 0) {
return [element.x, element.y, element.width, element.height];
}
const centerX = element.x + element.width / 2;
const centerY = element.y + element.height / 2;
const [left, top] = rotate(
element.x,
element.y,
centerX,
centerY,
element.angle,
);
const [right, bottom] = rotate(
element.x + element.width,
element.y + element.height,
centerX,
centerY,
element.angle,
);
return [
left < right ? left : right,
top < bottom ? top : bottom,
Math.abs(left - right),
Math.abs(top - bottom),
];
): [number, number, number, number] {
const bb = getCommonBoundingBox([element]);
return [bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY];
};
export const getDataURL = async (
export async function getDataURL(
file: ArrayBuffer,
mimeType: string,
): Promise<DataURL> => {
): Promise<DataURL> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
@@ -195,19 +177,47 @@ export const getDataURL = async (
});
};
export const getFontDataURL = async (
export async function getFontDataURL (
app: App,
fontFileName: string,
sourcePath: string,
name?: string,
): Promise<{ fontDef: string; fontName: string; dataURL: string }> => {
): Promise<{ fontDef: string; fontName: string; dataURL: string }> {
let fontDef: string = "";
let fontName = "";
let dataURL = "";
const f = app.metadataCache.getFirstLinkpathDest(fontFileName, sourcePath);
if (f) {
const ab = await app.vault.readBinary(f);
const mimeType = f.extension.startsWith("woff")
let mimeType = "";
let format = "";
switch (f.extension) {
case "woff":
mimeType = "application/font-woff";
format = "woff";
break;
case "woff2":
mimeType = "font/woff2";
format = "woff2";
break;
case "ttf":
mimeType = "font/ttf";
format = "truetype";
break;
case "otf":
mimeType = "font/otf";
format = "opentype";
break;
default:
mimeType = "application/octet-stream"; // Fallback if file type is unexpected
}
fontName = name ?? f.basename;
dataURL = await getDataURL(ab, mimeType);
const split = dataURL.split(";base64,", 2);
dataURL = `${split[0]};charset=utf-8;base64,${split[1]}`;
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}") format("${format}")}`;
/* const mimeType = f.extension.startsWith("woff")
? "application/font-woff"
: "font/truetype";
fontName = name ?? f.basename;
@@ -215,23 +225,23 @@ export const getFontDataURL = async (
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
const split = fontDef.split(";base64,", 2);
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
}
return { fontDef, fontName, dataURL };
};
export const base64StringToBlob = (base64String: string, mimeType: string): Blob => {
export function base64StringToBlob (base64String: string, mimeType: string): Blob {
const buffer = Buffer.from(base64String, 'base64');
return new Blob([buffer], { type: mimeType });
};
export const svgToBase64 = (svg: string): string => {
export function svgToBase64 (svg: string): string {
return `data:image/svg+xml;base64,${btoa(
unescape(encodeURIComponent(svg.replaceAll("&nbsp;", " "))),
)}`;
};
export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBuffer> => {
export async function getBinaryFileFromDataURL (dataURL: string): Promise<ArrayBuffer> {
if (!dataURL) {
return null;
}
@@ -254,12 +264,12 @@ export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBu
return bytes.buffer;
};
export const getSVG = async (
export async function getSVG (
scene: any,
exportSettings: ExportSettings,
padding: number,
srcFile: TFile|null, //if set, will replace markdown links with obsidian links
): Promise<SVGSVGElement> => {
): Promise<SVGSVGElement> {
let elements:ExcalidrawElement[] = scene.elements;
if(elements.some(el => el.type === "embeddable")) {
elements = JSON.parse(JSON.stringify(elements));
@@ -280,20 +290,25 @@ export const getSVG = async (
if(exportSettings.isMask) {
const cropObject = new CropImage(elements, scene.files);
svg = await cropObject.getCroppedSVG();
cropObject.destroy();
} else {
svg = await exportToSvg({
elements,
elements: elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
appState: {
...scene.appState,
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme != "light"
? scene.appState?.theme !== "light"
: false,
...scene.appState,
...exportSettings.frameRendering
? {frameRendering: exportSettings.frameRendering}
: {},
},
files: scene.files,
exportPadding: padding,
exportPadding: exportSettings.frameRendering ? 0 : padding,
exportingFrame: null,
renderEmbeddables: true,
skipInliningFonts: exportSettings.skipInliningFonts,
});
}
if(svg) {
@@ -321,29 +336,34 @@ export function filterFiles(files: Record<ExcalidrawElement["id"], BinaryFileDat
return filteredFiles;
}
export const getPNG = async (
export async function getPNG (
scene: any,
exportSettings: ExportSettings,
padding: number,
scale: number = 1,
): Promise<Blob> => {
): Promise<Blob> {
try {
if(exportSettings.isMask) {
const cropObject = new CropImage(scene.elements, scene.files);
return await cropObject.getCroppedPNG();
const blob = await cropObject.getCroppedPNG();
cropObject.destroy();
return blob;
}
return await exportToBlob({
elements: scene.elements,
elements: scene.elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
appState: {
...scene.appState,
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme != "light"
? scene.appState?.theme !== "light"
: false,
...scene.appState,
...exportSettings.frameRendering
? {frameRendering: exportSettings.frameRendering}
: {},
},
files: filterFiles(scene.files),
exportPadding: padding,
exportPadding: exportSettings.frameRendering ? 0 : padding,
mimeType: "image/png",
getDimensions: (width: number, height: number) => ({
width: width * scale,
@@ -352,16 +372,17 @@ export const getPNG = async (
}),
});
} catch (error) {
new Notice("Error exporting PNG - PNG too large, try a smaller resolution");
errorlog({ where: "Utils.getPNG", error });
return null;
}
};
export const getQuickImagePreview = async (
export async function getQuickImagePreview (
plugin: ExcalidrawPlugin,
path: string,
extension: "png" | "svg",
): Promise<any> => {
): Promise<any> {
if (!plugin.settings.displayExportedImageIfAvailable) {
return null;
}
@@ -378,38 +399,10 @@ export const getQuickImagePreview = async (
}
};
export const embedFontsInSVG = (
svg: SVGSVGElement,
plugin: ExcalidrawPlugin,
localOnly: boolean = false,
): SVGSVGElement => {
//replace font references with base64 fonts)
const includesVirgil = !localOnly &&
svg.querySelector("text[font-family^='Virgil']") != null;
const includesCascadia = !localOnly &&
svg.querySelector("text[font-family^='Cascadia']") != null;
const includesAssistant = !localOnly &&
svg.querySelector("text[font-family^='Assistant']") != null;
const includesLocalFont =
svg.querySelector("text[font-family^='LocalFont']") != null;
const defs = svg.querySelector("defs");
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
let style = defs.querySelector("style");
if (!style) {
style = document.createElement("style");
defs.appendChild(style);
}
style.innerHTML = `${includesVirgil ? VIRGIL_FONT : ""}${
includesCascadia ? CASCADIA_FONT : ""}${
includesAssistant ? ASSISTANT_FONT : ""
}${includesLocalFont ? plugin.fourthFontDef : ""}`;
}
return svg;
};
export const getImageSize = async (
export async function getImageSize (
src: string,
): Promise<{ height: number; width: number }> => {
): Promise<{ height: number; width: number }> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
@@ -421,7 +414,7 @@ export const getImageSize = async (
});
};
export const addAppendUpdateCustomData = (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement => {
export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement {
if(!newData) return el;
if(!el.customData) el.customData = {};
for (const key in newData) {
@@ -431,15 +424,23 @@ export const addAppendUpdateCustomData = (el: Mutable<ExcalidrawElement>, newDat
return el;
};
export const scaleLoadedImage = (
export function scaleLoadedImage (
scene: any,
files: any,
): { dirty: boolean; scene: any } => {
files: any
): { dirty: boolean; scene: any } {
let dirty = false;
if (!files || !scene) {
return { dirty, scene };
}
for (const f of files) {
for (const f of files.filter((f:any)=>{
if(!Boolean(EXCALIDRAW_PLUGIN)) return true; //this should never happen
const ef = EXCALIDRAW_PLUGIN.filesMaster.get(f.id);
if(!ef) return true; //mermaid SVG or equation
const file = EXCALIDRAW_PLUGIN.app.vault.getAbstractFileByPath(ef.path.replace(/#.*$/,"").replace(/\|.*$/,""));
if(!file || (file instanceof TFolder)) return false;
return (file as TFile).extension==="md" || EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
})) {
const [w_image, h_image] = [f.size.width, f.size.height];
const imageAspectRatio = f.size.width / f.size.height;
scene.elements
@@ -452,7 +453,7 @@ export const scaleLoadedImage = (
}
if(f.shouldScale) {
const elementAspectRatio = w_old / h_old;
if (imageAspectRatio != elementAspectRatio) {
if (imageAspectRatio !== elementAspectRatio) {
dirty = true;
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
@@ -475,7 +476,7 @@ export const scaleLoadedImage = (
return { dirty, scene };
};
export const setDocLeftHandedMode = (isLeftHanded: boolean, ownerDocument:Document) => {
export function setDocLeftHandedMode(isLeftHanded: boolean, ownerDocument:Document) {
const newStylesheet = ownerDocument.createElement("style");
newStylesheet.id = "excalidraw-left-handed";
newStylesheet.textContent = `.excalidraw .App-bottom-bar{justify-content:flex-end;}`;
@@ -488,10 +489,10 @@ export const setDocLeftHandedMode = (isLeftHanded: boolean, ownerDocument:Docume
}
}
export const setLeftHandedMode = (isLeftHanded: boolean) => {
export function setLeftHandedMode (isLeftHanded: boolean) {
const visitedDocs = new Set<Document>();
app.workspace.iterateAllLeaves((leaf) => {
const ownerDocument = app.isMobile?document:leaf.view.containerEl.ownerDocument;
const ownerDocument = DEVICE.isMobile?document:leaf.view.containerEl.ownerDocument;
if(!ownerDocument) return;
if(visitedDocs.has(ownerDocument)) return;
visitedDocs.add(ownerDocument);
@@ -509,7 +510,7 @@ export type LinkParts = {
page: number;
};
export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
export function getLinkParts (fname: string, file?: TFile): LinkParts {
// 1 2 3 4 5
const REG = /(^[^#\|]*)#?(\^)?([^\|]*)?\|?(\d*)x?(\d*)/;
const parts = fname.match(REG);
@@ -527,23 +528,24 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
};
};
export const compress = (data: string): string => {
return LZString.compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
export function compress (data: string): string {
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
};
export const decompress = (data: string): string => {
export function decompress (data: string): string {
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
};
export const isMaskFile = (
export function isMaskFile (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean => {
): boolean {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] !== "undefined")
) {
return Boolean(fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name]);
}
@@ -551,15 +553,16 @@ export const isMaskFile = (
return false;
};
export const hasExportTheme = (
export function hasExportTheme (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean => {
): boolean {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== "undefined")
) {
return true;
}
@@ -567,16 +570,17 @@ export const hasExportTheme = (
return false;
};
export const getExportTheme = (
export function getExportTheme (
plugin: ExcalidrawPlugin,
file: TFile,
theme: string,
): string => {
): string {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== "undefined")
) {
return fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name]
? "dark"
@@ -586,15 +590,33 @@ export const getExportTheme = (
return plugin.settings.exportWithTheme ? theme : "light";
};
export const hasExportBackground = (
export function shouldEmbedScene (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean => {
file: TFile
): boolean {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] !== "undefined")
) {
return fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name];
}
}
return plugin.settings.exportEmbedScene;
};
export function hasExportBackground (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== "undefined")
) {
return true;
}
@@ -602,15 +624,16 @@ export const hasExportBackground = (
return false;
};
export const getWithBackground = (
export function getWithBackground (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean => {
): boolean {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== "undefined")
) {
return !fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name];
}
@@ -618,15 +641,18 @@ export const getWithBackground = (
return plugin.settings.exportWithBackground;
};
export const getExportPadding = (
export function getExportPadding (
plugin: ExcalidrawPlugin,
file: TFile,
): number => {
): number {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if(!fileCache?.frontmatter) return plugin.settings.exportPaddingSVG;
if (fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] != null) {
if (
fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] !== "undefined")
) {
const val = parseInt(
fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name],
);
@@ -636,7 +662,10 @@ export const getExportPadding = (
}
//deprecated. Retained for backward compatibility
if (fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] != null) {
if (
fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] !== "undefined")
) {
const val = parseInt(
fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name],
);
@@ -649,12 +678,13 @@ export const getExportPadding = (
return plugin.settings.exportPaddingSVG;
};
export const getPNGScale = (plugin: ExcalidrawPlugin, file: TFile): number => {
export function getPNGScale (plugin: ExcalidrawPlugin, file: TFile): number {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] !== "undefined")
) {
const val = parseFloat(
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name],
@@ -667,7 +697,7 @@ export const getPNGScale = (plugin: ExcalidrawPlugin, file: TFile): number => {
return plugin.settings.pngExportScale;
};
export const isVersionNewerThanOther = (version: string, otherVersion: string): boolean => {
export function isVersionNewerThanOther (version: string, otherVersion: string): boolean {
const v = version.match(/(\d*)\.(\d*)\.(\d*)/);
const o = otherVersion.match(/(\d*)\.(\d*)\.(\d*)/);
@@ -682,9 +712,9 @@ export const isVersionNewerThanOther = (version: string, otherVersion: string):
)
}
export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
// 0 1 23 4 5 6 7 8 9
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|clippedframe=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|clippedframe=|taskbone)?([^\^\|]*))(.*)/);
if(!parts) {
return {
filepath: fname,
@@ -693,6 +723,7 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
blockref: "",
hasSectionref: false,
sectionref: "",
@@ -707,6 +738,7 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
hasTaskbone: (parts[4]==="taskbone") || (parts[7]==="taskbone"),
hasArearef: (parts[4]==="area=") || (parts[7]==="area="),
hasFrameref: (parts[4]==="frame=") || (parts[7]==="frame="),
hasClippedFrameref: (parts[4]==="clippedframe=") || (parts[7]==="clippedframe="),
blockref: parts[5],
hasSectionref: Boolean(parts[6]),
sectionref: parts[8],
@@ -715,14 +747,17 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
}
}
export const fragWithHTML = (html: string) =>
createFragment((frag) => (frag.createDiv().innerHTML = html));
export function fragWithHTML (html: string) {
return createFragment((frag) => (frag.createDiv().innerHTML = html));
}
export const errorlog = (data: {}) => {
export function errorlog (data: {}) {
console.error({ plugin: "Excalidraw", ...data });
};
export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export async function sleep (ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**REACT 18
//see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/b67d70c5196f30e2968f9da919d106ee66f2a5eb
@@ -730,27 +765,42 @@ export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(r
export const awaitNextAnimationFrame = async () => new Promise(requestAnimationFrame);
*/
export const log = console.log.bind(window.console);
export const debug = console.log.bind(window.console);
//export const debug = function(){};
export const getContainerElement = (
export function _getContainerElement (
element:
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
| null,
scene: ExcalidrawScene,
) => {
if (!element) {
scene: any,
) {
if (!element || !scene?.elements || element.type !== "text") {
return null;
}
if (element.containerId) {
return scene.elements.filter(el=>el.id === element.containerId)[0] ?? null;
return getContainerElement(element as ExcalidrawTextElement, arrayToMap(scene.elements))
//return scene.elements.find((el:ExcalidrawElement)=>el.id === element.containerId) ?? null;
}
return null;
};
export const updateFrontmatterInString = (data:string, keyValuePairs?: [string,string][]):string => {
/**
* Transforms array of objects containing `id` attribute,
* or array of ids (strings), into a Map, keyd by `id`.
*/
export function arrayToMap <T extends { id: string } | string>(
items: readonly T[] | Map<string, T>,
) {
if (items instanceof Map) {
return items;
}
return items.reduce((acc: Map<string, T>, element) => {
acc.set(typeof element === "string" ? element : element.id, element);
return acc;
}, new Map());
};
export function updateFrontmatterInString(data:string, keyValuePairs?: [string,string][]):string {
if(!data || !keyValuePairs) return data;
for(const kvp of keyValuePairs) {
const r = new RegExp(`${kvp[0]}:\\s.*\\n`,"g");
@@ -761,22 +811,27 @@ export const updateFrontmatterInString = (data:string, keyValuePairs?: [string,s
return data;
}
const isHyperLink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
function isHyperLink (link:string) {
return link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
}
export const isContainer = (el: ExcalidrawElement) => el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text");
export function isContainer (el: ExcalidrawElement) {
return el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text");
}
export const hyperlinkIsImage = (data: string):boolean => {
export function hyperlinkIsImage (data: string):boolean {
if(!isHyperLink(data)) false;
const corelink = data.split("?")[0];
return IMAGE_TYPES.contains(corelink.substring(corelink.lastIndexOf(".")+1));
}
export const hyperlinkIsYouTubeLink = (link:string): boolean =>
isHyperLink(link) &&
export function hyperlinkIsYouTubeLink (link:string): boolean {
return isHyperLink(link) &&
(link.startsWith("https://youtu.be") || link.startsWith("https://www.youtube.com") || link.startsWith("https://youtube.com") || link.startsWith("https//www.youtu.be")) &&
link.match(/(youtu.be\/|v=)([^?\/\&]*)/)!==null
}
export const getYouTubeThumbnailLink = async (youtubelink: string):Promise<string> => {
export async function getYouTubeThumbnailLink (youtubelink: string):Promise<string> {
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://youtu.be/z8UkHGpykYU?t=60
//https://www.youtube.com/watch?v=z8UkHGpykYU&ab_channel=VerbaltoVisual
@@ -800,7 +855,7 @@ export const getYouTubeThumbnailLink = async (youtubelink: string):Promise<strin
return `https://i.ytimg.com/vi/${videoId}/default.jpg`;
}
export const isCallerFromTemplaterPlugin = (stackTrace:string) => {
export function isCallerFromTemplaterPlugin (stackTrace:string) {
const lines = stackTrace.split("\n");
for (const line of lines) {
if (line.trim().startsWith("at Templater.")) {
@@ -810,7 +865,7 @@ export const isCallerFromTemplaterPlugin = (stackTrace:string) => {
return false;
}
export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
export function convertSVGStringToElement (svg: string): SVGSVGElement {
const divElement = document.createElement("div");
divElement.innerHTML = svg;
const firstChild = divElement.firstChild;
@@ -820,9 +875,11 @@ export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
return;
}
export const escapeRegExp = (str:string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export function escapeRegExp (str:string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
export const addIframe = (containerEl: HTMLElement, link:string, startAt?: number, style:string = "settings") => {
export function addIframe (containerEl: HTMLElement, link:string, startAt?: number, style:string = "settings") {
const wrapper = containerEl.createDiv({cls: `excalidraw-videoWrapper ${style}`})
wrapper.createEl("iframe", {
attr: {
@@ -836,18 +893,32 @@ export const addIframe = (containerEl: HTMLElement, link:string, startAt?: numbe
});
}
/**
* Transforms array of objects containing `id` attribute,
* or array of ids (strings), into a Map, keyd by `id`.
*/
export const arrayToMap = <T extends { id: string } | string>(
items: readonly T[] | Map<string, T>,
) => {
if (items instanceof Map) {
return items;
export interface FontMetrics {
unitsPerEm: number;
ascender: number;
descender: number;
lineHeight: number;
fontName: string;
}
export async function getFontMetrics(fontUrl: string, name: string): Promise<FontMetrics | null> {
try {
const font = await opentype.load(fontUrl);
const unitsPerEm = font.unitsPerEm;
const ascender = font.ascender;
const descender = font.descender;
const lineHeight = (ascender - descender) / unitsPerEm;
const fontName = font.names.fontFamily.en ?? name;
return {
unitsPerEm,
ascender,
descender,
lineHeight,
fontName,
};
} catch (error) {
console.error('Error loading font:', error);
return null;
}
return items.reduce((acc: Map<string, T>, element) => {
acc.set(typeof element === "string" ? element : element.id, element);
return acc;
}, new Map());
};
}

29
src/utils/WeakArray.ts Normal file
View File

@@ -0,0 +1,29 @@
export class WeakArray<T extends object> {
private weakArray: WeakRef<T>[] = [];
constructor() {}
push(obj: T) {
this.weakArray.push(new WeakRef(obj));
}
forEach(callback: (obj: T, index: number) => void) {
this.weakArray.forEach((ref, index) => {
const obj = ref.deref();
if (obj) {
callback(obj, index);
}
});
}
removeObjects(objectsToRemove: Set<T>) {
this.weakArray = this.weakArray.filter((ref) => {
const obj = ref.deref();
return obj && !objectsToRemove.has(obj);
});
}
clear() {
this.weakArray = [];
}
}

View File

@@ -559,4 +559,70 @@ img.excalidraw-cropped-pdfpage,
.excalidraw .pdf-toolbar,
.excalidraw .pdf-container {
width: 100%;
}
.ex-opacity-30 {
opacity: 0.3;
}
.ex-opacity-15 {
opacity: 0.15;
}
.ex-opacity-8 {
opacity: 0.08;
}
.ex-opacity-5 {
opacity: 0.05;
}
.ex-opacity-0 {
opacity: 0;
}
.popover .excalidraw-svg {
width: 100%;
max-width: inherit;
height: 100%;
max-height: inherit;
}
root {
--excalidraw-caret-color: initial;
}
textarea.excalidraw-wysiwyg, .excalidraw input {
caret-color: var(--excalidraw-caret-color);
}
.excalidraw-settings-links-container {
display: flex; /* Align SVG and text horizontally */
align-items: center; /* Center SVG and text vertically */
text-decoration: none; /* Remove underline from links */
color: inherit; /* Inherit text color */
text-align: center;
}
.excalidraw-settings-links-container a {
display: flex; /* Align children horizontally */
align-items: center; /* Center items vertically */
text-align: left;
}
.excalidraw-settings-links-container svg {
margin-right: 8px;
height: 30px;
width: 30px;
}
.excalidraw-rank {
text-align: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.excalidraw-rank svg {
height: 8rem;
width: 8rem;
}

View File

@@ -3,7 +3,7 @@
"baseUrl": ".",
"sourceMap": false,
"module": "ES2015",
"target": "es2017", //es2017 because script engine requires for async execution
"target": "es2018", //es2017 because script engine requires for async execution
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
@@ -18,11 +18,11 @@
"DOM.Iterable"
],
"jsx": "react",
"inlineSourceMap": true
"inlineSourceMap": true,
},
"include": [
"**/*.ts",
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
"src/types.d.ts"
"src/types/types.d.ts",
]
}

View File

@@ -3,7 +3,7 @@
"baseUrl": ".",
"sourceMap": false,
"module": "es2015",
"target": "es2017", //es2017 because script engine requires for async execution
"target": "es2018", //es2017 because script engine requires for async execution //es2018 for named capture groups
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
@@ -14,14 +14,14 @@
"dom",
"scripthost",
"es2015",
"esnext",
"ESNext",
"DOM.Iterable"
],
"jsx": "react"
"jsx": "react",
},
"include": [
"**/*.ts",
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
"src/types.d.ts"
"src/types/types.d.ts",
]
}