Compare commits

...

74 Commits

Author SHA1 Message Date
zsviczian
8240c7e240 2.4.1-rc-1 2024-09-03 11:32:23 +02:00
zsviczian
6f020e9574 Merge pull request #1987 from dmscode/master
Fix
2024-09-03 10:13:32 +02:00
dmscode
c9d5f74ed4 Fix
-I found that the operation to modify custom brushes is a double-click, not a long press;
- Added a website to find free commercial Chinese fonts.
2024-09-03 10:06:31 +08:00
zsviczian
78034f6dea svg 2024-09-02 19:30:45 +02:00
zsviczian
b81cb52614 svg change 2024-09-02 19:25:02 +02:00
zsviczian
56280cd893 reset latex size 2024-09-02 19:02:09 +02:00
zsviczian
e515586e6b Merge pull request #1984 from firai/reset-latex-ea-script
Add script to reset the sizes of LaTeX equations
2024-09-02 11:11:47 +02:00
firai
424af4c60d Add Reset LaTeX Size script 2024-09-02 02:18:52 +08:00
zsviczian
0a7048aca1 update golden ratio 2024-09-01 09:03:34 +02:00
zsviczian
dd812e0684 update golden ratio script 2024-09-01 09:01:11 +02:00
zsviczian
51b7aebbc3 2.4.0 2024-08-31 13:37:27 +02:00
zsviczian
01b1698934 Update bug_report.yml 2024-08-30 14:10:42 +02:00
zsviczian
1025054bf4 Update bug_report.yml 2024-08-30 14:03:31 +02:00
zsviczian
06b4986997 Update bug_report.yml 2024-08-30 14:02:40 +02:00
zsviczian
31716ebcbc Update bug_report.yml 2024-08-30 13:58:45 +02:00
zsviczian
2e1f28f67e Update bug_report.yml 2024-08-30 13:56:20 +02:00
zsviczian
7f2e2b2d45 Update bug_report.yml 2024-08-30 13:52:52 +02:00
zsviczian
b961435e01 Update bug_report.yml 2024-08-30 13:48:09 +02:00
zsviczian
c54fab9603 Update bug_report.yml 2024-08-30 13:46:48 +02:00
zsviczian
cc6832afd6 Rename bug_report.md to bug_report.md.x 2024-08-30 13:44:13 +02:00
zsviczian
29c41cb45a Create bug_report.yml 2024-08-30 13:38:43 +02:00
zsviczian
c18984a26b Merge pull request #1977 from zsviczian/window-onblur
save on window blur
2024-08-30 13:20:30 +02:00
zsviczian
23d1ad0da6 save on window blur 2024-08-30 11:19:43 +00:00
zsviczian
49173dc766 reset autosave timer the first time the drawing changes 2024-08-30 12:49:00 +02:00
zsviczian
03a563856d Merge pull request #1974 from zsviczian/autosave-tweaks
autosave tweaks
2024-08-30 10:33:13 +02:00
zsviczian
c3809c409d autosave tweaks 2024-08-30 08:32:27 +00:00
zsviczian
dfdca90ca5 2.4.0-rc-2 2024-08-29 23:18:01 +02:00
zsviczian
6a8e1735db update nvmrc to 18 2024-08-29 20:29:54 +02:00
zsviczian
c0e9a0553e minor build changes 2024-08-29 20:24:59 +02:00
zsviczian
e1501165d9 Merge pull request #1971 from dmscode/master
Update zh-cn.ts to hotekey override, hide spalsh, save active tool, r…
2024-08-29 15:04:15 +02:00
dmscode
3b0f706059 Update zh-cn.ts to hotekey override, hide spalsh, save active tool, remove comments in p… … 2024-08-29 18:27:50 +08:00
zsviczian
7d19662f68 Merge pull request #1970 from zsviczian/hotkey-override
hotekey override, hide spalsh, save active tool, remove comments in p…
2024-08-29 11:58:30 +02:00
zsviczian
5c949dc71c hotekey override, hide spalsh, save active tool, remove comments in prod build 2024-08-29 09:57:53 +00:00
zsviczian
0439d67a0c Merge pull request #1966 from dmscode/master
Update zh-cn.ts to 2.4.0-rc-1
2024-08-29 09:00:59 +02:00
dmscode
d3446a20b1 Update zh-cn.ts to 2.4.0-rc-1 2024-08-28 20:45:21 +08:00
zsviczian
5b37dc2e38 save on contentEl mouseleave 2024-08-28 12:34:56 +02:00
zsviczian
eee264918e 2.4.0-rc-1 2024-08-27 21:43:21 +02:00
zsviczian
89172a88f1 fix android preview render bug 2024-08-27 08:56:57 +02:00
zsviczian
ffdb054291 2.4.0-beta-10 2024-08-26 22:40:45 +02:00
zsviczian
200d39c408 Merge pull request #1960 from dmscode/master
Update zh-cn.ts and add Readme.zh-cn
2024-08-26 22:27:07 +02:00
zsviczian
4306574ace Update Excalidraw Writing Machine 2024-08-26 15:04:58 +02:00
zsviczian
12e3b90458 support wikilinks 2024-08-26 15:04:05 +02:00
dmscode
03364b5d2e Readme.zh-cn 2024-08-26 15:43:28 +08:00
dmscode
4e268991dc Update zh-cn.ts to 2.4.0-beta-9 2024-08-26 14:32:46 +08:00
zsviczian
429c84f940 updated rollup, tsconfig, rollup versions in package.json 2024-08-25 16:53:15 +02:00
zsviczian
e890e4489b replace json.stringify with proper processing, fix small issues with Ephemral state, added worker (inactive) 2024-08-25 16:08:12 +02:00
zsviczian
8466c42217 update Excalidraw package -42 is corrupted 2024-08-24 20:30:51 +02:00
zsviczian
353732f597 2.4.0-beta-9 2024-08-24 13:59:49 +02:00
zsviczian
5599d2507f update writing machine data 2024-08-23 12:43:27 +02:00
zsviczian
70cf6ffe70 Support embeddables 2024-08-23 12:41:33 +02:00
zsviczian
61c9277097 writing machine 2024-08-22 21:59:06 +02:00
zsviczian
401052efd3 svg icon take 2 2024-08-22 20:36:00 +02:00
zsviczian
a57a0e797d update writing machine svg 2024-08-22 20:33:52 +02:00
zsviczian
8f48853e2c updated writing machine script 2024-08-22 17:58:42 +02:00
zsviczian
a62148dc07 publish Excalidraw Writing Machine 2024-08-21 21:53:59 +02:00
zsviczian
3ae890bd86 2.4.0-beta-8 2024-08-19 15:57:23 +02:00
zsviczian
65a4cd4ba5 Merge pull request #1939 from dmscode/master
Update zh-cn.ts 2.4.0-beta-7
2024-08-19 15:38:56 +02:00
dmscode
f63b473bc1 Update zh-cn.ts to the commit "fixed getTemplate, migrade gridSize, gridStep" 2024-08-19 09:42:37 +08:00
稻米鼠
859a5ba03a Merge branch 'zsviczian:master' into master 2024-08-19 09:37:07 +08:00
zsviczian
832b97b179 fixed getTemplate, migrade gridSize, gridStep, 2024-08-18 17:15:03 +02:00
dmscode
e98d688d36 Update zh-cn.ts 2.4.0-beta-7 2024-08-16 15:53:39 +08:00
zsviczian
39318337fe updated set stroke width script 2024-08-16 08:13:55 +02:00
zsviczian
f21215be84 2.4.0-beta-7 2024-08-15 17:45:49 +02:00
zsviczian
0690525af8 Merge pull request #1937 from dmscode/master
Update zh-cn.ts to 2.4.0-beta-6
2024-08-15 17:41:32 +02:00
dmscode
b3176425c5 Merge branch 'master' of https://github.com/dmscode/obsidian-excalidraw-plugin 2024-08-15 18:56:30 +08:00
dmscode
9f2c18b6b6 Update zh-cn.ts to 2.4.0-beta-6 2024-08-15 18:56:20 +08:00
zsviczian
d529a04f48 2.4.0-beta-6, pdf++ crop support, double tap eraser disable 2024-08-14 20:02:41 +02:00
zsviczian
8786c5aa99 2.4.0-beta-5 2024-08-14 10:07:55 +02:00
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
65 changed files with 2810 additions and 589 deletions

85
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Bug report
description: When something is clearly broken. Everything else is a feature request.
title: "BUG: "
body:
- type: markdown
attributes:
value: |
⚠️ **Important: Please Read Before Submitting a Bug Report** ⚠️
I am a one-person team working on this plugin as a part-time hobby. I cannot handle a flood of poorly documented issues. **To ensure your report is considered, you must follow these guidelines**. If you don't, I will close the issue without review.
Before creating a bug report, please:
1. **Review recent [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases)** maybe there is already an answer.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if there is anything similar.
- type: markdown
attributes:
value: |
Please help by providing the following details. Bugs reported without the required information may be closed without review.
- type: markdown
attributes:
value: |
---
- type: checkboxes
id: search_existing_issues
attributes:
label: Have you searched for existing issues (including closed ones)?
description: Please confirm that you have searched the issue tracker before reporting a new issue.
options:
- label: Yes, I have searched the existing issues.
- type: checkboxes
id: verify_bug
attributes:
label: Does this bug persist in a new vault with only Excalidraw installed?
description: Please confirm that you have tested this issue in an empty Obsidian vault with no other plugins or themes installed.
options:
- label: Yes, I have verified the issue persists.
- type: textarea
id: environment
attributes:
label: "Your environment"
description: "Run `Command Palette/Show Debug info` in Obsidian and paste the result here."
placeholder: "Paste your Obsidian debug info here..."
- type: textarea
id: bug_description
attributes:
label: "Describe the bug"
description: "A clear and concise description of what the bug is."
placeholder: "Provide a detailed description of the issue..."
- type: textarea
id: steps_to_reproduce
attributes:
label: "Steps to reproduce"
description: "List the steps to reproduce the behavior."
placeholder: |
1. Go to '...'
2. Click on '...'
3. See error
- type: textarea
id: expected_behavior
attributes:
label: "Expected behavior"
description: "A clear and concise description of what you expected to happen."
placeholder: "Describe what you expected to happen..."
- type: textarea
id: additional_context
attributes:
label: "Additional context"
description: "Add any other context about the problem here."
placeholder: "Include any other information that may be helpful..."
- type: markdown
attributes:
value: |
**Attachments:**
If applicable, please attach any screenshots, screen recordings, or files by dragging and dropping them into the comment area or directly into any of the text fields above.

2
.nvmrc
View File

@@ -1 +1 @@
16
18

View File

@@ -2,31 +2,3 @@ The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:
## postprocess
postprocess is used in rollup.config.js.
However, the version available on npmjs does not work, after installing packages you need this update:
`npm install brettz9/rollup-plugin-postprocess#update --save-dev``
More info here: https://github.com/developit/rollup-plugin-postprocess/issues/10
## colormaster
1.2.1 misses 3 plugin references after installing the package you need to update
`node_modules/colormaster/package.json` adding the following to the `exports:` section:
```typescript
,
"./plugins/luv": {
"import": "./plugins/luv.mjs",
"require": "./plugins/luv.js",
"default": "./plugins/luv.mjs"
},
"./plugins/uvw": {
"import": "./plugins/uvw.mjs",
"require": "./plugins/uvw.js",
"default": "./plugins/uvw.mjs"
},
"./plugins/ryb": {
"import": "./plugins/ryb.mjs",
"require": "./plugins/ryb.js",
"default": "./plugins/ryb.mjs"
}
```

View File

@@ -1,5 +1,7 @@
# Excalidraw
[简体中文](./README.zh-cn.md)
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

285
README.zh-cn.md Normal file
View File

@@ -0,0 +1,285 @@
# Excalidraw
> 此说明当前更新至 2.4.0-beta-9。
Obsidian-Excalidraw 插件将 [Excalidraw](https://excalidraw.com/) 这一功能丰富的草图工具集成到 Obsidian 中。您可以在您的库中存储和编辑 Excalidraw 文件,可以将图形嵌入到文档中,还可以在 Excalidraw 中链接到文档和其他图形。有关 Excalidraw 功能的展示,请查看我的博客文章 [这里](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) 或观看以下视频。
## 视频演示
<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>
### 这是我完整的视频目录:
<a href="https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/Catalogue+of+Videos"><img width="380" alt="image" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/2577e5ad-7a21-4c62-acd5-4fe80c8a8a95"></a>
<br>
<details><summary>10 部分(稍微过时)视频演示</summary>
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;1 入门</a><br>
<a href="https://youtu.be/Iy_oVTq12Gw" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160312-8a743d00-e17c-11eb-9fa2-490ef4cbd59e.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;2 基本形状和功能</a><br>
<a href="https://youtu.be/QOL1KF7-kdc" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160323-96f89580-e17c-11eb-9bce-8eb1067a51bb.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;3 元素分组</a><br>
<a href="https://youtu.be/aSgcbfspvfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160332-9f50d080-e17c-11eb-98e9-fec60fe147d9.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;4 模板库</a><br>
<a href="https://youtu.be/MaJ5jJwBRWs" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160341-a546b180-e17c-11eb-9de8-d87fdc844c9c.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;5 嵌入</a><br>
<a href="https://youtu.be/MXzeCOEExNo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;6 链接</a><br>
<a href="https://youtu.be/R0IAg0s-wQE" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;7 Markdown</a><br>
<a href="https://youtu.be/ibdS7ykwpW4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;8 模板</a><br>
<a href="https://youtu.be/VRZVujfVab0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;9 Excalidraw 自动化</a><br>
<a href="https://youtu.be/D1iBYo1_jjc" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;10 杂项</a><br>
</details>
<details><summary>将内容嵌入 Excalidraw</summary>
<a href="https://www.youtube.com/watch?v=_c_0zpBJ4Xc&" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/138607067-ccb62f92-48a4-4880-ac6e-68c1bf86ac2c.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;图像元素</a><br>
<a href="https://youtu.be/r08wk-58DPk" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143732412-1c65227e-4381-406d-847a-b001ab3506ca.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;LaTeX 演示</a><br>
<a href="https://youtu.be/tsecSfnTMow" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143732440-90bfa029-8615-462e-ada3-c903d71a82c9.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Markdown 嵌入</a><br>
<a href="https://youtu.be/K6qZkTz8GHs" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143783906-15cee494-c6d5-4495-a2ca-74634e4e7355.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Markdown 嵌入高级功能</a><br>
<a href="https://youtu.be/Etskjw7a5zo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931461-0979b821-315a-41dd-86f1-31d169b7c127.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接到元素、垂直文本对齐、Markdown 样式</a><br>
</details>
<details><summary>脚本引擎商店 - Excalidraw 自动化</summary>
<a href="https://youtu.be/hePJcObHIso" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;介绍脚本引擎</a><br>
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;脚本引擎商店</a><br>
</details>
<details><summary>使用颜色</summary>
<a href="https://youtu.be/6PLGHBH9VZ4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773147-5418a0ab-6be5-4eb0-a8e4-d6af21b1b483.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;颜色 - Excalidraw 基础(自定义)</a><br>
<a href="https://youtu.be/epYNx2FSf2w" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773211-9e871be7-0795-4dc7-947e-c6c275e690d0.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Excalidraw 调色板(自定义)</a><br>
<a href="https://youtu.be/Amhlv6r9WvM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773268-400cfb1b-6bde-45e0-9e4b-91bbaa461cf0.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;“艺术”颜色渐变</a><br>
<a href="https://youtu.be/r9oB1SlK1GU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773527-ef35c8b9-1a6d-4415-9c7e-b667fb17535d.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;美丽草图的简单规则</a><br>
<a href="https://youtu.be/7gJDwNgQ6NU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/195988535-a133a9b9-d094-45ba-ba64-c994b9a1e0ef.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;ColorMaster 脚本编写</a><br>
</details>
<details><summary>链接和块引用</summary>
<a href="https://youtu.be/qiKuqMcNWgU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/171635214-30533c45-94fa-436e-83a9-b2ec99f190e2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接视觉思维的 6 种策略 v4</a><br>
<a href="https://youtu.be/yZQoJg2RCKI" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/185791706-3d9983ab-7cb1-4b27-a016-30c039d84e34.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;图像的块引用部分</a><br>
<a href="https://youtu.be/Etskjw7a5zo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931461-0979b821-315a-41dd-86f1-31d169b7c127.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接到元素、垂直文本对齐、Markdown 样式</a><br>
<a href="https://youtu.be/2Y8OhkGiTHg" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/152585752-7eb0371f-0bab-40f6-a194-3b48e5811735.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Excalidraw 原生超链接使用指南</a><br>
</details>
<details><summary>强大工具</summary>
<a href="https://youtu.be/NOuddK6xrr8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147283367-e5689385-ea51-4983-81a3-04d810d39f62.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;便签(自动换行)</a><br>
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;本地字体</a><br>
<a href="https://youtu.be/vlC1-iBvIfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/199207784-8bbe14e0-7d10-47d7-971d-20dce8dbd659.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;SVG 导入</a><br>
<a href="https://youtu.be/7gu4ETx7zro" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/202916770-28f2fa64-1ba2-4b40-a7fe-d721b42634f7.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;OCR</a><br>
<a href="https://youtu.be/U2LkBRBk4LY" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/159369910-6371f08d-b5fa-454d-9c6c-948f7e7a7d26.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;绑定/解绑文本与容器,前置标签自定义导出</a><br>
<a href="https://youtu.be/uZz5MgzWXiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/211054371-8872e01a-77d6-4afc-a0c2-86a55410a8d3.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;自定义笔支持</a><br>
</details>
<details><summary>生活质量改善</summary>
<a href="https://youtu.be/qbPIAZguJeo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/151705333-54e9ffd2-0bd7-4d02-b99e-0bd4e4708d4d.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;移动支持</a><br>
<a href="https://youtu.be/2v9TZmQNO8c" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/153676009-6f86b2d7-c248-49a2-b802-be21c6999e4f.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;托盘模式和可自定义调色板</a><br>
<a href="https://youtu.be/xHPGWR3m0c8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/154821232-a404b6cf-72fb-4ce4-9d53-619132dce491.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;压缩 JSON 和改进的保存/同步支持</a><br>
<a href="https://youtu.be/gMIKXyhS-dM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931428-b2269fd9-87bd-43ab-8558-5572f40dff93.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Obsidian 工具面板</a><br>
<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;橡皮擦、左利手模式、改进的文件名配置</a><br>
</details>
### Beta 测试
该插件遵循每月发布的计划。如果您希望获得更频繁的更新包括新功能例如excalidraw.com 上的新内容,但尚未在 Obsidian 中提供)和小的 bug 修复,请加入 beta 社区。
[![缩略图 - 20240803 Excalidraw 发布方法(自定义)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw 插件发布策略(手机)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## 功能
- 该插件将 Excalidraw 无缝集成到 Obsidian 中,包括命令面板操作、文件资源管理器功能、选项菜单命令和工具栏按钮。
- 在工具栏按钮或文件资源管理器中 <kbd>CTRL/CMD+鼠标左键</kbd> 以在新面板中创建/打开绘图。
### 设置
设置将允许您根据需要自定义 Excalidraw。该插件提供了大量设置。我尝试为这些设置添加有意义的解释所以请耐心查找对于大多数请求已经存在相关设置。
插件设置分为以下几个部分:
- **基本设置**:例如使用的默认文件夹。
- **保存**:压缩和自动保存间隔。
- **文件名**:配置自动生成的 Excalidraw 文件名。
- **显示**:影响 Excalidraw 处理的设置(例如:左利手模式、主题设置、鼠标滚轮和捏合缩放设置、适应缩放设置)。
- **链接和嵌入**:影响链接和嵌入项在 Excalidraw 画布上行为的设置。
- **Markdown 嵌入设置**:这些设置控制从您的 Vault 嵌入到 Excalidraw 绘图中的 Markdown 文档的行为。
- **嵌入与导出**:控制 Excalidraw 图像在嵌入到 Markdown 文档时的显示方式的设置。
- **自动导出设置**:您可以配置 Excalidraw 在每次保存时创建绘图的 PNG 或 SVG 副本。
- **兼容性功能**:如果您在 Obsidian 之外编辑 Excalidraw 绘图(例如在 LogSeq、Visual Studio、网页等请检查这些设置。
- **实验性功能**:有一些高级功能作为“巧妙”的 hacks 实现,包括定义本地字体、添加自定义图标以区分 Obsidian 文件资源管理器中的 Excalidraw 文件、OCR 设置等。
- **已安装脚本的设置**:从脚本库安装的一些脚本附带设置。脚本设置在您第一次运行脚本时安装。因此,要访问脚本的设置,请安装脚本,首次运行后在插件设置中查找设置。
#### 模板
- 新绘图的模板。该模板将恢复笔画属性。这意味着您可以在模板中设置笔画颜色、笔画宽度、不透明度、字体系列、字体大小、填充样式、笔画样式等的默认值。这同样适用于 ExcalidrawAutomate。
- 通过模板,您可以自定义 Excalidraw 使用的调色板。
- 切换到 Markdown 视图。
- 滚动到文件底部,找到 `"AppState": {`
- 在 AppState 部分末尾找到 `"customColorPalette": {`
- 您可以通过添加以下三个变量中的任何一个或全部来指定 Excalidraw 使用的 3 个调色板:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`
- 在每个变量的数组中添加有效 HTML 颜色的逗号分隔列表(例如,`#FF0000` 表示红色)。
- 有关更多帮助,请查看我上面的录像。
#### 导出
- 如果便携性对您很重要:
- 自动导出 SVG 和/或 PNG 文件,包括同步保持功能,这样您可以将 SVG/PNG 嵌入到文档中,而不是嵌入 Excalidraw 文件。
- 您可以通过添加 `excalidraw-autoexport` 前置字段键来覆盖单个文件的导出设置。该键的有效值为 `none``both``png``svg`
- 指定嵌入绘图的默认宽度。
- 兼容性功能以自动导出和保持同步 Markdown Excalidraw 文件及旧版 `.excalidraw` 文件。
- 实验性功能可在文件资源管理器中添加自定义标签以标记绘图文件。
- 启用/禁用自动保存。
### 将您的绘图嵌入到 Markdown 文档中
- 您可以使用以下格式自定义嵌入图像的大小和位置:
- `![[image.excalidraw|100]]`
- `![[image.excalidraw|100x100]]`
- `![[image.excalidraw|100|left]]`
- `![[image.excalidraw|right-wrap]]`
- `![[<filename.excalidraw>|<width>x<height>|<alignment>]]`
- 您可以通过 CSS 添加自定义 [对齐方式](https://www.scaler.com/topics/align-image-in-html/)。
- 出现在 `<alignment>` 中的任何文本将被添加到渲染的 SVG 元素样式和包装 DIV 元素中。
- 有关更多信息,请参见 [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css)。
- Excalidraw 绘图在 Obsidian Publish 中不显示。如果您希望在发布的文档中使用 Excalidraw可以在插件设置中的 `Embed & Export` 下进行配置,以便在创建新文件时自动将绘图的 PNG 或 SVG 版本插入到文档中。请参见 `type of file to insert into document`
-`Export settings` 下,您还可以配置自动导出图像的深色和浅色版本,以便您的发布网站支持深色和浅色模式。
### 超链接和拖放支持
![](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/images/excalidraw-modifiers.png)
#### 超链接
- 支持超链接,例如:
- `https://zsolt.blog`
- `[Obsidian](https://obsidian.md)`,以及
- 内部链接,例如在绘图文本中使用 `[[My file in vault|Alias]]`
- 如果您启用了 Obsidian 设置中的“文件与链接/自动更新内部链接”,则文件移动或重命名时链接会自动更新。
- 绘图中的链接会出现在文档的反向链接中。
- 支持嵌入:
- `![[myfile#^blockref]]` 将绘图转换为该块的嵌入文本。
- `![[myfile#section]]` 也有效,这将嵌入该部分。
- 您还可以通过在嵌入后加上最大字符数的花括号来指定嵌入文本的换行,例如 `![[myfile#^blockref]]{40}` 将在 40 个字符处换行。
- 为了方便,您还可以使用命令面板将链接插入到绘图中。
- <kbd>CTRL/CMD + 鼠标悬停</kbd> 可以调出链接的 Obsidian 快速预览。(在 Mac 上为 <kbd>CTRL+CMD+鼠标悬停</kbd>)。
- 使用块引用,您还可以在其他文档中引用和嵌入绘图中出现的文本。
#### 拖放支持
- 您可以从 Obsidian 文件资源管理器中拖动文件,它们将成为 Excalidraw 中指向这些文件的链接。有关各种修饰键组合,请参见上面的表格。
- 注意:将图像锚定到其 100% 尺寸是一个非常小众的功能,具有非常特定的行为,我主要是为自己开发的。
- (甚至 Excalidraw Obsidian 中的其他功能更是如此 - 也是主要为自己开发的 😉)。
- 每次打开 Excalidraw 绘图时,这将重置您嵌入的图像为 100% 尺寸,或者如果您在画布上嵌入了使用此功能插入的 Excalidraw 绘图,每次更新嵌入的绘图时,它将缩放回 100% 尺寸。
- 这意味着即使您在绘图中调整了图像的大小,下次打开文件或修改原始嵌入对象时,它也会重置为 100%。此功能在将绘图分解为单独的 Excalidraw 文件时非常有用,但当它们组合到单个画布上时,您希望各个部分保持其实际大小。我使用此功能从原子绘图构建“一页书”摘要。
- 您可以将文本从 Markdown 视图拖放到 Excalidraw 中。
- 您可以从浏览器中拖放网页地址,它们将成为链接。
- 您可以拖放 YouTube 链接和缩略图,它们将在 Excalidraw 中成为带缩略图的 YouTube 链接。
### LaTeX
使用命令面板操作“插入 LaTeX 公式”插入 LaTeX 公式。您可以在 Markdown 视图中编辑公式,或者通过 <kbd>CTRL/CMD + 鼠标左键</kbd> 点击公式进行编辑。
### 图像支持
- 在 iOS 和 Android 上,您可以通过按下 Excalidraw 中的添加图像按钮从相机添加图像。
- 您可以将图像复制/粘贴到绘图中。图像将保存在您的 Vault 中。
- 您可以按照上面的说明拖放图像。
- URL 链接到网络上的图像:您可以从网页将图像拖放到 Excalidraw。如果在将图像拖放到 Excalidraw 时按住 CTRL 键,图像将不会保存到您的 Vault 中。Excalidraw 将从 URL 加载图像。请注意,如果您没有互联网连接,或者这些图像从互联网上被删除,它们也会从您的绘图中消失。
- 如果您将图像 URL 粘贴到 Excalidraw只需点击 URL 复制,然后在 Excalidraw 画布上点击粘贴),图像将以链接形式插入到网络图像上。同样,图像不会保存到您的 Vault 中,只有链接会被保存。
- 如果您拖放 YouTube 视频链接,它将转换为一个缩略图,并带有指向视频的元素链接。
### 引用图像部分的块
有关更多详细信息,请参见此 [视频](https://youtu.be/yZQoJg2RCKI)。
- 当通过链接引用 Excalidraw 文件中的画布元素时,可以使用:
- 元素 ID 或章节标题(即包含 `# <章节标题>` 的文本元素)
- 例如 `[[file#^elementID]]`
- 您可以添加 `group=` 前缀,
- 例如 `[[file#^group=elementID]]`,或
- `area=` 前缀,
- 例如 `[[file#area=Section heading]]`
- 如果找到 `group=` 前缀Excalidraw 将选择与通过元素 ID块引用或章节标题引用的元素在同一组中的元素。
- 如果找到 `area=` 前缀Excalidraw 将在引用的元素周围插入图像的剪切部分。
- 请注意,当将 Excalidraw 嵌入为 PNG 到您的 Markdown 文档时,不支持 `area=` 选择器。
- 引用文本元素的元素 ID 而不带 `group=``area=` 前缀将以普通文本嵌入该元素。引用非文本元素(例如矩形、椭圆等)而不带 `group=``area=` 前缀将导致 Obsidian 错误,因为这些元素 ID 在 Excalidraw Markdown 文件中不能够作为块引用。
### Markdown
- 从 1.2.0 版本开始,绘图文件存储在 Markdown 文件中。
- 您可以为绘图添加标签。
- 您可以在绘图的 YAML 前置字段中添加元数据。
- 您在前置字段和 `# Text Elements` 标题之间添加的任何内容将被 Excalidraw 忽略,即您可以在这里添加任何内容,它将作为文档的一部分被保留。
- Excalidraw 文档现在会在图形视图中显示。
- 以下前置字段键将自定义绘图的显示方式 - 覆盖一般设置:
- `excalidraw-link-prefix: "📍"` 内部链接的预览前缀
- `excalidraw-url-prefix: "🌐"` 外部链接的预览前缀
- `excalidraw-link-brackets: true|false` 是否在预览中显示链接周围的括号
- `excalidraw-default-mode: view|zen` 默认以查看模式或禅模式打开此文档。默认查看模式非常适合演示幻灯片。
- 前置字段标签用于在文件级别自定义图像导出 [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519)。如果这些键存在,它们将覆盖默认的 Excalidraw 嵌入和导出设置。
- `excalidraw-export-transparent: true` true == 透明 / false == 有背景。
- `excalidraw-export-dark` true == 深色模式 / false == 浅色模式。
- `excalidraw-export-padding`:指定图像的导出边距。
- `excalidraw-export-pngscale`:这仅影响导出为 PNG。指定图像的导出比例。典型范围在 0.5 到 5 之间,但您也可以尝试其他值。
### 将完整的 Markdown 文件嵌入到您的绘图中
从 Obsidian 文件资源管理器中拖动所需文件,同时按住 <kbd>SHIFT</kbd> 将文件放到画布上。
- 使用命令面板操作:`从 Vault 插入 Markdown 文件`
- 使用自定义的 woff、woff2 或 TTF 字体来显示文档,您可以在 Excalidraw 设置中设置默认字体。
- 您可以为渲染 Markdown 文档的快照图像设置自定义 CSS。仅支持操作系统标准字体作为字体系列[Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list)、[Mac & iOS](https://developer.apple.com/fonts/system-fonts/)),此外,您可以使用上述设置添加一个额外的自定义字体。
- (要查看演示,请观看此 [视频](https://youtu.be/K6qZkTz8GHs) 并查看此
- [示例 CSS](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281))。
- 为了帮助样式设置,您可以查看 Excalidraw 创建的 Markdown 文档的 SVG 快照。
- 打开 Obsidian 开发者控制台 (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>),并
- 执行以下命令:`ExcalidrawAutomate.mostRecentMarkdownSVG`
- 您可以通过将以下前置字段键添加到您的 Markdown 文档,按文件控制嵌入 Markdown 文件的外观:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- 您可以在 [这里](https://www.w3schools.com/colors/colors_names.asp) 找到 CSS 颜色名称。
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- 切换到 Markdown 视图或使用 <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> 点击图像以编辑嵌入的属性:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
### 自定义字体、自定义笔、OCR 支持、SVG 导入
- 在插件设置中,您可以添加自定义的本地字体。有关更多详细信息,请参见此 [视频](https://youtu.be/eKFmrSQhFA4)。
- 该插件包括使用 Taskbone OCR 的 OCR 支持。有关更多详细信息,请参见此 [视频](https://youtu.be/7gu4ETx7zro)。
- 您可以将 SVG 文件转换为 Excalidraw 绘图(有一些限制)。有关更多详细信息,请参见此 [视频](https://youtu.be/vlC1-iBvIfo)。
- 您可以定义自定义笔和荧光笔,并将其固定到侧边栏。有关更多详细信息,请参见此 [视频](https://youtu.be/OjNhjaH2KjI)。使用 ExcalidrawAutomate您可以添加对 [自动切换](<ea-scripts/Auto Draw for Pen.md>) 笔的支持,以及对 [硬件橡皮擦按钮](<ea-scripts/Hardware Eraser Support.md>) 的支持。
### 脚本引擎
- 从 1.5.0 版本开始,您可以轻松执行 ExcalidrawAutomate 宏,并为它们分配命令面板快捷键,使用脚本引擎。您可以在 [这里](ea-scripts/README.md) 找到介绍视频和不断增加的可安装脚本库。
- 您可以通过将脚本和随附的 SVG 图标文件移动到文件夹中,将脚本组织成组,放在 Excalidraw 的 Obsidian 工具面板中。请参见演示 [视频](https://youtu.be/wTtaXmRJ7wg?t=16)。
### 其他
- 左利手模式
- 包含完整的
- [QuickAdd](https://github.com/chhoumann/quickadd)
- [Templater](https://silentvoid13.github.io/Templater/) 和
- [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) 支持,通过 ExcalidrawAutomate 实现。
- 查看 [详细帮助 + 示例](https://zsviczian.github.io/obsidian-excalidraw-plugin/)。
- 我还有一个 [YouTube ExcalidrawAutomate 播放列表](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1IR4nXxSlMA4PA5E-qpyHZ),里面有很多示例。
- 需要 Obsidian Sync 订阅:完整的绘图文件历史记录和设备之间的同步。
- 多语言支持:如果您想通过翻译插件来帮助,请与我联系。
## 反馈、问题、想法、问题
请在 [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian) 上参与关于 Excalidraw 插件的讨论。
请前往 [GitHub](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) 报告错误或请求增强功能。
---
## 感谢支持
如果您喜欢 Excalidraw请通过在 [https://ko-fi/zsolt](https://ko-fi.com/zsolt) 上请我喝杯咖啡来支持我的工作和热情。
请通过在 Twitter、Reddit 或其他您常用的社交媒体平台上分享 Obsidian Excalidraw 插件来帮助传播消息。
您可以在 Twitter 上找到我 [@zsviczian](https://twitter.com/zsviczian),以及我的博客 [zsolt.blog](https://zsolt.blog)。
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="200">](https://ko-fi.com/zsolt)
---
## Excalidraw 的朋友们
如果您喜欢 Excalidraw可以考虑尝试 [ExcaliBrain](https://github.com/zsviczian/excalibrain)(也可通过 Obsidian 社区插件获得)。
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>

View File

@@ -19,10 +19,10 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
// -------------------------------
const excalidrawTemplates = ea.getListOfTemplateFiles();
if(typeof window.ExcalidrawDeconstructElements === "undefined") {
window.ExcalidrawDeconstructElements = {
openDeconstructedImage: true,
templatePath: excalidrawTemplates?.[0].path??""
};
window.ExcalidrawDeconstructElements = {
openDeconstructedImage: true,
templatePath: excalidrawTemplates?.[0].path??""
};
}
const splitFolderAndFilename = (filepath) => {
@@ -36,13 +36,13 @@ const splitFolderAndFilename = (filepath) => {
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Templates"]) {
settings = {
"Templates" : {
value: "",
settings = {
"Templates" : {
value: "",
description: "Comma-separated list of template filepaths"
}
};
await ea.setScriptSettings(settings);
}
};
await ea.setScriptSettings(settings);
}
if(!settings["Default file name"]) {
@@ -80,31 +80,31 @@ ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
});

View File

@@ -0,0 +1,259 @@
/*
Generates a hierarchical Markdown document out of a visual layout of an article.
Watch this video to understand how the script is intended to work:
![Excalidraw Writing Machine YouTube Video](https://youtu.be/zvRpCOZAUSs)
You can download the sample Obsidian Templater file from [here](https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9)
You can download the demo PDF document showcased in the video from [here](https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf)
```js*/
const selectedElements = ea.getViewSelectedElements();
if (selectedElements.length !== 1 || selectedElements[0].type === "arrow") {
new Notice("Select a single element that is not an arrow and not a frame");
return;
}
const visited = new Set(); // Avoiding recursive infinite loops
delete window.ewm;
await ea.targetView.save();
//------------------
// Load Settings
//------------------
let settings = ea.getScriptSettings();
//set default values on first run
let didSettingsChange = false;
if(!settings["Template path"]) {
settings = {
"Template path" : {
value: "",
description: "The template file path that will receive the concatenated text. If the file includes <<<REPLACE ME>>> then it will be replaced with the generated text, if <<<REPLACE ME>>> is not present in the file the hierarchical markdown generated from the diagram will be added to the end of the template."
},
"ZK '# Summary' section": {
value: "Summary",
description: "The section in your visual zettelkasten file that contains the short written summary of the idea. This is the text that will be included in the hierarchical markdown file if visual ZK cards are included in your flow"
},
"ZK '# Source' section": {
value: "Source",
description: "The section in your visual zettelkasten file that contains the reference to your source. If present in the file, this text will be included in the output file as a reference"
},
"Embed image links": {
value: true,
description: "Should the resulting markdown document include the ![[embedded images]]?"
}
};
didSettingsChange = true;
}
if(!settings["Generate ![markdown](links)"]) {
settings["Generate ![markdown](links)"] = {
value: true,
description: "If you turn this off the script will generate ![[wikilinks]] for images"
}
didSettingsChange = true;
}
if(didSettingsChange) {
await ea.setScriptSettings(settings);
}
const ZK_SOURCE = settings["ZK '# Source' section"].value;
const ZK_SECTION = settings["ZK '# Summary' section"].value;
const INCLUDE_IMG_LINK = settings["Embed image links"].value;
const MARKDOWN_LINKS = settings["Generate ![markdown](links)"].value;
let templatePath = settings["Template path"].value;
//------------------
// Select template file
//------------------
const MSG = "Select another file"
let selection = MSG;
if(templatePath && app.vault.getAbstractFileByPath(templatePath)) {
selection = await utils.suggester([templatePath, MSG],[templatePath, MSG], "Use previous template or select another?");
if(!selection) {
new Notice("process aborted");
return;
}
}
if(selection === MSG) {
const files = app.vault.getMarkdownFiles().map(f=>f.path);
selection = await utils.suggester(files,files,"Select the template to use. ESC to not use a tempalte");
}
if(selection && selection !== templatePath) {
settings["Template path"].value = selection;
await ea.setScriptSettings(settings);
}
templatePath = selection;
//------------------
// supporting functions
//------------------
function getNextElementFollowingArrow(el, arrow) {
if (arrow.startBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.endBinding?.elementId);
}
if (arrow.endBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.startBinding?.elementId);
}
return null;
}
function getImageLink(f) {
if(MARKDOWN_LINKS) {
return `![${f.basename}](${encodeURI(f.path)})`;
}
return `![[${f.path}|${f.basename}]]`;
}
function getBoundText(el) {
const textId = el.boundElements?.find(x => x.type === "text")?.id;
const text = ea.getViewElements().find(x => x.id === textId)?.originalText;
return text ? text + "\n" : "";
}
async function getSectionText(file, section) {
const content = await app.vault.cachedRead(file);
const metadata = app.metadataCache.getFileCache(file);
if (!metadata || !metadata.headings) {
return null;
}
const targetHeading = metadata.headings.find(h => h.heading === section);
if (!targetHeading) {
return null;
}
const startPos = targetHeading.position.start.offset;
let endPos = content.length;
const nextHeading = metadata.headings.find(h => h.position.start.offset > startPos);
if (nextHeading) {
endPos = nextHeading.position.start.offset;
}
let sectionContent = content.slice(startPos, endPos).trim();
sectionContent = sectionContent.substring(sectionContent.indexOf('\n') + 1).trim();
// Remove Markdown comments enclosed in %%
sectionContent = sectionContent.replace(/%%[\s\S]*?%%/g, '').trim();
return sectionContent;
}
async function getBlockText(file, blockref) {
const content = await app.vault.cachedRead(file);
const blockPattern = new RegExp(`\\^${blockref}\\b`, 'g');
let blockPosition = content.search(blockPattern);
if (blockPosition === -1) {
return "";
}
const startPos = content.lastIndexOf('\n', blockPosition) + 1;
let endPos = content.indexOf('\n', blockPosition);
if (endPos === -1) {
endPos = content.length;
} else {
const nextBlockOrHeading = content.slice(endPos).search(/(^# |^\^|\n)/gm);
if (nextBlockOrHeading !== -1) {
endPos += nextBlockOrHeading;
} else {
endPos = content.length;
}
}
let blockContent = content.slice(startPos, endPos).trim();
blockContent = blockContent.replace(blockPattern, '').trim();
blockContent = blockContent.replace(/%%[\s\S]*?%%/g, '').trim();
return blockContent;
}
async function getElementText(el) {
if (el.type === "text") {
return el.originalText;
}
if (el.type === "image") {
const f = ea.getViewFileForImageElement(el);
if(!ea.isExcalidrawFile(f)) return f.name + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
let source = await getSectionText(f, ZK_SOURCE);
source = source ? ` (source:: ${source})` : "";
const summary = await getSectionText(f, ZK_SECTION) ;
if(summary) return (INCLUDE_IMG_LINK ? `${getImageLink(f)}\n${summary + source}` : summary + source) + "\n";
return f.name + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
}
if (el.type === "embeddable") {
const linkWithRef = el.link.match(/\[\[([^\]]*)]]/)?.[1];
if(!linkWithRef) return "";
const path = linkWithRef.split("#")[0];
const f = app.metadataCache.getFirstLinkpathDest(path, ea.targetView.file.path);
if(!f) return "";
if(f.extension !== "md") return f.name;
const ref = linkWithRef.split("#")[1];
if(!ref) return await app.vault.read(f);
if(ref.startsWith("^")) {
return await getBlockText(f, ref.substring(1));
} else {
return await getSectionText(f, ref);
}
}
return getBoundText(el);
}
//------------------
// Navigating the hierarchy
//------------------
async function crawl(el, level, isFirst = false) {
visited.add(el.id);
let result = await getElementText(el) + "\n";
// Process all arrows connected to this element
const boundElementsData = el.boundElements.filter(x => x.type === "arrow");
const isFork = boundElementsData.length > (isFirst ? 1 : 2);
if(isFork) level++;
for(const bindingData of boundElementsData) {
const arrow = ea.getViewElements().find(x=> x.id === bindingData.id);
const nextEl = getNextElementFollowingArrow(el, arrow);
if (nextEl && !visited.has(nextEl.id)) {
if(isFork) result += `\n${"#".repeat(level)} `;
const arrowLabel = getBoundText(arrow);
if (arrowLabel) {
// If the arrow has a label, add it as an additional level
result += arrowLabel + "\n";
result += await crawl(nextEl, level);
} else {
// If no label, continue to the next element
result += await crawl(nextEl, level);
}
}
};
return result;
}
window.ewm = "## " + await crawl(selectedElements[0], 2, true);
const outputPath = await ea.getAttachmentFilepath(`EWM - ${ea.targetView.file.name}.md`);
let result = templatePath
? await app.vault.read(app.vault.getAbstractFileByPath(templatePath))
: "";
if(result.match("<<<REPLACE ME>>>")) {
result = result.replaceAll("<<<REPLACE ME>>>",window.ewm);
} else {
result += window.ewm;
}
const outfile = await app.vault.create(outputPath,result);
setTimeout(()=>{
ea.openFileInNewOrAdjacentLeaf(outfile);
}, 250);

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="CurrentColor" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard">
<path stroke-width="2" d="M10 8h.01"/>
<path stroke-width="2" d="M12 12h.01"/>
<path stroke-width="2" d="M14 8h.01"/>
<path stroke-width="2" d="M16 12h.01"/>
<path stroke-width="2" d="M18 8h.01"/>
<path stroke-width="2" d="M6 8h.01"/>
<path stroke-width="2" d="M7 16h10"/>
<path stroke-width="2" d="M8 12h.01"/>
<path fill="none" stroke-width="2" d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

View File

@@ -13,6 +13,11 @@ Gravitational point of spiral: $$\left[x,y\right]=\left[ x + \frac{{\text{width}
Dimensions of inner rectangles in case of Double Spiral: $$[width, height] = \left[\frac{width\cdot(\phi^2+1)}{2\phi^2}\;, \;\frac{height\cdot(\phi^2+1)}{2\phi^2}\right]$$
```js*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.4.0")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const phi = (1 + Math.sqrt(5)) / 2; // Golden Ratio (φ)
const inversePhi = (1-1/phi);
const pointsPerCurve = 20; // Number of points per curve segment
@@ -33,18 +38,16 @@ if(!rect || rect.type !== "rectangle") {
}
window.excalidrawGoldenRatio.timer = setTimeout(()=>{delete window.excalidrawGoldenRatio;},2000);
window.excalidrawGoldenRatio.cycle = (window.excalidrawGoldenRatio.cycle+1)%5;
ea.copyViewElementsToEAforEditing(textEls);
ea.getElements().forEach(el=> {
el.fontSize = window.excalidrawGoldenRatio.cycle === 2
? el.fontSize / Math.pow(phi,4)
: el.fontSize * phi;
const font = ExcalidrawLib.getFontString(el);
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
ea.style.fontFamily = el.fontFamily;
ea.style.fontSize = el.fontSize;
const {width, height } = ea.measureText(el.originalText);
el.width = width;
el.height = height;
el.baseline = baseline;
});
ea.addElementsToView();
return;
@@ -631,7 +634,7 @@ modal.onOpen = async () => {
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("top-down","Top down")
.addOption("bottom-up","Bottom up")
.addOption("bottom-up","Bootom up")
.addOption("center-out","Center out")
.addOption("center-in","Center in")
.setValue(vDirection)

View File

@@ -61,6 +61,7 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[OCR - Optical Character Recognition](OCR%20-%20Optical%20Character%20Recognition.md)|The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and 2) will add the text to your drawing as a text element.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Organic Line](Organic%20Line.md)|Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Repeat Elements](Repeat%20Elements.md)|This script will detect the difference between 2 selected elements, including position, size, angle, stroke and background color, and create several elements that repeat these differences based on the number of repetitions entered by the user.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-elements.png)|[@1-2-3](https://github.com/1-2-3)|
|[Reset LaTeX Size](Reset%20LaTeX%20Size.md)|Reset the sizes of embedded LaTeX equations to the default sizes or a multiple of the default sizes.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reset-latex.jpg)|[@firai](https://github.com/firai)|
|[Reverse arrows](Reverse%20arrows.md)|Reverse the direction of **arrows** within the scope of selected elements.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reverse-arrow.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Scribble Helper](Scribble%20Helper.md)|iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the creates a text element at pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then opens the input prompt where you can modify this text with scribble.|![]('https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-scribble-helper.jpg')|[@zsviczian](https://github.com/zsviczian)|
|[Select Elements of Type](Select%20Elements%20of%20Type.md)|Prompts you with a list of the different element types in the active image. Only elements of the selected type will be selected on the canvas. If nothing is selected when running the script, then the script will process all the elements on the canvas. If some elements are selected when the script is executed, then the script will only process the selected elements.<br>The script is useful when, for example, you want to bring to front all the arrows, or want to change the color of all the text elements, etc.|![]('https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-element-of-type.jpg')|[@zsviczian](https://github.com/zsviczian)|

View File

@@ -0,0 +1,32 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reset-latex.jpg)
Reset the sizes of embedded LaTeX equations to the default sizes or a multiple of the default sizes.
```javascript
*/
if (!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.4.0")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
let elements = ea.getViewSelectedElements().filter((el)=>["image"].includes(el.type));
if (elements.length === 0) return;
scale = await utils.inputPrompt("Scale?", "Number", "1");
if (!scale) return;
scale = parseFloat(scale);
ea.copyViewElementsToEAforEditing(elements);
for (el of elements) {
equation = ea.targetView.excalidrawData.getEquation(el.fileId)?.latex;
if (!equation) return;
eqData = await ea.tex2dataURL(equation);
ea.getElement(el.id).width = eqData.size.width * scale;
ea.getElement(el.id).height = eqData.size.height * scale;
};
ea.addElementsToView(false, false);

View File

@@ -0,0 +1 @@
<svg class="skip" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect stroke-width="2" width="20" height="16" x="2" y="4" rx="2"/><path stroke-width="2" d="M12 9v11"/><path stroke-width="2" d="M2 9h13a2 2 0 0 1 2 2v9"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -8,6 +8,46 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
if(ea.verifyMinimumPluginVersion && ea.verifyMinimumPluginVersion("2.4.0")) {
const api = ea.getExcalidrawAPI();
let appState = api.getAppState();
let gridFrequency = appState.gridStep;;
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Major grid frequency`)
.addDropdown(dropdown => {
[2,3,4,5,6,7,8,9,10].forEach(grid=>dropdown.addOption(grid,grid));
dropdown
.setValue(gridFrequency)
.onChange(value => {
gridFrequency = value;
})
})
}
const gridSize = parseInt(await utils.inputPrompt(
"Grid size?",
null,
appState.GridSize?.toString()??"20",
null,
1,
false,
customControls
));
if(isNaN(gridSize)) return; //this is to avoid passing an illegal value to Excalidraw
const gridStep = isNaN(parseInt(gridFrequency)) ? appState.gridStep : parseInt(gridFrequency);
api.updateScene({
appState : {gridSize, gridStep, gridModeEnabled:true},
commitToHistory:false
});
}
// ----------------
// old script
// ----------------
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;

View File

@@ -9,7 +9,11 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
let width = (ea.getViewSelectedElement().strokeWidth??1).toString();
width = await utils.inputPrompt("Width?","number",width);
width = parseFloat(await utils.inputPrompt("Width?","number",width));
if(isNaN(width)) {
new Notice("Invalid number");
return;
}
const elements=ea.getViewSelectedElements();
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>el.strokeWidth=width);

File diff suppressed because one or more lines are too long

View File

@@ -89,6 +89,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Modify%20background%20color%20opacity.svg"/></div>|[[#Modify background color opacity]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.svg"/></div>|[[#Organic Line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line%20Legacy.svg"/></div>|[[#Organic Line Legacy]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reset%20LaTeX%20Size.svg"/></div>|[[#Reset LaTeX Size]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
@@ -119,6 +120,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.svg"/></div>|[[#Custom Zoom]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.svg"/></div>|[[#Excalidraw Writing Machine]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
@@ -389,6 +391,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<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/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Excalidraw Writing Machine
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.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/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
@@ -510,6 +518,14 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<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>
## Reset LaTeX Size
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reset%20LaTeX%20Size.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/firai'>@firai</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/Reset%20LaTeX%20Size.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Reset the sizes of embedded LaTeX equations to the default sizes or a multiple of the default sizes.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reset-latex.jpg'></td></tr></table>
## Set background color of unclosed line object by adding a shadow clone
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 861 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.3.0",
"version": "2.4.1-rc-1",
"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.3.0",
"version": "2.4.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

@@ -19,45 +19,50 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-36",
"@zsviczian/excalidraw": "0.17.1-obsidian-47",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
"@zsviczian/colormaster": "^1.2.2",
"gl-matrix": "^3.4.3",
"js-yaml": "^4.1.0",
"lucide-react": "^0.263.1",
"mathjax-full": "^3.2.2",
"monkey-around": "^2.3.0",
"nanoid": "^4.0.2",
"opentype.js": "^1.3.4",
"polybooljs": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roughjs": "^4.5.2",
"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",
"@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",
"@excalidraw/eslint-config": "^1.0.3",
"@excalidraw/prettier-config": "^1.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.2",
"@rollup/plugin-typescript": "^11.1.6",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.10.5",
"@types/opentype.js": "^1.3.8",
"@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",
"dotenv": "^16.4.5",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"lz-string": "^1.5.0",
@@ -65,18 +70,12 @@
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
"@zsviczian/rollup-plugin-postprocess": "^1.0.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"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"
"typescript": "^5.2.2"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -1,21 +1,19 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
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 LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
import postprocess from '@zsviczian/rollup-plugin-postprocess';
import cssnano from 'cssnano';
// Load environment variables
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
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}`);
@@ -83,9 +81,12 @@ const BASE_CONFIG = {
const getRollupPlugins = (tsconfig, ...plugins) => [
typescript2(tsconfig),
nodeResolve({ browser: true }),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
commonjs(),
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
nodeResolve({ browser: true, preferBuiltins: false }),
].concat(plugins);
const BUILD_CONFIG = {
@@ -96,48 +97,28 @@ const BUILD_CONFIG = {
format: 'cjs',
exports: 'default',
},
plugins: [
typescript2({
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
}),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
babel({
presets: [['@babel/preset-env', {
targets: {
ios: "15", // ios Compatibility //esmodules: true,
},
}]],
exclude: "node_modules/**",
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
plugins: getRollupPlugins(
{tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"},
...(isProd ? [
terser({
toplevel: false,
compress: { passes: 2 },
format: {
comments: false, // Remove all comments
},
}),
//!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"\),state=require\("@codemirror\/state"\),view=require\("@codemirror\/view"\)/,
`state=require("@codemirror/state"),view=require("@codemirror/view")` + packageString],
]),
] : [
postprocess([
[/var React = require\('react'\);/, packageString],
]),
postprocess([ [/var React = require\('react'\);/, packageString] ]),
]),
copy({
targets: [
{ src: 'manifest.json', dest: DIST_FOLDER },
],
verbose: true, // Optional: To display copied files in the console
targets: [ { src: 'manifest.json', dest: DIST_FOLDER } ],
verbose: true,
}),
],
),
};
const LIB_CONFIG = {

View File

@@ -11,7 +11,7 @@ import {
nanoid,
THEME_FILTER,
FRONTMATTER_KEYS,
getFontDefinition,
getCSSFontDefinition,
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
@@ -35,6 +35,7 @@ import {
svgToBase64,
isMaskFile,
getEmbeddedFilenameParts,
cropCanvas,
} from "./utils/Utils";
import { ValueOf } from "./types/types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
@@ -472,6 +473,8 @@ export class EmbeddedFilesLoader {
return null;
}
const app = this.plugin.app;
const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false;
const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false;
const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : "";
@@ -746,6 +749,8 @@ export class EmbeddedFilesLoader {
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
const scale = this.plugin.settings.pdfScale;
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
// Render the page
const renderPage = async (num:number) => {
@@ -766,6 +771,23 @@ export class EmbeddedFilesLoader {
};
await page.render(renderCtx).promise;
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
return cropCanvas(canvas, crop);
}
}
return canvas;
};
@@ -816,29 +838,29 @@ export class EmbeddedFilesLoader {
}
switch (fontName) {
case "Virgil":
fontDef = await getFontDefinition(1);
fontDef = await getCSSFontDefinition(1);
break;
case "Cascadia":
fontDef = await getFontDefinition(3);
fontDef = await getCSSFontDefinition(3);
break;
case "Assistant":
case "Helvetica":
fontDef = await getFontDefinition(2);
fontDef = await getCSSFontDefinition(2);
break;
case "Excalifont":
fontDef = await getFontDefinition(5);
fontDef = await getCSSFontDefinition(5);
break;
case "Nunito":
fontDef = await getFontDefinition(6);
fontDef = await getCSSFontDefinition(6);
break;
case "Lilita One":
fontDef = await getFontDefinition(7);
fontDef = await getCSSFontDefinition(7);
break;
case "Comic Shanns":
fontDef = await getFontDefinition(8);
fontDef = await getCSSFontDefinition(8);
break;
case "Liberation Sans":
fontDef = await getFontDefinition(9);
fontDef = await getCSSFontDefinition(9);
break;
case "":
fontDef = "";
@@ -921,12 +943,14 @@ export class EmbeddedFilesLoader {
mdDIV.style.display = "block";
mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial";
await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
//await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin);
mdDIV
.querySelectorAll(":scope > *[class^='frontmatter']")
.forEach((el) => mdDIV.removeChild(el));
await replaceBlobWithBase64(mdDIV); //because image cache returns a blob
const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']"))
for(let i=0;i<internalEmbeds.length;i++) {
const el = internalEmbeds[i];
@@ -1087,3 +1111,19 @@ export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promi
}
return id;
};
const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise<void> => {
const images = divElement.querySelectorAll<HTMLImageElement>('img[src^="blob:app://obsidian.md"]');
for (let img of images) {
const blobUrl = img.src;
try {
const response = await fetch(blobUrl);
const blob = await response.blob();
const base64 = await blobToBase64(blob);
img.src = `data:${blob.type};base64,${base64}`;
} catch (error) {
console.error(`Failed to fetch or convert blob: ${blobUrl}`, error);
}
}
};

View File

@@ -13,6 +13,7 @@ import {
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";
@@ -62,21 +63,21 @@ import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
import { t } from "src/lang/helpers";
import { ScriptEngine } from "src/Scripts";
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"
import A11yPlugin from "colormaster/plugins/accessibility"
import NamePlugin from "colormaster/plugins/name"
import LCHPlugin from "colormaster/plugins/lch";
import LUVPlugin from "colormaster/plugins/luv";
import LABPlugin from "colormaster/plugins/lab";
import UVWPlugin from "colormaster/plugins/uvw";
import XYZPlugin from "colormaster/plugins/xyz";
import HWBPlugin from "colormaster/plugins/hwb";
import HSVPlugin from "colormaster/plugins/hsv";
import RYBPlugin from "colormaster/plugins/ryb";
import CMYKPlugin from "colormaster/plugins/cmyk";
import { TInput } from "colormaster/types";
import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster";
import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony";
import MixPlugin from "@zsviczian/colormaster/plugins/mix"
import A11yPlugin from "@zsviczian/colormaster/plugins/accessibility"
import NamePlugin from "@zsviczian/colormaster/plugins/name"
import LCHPlugin from "@zsviczian/colormaster/plugins/lch";
import LUVPlugin from "@zsviczian/colormaster/plugins/luv";
import LABPlugin from "@zsviczian/colormaster/plugins/lab";
import UVWPlugin from "@zsviczian/colormaster/plugins/uvw";
import XYZPlugin from "@zsviczian/colormaster/plugins/xyz";
import HWBPlugin from "@zsviczian/colormaster/plugins/hwb";
import HSVPlugin from "@zsviczian/colormaster/plugins/hsv";
import RYBPlugin from "@zsviczian/colormaster/plugins/ryb";
import CMYKPlugin from "@zsviczian/colormaster/plugins/cmyk";
import { TInput } from "@zsviczian/colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -277,11 +278,11 @@ export class ExcalidrawAutomate {
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
}
public compressToBase64(str:string):string {
public compressToBase64(str:string): string {
return LZString.compressToBase64(str);
}
public decompressFromBase64(str:string):string {
public decompressFromBase64(str:string): string {
return LZString.decompressFromBase64(str);
}
@@ -716,6 +717,12 @@ export class ExcalidrawAutomate {
this.style.roundness ? "round":"sharp",
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
...template?.appState?.frameRendering
? {frameRendering: template.appState.frameRendering}
: {},
...template?.appState?.objectsSnapModeEnabled
? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled}
: {},
},
files: template?.files ?? {},
};
@@ -1026,6 +1033,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
@@ -1571,6 +1594,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
@@ -1896,15 +1939,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);
};
/**
@@ -2396,24 +2440,44 @@ 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)));
}
/**
@@ -2502,10 +2566,11 @@ export class ExcalidrawAutomate {
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available
* @param imageElement an image element from the active scene on targetView
* @param shouldWaitForImage if true, the function will wait for the image to load before returning the size
*/
async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}> {
async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getOriginalImageSize()");
@@ -2521,10 +2586,59 @@ export class ExcalidrawAutomate {
return null;
}
const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
const dataURL = ef.getImage(isDark);
let dataURL = ef.getImage(isDark);
if(!dataURL && !shouldWaitForImage) return;
if(!dataURL) {
let watchdog = 0;
while(!dataURL && watchdog < 50) {
await sleep(100);
dataURL = ef.getImage(isDark);
watchdog++;
}
if(!dataURL) return;
}
return await getImageSize(dataURL);
}
/**
* Resets the image to its original aspect ratio.
* If the image is resized then the function returns true.
* If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
* Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
* @param imageElement - the EA image element to be resized
* returns true if image was changed, false if image was not changed
*/
async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "resetImageAspectRatio()");
return null;
}
const size = await this.getOriginalImageSize(imgEl, true);
if (size) {
const originalArea = imgEl.width * imgEl.height;
const originalAspectRatio = size.width / size.height;
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
const centerX = imgEl.x + imgEl.width / 2;
const centerY = imgEl.y + imgEl.height / 2;
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
if(!this.getElement(imgEl.id)) {
this.copyViewElementsToEAforEditing([imgEl]);
}
const eaEl = this.getElement(imgEl.id);
eaEl.width = newWidth;
eaEl.height = newHeight;
eaEl.x = centerX - newWidth / 2;
eaEl.y = centerY - newHeight / 2;
return true;
}
}
return false;
}
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
@@ -2738,7 +2852,7 @@ export async function initExcalidrawAutomate(
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++) {
@@ -2747,7 +2861,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,
@@ -2757,11 +2873,11 @@ function getLineBox(points: [x: number, y: number][]) {
};
}
function getFontFamily(id: number) {
getFontFamilyString({fontFamily:id})
function getFontFamily(id: number):string {
return getFontFamilyString({fontFamily:id})
}
export async function initFonts() {
export async function initFonts():Promise<void> {
await excalidrawLib.registerFontsInCSS();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
@@ -2774,7 +2890,7 @@ 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;
@@ -2873,7 +2989,7 @@ 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 || filenameParts.hasClippedFrameref) {
@@ -2893,6 +3009,7 @@ async function getTemplate(
}
excalidrawData.destroy();
const filehead = data.substring(0, trimLocation);
return {
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
@@ -2900,7 +3017,7 @@ async function getTemplate(
hostFile: file,
}) : groupElements,
appState: scene.appState,
frontmatter: data.substring(0, trimLocation),
frontmatter: filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead,
files: scene.files,
hasSVGwithBitmap,
};
@@ -2932,7 +3049,7 @@ export async function createPNG(
depth: number,
padding?: number,
imagesDict?: any,
) {
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
@@ -3016,7 +3133,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);
@@ -3165,7 +3282,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({
@@ -3218,7 +3335,7 @@ export const search = async (view: ExcalidrawView) => {
const ea = view.plugin.ea;
ea.reset();
ea.setView(view);
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame");
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
if (elements.length === 0) {
return;
}
@@ -3312,6 +3429,62 @@ export const getFrameElementsMatchingQuery = (
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getElementsWithLinkMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: any) =>
el.link &&
query.some((q) => {
const text = el.link.toLowerCase().trim();
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getImagesMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
excalidrawData: ExcalidrawData,
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: ExcalidrawElement) =>
el.type === "image" &&
query.some((q) => {
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
const text = filename ?? equation;
if(!text) return false;
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
export const cloneElement = (el: ExcalidrawElement):any => {
const newEl = JSON.parse(JSON.stringify(el));
newEl.version = el.version + 1;

View File

@@ -35,6 +35,7 @@ import {
updateFrontmatterInString,
wrapTextAtCharLength,
arrayToMap,
compressAsync,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
@@ -49,6 +50,7 @@ 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";
import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -68,10 +70,34 @@ export enum AutoexportPreference {
inherit
}
export const REGEX_TAGS = {
// #[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+
// 1
EXPR: /(#[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu,
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_TAGS.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
const resultList = [];
while (!(parts = res.next()).done) {
resultList.push(parts);
}
return resultList;
},
getTag: (parts: IteratorResult<RegExpMatchArray, any>): string => {
return parts.value[1];
},
isTag: (parts: IteratorResult<RegExpMatchArray, any>): boolean => {
return parts.value[1]?.startsWith("#")
},
};
export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
//EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(((?:[^\(\)]|\([^\(\)]*\))*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1963
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_LINK.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
@@ -197,15 +223,28 @@ export function getJSON(data: string): { scene: string; pos: number } {
return { scene: data, pos: parts.value ? parts.value.index : 0 };
}
export function getMarkdownDrawingSection(
export async function getMarkdownDrawingSectionAsync (
jsonString: string,
compressed: boolean,
) {
return compressed
const result = compressed
? `## Drawing\n\x60\x60\x60compressed-json\n${await compressAsync(
jsonString,
)}\n\x60\x60\x60\n%%`
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
return result;
}
export function getMarkdownDrawingSection(
jsonString: string,
compressed: boolean,
): string {
const result = compressed
? `## 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%%`;
return result;
}
/**
@@ -720,6 +759,24 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
//girdSize, gridStep, previousGridSize, gridModeEnabled migration
if(this.scene.appState.hasOwnProperty("previousGridSize")) { //if previousGridSize was present this is legacy data
if(this.scene.appState.gridSize === null) {
this.scene.appState.gridSize = this.scene.appState.previousGridSize;
this.scene.appState.gridModeEnabled = false;
} else {
this.scene.appState.gridModeEnabled = true;
}
delete this.scene.appState.previousGridSize;
}
if(this.scene.appState?.gridColor?.hasOwnProperty("MajorGridFrequency")) { //if this is present, this is legacy data
if(this.scene.appState.gridColor.MajorGridFrequency>1) {
this.scene.gridStep = this.scene.appState.gridColor.MajorGridFrequency;
}
delete this.scene.appState.gridColor.MajorGridFrequency;
}
//once off migration of legacy scenes
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
const prompt = new ConfirmationPrompt(
@@ -810,7 +867,7 @@ export class ExcalidrawData {
? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
: data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
//Load Embedded files
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
const RE_ELEMENT_LINKS = /^(.{8}):\s*(.*)$/gm;
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
while (!(parts = linksRes.next()).done) {
elementLinkMap.set(parts.value[1], parts.value[2]);
@@ -1043,7 +1100,7 @@ export class ExcalidrawData {
return (
el.type !== "text" &&
el.link &&
el.link.startsWith("[[") &&
//el.link.startsWith("[[") &&
!this.elementLinks.has(el.id)
);
});
@@ -1051,8 +1108,6 @@ export class ExcalidrawData {
return result;
}
let jsonString = JSON.stringify(this.scene);
let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
for (const el of elements) {
@@ -1063,11 +1118,10 @@ export class ExcalidrawData {
if (el.id.length > 8) {
result = true;
id = nanoid();
jsonString = jsonString.replaceAll(el.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.)
updateElementIdsInScene(this.scene, el, id);
}
this.elementLinks.set(id, el.link);
}
this.scene = JSON.parse(jsonString);
return result;
}
@@ -1079,9 +1133,7 @@ export class ExcalidrawData {
//console.log("Excalidraw.Data.findNewTextElementsInScene()");
//get scene text elements
this.selectedElementIds = selectedElementIds;
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
let jsonString = JSON.stringify(this.scene);
const texts = this.scene.elements?.filter((el: any) => el.type === "text") as ExcalidrawTextElement[];
let dirty: boolean = false; //to keep track if the json has changed
let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
@@ -1097,7 +1149,7 @@ export class ExcalidrawData {
delete this.selectedElementIds[te.id];
this.selectedElementIds[id] = true;
}
jsonString = jsonString.replaceAll(te.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.)
updateElementIdsInScene(this.scene, te, id);
if (this.textElements.has(te.id)) {
//element was created with onBeforeTextSubmit
const text = this.textElements.get(te.id);
@@ -1119,11 +1171,6 @@ export class ExcalidrawData {
}
}
if (dirty) {
//reload scene json in case it has changed
this.scene = JSON.parse(jsonString);
}
return dirty;
}
@@ -1134,8 +1181,8 @@ export class ExcalidrawData {
(el: any) =>
el.type !== "text" &&
el.id === key &&
el.link &&
el.link.startsWith("[["),
el.link, //&&
//el.link.startsWith("[["),
);
if (el.length === 0) {
this.elementLinks.delete(key); //if no longer in the scene, delete the text element
@@ -1364,7 +1411,7 @@ export class ExcalidrawData {
* @returns markdown string
*/
disableCompression: boolean = false;
generateMD(deletedElements: ExcalidrawElement[] = []): string {
generateMDBase(deletedElements: ExcalidrawElement[] = []) {
let outString = this.textElementCommentedOut ? "%%\n" : "";
outString += `# Excalidraw Data\n## Text Elements\n`;
if (this.plugin.settings.addDummyTextElement) {
@@ -1376,10 +1423,10 @@ export class ExcalidrawData {
const element = this.scene.elements.filter((el:any)=>el.id===key);
let elementString = this.textElements.get(key).raw;
if(element && element.length===1 && element[0].link && element[0].rawText === element[0].originalText) {
if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
//if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
textElementLinks.set(key, element[0].link);
//elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
}
//}
}
outString += `${elementString} ^${key}\n\n`;
}
@@ -1431,14 +1478,33 @@ export class ExcalidrawData {
appState: this.scene.appState,
files: this.scene.files
}, null, "\t");
return (
return { outString, sceneJSONstring };
}
async generateMDAsync(deletedElements: ExcalidrawElement[] = []): Promise<string> {
const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
getMarkdownDrawingSection(
(await getMarkdownDrawingSectionAsync(
sceneJSONstring,
this.disableCompression ? false : this.plugin.settings.compress,
)
))
);
return result;
}
generateMDSync(deletedElements: ExcalidrawElement[] = []): string {
const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
(getMarkdownDrawingSection(
sceneJSONstring,
this.disableCompression ? false : this.plugin.settings.compress,
))
);
return result;
}
public async saveDataURLtoVault(dataURL: DataURL, mimeType: MimeType, key: FileId) {
@@ -1988,7 +2054,7 @@ export class ExcalidrawData {
}
public getEquationEntries() {
return this.equations.entries();
return this.equations?.entries();
}
public deleteEquation(fileId: FileId) {

View File

@@ -1,9 +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, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, 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 { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
@@ -27,6 +27,7 @@ declare namespace ExcalidrawLib {
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
exportingFrame?: ExcalidrawFrameLikeElement | null;
getDimensions?: (
width: number,
height: number,
@@ -170,11 +171,20 @@ declare namespace ExcalidrawLib {
var WelcomeScreen: any;
var TTDDialogTrigger: any;
var TTDDialog: any;
var DiagramToCodePlugin: (props: {
generate: GenerateDiagramToCode;
}) => any;
function getDataURL(file: Blob | File): Promise<DataURL>;
function destroyObsidianUtils(): void;
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
function getFontFamilies(): string[];
function registerFontsInCSS(): Promise<void>;
function getFontDefinition(fontFamily: number): Promise<string>;
function getCSSFontDefinition(fontFamily: number): Promise<string>;
function getTextFromElements (
elements: readonly ExcalidrawElement[],
separator?: string,
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
}

View File

@@ -16,6 +16,7 @@ import {
import {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawMagicFrameElement,
ExcalidrawTextElement,
FileId,
NonDeletedExcalidrawElement,
@@ -60,7 +61,9 @@ import {
ExcalidrawAutomate,
getTextElementsMatchingQuery,
cloneElement,
getFrameElementsMatchingQuery
getFrameElementsMatchingQuery,
getElementsWithLinkMatchingQuery,
getImagesMatchingQuery
} from "./ExcalidrawAutomate";
import { t } from "./lang/helpers";
import {
@@ -126,7 +129,7 @@ import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifier
import { setDynamicStyle } from "./utils/DynamicStyling";
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
import { CustomEmbeddable, renderWebView } from "./customEmbeddable";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, openTagSearch, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils";
import { imageCache } from "./utils/ImageCache";
import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
@@ -135,11 +138,13 @@ import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { nanoid } from "nanoid";
import { CustomMutationObserver, DEBUGGING, debug, log} from "./utils/DebugHelper";
import { extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
import { errorHTML, extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { SelectCard } from "./dialogs/SelectCard";
import { Packages } from "./types/types";
import React from "react";
import { diagramToHTML } from "./utils/matic";
import { IS_WORKER_SUPPORTED } from "./workers/compression-worker";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -247,6 +252,7 @@ type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall";
let windowMigratedDisableZoomOnce = false;
export default class ExcalidrawView extends TextFileView {
private freedrawLastActiveTimestamp: number = 0;
public exportDialog: ExportDialog;
public excalidrawData: ExcalidrawData;
//public excalidrawRef: React.MutableRefObject<any> = null;
@@ -399,7 +405,10 @@ export default class ExcalidrawView extends TextFileView {
preventAutozoom() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.preventAutozoom, "ExcalidrawView.preventAutozoom");
this.semaphores.preventAutozoom = true;
window.setTimeout(() => (this.semaphores.preventAutozoom = false), 1500);
window.setTimeout(() => {
if(!this.semaphores) return;
this.semaphores.preventAutozoom = false;
}, 1500);
}
public saveExcalidraw(scene?: any) {
@@ -729,10 +738,9 @@ export default class ExcalidrawView extends TextFileView {
return;
}
const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`);
try {
const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`);
if (allowSave) {
const scene = this.getScene();
@@ -758,6 +766,7 @@ export default class ExcalidrawView extends TextFileView {
//added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back
//can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607
if(this.semaphores?.viewunload) {
await this.prepareGetViewData();
const d = this.getViewData();
const plugin = this.plugin;
const file = this.file;
@@ -768,6 +777,7 @@ export default class ExcalidrawView extends TextFileView {
return;
}
await this.prepareGetViewData();
await super.save();
if (process.env.NODE_ENV === 'development') {
if (DEBUGGING) {
@@ -828,21 +838,29 @@ export default class ExcalidrawView extends TextFileView {
if(triggerReload) {
this.reload(true, this.file);
}
this.resetAutosaveTimer(); //next autosave period starts after save
}
// get the new file content
// if drawing is in Text Element Edit Lock, then everything should be parsed and in sync
// if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
getViewData() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewData, "ExcalidrawView.getViewData");
/**
* I moved the logic from getViewData to prepareGetViewData because getViewData is Sync and prepareGetViewData is async
* prepareGetViewData is async because of moving compression to a worker thread in 2.4.0
*/
private viewSaveData: string = "";
async prepareGetViewData(): Promise<void> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.prepareGetViewData, "ExcalidrawView.prepareGetViewData");
if (!this.excalidrawAPI || !this.excalidrawData.loaded) {
return this.data;
this.viewSaveData = this.data;
return;
}
const scene = this.getScene();
if(!scene) {
return this.data;
this.viewSaveData = this.data;
return;
}
//include deleted elements in save in case saving in markdown mode
@@ -874,17 +892,29 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView &&
this.isEditedAsMarkdownInOtherView();
}
const result = header + this.excalidrawData.generateMD(
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
) + tail;
const result = IS_WORKER_SUPPORTED
? (header + (await this.excalidrawData.generateMDAsync(
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
)) + tail)
: (header + (this.excalidrawData.generateMDSync(
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
)) + tail)
this.excalidrawData.disableCompression = false;
return result;
this.viewSaveData = result;
return;
}
if (this.compatibilityMode) {
return JSON.stringify(scene, null, "\t");
this.viewSaveData = JSON.stringify(scene, null, "\t");
return;
}
return this.data;
this.viewSaveData = this.data;
return;
}
getViewData() {
return this.viewSaveData ?? this.data;
}
private hiddenMobileLeaves:[WorkspaceLeaf,string][] = [];
@@ -900,6 +930,90 @@ export default class ExcalidrawView extends TextFileView {
}
}
async openLaTeXEditor(eqId: string) {
const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === eqId && el.type==="image") as ExcalidrawImageElement;
if(!el) {
return;
}
const fileId = el.fileId;
let equation = this.excalidrawData.getEquation(fileId)?.latex;
if(!equation) {
await this.save(false);
equation = this.excalidrawData.getEquation(fileId)?.latex;
if(!equation) return;
}
GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => {
if (!formula || formula === equation) {
return;
}
this.excalidrawData.setEquation(fileId, {
latex: formula,
isLoaded: false,
});
await this.save(false);
await updateEquation(
formula,
fileId,
this,
addFiles,
);
this.setDirty(1);
});
}
async openEmbeddedLinkEditor(imgId:string) {
const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === imgId && el.type==="image") as ExcalidrawImageElement;
if(!el) {
return;
}
const fileId = el.fileId;
const ef = this.excalidrawData.getFile(fileId);
if(!ef) {
return
}
if (!ef.isHyperLink && !ef.isLocalLink && ef.file) {
const handler = async (link:string) => {
if (!link || ef.linkParts.original === link) {
return;
}
ef.resetImage(this.file.path, link);
this.excalidrawData.setFile(fileId, ef);
this.setDirty(2);
await this.save(false);
await sleep(100);
if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) {
const ea = getEA(this) as ExcalidrawAutomate;
let imgEl = this.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as ExcalidrawImageElement;
if(!imgEl) {
ea.destroy();
return;
}
if(imgEl && await ea.resetImageAspectRatio(imgEl)) {
await ea.addElementsToView(false);
}
ea.destroy();
}
}
GenericInputPrompt.Prompt(
this,
this.plugin,
this.app,
t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"),
undefined,
ef.linkParts.original,
[{caption: "✅", action: (x:string)=>{x.replaceAll("\n","").trim()}}],
3,
false,
(container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}),
false
).then(handler.bind(this),()=>{});
return;
}
}
toggleDisableBinding() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleDisableBinding, "ExcalidrawView.toggleDisableBinding");
const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour;
@@ -1042,6 +1156,10 @@ export default class ExcalidrawView extends TextFileView {
? this.excalidrawData.getRawText(selectedText.id)
: selectedText.text);
if(linkText.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
const maybeObsidianLink = parseObsidianLink(linkText, this.app);
if(typeof maybeObsidianLink === "string") {
linkText = maybeObsidianLink;
@@ -1054,6 +1172,15 @@ export default class ExcalidrawView extends TextFileView {
const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
if(container) {
linkText = container.link;
if(linkText?.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
const maybeObsidianLink = parseObsidianLink(linkText, this.app);
if(typeof maybeObsidianLink === "string") {
linkText = maybeObsidianLink;
}
}
}
if(!linkText || partsArray.length === 0) {
@@ -1108,30 +1235,8 @@ export default class ExcalidrawView extends TextFileView {
if (selectedImage?.id) {
const imageElement = this.getScene().elements.find((el:ExcalidrawElement)=>el.id === selectedImage.id) as ExcalidrawImageElement;
if (this.excalidrawData.hasEquation(selectedImage.fileId)) {
(async () => {
await this.save(false);
selectedImage.fileId = imageElement.fileId;
const equation = this.excalidrawData.getEquation(
selectedImage.fileId,
).latex;
GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => {
if (!formula || formula === equation) {
return;
}
this.excalidrawData.setEquation(selectedImage.fileId, {
latex: formula,
isLoaded: false,
});
await this.save(false);
await updateEquation(
formula,
selectedImage.fileId,
this,
addFiles,
);
this.setDirty(1);
});
})();
this.updateScene({appState: {contextMenu: null}});
this.openLaTeXEditor(selectedImage.id);
return;
}
if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) {
@@ -1144,38 +1249,13 @@ export default class ExcalidrawView extends TextFileView {
await this.save(false); //in case pasted images haven't been saved yet
if (this.excalidrawData.hasFile(selectedImage.fileId)) {
const ef = this.excalidrawData.getFile(selectedImage.fileId);
if (!ef.isHyperLink && !ef.isLocalLink && linkClickType === "md-properties") {
if (
ef.file.extension === "md" &&
!this.plugin.isExcalidrawFile(ef.file)
) {
const handler = async (link:string) => {
if (!link || ef.linkParts.original === link) {
return;
}
ef.resetImage(this.file.path, link);
this.setDirty(2);
await this.save(false);
await this.loadSceneFiles();
}
GenericInputPrompt.Prompt(
this,
this.plugin,
this.app,
t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"),
undefined,
ef.linkParts.original,
[{caption: "✅", action: handler}],
1,
false,
(container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}),
false
).then(handler, () => {});
return;
}
const fileId = selectedImage.fileId;
const ef = this.excalidrawData.getFile(fileId);
if (!ef.isHyperLink && !ef.isLocalLink && ef.file && linkClickType === "md-properties") {
this.updateScene({appState: {contextMenu: null}});
this.openEmbeddedLinkEditor(selectedImage.id);
return;
}
let secondOrderLinks: string = " ";
const backlinks = this.app.metadataCache?.getBacklinksForFile(ef.file)?.data;
@@ -1315,7 +1395,7 @@ export default class ExcalidrawView extends TextFileView {
//final fallback to prevent resizing when text element is in edit mode
//this is to prevent jumping text due to on-screen keyboard popup
if (api.getAppState()?.editingElement?.type === "text") {
if (api.getAppState()?.editingTextElement) {
return;
}
this.zoomToFit(false);
@@ -1455,8 +1535,17 @@ export default class ExcalidrawView extends TextFileView {
}
};
const onBlurOrLeave = () => {
if(!this.excalidrawAPI || !this.excalidrawData.loaded || !this.isDirty()) {
return;
}
this.forceSave(true);
};
this.registerDomEvent(this.ownerWindow, "keydown", onKeyDown, false);
this.registerDomEvent(this.ownerWindow, "keyup", onKeyUp, false);
this.registerDomEvent(this.contentEl, "mouseleave", onBlurOrLeave, false);
this.registerDomEvent(this.ownerWindow, "blur", onBlurOrLeave, false);
});
this.setupAutosaveTimer();
@@ -1626,9 +1715,10 @@ export default class ExcalidrawView extends TextFileView {
warningUnknowSeriousError();
return;
}
const st = api.getAppState();
const isEditing = st.editingElement !== null;
const isDragging = st.draggingElement !== null;
const st = api.getAppState() as AppState;
const isFreedrawActive = (st.activeTool?.type === "freedraw") && (this.freedrawLastActiveTimestamp > (Date.now()-2000));
const isEditingText = st.editingTextElement !== null;
const isEditingNewElement = st.newElement !== null;
//this will reset positioning of the cursor in case due to the popup keyboard,
//or the command palette, or some other unexpected reason the onResize would not fire...
this.refreshCanvasOffset();
@@ -1638,8 +1728,9 @@ export default class ExcalidrawView extends TextFileView {
!this.semaphores.forceSaving &&
!this.semaphores.autosaving &&
!this.semaphores.embeddableIsEditingSelf &&
!isEditing &&
!isDragging //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
!isFreedrawActive &&
!isEditingText &&
!isEditingNewElement //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
) {
//console.log("autosave");
this.autosaveTimer = null;
@@ -1647,7 +1738,7 @@ export default class ExcalidrawView extends TextFileView {
this.semaphores.autosaving = true;
//changed from await to then to avoid lag during saving of large file
this.save().then(()=>this.semaphores.autosaving = false);
}
}
this.autosaveTimer = window.setTimeout(
timer,
this.autosaveInterval,
@@ -1681,7 +1772,6 @@ export default class ExcalidrawView extends TextFileView {
this.autosaveFunction,
this.autosaveInterval,
);
}
unload(): void {
@@ -1928,7 +2018,7 @@ export default class ExcalidrawView extends TextFileView {
state.match &&
state.match.content &&
state.match.matches &&
state.match.matches.length === 1 &&
state.match.matches.length >= 1 &&
state.match.matches[0].length === 2
) {
query = [
@@ -2007,6 +2097,7 @@ export default class ExcalidrawView extends TextFileView {
if(images.length>0) {
this.preventAutozoom();
window.setTimeout(()=>this.zoomToElements(!api.getAppState().viewModeEnabled, images));
return;
}
}
}
@@ -2023,7 +2114,7 @@ export default class ExcalidrawView extends TextFileView {
)) {
const cleanQuery = cleanSectionHeading(query[0]);
const sections = await this.getBackOfTheNoteSections();
if(sections.includes(cleanQuery)) {
if(sections.includes(cleanQuery) || this.data.includes(query[0])) {
this.setMarkdownView(state);
return;
}
@@ -2037,6 +2128,7 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear");
this.viewSaveData = "";
this.canvasNodeFactory.purgeNodes();
this.embeddableRefs.clear();
this.embeddableLeafRefs.clear();
@@ -2213,13 +2305,13 @@ export default class ExcalidrawView extends TextFileView {
});
}
private getGridColor(bgColor: string, st: AppState):{Bold: string, Regular: string, MajorGridFrequency: number} {
private getGridColor(bgColor: string, st: AppState):{Bold: string, Regular: string} {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getGridColor, "ExcalidrawView.getGridColor", bgColor, st);
const cm = this.plugin.ea.getCM(bgColor);
const isDark = cm.isDark();
const Regular = (isDark ? cm.lighterBy(7) : cm.darkerBy(7)).stringHEX({alpha: false});
const Bold = (isDark ? cm.lighterBy(14) : cm.darkerBy(14)).stringHEX({alpha: false});
return {Bold, Regular, MajorGridFrequency:st.gridColor.MajorGridFrequency};
return {Bold, Regular};
}
public activeLoader: EmbeddedFilesLoader = null;
@@ -2416,7 +2508,7 @@ export default class ExcalidrawView extends TextFileView {
*
* @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
*/
private async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) {
public async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadDrawing, "ExcalidrawView.loadDrawing", justloaded, deletedElements);
const excalidrawData = this.excalidrawData.scene;
this.semaphores.justLoaded = justloaded;
@@ -2528,6 +2620,10 @@ export default class ExcalidrawView extends TextFileView {
public setDirty(location?:number) {
if(this.semaphores.saving) return; //do not set dirty if saving
if(!this.isDirty()) {
//the autosave timer should start when the first stroke was made... thus avoiding an immediate impact by saving right then
this.resetAutosaveTimer();
}
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,`ExcalidrawView.setDirty, location:${location}`);
this.semaphores.dirty = this.file?.path;
this.actionButtons['save'].querySelector("svg").addClass("excalidraw-dirty");
@@ -3230,6 +3326,7 @@ export default class ExcalidrawView extends TextFileView {
version: 2,
source: GITHUB_RELEASES+PLUGIN_VERSION,
elements: el,
//see also ExcalidrawAutomate async create(
appState: {
theme: st.theme,
viewBackgroundColor: st.viewBackgroundColor,
@@ -3245,17 +3342,20 @@ export default class ExcalidrawView extends TextFileView {
currentItemTextAlign: st.currentItemTextAlign,
currentItemStartArrowhead: st.currentItemStartArrowhead,
currentItemEndArrowhead: st.currentItemEndArrowhead,
currentItemArrowType: st.currentItemArrowType,
scrollX: st.scrollX,
scrollY: st.scrollY,
zoom: st.zoom,
currentItemRoundness: st.currentItemRoundness,
gridSize: st.gridSize,
gridStep: st.gridStep,
gridModeEnabled: st.gridModeEnabled,
gridColor: st.gridColor,
colorPalette: st.colorPalette,
currentStrokeOptions: st.currentStrokeOptions,
previousGridSize: st.previousGridSize,
frameRendering: st.frameRendering,
objectsSnapModeEnabled: st.objectsSnapModeEnabled,
activeTool: st.activeTool,
},
prevTextMode: this.prevTextMode,
files,
@@ -3372,7 +3472,7 @@ export default class ExcalidrawView extends TextFileView {
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element);
if(!this.lastMouseEvent) return;
const st = this.excalidrawAPI?.getAppState();
if(st?.editingElement || st?.draggingElement) return; //should not activate hover preview when element is being edited or dragged
if(st?.editingTextElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged
if(this.semaphores.wheelTimeout) return;
//if link text is not provided, try to get it from the element
if (!linktext) {
@@ -3660,6 +3760,9 @@ export default class ExcalidrawView extends TextFileView {
}
private onChange (et: ExcalidrawElement[], st: AppState) {
if(st.newElement?.type === "freedraw") {
this.freedrawLastActiveTimestamp = Date.now();
}
this.viewModeEnabled = st.viewModeEnabled;
if (this.semaphores.justLoaded) {
const elcount = this.excalidrawData?.scene?.elements?.length ?? 0;
@@ -3693,11 +3796,11 @@ export default class ExcalidrawView extends TextFileView {
return;
}
if (
st.editingElement === null &&
st.editingTextElement === null &&
//Removed because of
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565
/*st.resizingElement === null &&
st.draggingElement === null &&
st.newElement === null &&
st.editingGroupId === null &&*/
st.editingLinearElement === null
) {
@@ -3721,6 +3824,7 @@ export default class ExcalidrawView extends TextFileView {
private onPaste (data: ClipboardData, event: ClipboardEvent | null) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event);
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const ea = this.getHookServer();
if(data && ea.onPasteHook) {
const res = ea.onPasteHook({
@@ -3766,8 +3870,14 @@ export default class ExcalidrawView extends TextFileView {
ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]);
ea.destroy();
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, this.currentPosition);
if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
const ea = getEA(this) as ExcalidrawAutomate;
await ea.addImage(this.currentPosition.x, this.currentPosition.y,link);
ea.addElementsToView(false,false).then(()=>ea.destroy());
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, this.currentPosition);
}
}
this.setDirty(9);
})) {
@@ -3777,7 +3887,6 @@ export default class ExcalidrawView extends TextFileView {
const quoteWithRef = obsidianPDFQuoteWithRef(data.text);
if(quoteWithRef) {
const ea = getEA(this) as ExcalidrawAutomate;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const st = api.getAppState();
const strokeC = st.currentItemStrokeColor;
const viewC = st.viewBackgroundColor;
@@ -3805,6 +3914,77 @@ export default class ExcalidrawView extends TextFileView {
if (data.elements) {
window.setTimeout(() => this.save(), 30); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements
}
//process pasted text after it was processed into elements by Excalidraw
//I let Excalidraw handle the paste first, e.g. to split text by lines
//Only process text if it includes links or embeds that need to be parsed
if(data && data.text && data.text.match(/(\[\[[^\]]*]])|(\[[^\]]*]\([^)]*\))/gm)) {
const prevElements = api.getSceneElements().filter(el=>el.type === "text").map(el=>el.id);
window.setTimeout(async ()=>{
const sceneElements = api.getSceneElementsIncludingDeleted() as Mutable<ExcalidrawElement>[];
const newElements = sceneElements.filter(el=>el.type === "text" && !el.isDeleted && !prevElements.includes(el.id)) as ExcalidrawTextElement[];
//collect would-be image elements and their corresponding files and links
const imageElementsMap = new Map<ExcalidrawTextElement, [string, TFile]>();
let element: ExcalidrawTextElement;
const callback = (link: string, file: TFile) => {
imageElementsMap.set(element, [link, file]);
}
newElements.forEach((el:ExcalidrawTextElement)=>{
element = el;
isTextImageTransclusion(el.originalText,this,callback);
});
//if there are no image elements, save and return
//Save will ensure links and embeds are parsed
if(imageElementsMap.size === 0) {
this.save(false); //saving because there still may be text transclusions
return;
};
//if there are image elements
//first delete corresponding "old" text elements
for(const [el, [link, file]] of imageElementsMap) {
const clone = cloneElement(el);
clone.isDeleted = true;
this.excalidrawData.deleteTextElement(clone.id);
sceneElements[sceneElements.indexOf(el)] = clone;
}
this.updateScene({elements: sceneElements, storeAction: "update"});
//then insert images and embeds
//shift text elements down to make space for images and embeds
const ea:ExcalidrawAutomate = getEA(this);
let offset = 0;
for(const el of newElements) {
const topleft = {x: el.x, y: el.y+offset};
if(imageElementsMap.has(el)) {
const [link, file] = imageElementsMap.get(el);
if(IMAGE_TYPES.contains(file.extension)) {
const id = await insertImageToView (ea, topleft, file, undefined, false);
offset += ea.getElement(id).height - el.height;
} else if(file.extension !== "pdf") {
//isTextImageTransclusion will not return text only markdowns, this is here
//for the future when we may want to support other embeddables
const id = await insertEmbeddableToView (ea, topleft, file, link, false);
offset += ea.getElement(id).height - el.height;
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, topleft);
}
} else {
if(offset !== 0) {
ea.copyViewElementsToEAforEditing([el]);
ea.getElement(el.id).y = topleft.y;
}
}
}
await ea.addElementsToView(false,true);
ea.selectElementsInView(newElements.map(el=>el.id));
ea.destroy();
},200) //parse transclusion and links after paste
}
return true;
}
@@ -5008,6 +5188,70 @@ export default class ExcalidrawView extends TextFileView {
);
};
private diagramToCode() {
return this.packages.react.createElement(
this.packages.excalidrawLib.DiagramToCodePlugin,
{
generate: async ({ frame, children }:
{frame: ExcalidrawMagicFrameElement, children: readonly ExcalidrawElement[]}) => {
const appState = this.excalidrawAPI.getAppState();
try {
const blob = await this.packages.excalidrawLib.exportToBlob({
elements: children,
appState: {
...appState,
exportBackground: true,
viewBackgroundColor: appState.viewBackgroundColor,
},
exportingFrame: frame,
files: this.excalidrawAPI.getFiles(),
mimeType: "image/jpeg",
});
const dataURL = await this.packages.excalidrawLib.getDataURL(blob);
const textFromFrameChildren = this.packages.excalidrawLib.getTextFromElements(children);
const response = await diagramToHTML ({
image:dataURL,
apiKey: this.plugin.settings.openAIAPIToken,
text: textFromFrameChildren,
theme: appState.theme,
});
if (!response.ok) {
const json = await response.json();
const text = json.error?.message || "Unknown error during generation";
return {
html: errorHTML(text),
};
}
const json = await response.json();
if(json.choices[0].message.content == null) {
return {
html: errorHTML("Nothing generated"),
};
}
const message = json.choices[0].message.content;
const html = message.slice(
message.indexOf("<!DOCTYPE html>"),
message.indexOf("</html>") + "</html>".length,
);
return { html };
} catch (err: any) {
return {
html: errorHTML("Request failed"),
};
}
},
}
);
}
private ttdDialogTrigger() {
return this.packages.react.createElement(
this.packages.excalidrawLib.TTDDialogTrigger,
@@ -5016,6 +5260,7 @@ export default class ExcalidrawView extends TextFileView {
}
private renderWelcomeScreen () {
if(!this.plugin.settings.showSplashscreen) return null;
const React = this.packages.react;
const {WelcomeScreen} = this.packages.excalidrawLib;
const filecount = this.app.vault.getFiles().filter(f=>this.plugin.isExcalidrawFile(f)).length;
@@ -5256,14 +5501,14 @@ export default class ExcalidrawView extends TextFileView {
//...again, just aweful, but works.
const st = api.getAppState();
//isEventOnSameElement attempts to solve https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingElement is not null
const isEventOnSameElement = this.editingTextElementId === st.editingElement?.id;
const isKeyboardOutEvent:Boolean = st.editingElement && st.editingElement.type === "text" && !isEventOnSameElement;
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingTextElement is not null
const isEventOnSameElement = this.editingTextElementId === st.editingTextElement?.id;
const isKeyboardOutEvent:Boolean = st.editingTextElement && !isEventOnSameElement;
const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent;
this.editingTextElementId = isKeyboardOutEvent ? st.editingElement.id : null;
this.editingTextElementId = isKeyboardOutEvent ? st.editingTextElement.id : null;
if(isKeyboardOutEvent) {
const appToolHeight = (this.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0;
const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingElement.y}, st).y;
const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingTextElement.y}, st).y;
const scrollViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:-st.scrollY}, st).y;
const delta = editingElViewY - scrollViewY;
const isElementAboveKeyboard = height > (delta + appToolHeight*2)
@@ -5454,6 +5699,7 @@ export default class ExcalidrawView extends TextFileView {
this.renderCustomActionsMenu(),
this.renderWelcomeScreen(),
this.ttdDialog(),
this.diagramToCode(),
this.ttdDialogTrigger(),
),
this.renderToolsPanel(observer),
@@ -5634,15 +5880,24 @@ export default class ExcalidrawView extends TextFileView {
let match = getTextElementsMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.type === "text"),
query,
exactMatch
exactMatch,
).concat(getFrameElementsMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.type === "frame"),
query,
exactMatch
exactMatch,
)).concat(getElementsWithLinkMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.link),
query,
exactMatch,
)).concat(getImagesMatchingQuery(
elements,
query,
this.excalidrawData,
exactMatch,
));
if (match.length === 0) {
new Notice("I could not find a matching text element");
new Notice(t("NO_SEARCH_RESULT"));
return false;
}
@@ -5667,7 +5922,7 @@ export default class ExcalidrawView extends TextFileView {
const zoomLevel = this.plugin.settings.zoomToFitMaxLevel;
if (selectResult) {
api.selectElements(elements);
api.selectElements(elements, true);
}
api.zoomToFit(elements, zoomLevel, 0.05);
}
@@ -5681,9 +5936,14 @@ export default class ExcalidrawView extends TextFileView {
return api.getSceneElements();
}
public getViewSelectedElements(): ExcalidrawElement[] {
/**
*
* @param deepSelect: if set to true, child elements of the selected frame will also be selected
* @returns
*/
public getViewSelectedElements(includFrameChildren: boolean = true): ExcalidrawElement[] {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewSelectedElements, "ExcalidrawView.getViewSelectedElements");
const api = this.excalidrawAPI;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!api) {
return [];
}
@@ -5695,6 +5955,9 @@ export default class ExcalidrawView extends TextFileView {
if (!selectedElementsKeys) {
return [];
}
const elementIDs = new Set<string>();
const elements: ExcalidrawElement[] = api
.getSceneElements()
.filter((e: any) => selectedElementsKeys.includes(e.id));
@@ -5712,15 +5975,27 @@ export default class ExcalidrawView extends TextFileView {
.map((be) => be.id)[0],
);
const elementIDs = elements
.map((el) => el.id)
.concat(containerBoundTextElmenetsReferencedInElements);
if(includFrameChildren && elements.some(el=>el.type === "frame")) {
elements.filter(el=>el.type === "frame").forEach(frameEl => {
api.getSceneElements()
.filter(el=>el.frameId === frameEl.id)
.forEach(el=>elementIDs.add(el.id))
})
}
elements.forEach(el=>elementIDs.add(el.id));
containerBoundTextElmenetsReferencedInElements.forEach(id=>elementIDs.add(id));
return api
.getSceneElements()
.filter((el: ExcalidrawElement) => elementIDs.contains(el.id));
.filter((el: ExcalidrawElement) => elementIDs.has(el.id));
}
/**
*
* @param prefix - defines the default button.
* @returns
*/
public async copyLinkToSelectedElementToClipboard(prefix:string) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.copyLinkToSelectedElementToClipboard, "ExcalidrawView.copyLinkToSelectedElementToClipboard", prefix);
const elements = this.getViewSelectedElements();
@@ -5747,58 +6022,59 @@ export default class ExcalidrawView extends TextFileView {
: this.plugin.ea.getLargestElement(elements).id;
}
const isFrame = elements.some(el=>el.id === elementId && el.type==="frame");
const frames = elements.filter(el=>el.type==="frame");
const hasFrame = frames.length === 1;
const hasGroup = elements.some(el=>el.groupIds && el.groupIds.length>0);
let button = {
area: {caption: "Area", action:()=>{prefix="area="; return;}},
link: {caption: "Link", action:()=>{prefix="";return}},
group: {caption: "Group", action:()=>{prefix="group="; return;}},
frame: {caption: "Frame", action:()=>{prefix="frame="; elementId = frames[0].id; return;}},
clippedframe: {caption: "Clipped Frame", action:()=>{prefix="clippedframe="; ; elementId = frames[0].id; return;}},
}
let buttons = [];
if(isFrame) {
switch(prefix) {
case "clippedframe=":
buttons = [
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
];
break;
case "area=":
case "group=":
case "frame=":
buttons = [
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
]
}
} else {
switch(prefix) {
case "area=":
buttons = [
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Group", action:()=>{prefix="group="; return;}},
];
break;
case "group=":
buttons = [
{caption: "Group", action:()=>{prefix="group="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}},
]
}
switch(prefix) {
case "area=":
buttons = [
button.area,
button.link,
...hasGroup ? [button.group] : [],
...hasFrame ? [button.frame, button.clippedframe] : [],
];
break;
case "group=":
buttons = [
...hasGroup ? [button.group] : [],
button.link,
button.area,
...hasFrame ? [button.frame, button.clippedframe] : [],
];
break;
case "frame=":
buttons = [
...hasFrame ? [button.frame, button.clippedframe] : [],
...hasGroup ? [button.group] : [],
button.link,
button.area,
];
break;
case "clippedframe=":
buttons = [
...hasFrame ? [button.clippedframe, button.frame] : [],
...hasGroup ? [button.group] : [],
button.link,
button.area,
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}},
...hasFrame ? [button.frame, button.clippedframe] : [],
]
}
const alias = await ScriptEngine.inputPrompt(

View File

@@ -1,4 +1,5 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
@@ -25,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, DEBUGGING } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
@@ -38,8 +39,11 @@ interface imgElementAttributes {
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = false;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
@@ -60,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}:{
@@ -74,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
@@ -140,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
@@ -171,6 +178,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
@@ -238,6 +246,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
@@ -300,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]);
@@ -347,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);
@@ -375,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;
@@ -502,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));
};
@@ -510,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
@@ -541,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: "",
@@ -577,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*%?))?\|?(.*)/);
@@ -596,6 +612,7 @@ 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);
@@ -606,7 +623,11 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
isPrinting: boolean,
isMarkdownReadingMode: boolean,
isHoverPopover: boolean,
) => {
(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;
@@ -624,11 +645,11 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
return;
}
@@ -656,14 +677,14 @@ const tmpObsidianWYSIWYG = async (
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
//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 isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
@@ -675,7 +696,7 @@ const tmpObsidianWYSIWYG = async (
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
@@ -762,6 +783,7 @@ const tmpObsidianWYSIWYG = async (
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -771,12 +793,43 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
//firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
const isFrontmatter = el.hasClass("mod-frontmatter") || el.firstElementChild?.hasClass("frontmatter");
if(isPrinting && isFrontmatter) {
return;
}
//@ts-ignore
const containerEl = ctx.containerEl;
(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 isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view"));
const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover"));
const isPreview = (isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes);
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if(isPrinting && plugin.settings.renderImageInMarkdownToPDF) {
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
if (!isPreview && embeddedItems.length === 0) {
if(isFrontmatter) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId) && !el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
const isAreaGroupFrameRef = el.querySelectorAll('[data-heading^="Unable to find"]').length === 1;
if(!isAreaGroupFrameRef) {
return;
}
}
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
@@ -785,8 +838,7 @@ 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"));
const isPrinting = Boolean(document.body.querySelectorAll("body > .print"));
if (excalidrawFile && !isPrinting) {
if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) {
el.style.display = "none";
return;
}

View File

@@ -0,0 +1,3 @@
export const TAG_PDFEXPORT = "PDFExport";
export const TAG_MDREADINGMODE = "MDReadingMode";
export const TAG_AUTOEXPORT = "Autoexport";

View File

@@ -9,6 +9,11 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const THEME = {
LIGHT: "light",
DARK: "dark",
} as const;
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_ELEMENTLINKS = "## Element Links";
@@ -97,7 +102,7 @@ export const {
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getFontDefinition,
getCSSFontDefinition,
} = excalidrawLib;
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
@@ -116,7 +121,7 @@ export const DEVICE: DeviceType = {
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
isWindows: document.body.hasClass("mod-windows"),
isIOS: document.body.hasClass("is-ios"),
isAndroid: document.body.hasClass("is-android")
isAndroid: document.body.hasClass("is-android"),
};
export const ROOTELEMENTSIZE = (() => {

View File

@@ -416,8 +416,14 @@ function RenderObsidianView(
}
} else if (leafRef.current?.node) {
//Handle canvas node
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
if(view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) {
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
} else {
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
}
}, [
containerRef,
@@ -428,7 +434,8 @@ function RenderObsidianView(
element,
view,
isEditingRef,
view.canvasNodeFactory
view.canvasNodeFactory,
themeRef.current
]);
return null;

162
src/dialogs/HotkeyEditor.ts Normal file
View File

@@ -0,0 +1,162 @@
import { BaseComponent, Setting, Modifier } from 'obsidian';
import { DEVICE } from 'src/constants/constants';
import { t } from 'src/lang/helpers';
import { ExcalidrawSettings } from 'src/settings';
import { modifierLabel } from 'src/utils/ModifierkeyHelper';
import { fragWithHTML } from 'src/utils/Utils';
export class HotkeyEditor extends BaseComponent {
private settings: ExcalidrawSettings;
private containerEl: HTMLElement;
private capturing: boolean = false;
private activeModifiers: Modifier[] = [];
public isDirty: boolean = false;
private applySettingsUpdate: Function;
// Store bound event handlers
private boundKeydownHandler: (event: KeyboardEvent) => void;
private boundKeyupHandler: (event: KeyboardEvent) => void;
constructor(containerEl: HTMLElement, settings: ExcalidrawSettings, applySettingsUpdate: Function) {
super();
this.containerEl = containerEl.createDiv();
this.settings = settings;
this.applySettingsUpdate = applySettingsUpdate;
// Bind the event handlers once in the constructor
this.boundKeydownHandler = this.onKeydown.bind(this);
this.boundKeyupHandler = this.onKeyup.bind(this);
}
onload(): void {
this.render();
}
private render(): void {
// Clear previous content
this.containerEl.empty();
// Render current overrides
this.settings.modifierKeyOverrides.forEach((override, index) => {
const key = override.key.toUpperCase();
new Setting(this.containerEl)
.setDesc(fragWithHTML(`<b>Code:</b> <kbd>${override.modifiers.join("+")} + ${key}</kbd> | ` +
`<b>Apple:</b> <kbd>${modifierLabel(override.modifiers, "Mac")} + ${key}</kbd> | ` +
`<b>Windows:</b> <kbd>${modifierLabel(override.modifiers, "Other")} + ${key}</kbd>`))
.addButton((button) =>
button
.setButtonText(t("HOTKEY_BUTTON_REMOVE"))
.setCta()
.onClick(() => {
this.settings.modifierKeyOverrides.splice(index, 1);
this.isDirty = true;
this.applySettingsUpdate();
this.render();
})
);
});
// Render Add New Override or Capture Instruction
if (this.capturing) {
new Setting(this.containerEl)
.setName(t("HOTKEY_PRESS_COMBO_NANE"))
.setDesc(t("HOTKEY_PRESS_COMBO_DESC"))
.controlEl.style.cursor = 'pointer';
} else {
new Setting(this.containerEl)
.addButton((button) =>
button
.setButtonText(t("HOTKEY_BUTTON_ADD_OVERRIDE"))
.setCta()
.onClick(() => this.startCapture())
);
}
}
private startCapture(): void {
this.capturing = true;
this.activeModifiers = [];
this.render();
// Use the pre-bound handlers
window.addEventListener('keydown', this.boundKeydownHandler);
window.addEventListener('keyup', this.boundKeyupHandler);
}
private onKeydown(event: KeyboardEvent): void {
event.preventDefault();
event.stopPropagation();
const modifiers = this.getModifiersFromEvent(event);
// If only modifiers are pressed, update activeModifiers and continue listening
if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) {
this.activeModifiers = modifiers;
return;
}
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
// Check for duplicate overrides
const exists = this.settings.modifierKeyOverrides.some(
(override) =>
override.key === key &&
override.modifiers.length === modifiers.length &&
override.modifiers.every((mod) => modifiers.includes(mod))
);
if (!exists) {
this.settings.modifierKeyOverrides.push({ modifiers, key });
this.isDirty = true;
this.applySettingsUpdate();
}
this.stopCapture();
}
private onKeyup(event: KeyboardEvent): void {
// If all modifier keys are released, stop capturing
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
this.stopCapture();
}
}
private stopCapture(): void {
this.capturing = false;
// Use the pre-bound handlers for removal
window.removeEventListener('keydown', this.boundKeydownHandler);
window.removeEventListener('keyup', this.boundKeyupHandler);
this.render();
}
public unload(): void {
// Ensure listeners are removed when the component is unloaded
this.stopCapture();
}
private getModifiersFromEvent(event: KeyboardEvent): Modifier[] {
const modifiers: Modifier[] = [];
if (DEVICE.isMacOS && event.metaKey) {
modifiers.push('Mod');
} else if (!DEVICE.isMacOS && event.ctrlKey) {
modifiers.push('Mod');
}
if (DEVICE.isMacOS && event.ctrlKey) {
modifiers.push('Ctrl');
}
if (!DEVICE.isMacOS && event.metaKey) {
modifiers.push('Meta');
}
if (event.shiftKey) {
modifiers.push('Shift');
}
if (event.altKey) {
modifiers.push('Alt');
}
return modifiers;
}
}

View File

@@ -7,7 +7,6 @@ import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;

View File

@@ -3,7 +3,6 @@ import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
destroy() {

View File

@@ -7,7 +7,6 @@ import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;

View File

@@ -5,7 +5,6 @@ import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
private drawingPath: string;

View File

@@ -5,7 +5,6 @@ import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;

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";
@@ -48,6 +50,7 @@ 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;
@@ -120,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;
@@ -138,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) {
@@ -161,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.`;
@@ -211,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;
@@ -222,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) => {
@@ -244,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;
@@ -391,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":
@@ -404,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);

View File

@@ -17,6 +17,49 @@ 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.4.0": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/LtuAaqY_DNc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Flowcharts with CTRL/CMD+Arrow and ALT/OPT+Arrow keys
- Improved PDF Support
- PDF++ cropped area paste
- Import PDF into frames
- Element links with metadata
- Obisidan Hotkey overrides
- Support for Zotero style markdown links
## QoL
- Much improved freedraw flow, less autosave glitches
- Link editor CTRL+Meta/(CTRL+CMD) + click or via the command palette "Open the image-link or LaTeX-formula editor.
- Improved search and search results
- Disable double tap ereaser activation in pen mode
- Single click editing of markdown embeddables
- Set grid size and frequency
- Improved paste
- Pan & Zoom while editing Text
- Save active too-state (e.g. tool-lock) with the drawing
- Show/hide "sword" splashscreen in new drawings
## Fixed
- Duplicate line points when Alt+click adding new points in line editor- - Excalidraw Automate measureText, impacting gate placement in ExcaliBrain
- If a group includes a frame, the image reference will include all the elements in the frame, not just the frame
- Excalidraw rendering issues in markdown preview
- Markdown pages embedded in Excalidraw were broken
- Drawing did not save arrow type
- Fixed rendering of newly pasted links
## ExcalidrawAutomate
- new functions
- tex2dataURL
- addElementsToFrame
- resetImageAspectRatio
- Changed
- getViewSelectedElements(includeFrameChildren: boolean = true);
- getOriginalImageSize with option to wait for the image to load
`,
"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)

View File

@@ -9,7 +9,6 @@ export enum openDialogAction {
}
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
public app: App;
private plugin: ExcalidrawPlugin;
private action: openDialogAction;
private onNewPane: boolean;

View File

@@ -20,7 +20,7 @@ import { t } from "src/lang/helpers";
import { ExcalidrawElement, getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { REGEX_LINK } from "src/ExcalidrawData";
import { REGEX_LINK, REGEX_TAGS } from "src/ExcalidrawData";
import { ScriptEngine } from "src/Scripts";
import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/ExcalidrawViewUtils";
@@ -211,15 +211,15 @@ export class GenericInputPrompt extends Modal {
}, 30);
}
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback);
textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written
textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down
textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down
textComponent.inputEl.addEventListener('input', checkcaret); // Other input events
textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions
textComponent.inputEl.addEventListener('cut', checkcaret);
textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event
textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback.bind(this));
textComponent.inputEl.addEventListener('keyup', checkcaret.bind(this)); // Every character written
textComponent.inputEl.addEventListener('pointerup', checkcaret.bind(this)); // Click down
textComponent.inputEl.addEventListener('touchend', checkcaret.bind(this)); // Click down
textComponent.inputEl.addEventListener('input', checkcaret.bind(this)); // Other input events
textComponent.inputEl.addEventListener('paste', checkcaret.bind(this)); // Clipboard actions
textComponent.inputEl.addEventListener('cut', checkcaret.bind(this));
textComponent.inputEl.addEventListener('select', checkcaret.bind(this)); // Some browsers support this event
textComponent.inputEl.addEventListener('selectionchange', checkcaret.bind(this));// Some browsers support this event
return textComponent;
}
@@ -272,18 +272,18 @@ export class GenericInputPrompt extends Modal {
this.createButton(
actionButtonContainer,
"✅",
this.submitClickCallback,
this.submitClickCallback.bind(this),
).setCta().buttonEl.style.marginRight = "0";
}
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL"));
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback.bind(this), t("PROMPT_BUTTON_CANCEL"));
if(this.displayEditorButtons) {
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback.bind(this), "Delete");
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
if(this.view) {
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK"));
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback.bind(this), t("PROMPT_BUTTON_INSERT_LINK"));
}
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE"));
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback.bind(this), t("PROMPT_BUTTON_UPPERCASE"));
}
}
@@ -342,8 +342,13 @@ export class GenericInputPrompt extends Modal {
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
}
private submitClickCallback = () => this.submit();
private cancelClickCallback = () => this.cancel();
private submitClickCallback () {
this.submit();
}
private cancelClickCallback () {
this.cancel();
}
private keyDownCallback = (evt: KeyboardEvent) => {
if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) {
@@ -668,10 +673,10 @@ export class ConfirmationPrompt extends Modal {
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback);
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback.bind(this));
cancelButton.buttonEl.style.marginRight = "0.5rem";
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback);
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback.bind(this));
confirmButton.buttonEl.style.marginRight = "0";
cancelButton.buttonEl.focus();
@@ -683,12 +688,12 @@ export class ConfirmationPrompt extends Modal {
return button;
}
private cancelClickCallback = () => {
private cancelClickCallback() {
this.didConfirm = false;
this.close();
};
private confirmClickCallback = () => {
private confirmClickCallback() {
this.didConfirm = true;
this.close();
};
@@ -714,18 +719,28 @@ export async function linkPrompt (
view?: ExcalidrawView,
message: string = "Select link to open",
):Promise<[file:TFile, linkText:string, subpath: string]> {
const partsArray = REGEX_LINK.getResList(linkText);
const linksArray = REGEX_LINK.getResList(linkText);
const tagsArray = REGEX_TAGS.getResList(linkText);
let subpath: string = null;
let file: TFile = null;
let parts = partsArray[0];
if (partsArray.length > 1) {
let parts = linksArray[0] ?? tagsArray[0];
const itemsDisplay = [
...linksArray.filter(p=> Boolean(p.value)).map(p => {
const alias = REGEX_LINK.getAliasOrLink(p);
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
}),
...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)),
];
const items = [
...linksArray.filter(p=>Boolean(p.value)),
...tagsArray.filter(x=> Boolean(x.value)),
];
if (items.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)),
itemsDisplay,
items,
message,
);
if(!parts) return;
@@ -735,8 +750,8 @@ export async function linkPrompt (
return;
}
if (!parts.value) {
openTagSearch(linkText, app);
if (REGEX_TAGS.isTag(parts)) {
openTagSearch(REGEX_TAGS.getTag(parts), app);
return;
}

View File

@@ -233,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;",
@@ -311,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;",
@@ -387,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: "",
},
{
@@ -489,8 +507,9 @@ 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: ""
},
{
@@ -531,8 +550,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
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.",
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}>",
desc: "Returns the size of the image element at 100% (i.e. the original size) or undefined if the data URL is not available.\n"+
"If shouldWaitForImage is true, the function will wait for the view to load the image before returning the size.\n"+
"This is an async function, you need to await the result.",
after: "",
},
{
field: "resetImageAspectRatio",
code: "async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean>",
desc: "Resets the image to its original aspect ratio.\n" +
"If the image is resized then the function returns true.\n" +
"If the image element is not in EA (only in the view), then if the image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).\n" +
"Note you need to run await ea.addElementsToView(false); to add the modified image to the view.",
after: "",
},
{

View File

@@ -2,6 +2,7 @@ import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// English
@@ -36,6 +37,7 @@ export default {
TRANSCLUDE: "Embed a drawing",
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
TOGGLE_SPLASHSCREEN: "Show splash screen in new drawings",
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",
@@ -53,7 +55,7 @@ export default {
COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)",
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
INSERT_LINK_TO_ELEMENT:
`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.`,
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link.`,
INSERT_LINK_TO_ELEMENT_GROUP:
"Copy 'group=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_AREA:
@@ -79,7 +81,7 @@ export default {
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.`,
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}).`,
ENTER_LATEX: "Enter a valid LaTeX expression",
READ_RELEASE_NOTES: "Read latest release notes",
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
@@ -92,14 +94,20 @@ export default {
ANNOTATE_IMAGE : "Annotate image in Excalidraw",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert active PDF page as image",
RESET_IMG_TO_100: "Set selected image element size to 100% of original",
RESET_IMG_ASPECT_RATIO: "Reset selected image element aspect ratio",
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
//ExcalidrawView.ts
NO_SEARCH_RESULT: "Didn't find a matching element in the drawing",
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>",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the Embedded File link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>" +
"For markdown-page images follow this format when editing your link: <mark>filename#^blockref|WIDTHxMAXHEIGHT</mark><br>" +
"You can anchor Excalidraw images to 100% of their size by adding <code>|100%</code> to the end of the link.<br>" +
"You can change the PDF page by changing <code>#page=1</code> to <code>#page=2</code> etc.<br>" +
"PDF rect crop values are: <code>left, bottom, right, top</code>. Eg.: <code>#rect=0,0,500,500</code><br>",
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.",
@@ -322,24 +330,33 @@ FILENAME_HEAD: "Filename",
DEFAULT_PEN_MODE_NAME: "Pen mode",
DEFAULT_PEN_MODE_DESC:
"Should pen mode be automatically enabled when opening Excalidraw?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
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<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 image in hover preview for MD files",
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:
"This setting effects files that have the <b>excalidraw-open-md: true</b> frontmatter key.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
"...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 as image when in markdown reading mode of an Excalidraw file",
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 image when EXPORT TO PDF in markdown mode",
"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 or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <a href='#"+TAG_PDFEXPORT+"'>PDF Export</a> under 'Embedding and Exporting' further below.</li></ul><br>" +
"You must close the active excalidraw/markdown file and reopen it for this change to take effect.",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render the file as an image when exporting an Excalidraw file to PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"Must close the active excalidraw/markdown file and reopen for this change to take effect.<br>When you are printing the markdown side of the note to PDF (aka. the back side of the drawing), should the Excalidraw drawing be rendered as an image?<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>",
"This setting controls the behavior of Excalidraw when exporting an Excalidraw file to PDF in markdown view mode using Obsidian's <b>Export to PDF</b> feature.<br>" +
"<ul><li>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
HOTKEY_OVERRIDE_HEAD: "Hotkey overrides",
HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
`you can add <code>${labelCTRL()}+G</code> if you want to default to Group Object in Excalidraw instead of opening Graph View.`,
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -465,7 +482,11 @@ FILENAME_HEAD: "Filename",
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
MD_HEAD: "Embed markdown into Excalidraw as image",
MD_EMBED_CUSTOMDATA_HEAD_NAME: "Interactive Markdown Files",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `These settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_EMBED_CUSTOMDATA_HEAD_DESC: `The below settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_EMBED_SINGLECLICK_EDIT_NAME: "Single click to edit embedded markdown",
MD_EMBED_SINGLECLICK_EDIT_DESC:
"Single click on an embedded markdown file to edit it. " +
"When turned off, the markdown file will first open in preview mode, then switch to edit mode when you click on it again.",
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
MD_TRANSCLUDE_WIDTH_DESC:
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
@@ -526,7 +547,7 @@ FILENAME_HEAD: "Filename",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"If found, use the already exported image for preview",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"This setting works in conjunction with the Auto-export SVG/PNG setting. If an exported image that matches the file name of the drawing " +
"This setting works in conjunction with the <a href='#"+TAG_AUTOEXPORT+"'>Auto-export SVG/PNG</a> setting. If an exported image that matches the file name of the drawing " +
"is available, use that image instead of generating a preview image on the fly. This will result in faster previews especially when you have many embedded objects in the drawing, however, " +
"it may happen that your latest changes are not displayed and that the image will not automatically match your Obsidian theme in case you have changed the " +
"Obsidian theme since the export was created. This setting only applies to embedding images into markdown documents. " +
@@ -557,7 +578,7 @@ FILENAME_HEAD: "Filename",
EMBED_TYPE_NAME: "Type of file to insert into the document",
EMBED_TYPE_DESC:
"When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original Excalidraw file " +
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"or a PNG or an SVG copy. You need to enable <a href='#"+TAG_AUTOEXPORT+"'>auto-export PNG / SVG</a> (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
@@ -723,6 +744,12 @@ FILENAME_HEAD: "Filename",
"the developer of Taskbone (as you can imagine, there is no such thing as 'free', providing this awesome OCR service costs some money to the developer of Taskbone), you can " +
"purchase a paid API key from <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a>. In case you have purchased a key, simply overwrite this auto generated free-tier API-key with your paid key.",
//HotkeyEditor
HOTKEY_PRESS_COMBO_NANE: "Press your hotkey combination",
HOTKEY_PRESS_COMBO_DESC: "Please press the desired key combination",
HOTKEY_BUTTON_ADD_OVERRIDE: "Add New Override",
HOTKEY_BUTTON_REMOVE: "Remove",
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",
SELECT_COMMAND: "Select a command then press enter.",
@@ -767,7 +794,7 @@ FILENAME_HEAD: "Filename",
TOGGLE_FRAME_RENDERING: "Toggle frame rendering",
TOGGLE_FRAME_CLIPPING: "Toggle frame clipping",
OPEN_LINK_CLICK: "Open Link",
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
OPEN_LINK_PROPS: "Open the image-link or LaTeX-formula editor",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "Narrow to heading...",
@@ -835,4 +862,16 @@ FILENAME_HEAD: "Filename",
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

@@ -2,6 +2,7 @@ import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// 简体中文
@@ -36,6 +37,7 @@ export default {
TRANSCLUDE: "嵌入绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LEFTHANDED_MODE: "切换为左手模式",
TOGGLE_SPLASHSCREEN: "在新绘图中显示启动画面",
FLIP_IMAGE: "打开当前所选 excalidraw 图像的“背景笔记”",
NEW_IN_NEW_PANE: "新建绘图 - 于新面板",
NEW_IN_NEW_TAB: "新建绘图 - 于新页签",
@@ -53,7 +55,7 @@ export default {
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()} 可观看视频演示。`,
`复制所选元素为内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组为内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )。`,
INSERT_LINK_TO_ELEMENT_GROUP:
"复制所选元素所在分组为嵌入链接(形如 ![[file#^group=id]] ",
INSERT_LINK_TO_ELEMENT_AREA:
@@ -79,7 +81,7 @@ export default {
ERROR_TRY_AGAIN: "请重试。",
PASTE_CODEBLOCK: "粘贴代码块",
INSERT_LATEX:
`插入 LaTeX 公式到当前绘图`,
`插入 LaTeX 公式(例如:\\binom{n}{k} = \\frac{n!}{k!(n-k)!})。`,
ENTER_LATEX: "输入 LaTeX 表达式",
READ_RELEASE_NOTES: "阅读本插件的更新说明",
RUN_OCR: "OCR 完整画布:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
@@ -91,15 +93,21 @@ export default {
CROP_IMAGE: "对图片裁剪并添加蒙版",
ANNOTATE_IMAGE : "在 Excalidraw 中标注图像",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "将当前激活的的 PDF 页面作为图片插入",
RESET_IMG_TO_100: "重图像元素的尺寸为 100%",
RESET_IMG_TO_100: "重图像元素的尺寸为 100%",
RESET_IMG_ASPECT_RATIO: "重置所选图像元素的纵横比",
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
//ExcalidrawView.ts
NO_SEARCH_RESULT: "在绘图中未找到匹配的元素",
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",
LINKLIST_SECOND_ORDER_LINK: "二级链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "自定义链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "不要在文件名周围添加[[方括号Wiki 格式链接)]]<br>编辑链接时请遵循以下格式:<br><mark>文件名#^块引用|宽度x最大高度</mark>",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "自定义嵌入文件链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "不要在文件名周围添加 [[方括号]]<br>" +
"对于 markdown 图像,在编辑链接时请遵循以下格式:<mark>文件名#^块引用|宽度x最大高度</mark><br>" +
"您可以通过在链接末尾添加 <code>|100%</code> 来将 Excalidraw 图像锚定为 100% 的大小。<br>" +
"您可以通过将 <code>#page=1</code> 更改为 <code>#page=2</code> 等来更改 PDF 页码。<br>" +
"PDF 矩形裁剪值为:<code>左, 下, 右, 上</code>。例如:<code>#rect=0,0,500,500</code><br>",
FRAME_CLIPPING_ENABLED: "渲染框架:已启用",
FRAME_CLIPPING_DISABLED: "渲染框架:已禁用",
ARROW_BINDING_INVERSE_MODE: "反转模式:默认方向按键已禁用。需要时请使用 Ctrl/CMD 临时启用。",
@@ -322,24 +330,33 @@ FILENAME_HEAD: "文件名",
DEFAULT_PEN_MODE_NAME: "触控笔模式Pen mode",
DEFAULT_PEN_MODE_DESC:
"打开绘图时,是否自动开启触控笔模式?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "启用手写模式下的双击橡皮擦功能",
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_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"这个设置影响 frontmatter 中具有 <b>excalidraw-open-md: true</b> 的文件。",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "在 Markdown 文件阅读模式下渲染为图片",
"...即使文件具有 `<b>excalidraw-open-md: true</b>` frontmatter 属性。<br>" +
"当此设置关闭且文件默认设置为以 md 格式打开时,悬停预览将显示文档的 Markdown 部分(背景笔记)。" +
"",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Excalidraw 文件在 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 时渲染为图",
"当您处于 Markdown 阅读模式(即查看绘图的背景笔记)时Excalidraw 绘图是否应该渲染为图像" +
"此设置不会影响您 Excalidraw 模式的绘图显示,或者在将绘图嵌入 Markdown 文档时,或在渲染悬停预览时。<br><ul>" +
"<li>请参阅下面‘嵌入和导出’部分的 <a href='#"+TAG_PDFEXPORT+"'>PDF 导出</a> 相关设置。</li></ul><br>" +
"您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在将 Excalidraw 文件导出为 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>",
"处于 Markdown 视图模式时,此设置控制 Excalidraw 在使用 Obsidian 的 <b>导出为 PDF</b> 功能时,将 Excalidraw 文件导出为 PDF 的行为。<br>" +
"<ul><li>当 <b>启用</b> 时PDF 将仅显示 Excalidraw 绘图;</li>" +
"<li>当 <b>禁用</b> 时PDF 将显示文档的 Markdown 部分(背景笔记)。</li></ul>" +
"请参阅上面‘外观和行为’部分的 <<a href='#"+TAG_MDREADINGMODE+"'>>Markdown 阅读模式</a> 相关设置。" +
"⚠️ 注意,您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。⚠️",
HOTKEY_OVERRIDE_HEAD: "热键覆盖",
HOTKEY_OVERRIDE_DESC: `一些 Excalidraw 的热键,例如 ${labelCTRL()}+Enter 用于编辑文本,或 ${labelCTRL()}+K 用于创建元素链接。` +
"与 Obsidian 的热键设置发生冲突。您在下面添加的热键组合将在使用 Excalidraw 时覆盖 Obsidian 的热键设置," +
`因此如果您希望在 Excalidraw 中默认选择“组合对象”,而不是打开“图形视图”,您可以添加 ${labelCTRL()}+G。`,
THEME_HEAD: "主题和样式",
ZOOM_HEAD: "缩放",
DEFAULT_PINCHZOOM_NAME: "允许在触控笔模式下进行双指缩放",
@@ -465,7 +482,11 @@ FILENAME_HEAD: "文件名",
EMBED_TOEXCALIDRAW_DESC: "包括:以图像形式嵌入到绘图中的 PDF 文档、以交互形式嵌入到绘图中的 Markdown 文档MD-Embeddable、以图像形式嵌入的 Markdown 文档MD-Embed等。",
MD_HEAD: "以图像形式嵌入到绘图中的 Markdown 文档MD-Embed",
MD_EMBED_CUSTOMDATA_HEAD_NAME: "以交互形式嵌入到绘图中的 Markdown 文档MD-Embeddable",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `这些选项不会影响到已存在的 MD-Embeddable。MD-Embeddable 的主题风格在“显示 & 行为”小节设置`,
MD_EMBED_CUSTOMDATA_HEAD_DESC: `以下设置只会影响以后的嵌入。已存在的嵌入保持不变。嵌入框的主题设置位于 “Excalidraw 外观和行为” 部分`,
MD_EMBED_SINGLECLICK_EDIT_NAME: "单击以编辑嵌入的 markdown。",
MD_EMBED_SINGLECLICK_EDIT_DESC:
"单击嵌入的 markdown 文件以进行编辑。 " +
"当此功能关闭时markdown 文件将首先以预览模式打开,然后在您再次单击时切换到编辑模式。",
MD_TRANSCLUDE_WIDTH_NAME: "MD-Embed 的默认宽度",
MD_TRANSCLUDE_WIDTH_DESC:
"MD-Embed 的宽度。该选项会影响到折行,以及图像元素的宽度。<br>" +
@@ -526,7 +547,7 @@ FILENAME_HEAD: "文件名",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"将之前已导出的图像作为预览图",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"该选项与自动导出 SVG/PNG 副本选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项与<a href='#"+TAG_AUTOEXPORT+"'>自动导出 SVG/PNG 副本</a>选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项能够提高 Markdown 文档的打开速度,尤其是当嵌入到 Markdown 文档中的绘图文件中含有大量图像或 MD-Embed 时。" +
"但是,该选项也可能导致预览图无法立即响应你对绘图文件或者 Obsidian 主题风格的修改。<br>" +
"该选项仅作用于嵌入到 Markdown 文档中的绘图。" +
@@ -557,7 +578,7 @@ FILENAME_HEAD: "文件名",
EMBED_TYPE_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令的源文件类型",
EMBED_TYPE_DESC:
"在命令面板中执行“嵌入绘图到当前 Markdown 文档中”系列命令时,要嵌入绘图文件本身,还是嵌入其 PNG 或 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的<a href='#"+TAG_AUTOEXPORT+"'>自动导出 PNG / SVG 副本</a>。<br>" +
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
@@ -606,7 +627,7 @@ FILENAME_HEAD: "文件名",
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "若开启Excalidraw 将导出两个文件filename.dark.png或 filename.dark.svg和 filename.light.png或 filename.light.svg。<br>"+
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
COMPATIBILITY_HEAD: "兼容性设置",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
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>"+
@@ -655,7 +676,7 @@ FILENAME_HEAD: "文件名",
"我建议尝试多个值来设置这个参数。当您放大一个较大的 PNG 图像时,如果图像突然从视图中消失,那就说明您已经达到了极限。默认值为 1。此设置对 iOS 无效。",
CUSTOM_PEN_HEAD: "自定义画笔",
CUSTOM_PEN_NAME: "自定义画笔工具的数量",
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单按钮旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单按钮旁边切换自定义画笔。长按(双击)画笔按钮可以修改其样式。",
EXPERIMENTAL_HEAD: "杂项",
EXPERIMENTAL_DESC: `包括:默认的 LaTeX 公式字段建议绘图文件的类型标识符OCR 等设置。`,
EA_HEAD: "Excalidraw 自动化",
@@ -706,8 +727,8 @@ FILENAME_HEAD: "文件名",
"若在 excalidraw.com 或者其他版本的 Excalidraw 中打开,使用本地字体的文本会变回系统默认字体。",
FOURTH_FONT_NAME: "本地字体文件",
FOURTH_FONT_DESC:
"选择库文件夹中的一个 .ttf.woff 或 .woff2 字体文件作为本地字体文件。" +
"若未选择文件,则使用默认的 Virgil 字体。",
"选择库文件夹中的一个 .ttf.woff 或 .woff2 字体文件作为本地字体文件。若未选择文件,则使用默认的 Virgil 字体。"+
"<mark>译者注:</mark>您可以在<a href='https://wangchujiang.com/free-font/' target='_blank'>Free Font</a>获取免费商用字体。",
SCRIPT_SETTINGS_HEAD: "已安装脚本的设置",
SCRIPT_SETTINGS_DESC: "有些 Excalidraw 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
TASKBONE_HEAD: "Taskbone OCR光学符号识别",
@@ -723,6 +744,12 @@ FILENAME_HEAD: "文件名",
"Taskbone 的开发者您懂的没有人能用爱发电Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
"到 <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a> 购买一个商用 API key。购买后请将它填写到旁边这个文本框里替换掉原本自动生成的免费 API key。",
//HotkeyEditor
HOTKEY_PRESS_COMBO_NANE: "按下您的组合键",
HOTKEY_PRESS_COMBO_DESC: "请按下所需的组合键",
HOTKEY_BUTTON_ADD_OVERRIDE: "添加新的(热键)覆写",
HOTKEY_BUTTON_REMOVE: "移除",
//openDrawings.ts
SELECT_FILE: "选择一个文件后按回车。",
SELECT_COMMAND: "选择一个命令后按回车。",
@@ -767,7 +794,7 @@ FILENAME_HEAD: "文件名",
TOGGLE_FRAME_RENDERING: "开启或关闭框架渲染",
TOGGLE_FRAME_CLIPPING: "开启或关闭框架裁剪",
OPEN_LINK_CLICK: "打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "编辑所选 MD-Embed 的内部链接,或者打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "打开图像链接或 LaTeX 公式编辑器",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "缩放至标题",
@@ -835,4 +862,16 @@ FILENAME_HEAD: "文件名",
FRAME_SETTIGNS_NAME: "显示框架名称",
FRAME_SETTINGS_OUTLINE: "显示框架外边框",
FRAME_SETTINGS_CLIP: "启用框架裁剪",
};
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "要导入的页面",
IPM_SELECT_PAGES_TO_IMPORT: "请选择页面以进行导入",
IPM_ADD_BORDER_BOX_NAME: "添加带边框的盒子容器",
IPM_ADD_FRAME_NAME: "添加页面到框架",
IPM_ADD_FRAME_DESC: "为了更方便的操作,我建议将页面锁定在框架内。" +
"如果,你将锁定页面在框架内,则唯一的解锁方法是右键点击框架,选择‘从框架中移除元素’,然后解锁页面。",
IPM_GROUP_PAGES_NAME: "建立页面组",
IPM_GROUP_PAGES_DESC: "这将把所有页面建立为一个单独的组。如果您在导入后锁定页面,建议使用此方法,因为这样可以更方便地解锁整个组,而不是逐个解锁。",
IPM_SELECT_PDF: "请选择一个 PDF 文件",
};

View File

@@ -138,6 +138,7 @@ import { showFrameSettings } from "./dialogs/FrameSettings";
import { ExcalidrawLib } from "./ExcalidrawLib";
import { Rank, SwordColors } from "./menu/ActionIcons";
import { RankMessage } from "./dialogs/RankMessage";
import { initCompressionWorker, terminateCompressionWorker } from "./workers/compression-worker";
declare let EXCALIDRAW_PACKAGES:string;
declare let react:any;
@@ -311,6 +312,7 @@ export default class ExcalidrawPlugin extends Plugin {
}*/
async onload() {
initCompressionWorker();
this.loadTimestamp = Date.now();
addIcon(ICON_NAME, EXCALIDRAW_ICON);
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
@@ -1794,12 +1796,80 @@ export default class ExcalidrawPlugin extends Plugin {
const eaEl = ea.getElement(el.id);
//@ts-ignore
eaEl.width = size.width; eaEl.height = size.height;
ea.addElementsToView(false,false,false);
await ea.addElementsToView(false,false,false);
}
ea.destroy();
})()
}
})
this.addCommand({
id: "reset-image-ar",
name: t("RESET_IMG_ASPECT_RATIO"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (!view) return false;
if (!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el => el.type === "image");
if (els.length !== 1) {
if (checking) return false;
new Notice("Select a single image element and try again");
return false;
}
if (checking) return true;
(async () => {
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
if (!ef) {
await view.forceSave();
let ef = view.excalidrawData.getFile(el.fileId);
new Notice("Select a single image element and try again");
return false;
}
const ea = new ExcalidrawAutomate(this, view);
if (await ea.resetImageAspectRatio(el)) {
await ea.addElementsToView(false, false, false);
}
ea.destroy();
})();
}
});
this.addCommand({
id: "open-link-props",
name: t("OPEN_LINK_PROPS"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (!view) return false;
if (!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el => el.type === "image");
if (els.length !== 1) {
if (checking) return false;
new Notice("Select a single image element and try again");
return false;
}
if (checking) return true;
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
let eq = view.excalidrawData.getEquation(el.fileId);
if (!ef && !eq) {
view.forceSave();
new Notice("Please try again.");
return false;
}
if(ef) {
view.openEmbeddedLinkEditor(el.id);
}
if(eq) {
view.openLaTeXEditor(el.id);
}
}
});
this.addCommand({
id: "convert-card-to-file",
name: t("CONVERT_CARD_TO_FILE"),
@@ -2756,37 +2826,53 @@ export default class ExcalidrawPlugin extends Plugin {
this.popScope = null;
}
if (newActiveviewEV) {
const scope = this.app.keymap.getRootScope();
const handler_ctrlEnter = scope.register(["Mod"], "Enter", () => true);
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const handler_ctrlK = scope.register(["Mod"], "k", () => true);
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const handler_ctrlF = scope.register(["Mod"], "f", () => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
search(view);
return true;
}
return false;
});
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const overridSaveShortcut = (
this.forceSaveCommand &&
this.forceSaveCommand.hotkeys[0].key === "s" &&
this.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
)
const saveHandler = overridSaveShortcut
? scope.register(["Ctrl"], "s", () => this.forceSaveActiveView(false))
: undefined;
if(saveHandler) {
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
}
this.popScope = () => {
scope.unregister(handler_ctrlEnter);
scope.unregister(handler_ctrlK);
scope.unregister(handler_ctrlF);
Boolean(saveHandler) && scope.unregister(saveHandler);
this.registerHotkeyOverrides();
}
}
public registerHotkeyOverrides() {
//this is repeated here because the same function is called when settings is closed after hotkeys have changed
if (this.popScope) {
this.popScope();
this.popScope = null;
}
if(!this.activeExcalidrawView) {
return;
}
const scope = this.app.keymap.getRootScope();
// Register overrides from settings
const overrideHandlers = this.settings.modifierKeyOverrides.map(override => {
return scope.register(override.modifiers, override.key, () => true);
});
// Force handlers to the front of the list
overrideHandlers.forEach(() => scope.keys.unshift(scope.keys.pop()));
const handler_ctrlF = scope.register(["Mod"], "f", () => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
search(view);
return true;
}
return false;
});
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const overridSaveShortcut = (
this.forceSaveCommand &&
this.forceSaveCommand.hotkeys[0].key === "s" &&
this.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
)
const saveHandler = overridSaveShortcut
? scope.register(["Ctrl"], "s", () => this.forceSaveActiveView(false))
: undefined;
if(saveHandler) {
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
}
this.popScope = () => {
overrideHandlers.forEach(handler => scope.unregister(handler));
scope.unregister(handler_ctrlF);
Boolean(saveHandler) && scope.unregister(saveHandler);
}
}
@@ -3248,6 +3334,7 @@ export default class ExcalidrawPlugin extends Plugin {
react = null;
reactDOM = null;
excalidrawLib = null;
terminateCompressionWorker();
}
public async embedDrawing(file: TFile) {

View File

@@ -365,13 +365,10 @@ export const ICONS = {
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 22h14a2 2 0 0 0 2-2V7.5L14.5 2H6a2 2 0 0 0-2 2v7"/>
<polyline
points="14 2 14 8 20 8"
fill="var(--icon-fill-color)"
/>
<path d="m10 18 3-3-3-3"/>
<path d="M4 18v-1a2 2 0 0 1 2-2h6"/>
<path d="M10 12.5 8 15l2 2.5"/>
<path d="m14 12.5 2 2.5-2 2.5"/>
<path d="M14 2v4a2 2 0 0 0 2 2h4"/>
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/>
</svg>
),
//fa-brands fa-markdown
@@ -780,7 +777,11 @@ export const penIcon = (pen: PenStyle) => {
strokeLinecap="round"
strokeLinejoin="round"
>
<path fill={pen.strokeColor??"var(--icon-fill-color)"} strokeWidth="2" d="m9 11-6 6v3h9l3-3"></path>
<path fill={
pen.strokeColor??"var(--icon-fill-color)"}
strokeWidth="2" d="m9 11-6 6v3h9l3-3"
style={pen.strokeColor ? { filter: "var(--theme-filter)" } : {}}
></path>
<path fill="none" strokeWidth="2" d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"></path>
</svg>
)
@@ -794,6 +795,7 @@ export const penIcon = (pen: PenStyle) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={pen.strokeColor ? { filter: "var(--theme-filter)" } : {}}
>
<path strokeWidth="2" d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
</svg>
@@ -825,6 +827,7 @@ export const penIcon = (pen: PenStyle) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={pen.strokeColor ? { filter: "var(--theme-filter)" } : {}}
>
<path d="M331 315l158.4-215L460.1 70.6 245 229 331 315zm-187 5l0 0V248.3c0-15.3 7.2-29.6 19.5-38.6L436.6 8.4C444 2.9 453 0 462.2 0c11.4 0 22.4 4.5 30.5 12.6l54.8 54.8c8.1 8.1 12.6 19 12.6 30.5c0 9.2-2.9 18.2-8.4 25.6L350.4 396.5c-9 12.3-23.4 19.5-38.6 19.5H240l-25.4 25.4c-12.5 12.5-32.8 12.5-45.3 0l-50.7-50.7c-12.5-12.5-12.5-32.8 0-45.3L144 320zM23 466.3l63-63 70.6 70.6-31 31c-4.5 4.5-10.6 7-17 7H40c-13.3 0-24-10.7-24-24v-4.7c0-6.4 2.5-12.5 7-17z"/>
</svg>
@@ -837,6 +840,7 @@ export const penIcon = (pen: PenStyle) => {
strokeWidth="2"
fill={pen.strokeColor??"var(--icon-fill-color)"}
stroke={pen.strokeColor??"var(--icon-fill-color)"}
style={pen.strokeColor ? { filter: "var(--theme-filter)" } : {}}
>
<path d="m-.58 95.628.22-.89q.22-.89.49-2.44.26-1.54.77-3.35t1.31-3.43q.79-1.61.2-.26-.6 1.34-.03-.14.58-1.49 1.54-2.97.96-1.49 2.54-3.18 1.59-1.68 3.46-2.96 1.86-1.27.81-.54-1.04.73.6-.46 1.64-1.19 2.8-1.81 1.16-.63.16-.08-.99.54 1.21-.5 2.2-1.03 1.11-.58-1.1.45-.03-.07 1.06-.53 2.32-.82 1.26-.3 2.91-.52 1.64-.23 3.05-.18 1.4.05 2.5.12 1.09.07 2.25.24 1.16.17 2.3.49 1.15.32 2.11.78.96.47 2.21 1.01 1.25.55 2.37 1.04t2.34.89q1.22.4 2.5.65 1.29.25 2.44.33 1.16.08 2.35.17 1.18.08 2.26-.1 1.08-.19 2-1.1.92-.91 1.25-1.93.32-1.02.38-2.15t.57.21q.51 1.34-.03-.02-.55-1.37-.96-2.83-.41-1.45.5-.67.92.79-.03-.06-.95-.85-1.52-1.8-.57-.94-1.5-1.52-.93-.57-1.94-1.22-1.01-.65-1.97-1.88-.96-1.22-1.44-2.54-.49-1.32-.65-2.57-.17-1.24-.11-2.35.06-1.11.31-2.91.24-1.79.76-2.77.51-.97 1.29-1.8.77-.84 1.64-1.55.88-.72 1.9-1.33 1.03-.61 2.43-1.15 1.41-.55 2.69-.92 1.29-.37 2.81-.68 1.53-.31 2.83-.58 1.31-.27 2.78-.47 1.47-.2 2.58-.49 1.12-.28 2.19-.58 1.08-.31 2.13-.73 1.05-.42 2.44-1.32 1.39-.9 2.68-1.85 1.3-.95 2.25-1.87.95-.91 2.06-2 1.11-1.09 1.92-1.93.82-.84 1.54-1.82.71-.98 1.4-1.88.69-.9 1.38-1.96.69-1.07 1.25-2.04.55-.97 1.21-1.94.65-.96 1.35-1.79.69-.83 1.46-1.74.77-.9 1.66-1.73.89-.84 2.72-2.31 1.84-1.48 1.84-1.47v.01l-1.1 1.03q-1.1 1.02-1.77 1.92-.68.9-1.39 1.85-.71.96-1.41 1.91-.7.96-1.19 1.92-.48.95-.95 1.92-.46.96-.9 1.95-.43.99-1.02 2.16-.59 1.17-1.17 2.15-.58.97-1.23 2.13t-1.29 2.02q-.64.86-1.3 1.73-.66.88-1.42 1.78-.75.9-1.72 2.03-.97 1.14-1.81 1.89-.85.75-1.98 1.71-1.14.96-2.05 1.61-.91.64-2.42 1.38-1.51.73-2.71 1.21t-2.49.92q-1.3.44-2.35.69-1.06.24-2.1.59-1.03.34-2.06.74-1.03.4-2.29.94-1.26.54-2.27 1.11-1.01.58-1.57 1.57-.56.99-.81 2.06-.25 1.08.56 2.02.8.94-.21-.02-1.02-.96-.01-.48 1 .49 1.87 1.25.87.77 0 0-.88-.77.46-.01 1.34.75 2.6 1.68 1.26.94 2.08 2.03.81 1.09.01.27-.8-.82.3.26 1.11 1.08 1.71 2.1.61 1.02 1.21 2.25.6 1.24.92 2.36.32 1.12-.16.13-.49-.98.02.36.51 1.35.71 2.69.2 1.34.24 2.46.03 1.12-.09 2.42-.13 1.29-.72 3.21-.6 1.92-1.4 3.49-.81 1.58-1.77 2.83-.96 1.24-2.88 2.72-1.92 1.48-2.95 1.85-1.04.36-2.47.76-1.44.41-3.33.72-1.89.32-3.37.41-1.48.09-2.63.15-1.15.05-2.74-.06-1.59-.1-2.8-.29-1.2-.19-3.2-.63-1.99-.45-3.63-.92-1.63-.48-3.28-.79-1.65-.31-2.76-.2-1.11.1-2.21.42-1.11.32.39-.29 1.49-.6-.12.21-1.61.8-.39.19 1.21-.61.29.13-.92.74-1.83 1.34-.92.61.15-.19t.3-.05q-.77.75-1.58 1.57-.81.82.01-.18.82-1 .24.23t-.72 2.72q-.15 1.48-.08 2.4.07.91-.19 2.16-.26 1.26-.81 2.41-.55 1.16-1.36 2.15t-1.84 1.75q-1.03.77-2.21 1.27t-2.44.7q-1.27.2-2.53.1-1.28-.11-2.49-.52-1.22-.41-2.3-1.1-1.08-.68-1.96-1.61-.89-.92-1.52-2.04-.64-1.11-.99-2.34-.36-1.23-.41-2.51l-.04-1.27Z"/>
</svg>
@@ -849,6 +853,7 @@ export const penIcon = (pen: PenStyle) => {
strokeWidth="2"
fill={pen.strokeColor??"var(--icon-fill-color)"}
stroke={pen.strokeColor??"var(--icon-fill-color)"}
style={pen.strokeColor ? { filter: "var(--theme-filter)" } : {}}
>
<path d="m10 103.405.13-1.22q.14-1.22 1.3-3.16 1.15-1.94 2.74-3.46 1.59-1.53 3.35-2.72 1.77-1.2 4-1.95 2.23-.76 4.45-1t4.86-.4q2.64-.15 5.14-.34 2.51-.19 4.85-.94 2.35-.75 4.55-1.71 2.21-.97 4.16-2.26 1.95-1.3 4.03-2.97 2.07-1.67 3.85-3.05 1.78-1.37 3.72-2.48 1.94-1.11 3.3-2.99 1.36-1.89 2.58-3.74 1.22-1.85-.63-3.42-1.85-1.57-3.82-2.86-1.97-1.3-4.11-2.08-2.15-.78-4.21-1.6-2.06-.81-4.02-1.96-1.96-1.14-3.71-2.48-1.74-1.33-3.37-2.77-1.63-1.43-3.23-3.62-1.6-2.18-2.23-4.64-.62-2.46-.36-4.96.27-2.49 1.19-4.46.91-1.97 2.42-3.7 1.5-1.73 3.5-3.15t4.11-2.28q2.1-.86 4.33-1.44 2.24-.58 4.92-.84 2.68-.26 4.83-.19t4.69.35q2.53.28 4.75.66 2.23.38 4.48.2 2.26-.19 4.43-1.3 2.17-1.12 4.2-2.36 2.04-1.24 3.93-2.43 1.9-1.19 3.84-2.14 1.95-.95 4.04-1.78 2.09-.83 4.56-2.28 2.46-1.46 2.46-1.45h.01q.01 0-1.38 1.3-1.38 1.29-3.08 2.59-1.7 1.3-3.5 2.5t-3.42 2.65q-1.62 1.45-3.18 3-1.57 1.56-3.37 3.13-1.8 1.57-3.6 2.91-1.81 1.33-3.92 2.12t-4.24.92q-2.13.14-4.31.26-2.18.12-4.5.39t-4.56.88q-2.25.61-4.24 1.6-1.99 1-3.83 2.29-1.83 1.29.18 2.44 2.01 1.15 4.2 1.92 2.2.78 4.34 1 2.15.22 4.4.69 2.25.46 4.34 1.16 2.08.71 4.33 1.91 2.25 1.21 4.11 2.73 1.87 1.52 3.68 4.03 1.82 2.5 2.74 5 .93 2.5 1.18 5.03.26 2.53-.04 4.81t-1.4 4.85q-1.09 2.58-2.4 4.26-1.3 1.68-3.1 3.44t-4.02 3.62q-2.23 1.85-4.32 3.07-2.08 1.23-4.34 1.99-2.25.76-4.46 1.96t-4.37 2.14q-2.15.93-4.22 1.81t-4.36 1.35q-2.3.46-4.52.82-2.22.35-4.76.38-2.54.04-4.87-.28t-4.67-.67q-2.34-.35-4.72-.54-2.39-.19-4.64.37-2.25.56-4.16 1.66-1.91 1.11-3.52 2.71-1.61 1.6-2.55 2.39l-.94.78Z"/>
</svg>
@@ -863,6 +868,7 @@ export const penIcon = (pen: PenStyle) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={pen.strokeColor ? { filter: "var(--theme-filter)" } : {}}
>
<path d="M453.3 19.3l39.4 39.4c25 25 25 65.5 0 90.5l-52.1 52.1 0 0-1-1 0 0-16-16-96-96-17-17 52.1-52.1c25-25 65.5-25 90.5 0zM241 114.9c-9.4-9.4-24.6-9.4-33.9 0L105 217c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9L173.1 81c28.1-28.1 73.7-28.1 101.8 0L288 94.1l17 17 96 96 16 16 1 1-17 17L229.5 412.5c-48 48-109.2 80.8-175.8 94.1l-25 5c-7.9 1.6-16-.9-21.7-6.6s-8.1-13.8-6.6-21.7l5-25c13.3-66.6 46.1-127.8 94.1-175.8L254.1 128 241 114.9z"/>
</svg>

View File

@@ -3,6 +3,7 @@ import {
ButtonComponent,
DropdownComponent,
getIcon,
Modifier,
normalizePath,
PluginSettingTab,
Setting,
@@ -37,8 +38,9 @@ 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";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { HotkeyEditor } from "./dialogs/HotkeyEditor";
export interface ExcalidrawSettings {
folder: string;
@@ -78,6 +80,7 @@ export interface ExcalidrawSettings {
matchThemeTrigger: boolean;
defaultMode: string;
defaultPenMode: "never" | "mobile" | "always";
penModeDoubleTapEraser: boolean;
penModeCrosshairVisible: boolean;
renderImageInMarkdownReadingMode: boolean,
renderImageInHoverPreviewForMDNotes: boolean,
@@ -168,6 +171,7 @@ export interface ExcalidrawSettings {
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfFrame: boolean;
pdfGapSize: number;
pdfGroupPages: boolean;
pdfLockAfterImport: boolean;
@@ -181,6 +185,7 @@ export interface ExcalidrawSettings {
COLOR: string,
};
embeddableMarkdownDefaults: EmbeddableMDCustomProps;
markdownNodeOneClickEditing: boolean;
canvasImmersiveEmbed: boolean,
startupScriptPath: string,
openAIAPIToken: string,
@@ -201,6 +206,8 @@ export interface ExcalidrawSettings {
longPressMobile: number;
isDebugMode: boolean;
rank: Rank;
modifierKeyOverrides: {modifiers: Modifier[], key: string}[];
showSplashscreen: boolean;
}
declare const PLUGIN_VERSION:string;
@@ -217,8 +224,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
onceOffCompressFlagReset: false,
onceOffGPTVersionReset: false,
autosave: true,
autosaveIntervalDesktop: 15000,
autosaveIntervalMobile: 10000,
autosaveIntervalDesktop: 30000,
autosaveIntervalMobile: 20000,
drawingFilenamePrefix: "Drawing ",
drawingEmbedPrefixWithFilename: true,
drawingFilnameEmbedPostfix: " ",
@@ -243,6 +250,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
matchThemeTrigger: false,
defaultMode: "normal",
defaultPenMode: "never",
penModeDoubleTapEraser: true,
penModeCrosshairVisible: true,
renderImageInMarkdownReadingMode: false,
renderImageInHoverPreviewForMDNotes: false,
@@ -339,6 +347,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfFrame: false,
pdfGapSize: 20,
pdfGroupPages: false,
pdfLockAfterImport: true,
@@ -362,6 +371,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
borderOpacity: 0,
filenameVisible: false,
},
markdownNodeOneClickEditing: false,
canvasImmersiveEmbed: true,
startupScriptPath: "",
openAIAPIToken: "",
@@ -458,6 +468,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
longPressMobile: 500,
isDebugMode: false,
rank: "Bronze",
modifierKeyOverrides: [
{modifiers: ["Mod"], key:"Enter"},
{modifiers: ["Mod"], key:"k"},
{modifiers: ["Mod"], key:"G"},
],
showSplashscreen: true,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -466,6 +482,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
private requestReloadDrawings: boolean = false;
private requestUpdatePinnedPens: boolean = false;
private requestUpdateDynamicStyling: boolean = false;
private hotkeyEditor: HotkeyEditor;
//private reloadMathJax: boolean = false;
//private applyDebounceTimer: number = 0;
@@ -492,21 +509,25 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}
this.plugin.saveSettings();
if (this.requestUpdatePinnedPens) {
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedCustomPens()
})
}
if (this.requestUpdateDynamicStyling) {
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) {
setDynamicStyle(this.plugin.ea,v.view,v.view.previousBackgroundColor,this.plugin.settings.dynamicStyling);
}
})
}
this.hotkeyEditor.unload();
if (this.hotkeyEditor.isDirty) {
this.plugin.registerHotkeyOverrides();
}
if (this.requestReloadDrawings) {
const exs =
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for (const v of exs) {
if (v.view instanceof ExcalidrawView) {
await v.view.save(false);
@@ -741,7 +762,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("AUTOSAVE_INTERVAL_DESKTOP_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("15000", "Frequent (every 15 seconds)")
.addOption("15000", "Very frequent (every 15 seconds)")
.addOption("30000", "Frequent (every 30 seconds)")
.addOption("60000", "Moderate (every 60 seconds)")
.addOption("300000", "Rare (every 5 minutes)")
.addOption("900000", "Practically never (every 15 minutes)")
@@ -757,7 +779,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("AUTOSAVE_INTERVAL_MOBILE_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("10000", "Frequent (every 10 seconds)")
.addOption("10000", "Very frequent (every 10 seconds)")
.addOption("20000", "Frequent (every 20 seconds)")
.addOption("30000", "Moderate (every 30 seconds)")
.addOption("60000", "Rare (every 1 minute)")
.addOption("300000", "Practically never (every 5 minutes)")
@@ -1024,6 +1047,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
@@ -1036,7 +1070,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
const readingModeEl = 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) =>
@@ -1047,6 +1081,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
readingModeEl.nameEl.setAttribute("id",TAG_MDREADINGMODE);
new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME"))
@@ -1077,6 +1112,29 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
addIframe(detailsEl, "H8Njp7ZXYag",999);
new Setting(detailsEl)
.setName(t("TOGGLE_SPLASHSCREEN"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.showSplashscreen)
.onChange((value)=> {
this.plugin.settings.showSplashscreen = value;
this.applySettingsUpdate();
})
)
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("HOTKEY_OVERRIDE_HEAD"),
cls: "excalidraw-setting-h3",
});
detailsEl.createEl("span", {}, (el) => {
el.innerHTML = t("HOTKEY_OVERRIDE_DESC");
});
this.hotkeyEditor = new HotkeyEditor(detailsEl, this.plugin.settings, this.applySettingsUpdate);
this.hotkeyEditor.onload();
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("THEME_HEAD"),
@@ -1837,7 +1895,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
});
addIframe(detailsEl, "wTtaXmRJ7wg",171);
new Setting(detailsEl)
const pdfExportEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC")))
.addToggle((toggle) =>
@@ -1848,6 +1906,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
pdfExportEl.nameEl.setAttribute("id",TAG_PDFEXPORT);
new Setting(detailsEl)
.setName(t("EXPORT_EMBED_SCENE_NAME"))
@@ -1987,6 +2046,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text: t("EXPORT_HEAD"),
cls: "excalidraw-setting-h4",
});
detailsEl.setAttribute("id",TAG_AUTOEXPORT);
new Setting(detailsEl)
.setName(t("EXPORT_SYNC_NAME"))
@@ -2107,7 +2167,26 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text: t("MD_EMBED_CUSTOMDATA_HEAD_NAME"),
cls: "excalidraw-setting-h3",
});
detailsEl.createEl("span", {text: t("MD_EMBED_CUSTOMDATA_HEAD_DESC")});
new Setting(detailsEl)
.setName(t("MD_EMBED_SINGLECLICK_EDIT_NAME"))
.setDesc(fragWithHTML(t("MD_EMBED_SINGLECLICK_EDIT_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.markdownNodeOneClickEditing)
.onChange(async (value) => {
this.plugin.settings.markdownNodeOneClickEditing = value;
this.applySettingsUpdate();
})
);
detailsEl.createEl("hr", { cls: "excalidraw-setting-hr" });
detailsEl.createEl("span", {}, (el) => {
el.innerHTML = t("MD_EMBED_CUSTOMDATA_HEAD_DESC");
});
new EmbeddalbeMDFileCustomDataSettingsComponent(
detailsEl,

View File

@@ -22,12 +22,13 @@ export type DeviceType = {
isMacOS: boolean,
isWindows: boolean,
isIOS: boolean,
isAndroid: boolean
isAndroid: boolean,
};
declare global {
interface Window {
ExcalidrawAutomate: ExcalidrawAutomate;
pdfjsLib: any;
}
}

4
src/types/worker.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "web-worker:*" {
const WorkerFactory: new (options: any) => Worker;
export default WorkerFactory;
}

View File

@@ -249,4 +249,15 @@ export const extractCodeBlocks = (markdown: string): { data: string, type: strin
}
return result;
}
}
export const errorHTML = (message: string) => `<html>
<body style="margin: 0; text-align: center">
<div style="display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100vh; padding: 0 60px">
<div style="color:red">There was an error during generation</div>
</br>
</br>
<div>${message}</div>
</div>
</body>
</html>`;

View File

@@ -10,9 +10,33 @@ 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,fn,...messages);
console.log(fnName, ...messages);
};
let timestamp: number[] = [];
let tsOrigin: number = 0;
export function tsInit(msg: string) {
tsOrigin = Date.now();
timestamp = [tsOrigin, tsOrigin, tsOrigin, tsOrigin, tsOrigin]; // Initialize timestamps for L0 to L4
console.log("0ms: " + msg);
}
export function ts(msg: string, level: number) {
if (level < 0 || level > 4) {
console.error("Invalid level. Please use level 0, 1, 2, 3, or 4.");
return;
}
const now = Date.now();
const diff = now - timestamp[level];
timestamp[level] = now;
const elapsedFromOrigin = now - tsOrigin;
console.log(`L${level} (${elapsedFromOrigin}ms) ${diff}ms: ${msg}`);
}
export class CustomMutationObserver {
private originalCallback: MutationCallback;
private observer: MutationObserver | null;

View File

@@ -1,5 +1,5 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ColorMaster } from "colormaster";
import { ColorMaster } from "@zsviczian/colormaster";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { DynamicStyle } from "src/types/types";
@@ -74,6 +74,7 @@ export const setDynamicStyle = (
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
const styleObject:{[x: string]: string;} = {
['backgroundColor']: str(cmBG()),
[`--color-primary`]: str(accent()),
[`--color-surface-low`]: str(gray1()),
[`--color-surface-mid`]: str(gray1()),

View File

@@ -0,0 +1,45 @@
import { ExcalidrawArrowElement, ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
export function updateElementIdsInScene(
{elements: sceneElements}: {elements: Mutable<ExcalidrawElement>[]},
elementToChange: Mutable<ExcalidrawElement>,
newID: string
) {
if(elementToChange.type === "text") {
const textElement = elementToChange as Mutable<ExcalidrawTextElement>;
if(textElement.containerId) {
const containerEl = sceneElements.find(el=>el.id === textElement.containerId) as unknown as Mutable<ExcalidrawElement>;
containerEl.boundElements?.filter(x=>x.id === textElement.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
}
}
if(elementToChange.boundElements?.length>0) {
elementToChange.boundElements.forEach( binding => {
const boundEl = sceneElements.find(el=>el.id === binding.id) as unknown as Mutable<ExcalidrawElement>;
boundEl.boundElements?.filter(x=>x.id === elementToChange.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
if(boundEl.type === "arrow") {
const arrow = boundEl as Mutable<ExcalidrawArrowElement>;
if(arrow.startBinding?.elementId === elementToChange.id) {
arrow.startBinding.elementId = newID;
}
if(arrow.endBinding?.elementId === elementToChange.id) {
arrow.endBinding.elementId = newID;
}
}
});
}
if(elementToChange.type === "frame") {
sceneElements.filter(el=>el.frameId === elementToChange.id).forEach(x => {
(x.frameId as Mutable<string>) = newID;
});
}
elementToChange.id = newID;
}

View File

@@ -2,10 +2,10 @@
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, getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData";
import ExcalidrawView from "src/ExcalidrawView";
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getLinkParts } from "./Utils";
import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./Utils";
import { cleanSectionHeading } from "./ObsidianUtils";
import { getEA } from "src";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
@@ -18,8 +18,9 @@ export async function insertImageToView(
position: { x: number, y: number },
file: TFile | string,
scale?: boolean,
shouldInsertToView: boolean = true,
):Promise<string> {
ea.clear();
if(shouldInsertToView) {ea.clear();}
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
const api = ea.getExcalidrawAPI();
@@ -30,7 +31,7 @@ export async function insertImageToView(
file,
scale,
);
await ea.addElementsToView(false, true, true);
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
return id;
}
@@ -39,12 +40,13 @@ export async function insertEmbeddableToView (
position: { x: number, y: number },
file?: TFile,
link?: string,
shouldInsertToView: boolean = true,
):Promise<string> {
ea.clear();
if(shouldInsertToView) {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, link??file);
return await insertImageToView(ea, position, link??file, undefined, shouldInsertToView);
} else {
const id = ea.addEmbeddable(
position.x,
@@ -54,7 +56,7 @@ export async function insertEmbeddableToView (
link,
file,
);
await ea.addElementsToView(false, true, true);
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
return id;
}
}
@@ -72,24 +74,26 @@ export function getLinkTextFromLink (text: string): string {
return linktext;
}
export function openTagSearch (link:string, app: App, view?: ExcalidrawView) {
const tags = link
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
.next();
if (!tags.value || tags.value.length < 2) {
export function openTagSearch(link: string, app: App, view?: ExcalidrawView) {
const tags = REGEX_TAGS.getResList(link);
if (!tags.length || !tags[0].value || tags[0].value.length < 2) {
return;
}
const search = app.workspace.getLeavesOfType("search");
if (search.length == 0) {
if (search.length === 0) {
return;
}
//@ts-ignore
search[0].view.setQuery(`tag:${tags.value[1]}`);
search[0].view.setQuery(`tag:${tags[0].value[1]}`);
app.workspace.revealLeaf(search[0]);
if (view && view.isFullscreen()) {
view.exitFullscreen();
}
return;
}
@@ -367,6 +371,9 @@ export function isTextImageTransclusion (
const link = match.value[1] ?? match.value[2];
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
if(view.file === file) {
if(link?.split("#")[1] && !isImagePartRef(getEmbeddedFilenameParts(link))) {
return false;
}
new Notice(t("RECURSIVE_INSERT_ERROR"));
return false;
}

View File

@@ -291,9 +291,7 @@ export const blobToBase64 = async (blob: Blob): Promise<string> => {
}
export const getPDFDoc = async (f: TFile): Promise<any> => {
//@ts-ignore
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
//@ts-ignore
return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
}

View File

@@ -253,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);
@@ -275,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) => {
@@ -308,7 +308,9 @@ 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;

View File

@@ -1,3 +1,4 @@
import { Modifier } from "obsidian";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawSettings } from "src/settings";
export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: boolean};
@@ -177,4 +178,26 @@ export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => {
export const anyModifierKeysPressed = (e: ModifierKeys): boolean => {
return e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
}
export function modifierLabel(modifiers: Modifier[], platform?: "Mac" | "Other"): string {
const isMacPlatform = platform === "Mac" ||
(platform === undefined && (DEVICE.isIOS || DEVICE.isMacOS));
return modifiers.map(modifier => {
switch (modifier) {
case "Mod":
return isMacPlatform ? "CMD" : "CTRL";
case "Ctrl":
return "CTRL";
case "Meta":
return isMacPlatform ? "CMD" : "WIN";
case "Shift":
return "SHIFT";
case "Alt":
return isMacPlatform ? "OPTION" : "ALT";
default:
return modifier;
}
}).join("+");
}

View File

@@ -409,4 +409,4 @@ export async function closeLeafView(leaf: WorkspaceLeaf) {
type: "empty",
state: {},
});
}
}

View File

@@ -28,6 +28,7 @@ import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./Obsidia
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
import { CropImage } from "./CropImage";
import opentype from 'opentype.js';
import { runCompressionWorker } from "src/workers/compression-worker";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -528,12 +529,37 @@ export function getLinkParts (fname: string, file?: TFile): LinkParts {
};
};
export async function compressAsync (data: string): Promise<string> {
return await runCompressionWorker(data, "compress");
}
export function compress (data: string): string {
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
result += compressed.slice(i, i + chunkSize) + '\n\n';
}
return result.trim();
};
export function decompress (data: string): string {
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
export async function decompressAsync (data: string): Promise<string> {
return await runCompressionWorker(data, "decompress");
};
export function decompress (data: string, isAsync:boolean = false): string {
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\n' && char !== '\r') {
cleanedData += char;
}
}
return LZString.decompressFromBase64(cleanedData);
};
export function isMaskFile (
@@ -747,6 +773,10 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
}
}
export function isImagePartRef (parts: FILENAMEPARTS): boolean {
return (parts.hasGroupref || parts.hasArearef || parts.hasFrameref || parts.hasClippedFrameref);
}
export function fragWithHTML (html: string) {
return createFragment((frag) => (frag.createDiv().innerHTML = html));
}
@@ -922,3 +952,20 @@ export async function getFontMetrics(fontUrl: string, name: string): Promise<Fon
return null;
}
}
// Thanks https://stackoverflow.com/a/54555834
export function cropCanvas(
srcCanvas: HTMLCanvasElement,
crop: { left: number, top: number, width: number, height: number },
output: { width: number, height: number } = { width: crop.width, height: crop.height })
{
const dstCanvas = createEl('canvas');
dstCanvas.width = output.width;
dstCanvas.height = output.height;
dstCanvas.getContext('2d')!.drawImage(
srcCanvas,
crop.left, crop.top, crop.width, crop.height,
0, 0, output.width, output.height
);
return dstCanvas;
}

90
src/utils/matic.ts Normal file
View File

@@ -0,0 +1,90 @@
import { THEME } from "../constants/constants";
import type { Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import type { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import type { OpenAIInput, OpenAIOutput } from "@zsviczian/excalidraw/types/excalidraw/data/ai/types";
export type MagicCacheData =
| {
status: "pending";
}
| { status: "done"; html: string }
| {
status: "error";
message?: string;
code: "ERR_GENERATION_INTERRUPTED" | string;
};
const SYSTEM_PROMPT = `You are a skilled front-end developer who builds interactive prototypes from wireframes, and is an expert at CSS Grid and Flex design.
Your role is to transform low-fidelity wireframes into working front-end HTML code.
YOU MUST FOLLOW FOLLOWING RULES:
- Use HTML, CSS, JavaScript to build a responsive, accessible, polished prototype
- Leverage Tailwind for styling and layout (import as script <script src="https://cdn.tailwindcss.com"></script>)
- Inline JavaScript when needed
- Fetch dependencies from CDNs when needed (using unpkg or skypack)
- Source images from Unsplash or create applicable placeholders
- Interpret annotations as intended vs literal UI
- Fill gaps using your expertise in UX and business logic
- generate primarily for desktop UI, but make it responsive.
- Use grid and flexbox wherever applicable.
- Convert the wireframe in its entirety, don't omit elements if possible.
If the wireframes, diagrams, or text is unclear or unreadable, refer to provided text for clarification.
Your goal is a production-ready prototype that brings the wireframes to life.
Please output JUST THE HTML file containing your best attempt at implementing the provided wireframes.`;
export async function diagramToHTML({
image,
apiKey,
text,
theme = THEME.LIGHT,
}: {
image: DataURL;
apiKey: string;
text: string;
theme?: Theme;
}) {
const body: OpenAIInput.ChatCompletionCreateParamsBase = {
model: "gpt-4-vision-preview",
// 4096 are max output tokens allowed for `gpt-4-vision-preview` currently
max_tokens: 4096,
temperature: 0.1,
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
{
role: "user",
content: [
{
type: "image_url",
image_url: {
url: image,
detail: "high",
},
},
{
type: "text",
text: `Above is the reference wireframe. Please make a new website based on these and return just the HTML file. Also, please make it for the ${theme} theme. What follows are the wireframe's text annotations (if any)...`,
},
{
type: "text",
text,
},
],
},
],
};
let result:
| ({ ok: true } & OpenAIOutput.ChatCompletion)
| ({ ok: false } & OpenAIOutput.APIError);
return await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
}

View File

@@ -0,0 +1,100 @@
function createWorkerBlob(jsCode:string) {
// Create a new Blob with the JavaScript code
const blob = new Blob([jsCode], { type: 'text/javascript' });
// Create a URL for the Blob
const url = URL.createObjectURL(blob);
return url;
}
const workerCode = `
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
self.onmessage = function(e) {
const { data, action } = e.data;
try {
switch (action) {
case 'compress':
if (!data) throw new Error("No input string provided for compression.");
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
result += compressed.slice(i, i + chunkSize) + '\\n\\n';
}
self.postMessage({ compressed: result.trim() });
break;
case 'decompress':
if (!data) throw new Error("No input string provided for decompression.");
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\\n' && char !== '\\r') {
cleanedData += char;
}
}
const decompressed = LZString.decompressFromBase64(cleanedData);
self.postMessage({ decompressed });
break;
default:
throw new Error("Unknown action.");
}
} catch (error) {
// Post the error message back to the main thread
self.postMessage({ error: error.message });
}
};
`;
let worker:Worker | null = null;
export function initCompressionWorker() {
if(!worker) {
worker = new Worker(createWorkerBlob(workerCode));
}
}
export async function runCompressionWorker(data:string, action: 'compress' | 'decompress'): Promise<string> {
return new Promise((resolve, reject) => {
worker.onmessage = function(e) {
const { compressed, decompressed, error } = e.data;
if (error) {
reject(new Error(error));
} else if (compressed || decompressed) {
resolve(compressed || decompressed);
} else {
reject(new Error('Unexpected response from worker'));
}
};
// Set up the worker's error handler
worker.onerror = function(error) {
reject(new Error(error.message));
};
// Post the message to the worker
worker.postMessage({ data, action });
});
}
export function terminateCompressionWorker() {
worker.terminate();
worker = null;
}
export let IS_WORKER_SUPPORTED = false;
function canCreateWorkerFromBlob() {
try {
const blob = new Blob(["self.onmessage = function() {}"]);
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.terminate();
URL.revokeObjectURL(url);
IS_WORKER_SUPPORTED = true;
} catch (e) {
IS_WORKER_SUPPORTED = false;
}
}
canCreateWorkerFromBlob();

View File

@@ -625,4 +625,8 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.excalidraw-rank svg {
height: 8rem;
width: 8rem;
}
.excalidraw .color-picker-content input[type="color"] {
filter: var(--theme-filter);
}

View File

@@ -2,9 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "ES2015",
"target": "es2018", //es2017 because script engine requires for async execution
"allowJs": true,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
@@ -13,8 +13,7 @@
"lib": [
"dom",
"scripthost",
"es2015",
"esnext",
"es2022",
"DOM.Iterable"
],
"jsx": "react",

View File

@@ -2,9 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "es2015",
"target": "es2018", //es2017 because script engine requires for async execution //es2018 for named capture groups
"allowJs": true,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
@@ -13,8 +13,7 @@
"lib": [
"dom",
"scripthost",
"es2015",
"ESNext",
"es2022",
"DOM.Iterable"
],
"jsx": "react",