Compare commits

..

32 Commits

Author SHA1 Message Date
Zsolt Viczian
21ff1833a8 1.2.10 2021-08-01 19:32:37 +02:00
Zsolt Viczian
1b931abd38 changed to my own Excalidraw package 2021-07-31 19:21:06 +02:00
zsviczian
fe28098776 1.2.9 release 2021-07-23 20:10:44 +02:00
zsviczian
72c158e08b save.settings 2021-07-23 19:13:52 +02:00
Zsolt Viczian
5133ab028b style changes context menu 2021-07-14 21:26:47 +02:00
Zsolt Viczian
5fc0f70ded 1.2.8 2021-07-14 20:47:23 +02:00
Zsolt Viczian
08f489f1c5 1.2.8 autosave safeguard, merged file error 2021-07-14 20:04:08 +02:00
zsviczian
dd476b564a workaround to solve issue #94
trim part after final closing } curly bracket
2021-07-14 11:55:40 +02:00
zsviczian
b7a7c9473e dirty changed to file.path
to ensure the plugin won't accidentally overwrite the next file
2021-07-14 11:29:30 +02:00
Zsolt Viczian
cc7dc16810 1.2.7 autosave+template warning 2021-07-13 22:47:38 +02:00
Zsolt Viczian
c45faef141 1.2.6 2021-07-12 23:07:40 +02:00
Zsolt Viczian
ae31ad0870 1.2.5 fame update for .png and .svg when migrating 2021-07-12 07:03:31 +02:00
Zsolt Viczian
ba88ced2ba 1.2.4 2021-07-11 23:28:46 +02:00
Zsolt Viczian
803fb9e234 1.2.3 2021-07-11 08:42:29 +02:00
Zsolt Viczian
d77249088f 1.2.2 text lock/unlock solved 2021-07-10 23:32:25 +02:00
Zsolt Viczian
e6ad7aa304 Excalidraw 0.9 2021-07-10 20:45:09 +02:00
Zsolt Viczian
fdb71a0d03 JSON into codeblock, hover observer corrected 2021-07-10 17:11:02 +02:00
Zsolt Viczian
90c55211ba 1.2.0 2021-07-10 14:45:00 +02:00
zsviczian
8d2c064ee3 Update README1.2.md 2021-07-10 14:31:03 +02:00
Zsolt Viczian
b8a95392f1 readme 1.2 - draft 2021-07-10 14:20:20 +02:00
zsviczian
5430bc4b38 Update README1.2.md 2021-07-10 13:20:30 +02:00
Zsolt Viczian
c9d12c7295 readme 1.2 2021-07-10 12:26:44 +02:00
Zsolt Viczian
c37cbd7e31 improved hover preivew, experimental file tagging 2021-07-10 11:59:18 +02:00
Zsolt Viczian
fbc342189b getScene() returns JSON object not string 2021-07-09 20:28:06 +02:00
Zsolt Viczian
454c68b4b9 1.2.0-beta-1 2021-07-08 22:59:12 +02:00
Zsolt Viczian
09889d7ed3 1.2.0-alpha-4 2021-07-07 23:03:36 +02:00
Zsolt Viczian
096efc45d7 Added migration modal window to help with conversion 2021-07-06 22:16:51 +02:00
Zsolt Viczian
55291d8c27 two minor errors corrected 2021-07-05 21:30:40 +02:00
Zsolt Viczian
e81787ee4b 1.2.0-alpha-3 2021-07-04 22:37:04 +02:00
Zsolt Viczian
ebcf807501 1.2.0-alpha-2 2021-07-04 12:15:42 +02:00
Zsolt Viczian
bd155eced3 language file cleanup 2021-07-04 06:59:32 +02:00
Zsolt Viczian
2b7d0d5dc2 [[ encoding (incl. migration), template link. 2021-07-03 17:02:40 +02:00
19 changed files with 1357 additions and 631 deletions

212
README.md
View File

@@ -1,185 +1,71 @@
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 and you can transclude drawings into your documents. 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.
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.
![image](https://user-images.githubusercontent.com/14358394/115983515-d06c2c80-a5a1-11eb-8d12-c7df91d18107.png)
![image](https://user-images.githubusercontent.com/14358394/125159831-336d6880-e17a-11eb-8a3d-ceabc2555a08.png)
## Important notice to the 1.1.x update!
# Important notice to the 1.2.0 update
Thank you for updating to Excalidraw 1.1.x!
This version comes with tons of new features and possibilities.
I have improved how drawings are embedded! You no longer need an Excalidraw codeblock. You can now embed drawings just like any other images: `![[my drawing.excalidraw]]` or `![[my drawing.excalidraw|500|left]]` or `![[my drawing.excalidraw|right-wrap]]`, `![alttext|500|right](drawing.excalidraw)`, `![](folder/drawing.excalidraw)`, etc. You get the idea.
Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. If you want, you can also continue to use your exisiting drawings in compatibility mode (e.g. if you use Logseq and Obsidian in parallel). During conversion your existing `*.excalidraw` files will be replaced with new `*.excalidraw.md` files.
### Detailed release notes are under the How to videos.
## Conversion and compatibility
To convert files you have the following options:
- Click `CONVERT FILES` in the migration dialog when installing 1.2.0
- In the Command Palette select `Excalidraw: Convert *.excalidraw files to *.excalidraw.md files` to convert all `*.excalidraw` files to `*.excalidraw.md` files.
- To convert files individually:
- Right click an `*.excalidraw` file in File Explorer and select one of the following options:
- `*.excalidraw => *.excalidraw.md`
- `*.excalidraw => *.md (Logseq compatibility)`: This option will retain the original *.excalidraw file next to the new Obsidian format. Make sure you also enable additional `Compatibility features` in `Settings` for a full solution.
- Open a legacy `*.excalidraw` file and select `Convert to new format` from the `Options Menu` in the Excalidraw view.
# Video walkthrough
| | | |
|----|----|----|
|[![Obsidian-Excalidraw 1.2.0 update - Major IMPROVEMENTS](https://user-images.githubusercontent.com/14358394/124356817-7b3f3d80-dc18-11eb-932d-363bb373c5ab.jpg)](https://youtu.be/UxJLLYtgDKE)|[![1 Getting Started](https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg)](https://youtu.be/sY4FoflGaiM)|[![2 Basic shapes and features](https://user-images.githubusercontent.com/14358394/125160312-8a743d00-e17c-11eb-9fa2-490ef4cbd59e.jpg)](https://youtu.be/Iy_oVTq12Gw)|
|[![3 Groups](https://user-images.githubusercontent.com/14358394/125160323-96f89580-e17c-11eb-9bce-8eb1067a51bb.jpg)](https://youtu.be/QOL1KF7-kdc)|[![4 Stencil](https://user-images.githubusercontent.com/14358394/125160332-9f50d080-e17c-11eb-98e9-fec60fe147d9.jpg)](https://youtu.be/aSgcbfspvfo)|[![5 embedding](https://user-images.githubusercontent.com/14358394/125160341-a546b180-e17c-11eb-9de8-d87fdc844c9c.jpg)](https://youtu.be/MaJ5jJwBRWs)|
|[![6 Links](https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg)](https://youtu.be/MXzeCOEExNo)|[![7 Markdown](https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg)](https://youtu.be/R0IAg0s-wQE)|[![8 Templates](https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg)](https://youtu.be/ibdS7ykwpW4)|
|[![9 Excalidraw Automate](https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg)](https://youtu.be/VRZVujfVab0)|[![10 Miscellaneous](https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg)](https://youtu.be/D1iBYo1_jjc)||
# Key features
- The plugin saves drawings to your vault as a file with the *.excalidraw* file extension.
- The plugin adds the following actions to the **command palette**:
- Create a new drawing
- Find and edit existing drawings in your vault,
- Transclude (embed) a drawing into a document, and
- Export a drawing as PNG or SVG.
- Insert vault internal-link into drawing
- You can also use the **file explorer** in your vault to open existing Excalidraw files.
- Use the **ribbon button** to create a new drawing, CTRL+Click to open on a new page.
- Open settings to set up
- a **default folder** for new drawings,
- a **Template** by first creating a drawing, customizing it the way you like it, and specifying the file as the template in settings,
- Excalidraw to **automatically export SVG and/or PNG** files for your drawings, and to keep those in sync with your drawing,
- default width of embedded drawings
- You can also customize the **size and position of the embedded image** using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added as style to the SVG element and the wrapper DIV element. Check below and styles.css for more insight.
- Supports hyperlinks e.g. `https://zsolt.blog` and internal links e.g. `[[My file in vault]]` in drawing text. Ctrl/meta + click on a text element.
- Square brackets can be omitted if the entire text element is an internal link. i.e. the following two text elements `Check out the [[requirements specification]]!!` and `requirements specification` will both represent a link to `requirements specification.md`.
- When files are moved/renamed in your vault, text elements that are recognized links will also get updated. Check corresponding setting.
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Read detailed help + examples: [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Temporary hack/workaround to enable Obsidian Sync for Excalidraw files. This enables almost real-time two-way sync for Excalidraw files between your devices. You can draw on your iPad with your pencil, on your Android with your stylus, and the image will be available in Obsidian on your desktop as well and vice versa.
# How to?
Part 1: Intro to Obsidian-Excalidraw - Start a new drawing (3:12)
[![Part 1: Intro to Obsidian-Excalidraw - Start a new drawing](https://user-images.githubusercontent.com/14358394/115983840-05797e80-a5a4-11eb-93cd-bae4b1973f72.jpg)](https://youtu.be/i-hIfY-Ecjg)
Part 2: Intro to Obsidian-Excalidraw - Basic features (6:06)
[![Part 2: Intro to Obsidian-Excalidraw - Basic features](https://user-images.githubusercontent.com/14358394/115983902-699c4280-a5a4-11eb-973d-2ba1bd7ac2db.jpg)](https://youtu.be/-dk7pvdl-H0)
Part 3: Intro to Obsidian-Excalidraw - Advanced features (3:26)
[![Part 3: Intro to Obsidian-Excalidraw - Advanced features](https://user-images.githubusercontent.com/14358394/115983916-7de03f80-a5a4-11eb-8f36-4ad516ef9e80.jpg)](https://youtu.be/2cKlEwo8WU0)
Part 4: Intro to Obsidian-Excalidraw - Setting up a template (1:45)
[![Part 4: Intro to Obsidian-Excalidraw - Setting up a template](https://user-images.githubusercontent.com/14358394/115983929-92bcd300-a5a4-11eb-9d4f-03e5cb9e3ebf.jpg)](https://youtu.be/oNPYZEpmuJ8)
Part 5: Intro to Obsidian-Excalidraw - Stencil Library (3:16)
[![Part 5: Intro to Obsidian-Excalidraw - Stencil Library](https://user-images.githubusercontent.com/14358394/115983944-a8ca9380-a5a4-11eb-8a69-e74ae00d95be.jpg)](https://youtu.be/rLx-9FvlzgI)
Part 6: Intro to Obsidian-Excalidraw: Embedding drawings (2:08)
[![Part 6: Intro to Obsidian-Excalidraw: Embedding drawings](https://user-images.githubusercontent.com/14358394/115983954-bbdd6380-a5a4-11eb-9243-f0151451afcd.jpg)](https://youtu.be/JQeJ-Hh-xAI)
# Release Notes
## 1.1.10
- When you CTRL-Click a grouped collection of objects Excalidraw will open the page based on the embedded text.
- I added a setting to disable the CTRL-click functionality should it interfere with default Excalidraw behavior for you. In my experience double-clicking achieves the same outcome as a CTRL-click on an element in a grouped collection of objects, but if you use the CTRL-click feature to select an element of a group frequently, and find the "CTRL-click to open a link" feature annoying, you can now disable it.
## 1.1.9
- I modified the behavior of Excalidraw text element links.
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
- CTRL+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
- Settings will allow you to customzie Excalidraw to your needs:
- Default folder for new drawings and define custom filename pattern for new drawings.
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- If portability is important to you: Auto-export SVG and/or PNG files including keep-in-sync feature so you can embed svg/png into your documents instead of embedding excalidraw files.
- Specify the default width of embedded drawings.
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy .excalidraw files.
- Experimental feature to add custom TAG to file expolorer to mark drawing files.
- Enable / disable autosave.
- You can customize the size and position of the embedded images using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element. Check below and styles.css for more insight.
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enalbled.
- Links in drawings will show up in backlinks of documents
- Transclusions are supported i.e. `![[myfile#^blockref]]` will convert in the drawing into the transcluded text
- For convenience you can also use the command palette to insert links into drawings
- CTRL/META + CLICK a text element to open it as a link.
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
- CTRL/META + SHIFT + CLICK to open the file in a new pane
- CTRL/META + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
- I added a setting to limit link functionality to `[[valid Obsidian links]]` only. By default, the full text of a text element is treated as a link unless it contains a `[[valid internal link]]`, in which case only the `[[internal link]]` is used. The new setting may be beneficial if you want to avoid unexpected updates to text in your drawings. This may happen if a text element in a drawing accidentally matches a file in your vault, and you happen to rename or move that file. By limiting the link behavior to `[[valid internal links]]` only, these accidental matches can be avoided. This is not frequent but happened to me recently.
- LaTeX symbol support. I resolved issue [#75](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/75) by adding a new command palette option ("Insert LaTeX-symbol") to insert an expression containing a LaTeX symbol or a simple formula. Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
## 1.1.8
- Improvements to links
- You can now use square brackets to denote links. i.e. the text element `Which are my [[favorite books]]?` will be a link to `favorite books.md`.
- Square brackets can still be omitted if the entire text element is an internal link. i.e. the following two text elements `Check out the [[requirements specification]]!!` and `requirements specification` will both represent a link to `requirements specification.md`.
- When files are moved/renamed in your vault, text elements that are recognized links will also get updated in your drawings.
- I added a new command palette option to insert an internal link into a file in your vault to the active drawing. While a drawing is open press ctrl/cmd+p and select `Excalidraw: Insert link to file`.
- I Added CTRL/CMD + hover quick preview for Excalidraw files
[![Obsidian-Excalidraw 1.1.8 - Links enhanced](https://user-images.githubusercontent.com/14358394/120925953-31c40700-c6db-11eb-904d-65300e91815e.jpg)](https://youtu.be/qT_NQAojkzg)
## 1.1.6
[![Obsidian-Excalidraw 1.1.6 - Links](https://user-images.githubusercontent.com/14358394/119559279-bdb46580-bda2-11eb-88cb-7614dc452034.jpg)](https://youtu.be/FDsMH-aLw_I)
## 1.1.5
- The template will now restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- Added settings to customize the autogenerated filename
- Minor fixes for occasional console.log errors.
## 1.1.0
- ALT+Enter and CTRL+ALT+Enter on the filename in edit mode will open up the Excalidraw editor. Click and CTRL+Click on the image in preview mode will also bring up the Excalidraw editor as expected.
- I have also added two new Command Palette commands. Both create a new drawing and immediately embed it in the document you are editing, one will open the drawing in a new workspace pane, the other within the currently active pane.
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian)
In a nice collaboration with Ozan, his Image in Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode.
### MIGRATION to 1.1.0
I have added a Migration command to the Command Palette. When you select this, the program will run a search and replace for all the excalidraw codeblocks in your vault and will convert them to the new format.
## 1.0.12 Freehand drawing
- now includes the new freehand drawing features from Excalidraw.com
- If you use Obsydian sync with Excalidraw sync, be sure to update all your devices to the new version, as the old excalidraw will simply delete the freehand drawn images and/or simply not show the drawing.
### Temporary workaround - use it only if you are ok with hacky solutions
- I implemented a temporary workaround to enable Obsidian Sync for Excalidraw files. This enables almost real-time two-way sync between your devices. You can draw on your iPad with your pencil, on your Android with your stylus, and the image will be available in Obsidian as well and vice versa.
- By enabling this feature Excalidraw will sync drawings to a sync folder where drawings are stored in an ".md" file. This will allow Obsidian sync to synchronize Excalidraw drawings as well... Whenever your drawing changes, the corresponding file in the sync folder will also get updated. Similarly, whenever a file is synchronized to the sync folder by Obsidian sync, Excalidraw will sync it with the .excalidraw file in your vault.
- Because this is a temporary workaround until Obsidian sync is ready, I didn't implement extensive application logic to manage sync. Sync might get confused requiring some manual intervention.
### QoL improvement
- I added an autosave feature. Your active drawing gets saved every 30 seconds if you've made changes to it. Drawings otherwise get saved when the window loses focus, or when you close the drawing, etc. Autosave limits the risk of accidental data loss on mobiles when you "swipe out" Obsidian to close it.
## 1.0.10
[![Obsidian-Excalidraw 1.0.10 update](https://user-images.githubusercontent.com/14358394/117579017-60a58800-b0f1-11eb-8553-7820964662aa.jpg)](https://youtu.be/W7pWXGIe4rQ)
## 1.0.8 and 1.0.9 (minor fixes)
[![Obsidian-Excalidraw 1.0.8 update](https://user-images.githubusercontent.com/14358394/117492534-029e6680-af72-11eb-90a3-086e67e70c1c.jpg)](https://youtu.be/AtEhmHJjnxM)
### QoL improvements
- Adds context menu to File Explorer to create new drawings
- Adds a new command to the palette: “Transclude (embed) the most recently edited Excalidraw drawing”
- Automatically update file-links in transclusions when you rename or move your drawing
- Saves drawing and updates all active pre-views when drawing loses focus
- File is closed and removed when you select “Delete file” from more options
- Saves drawing when exiting Obsidian
- Fixes pen positioning bug with sliding panes after panes scroll
### ExcalidrawAutomte full Templater and DataviewJS support
You now have ultimate flexibility over your Excalidraw templates using Templater and Dataview.
- Detailed documentation available [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- I created few examples from the simple to the more complex
- Simple use-case: Creating a drawing using a custom template and following a file and folder naming convention of your choice.
- Complex use-case: Create a mindmap from a tabulated outline.
![Drawing 2021-05-05 20 52 34](https://user-images.githubusercontent.com/14358394/117194124-00a69d00-ade4-11eb-8b75-5e18a9cbc3cd.png)
## 1.0.6 and 1.0.7
[![1.0.6 Update](https://user-images.githubusercontent.com/14358394/116312909-58725200-a7ad-11eb-89b9-c67cb48ffebb.jpg)](https://youtu.be/ipZPbcP2B0M)
### SVG styling when embedding
- 1.0.7 adds further flexibility to styling
- new formatting option for the code block embedding
- Valid values: `left`, `right`, `left-wrap`, `right-wrap`... but anything after the last `|` character will be added to the class of the SVG element and the wrapper DIV element.
Here is the corresponding CSS:
```css
img.excalidraw-svg-right-wrap {
float: right;
margin: 0px 0px 20px 20px;
}
img.excalidraw-svg-left-wrap {
float: left;
margin: 0px 35px 20px 0px;
}
img.excalidraw-svg-right {
float: right;
}
img.excalidraw-svg-left {
float: left;
}
div.excalidraw-svg-right,
div.excalidraw-svg-left {
display: table;
width: 100%;
}
```
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
- Since 1.2.0 Drawing files are stored in Markdown files
- You can add tags to drawings
- You can add metadata to the YAML front matter of drawings
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
- Excalidraw documents now show in graph view.
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
# Known issues
- I have seen two cases when adding a stencil library did not work. In both cases, the end solution was a reinstall of Obsidian. The root cause is not clear, but maybe because of the incremental updates of Obsidian from an early version.
- Mobile support
- Positioning of the pen gets misaligned after you open the command palette.
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
### Resolved known issues:
- Resolved with 1.0.10 Temporary workaround:
- Sync does not support .excalidraw files. This issue will be addressed in a later release of Obsidian sync. Until then, you can use my temporary workaround.
- Resolved with Obsidian mobile 0.18:
- On mobile (iOS and Android): As you draw left to right it opens left sidebar. Draw right to left, opens right sidebar. Draw down, opens commands palette. So seems open is emulating the gestures, even when drawing towards the center.
# Tips and tricks
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode.
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode. Note that Ozan's plugin will only display Excalidraw drawings if the link ends with `.md` or `.excalidraw`. i.e. the following drawing will show in Edit Mode `![[My Drawing.md]]`, but wiki links such as `[[My Drawing]]` will not.
# Feedback, questions, ideas, problems
Join the conversation about the Excalidraw plugin on [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian)

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,21 @@
import ExcalidrawPlugin from "./main";
import {
ExcalidrawElement,
FillStyle,
StrokeStyle,
StrokeSharpness,
FontFamily,
} from "@excalidraw/excalidraw/types/element/types";
} from "@zsviczian/excalidraw/types/element/types";
import {
normalizePath,
Notice,
TFile
} from "obsidian"
import ExcalidrawView from "./ExcalidrawView";
import { getJSON } from "./ExcalidrawData";
import { CASCADIA_FONT, FRONTMATTER, nanoid, VIRGIL_FONT } from "./constants";
import {
FRONTMATTER,
nanoid,
JSON_parse
} from "./constants";
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
@@ -31,7 +34,7 @@ export interface ExcalidrawAutomate extends Window {
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: FontFamily;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
@@ -171,7 +174,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
plugin.createDrawing(
params?.filename ? params.filename + '.excalidraw' : this.plugin.getNextDefaultFilename(),
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
params?.onNewPane ? params.onNewPane : false,
params?.foldername ? params.foldername : this.plugin.settings.folder,
FRONTMATTER + exportSceneToMD(
@@ -207,8 +210,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
for (let i=0;i<this.elementIds.length;i++) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getSVG(
JSON.stringify({
return await ExcalidrawView.getSVG(
{//createDrawing
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
@@ -217,21 +220,21 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
"theme": template ? template.appState.theme : this.canvas.theme,
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
}
}),
},//),
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme
}
)
},
async createPNG(templatePath?:string) {
async createPNG(templatePath?:string, scale:number=1) {
const template = templatePath ? (await getTemplate(templatePath)) : null;
let elements = template ? template.elements : [];
for (let i=0;i<this.elementIds.length;i++) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getPNG(
JSON.stringify({
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
@@ -240,11 +243,12 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
"theme": template ? template.appState.theme : this.canvas.theme,
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
}
}),
},
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme
}
},
scale
)
},
addRect(topX:number, topY:number, width:number, height:number):string {
@@ -373,7 +377,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
this.canvas.theme = "light";
this.canvas.viewBackgroundColor="#FFFFFF";
},
isExcalidrawFile(f:TFile) {
isExcalidrawFile(f:TFile):boolean {
return this.plugin.isExcalidrawFile(f);
}
@@ -440,7 +444,6 @@ async function initFonts () {
for (let i=1;i<=3;i++) {
await (document as any).fonts.load('20px ' + getFontFamily(i));
}
console.log("Fonts Ready");
}
export function measureText (newText:string, fontSize:number, fontFamily:number) {
@@ -477,7 +480,7 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
if(file && file instanceof TFile) {
const data = await vault.read(file);
const excalidrawData = JSON.parse(getJSON(data));
const excalidrawData = JSON_parse(getJSON(data));
return {
elements: excalidrawData.elements,
appState: excalidrawData.appState,
@@ -496,7 +499,7 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
*/
export function exportSceneToMD(data:string): string {
if(!data) return "";
const excalidrawData = JSON.parse(data);
const excalidrawData = JSON_parse(data);
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
let outString = '# Text Elements\n';
let id:string;
@@ -511,5 +514,8 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + '# Drawing\n'+ data;
return outString + '# Drawing\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
+ data + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
}

View File

@@ -1,19 +1,32 @@
import { App, TFile } from "obsidian";
import { nanoid} from "./constants";
import { App, normalizePath, TFile } from "obsidian";
import {
nanoid,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
} from "./constants";
import { measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { ExcalidrawSettings } from "./settings";
import {
JSON_parse
} from "./constants";
import { ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
import { rawListeners } from "process";
import { TextMode } from "./ExcalidrawView";
import { link } from "fs";
const DRAWING_REG = /\n# Drawing\n(```json\n)?(.*)(```)?/gm;
//![[link|alias]]![alias](link)
//1 2 3 4 5 6
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
export function getJSON(data:string):string {
const findJSON = /\n# Drawing\n(.*)/gm
const res = data.matchAll(findJSON);
const res = data.matchAll(DRAWING_REG);
const parts = res.next();
if(parts.value && parts.value.length>1) {
return parts.value[1];
const result = parts.value[2];
return result.substr(0,result.lastIndexOf("}")+1); //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
}
return data;
}
@@ -25,9 +38,8 @@ export class ExcalidrawData {
private settings:ExcalidrawSettings;
private app:App;
private showLinkBrackets: boolean;
private linkIndicator: string;
private allowParse: boolean = false;
private linkPrefix: string;
private textMode: TextMode = TextMode.raw;
constructor(plugin: ExcalidrawPlugin) {
this.settings = plugin.settings;
this.app = plugin.app;
@@ -38,22 +50,33 @@ export class ExcalidrawData {
* @param {TFile} file - the MD file containing the Excalidraw drawing
* @returns {boolean} - true if file was loaded, false if there was an error
*/
public async loadData(data: string,file: TFile, allowParse:boolean):Promise<boolean> {
//console.log("Excalidraw.Data.loadData()",{data:data,allowParse:allowParse,file:file});
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
this.showLinkBrackets = this.settings.showLinkBrackets;
this.linkIndicator = this.settings.linkIndicator;
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
this.setShowLinkBrackets();
this.setLinkPrefix();
//Load scene: Read the JSON string after "# Drawing"
this.scene = null;
let parts = data.matchAll(/\n# Drawing\n(.*)/gm).next();
if (this.settings.syncExcalidraw) {
const excalfile = file.path.substring(0,file.path.lastIndexOf('.md')) + '.excalidraw';
const f = this.app.vault.getAbstractFileByPath(excalfile);
if(f && f instanceof TFile && f.stat.mtime>file.stat.mtime) { //the .excalidraw file is newer then the .md file
const d = await this.app.vault.read(f);
this.scene = JSON.parse(d);
}
}
//Load scene: Read the JSON string after "# Drawing"
let parts = data.matchAll(DRAWING_REG).next();
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
this.scene = JSON.parse(parts.value[1]);
if(!this.scene) { //scene was not loaded from .excalidraw
const scene = parts.value[2];
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
}
//Trim data to remove the JSON string
data = data.substring(0,parts.value.index);
@@ -75,17 +98,34 @@ export class ExcalidrawData {
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
//e.g. if the entire text elements section was deleted.
this.findNewTextElementsInScene();
await this.setAllowParse(allowParse,true);
await this.setTextMode(textMode,true);
return true;
}
public async setAllowParse(allowParse:boolean,forceupdate:boolean=false) {
this.allowParse = allowParse;
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
this.setShowLinkBrackets();
this.setLinkPrefix();
this.scene = JSON.parse(data);
this.findNewTextElementsInScene();
await this.setTextMode(TextMode.raw,true);
return true;
}
public async setTextMode(textMode:TextMode,forceupdate:boolean=false) {
this.textMode = textMode;
await this.updateSceneTextElements(forceupdate);
}
/**
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
* Depending on textMode, TextElements will receive their raw or parsed values
* @param forceupdate : will update text elements even if text contents has not changed, this will
* correct sizing issues
*/
private async updateSceneTextElements(forceupdate:boolean=false) {
//console.log("Excalidraw.Data.updateSceneTextElements(), forceupdate",forceupdate);
//update a single text element in the scene if the newText is different
const update = (sceneTextElement:any, newText:string) => {
if(forceupdate || newText!=sceneTextElement.text) {
@@ -106,8 +146,8 @@ export class ExcalidrawData {
}
private async getText(id:string):Promise<string> {
if (this.allowParse) {
if(!this.textElements.get(id)?.parsed) {
if (this.textMode == TextMode.parsed) {
if(!this.textElements.get(id).parsed) {
const raw = this.textElements.get(id).raw;
this.textElements.set(id,{raw:raw, parsed: await this.parse(raw)})
}
@@ -141,14 +181,19 @@ export class ExcalidrawData {
id=nanoid();
jsonString = jsonString.replaceAll(te.id,id); //brute force approach to replace all occurances (e.g. links, groups,etc.)
}
if(!this.textElements.has(id)) {
if(te.id.length > 8 && this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit
const element = this.textElements.get(te.id);
this.textElements.set(id,{raw: element.raw, parsed: element.parsed})
this.textElements.delete(te.id);
dirty = true;
} else if(!this.textElements.has(id)) {
dirty = true;
this.textElements.set(id,{raw: te.text, parsed: null});
this.parseasync(id,te.text);
}
}
if(dirty) { //reload scene json in case it has changed
this.scene = JSON.parse(jsonString);
this.scene = JSON_parse(jsonString);
}
return dirty;
@@ -194,7 +239,7 @@ export class ExcalidrawData {
this.textElements.set(key,{raw: el[0].text,parsed: null});
this.parseasync(key,el[0].text);
} else {
const text = this.allowParse ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
const text = (this.textMode == TextMode.parsed) ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
if(text != el[0].text) {
this.textElements.set(key,{raw: el[0].text,parsed: null});
this.parseasync(key,el[0].text);
@@ -208,6 +253,22 @@ export class ExcalidrawData {
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
}
private parseLinks(text:string, position:number, parts:any):string {
let outString = null;
if (parts.value[2]) {
outString = text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
} else {
outString = text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
}
return outString;
}
/**
* Process aliases and block embeds
* @param text
@@ -219,11 +280,11 @@ export class ExcalidrawData {
//1 2
const REG_FILE_BLOCKREF = /(.*)#\^(.*)/g;
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
if(!parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
if(parts.done || !parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
const contents = await this.app.vault.cachedRead(file);
//get transcluded line and take the part before ^blockref
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2] + "\\n");
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2]);
const res = contents.match(REG_TRANSCLUDE);
if(res) return res[1];
return text;//if blockref not found in file, return the input string
@@ -238,29 +299,62 @@ export class ExcalidrawData {
if (parts.value[1] || parts.value[4]) { //transclusion
outString += text.substring(position,parts.value.index) +
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
} else if (parts.value[2]) {
linkIcon = true;
outString += text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
} else {
const parsedLink = this.parseLinks(text,position,parts);
if(parsedLink) {
linkIcon = true;
outString += parsedLink;
}
}
position = parts.value.index + parts.value[0].length;
}
outString += text.substring(position,text.length);
if (linkIcon) {
outString = this.linkPrefix + outString;
}
return outString;
}
/**
* Does a quick parse of the raw text. Returns the parsed string if raw text does not include a transclusion.
* Return null if raw text includes a transclusion.
* This is implemented in a separate function, because by nature resolving a transclusion is an asynchronious
* activity. Quick parse gets the job done synchronously if possible.
* @param text
*/
private quickParse(text:string):string {
const hasTransclusion = (text:string):boolean => {
const res = text.matchAll(REG_LINK_BACKETS);
let parts;
while(!(parts=res.next()).done) {
if (parts.value[1] || parts.value[4]) return true;
}
return false;
}
if (hasTransclusion(text)) return null;
let outString = "";
let position = 0;
const res = text.matchAll(REG_LINK_BACKETS);
let linkIcon = false;
let parts;
while(!(parts=res.next()).done) {
const parsedLink = this.parseLinks(text,position,parts);
if(parsedLink) {
linkIcon = true;
outString += text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
outString += parsedLink;
}
position = parts.value.index + parts.value[0].length;
}
outString += text.substring(position,text.length);
if (linkIcon) {
outString = this.linkIndicator + outString;
outString = this.linkPrefix + outString;
}
return outString;
}
/**
* Generate markdown file representation of excalidraw drawing
* @returns markdown string
@@ -271,21 +365,24 @@ export class ExcalidrawData {
for(const key of this.textElements.keys()){
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
}
return outString + '# Drawing\n' + JSON.stringify(this.scene);
return outString + '# Drawing\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
+ JSON.stringify(this.scene) + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
}
public syncElements(newScene:any):boolean {
//console.log("Excalidraw.Data.syncElements()");
this.scene = JSON.parse(newScene);
const result = this.findNewTextElementsInScene();
this.scene = newScene;//JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
this.updateTextElementsFromSceneRawOnly();
return result;
}
public async updateScene(newScene:any){
//console.log("Excalidraw.Data.updateScene()");
this.scene = JSON.parse(newScene);
const result = this.findNewTextElementsInScene();
this.scene = JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
await this.updateTextElementsFromScene();
if(result) {
await this.updateSceneTextElements();
@@ -297,5 +394,51 @@ export class ExcalidrawData {
public getRawText(id:string) {
return this.textElements.get(id)?.raw;
}
public getParsedText(id:string):string {
return this.textElements.get(id)?.parsed;
}
}
public setTextElement(element:ExcalidrawTextElement, rawText:string, updateScene:Function):string {
const parseResult = this.quickParse(rawText); //will return the parsed result if raw text does not include transclusion
if(parseResult) { //No transclusion
this.textElements.set(element.id,{raw: rawText,parsed: parseResult});
return parseResult;
}
//transclusion needs to be resolved asynchornously
this.parse(rawText).then((parsedText:string)=> {
this.textElements.set(element.id,{raw: rawText,parsed: parsedText});
if(parsedText && element.text!=rawText) {
updateScene();
}
});
return null;
}
public deleteTextElement(id:string) {
this.textElements.delete(id);
}
private setLinkPrefix():boolean {
const linkPrefix = this.linkPrefix;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX]!=null) {
this.linkPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX];
} else {
this.linkPrefix = this.settings.linkPrefix;
}
return linkPrefix != this.linkPrefix;
}
private setShowLinkBrackets():boolean {
const showLinkBrackets = this.showLinkBrackets;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=null) {
this.showLinkBrackets=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=false;
} else {
this.showLinkBrackets = this.settings.showLinkBrackets;
}
return showLinkBrackets != this.showLinkBrackets;
}
}

View File

@@ -6,15 +6,16 @@ import {
WorkspaceItem,
Notice,
Menu,
TAbstractFile,
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
import {
AppState,
LibraryItems
} from "@excalidraw/excalidraw/types/types";
} from "@zsviczian/excalidraw/types/types";
import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
@@ -25,17 +26,26 @@ import {
PNG_ICON_NAME,
SVG_ICON_NAME,
FRONTMATTER_KEY,
UNLOCK_ICON_NAME,
LOCK_ICON_NAME,
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
EXIT_FULLSCREEN_ICON_NAME,
FULLSCREEN_ICON_NAME,
JSON_parse
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, randomInteger, splitFolderAndFilename } from "./Utils";
import { Prompt } from "./Prompt";
declare let window: ExcalidrawAutomate;
export enum TextMode {
parsed,
raw
}
interface WorkspaceItemExt extends WorkspaceItem {
containerEl: HTMLElement;
}
@@ -58,12 +68,16 @@ export default class ExcalidrawView extends TextFileView {
private excalidrawRef: React.MutableRefObject<any> = null;
private justLoaded: boolean = false;
private plugin: ExcalidrawPlugin;
private dirty: boolean = false;
private autosaveTimer: any = null;
public isTextLocked:boolean = false;
private lockedElement:HTMLElement;
private unlockedElement:HTMLElement;
private dirty: string = null;
public autosaveTimer: any = null;
public autosaving:boolean = false;
public textMode:TextMode = TextMode.raw;
private textIsParsed_Element:HTMLElement;
private textIsRaw_Element:HTMLElement;
private gotoFullscreen:HTMLElement;
private exitFullscreen:HTMLElement;
private preventReload:boolean = true;
public compatibilityMode: boolean = false;
id: string = (this.leaf as any).id;
@@ -73,18 +87,29 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawData = new ExcalidrawData(plugin);
}
public async saveSVG(data?: string) {
if(!data) {
public saveExcalidraw(scene?: any){
if(!scene) {
if (!this.getScene) return false;
data = this.getScene();
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.svg';
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));
else this.app.vault.create(filepath,JSON.stringify(scene));
}
public async saveSVG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const svg = ExcalidrawView.getSVG(data,exportSettings);
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
if(!svg) return;
const svgString = ExcalidrawView.embedFontsInSVG(svg).outerHTML;
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
@@ -102,18 +127,18 @@ export default class ExcalidrawView extends TextFileView {
return svg;
}
public async savePNG(data?: string) {
if(!data) {
public async savePNG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
data = this.getScene();
scene = this.getScene();
}
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(data,exportSettings);
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.png';
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
@@ -121,33 +146,49 @@ export default class ExcalidrawView extends TextFileView {
async save(preventReload:boolean=true) {
this.preventReload = preventReload;
this.dirty = null;
await super.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 a.k.a async is not required.
// if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
getViewData () {
//console.log("ExcalidrawView.getViewData()");
if(this.getScene) {
const scene = this.getScene();
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
if(this.excalidrawData.syncElements(scene)) {
if(!this.getScene) return this.data;
if(!this.compatibilityMode) {
if(this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
this.loadDrawing(false);
}
let trimLocation = this.data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
if(trimLocation == -1) return this.data;
const scene = this.excalidrawData.scene;
if(!this.autosaving) {
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
}
const header = this.data.substring(0,trimLocation)
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + (this.isTextLocked ? "locked\n" : "unlocked\n"));
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (this.textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
return header + this.excalidrawData.generateMD();
}
else return this.data;
if(this.compatibilityMode) {
this.excalidrawData.syncElements(this.getScene());
const scene = this.excalidrawData.scene;
if(!this.autosaving) {
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
}
return JSON.stringify(scene);
}
return this.data;
}
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
let text:string = this.isTextLocked
let text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(this.getSelectedId())
: this.getSelectedText();
if(!text) {
@@ -162,7 +203,17 @@ export default class ExcalidrawView extends TextFileView {
//1 2 3 4 5 6
const parts = text.matchAll(REG_LINK_BACKETS).next();
if(!parts.value) {
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
if(!tags.value || tags.value.length<2) {
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
return;
}
const search=this.app.workspace.getLeavesOfType("search");
if(search.length==0) return;
//@ts-ignore
search[0].view.setQuery("tag:"+tags.value[1]);
this.app.workspace.revealLeaf(search[0]);
if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
return;
}
@@ -187,22 +238,13 @@ export default class ExcalidrawView extends TextFileView {
}
try {
const f = view.file;
if(ev.shiftKey && this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
} catch (e) {
new Notice(e,4000);
}
}
download(encoding:string,data:any,filename:string) {
let element = document.createElement('a');
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
onload() {
//console.log("ExcalidrawView.onload()");
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
@@ -210,51 +252,83 @@ export default class ExcalidrawView extends TextFileView {
this.plugin.triggerEmbedUpdates();
});
this.unlockedElement = this.addAction(UNLOCK_ICON_NAME,t("LOCK"), (ev) => this.lock(true));
this.lockedElement = this.addAction(LOCK_ICON_NAME,t("UNLOCK"), (ev) => this.lock(false));
this.textIsRaw_Element = this.addAction(TEXT_DISPLAY_RAW_ICON_NAME,t("RAW"), (ev) => this.changeTextMode(TextMode.parsed));
this.textIsParsed_Element = this.addAction(TEXT_DISPLAY_PARSED_ICON_NAME,t("PARSED"), (ev) => this.changeTextMode(TextMode.raw));
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
this.gotoFullscreen = this.addAction(FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
this.exitFullscreen = this.addAction(EXIT_FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
this.exitFullscreen.hide();
//@ts-ignore
if(this.app.isMobile) this.gotoFullscreen.hide();
//this is to solve sliding panes bug
if (this.app.workspace.layoutReady) {
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
} else {
this.registerEvent(this.app.workspace.on('layout-ready', async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})));
this.app.workspace.onLayoutReady(
async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})
);
}
this.setupAutosaveTimer();
}
public async lock(locked:boolean,reload:boolean=true) {
//console.log("ExcalidrawView.lock(), locked",locked, "reload",reload);
this.isTextLocked = locked;
if(locked) {
this.unlockedElement.hide();
this.lockedElement.show();
private toggleFullscreen() {
//@ts-ignore
if(this.app.isMobile) return;
if(this.exitFullscreen.style.display=="none") {
this.containerEl.requestFullscreen();
this.gotoFullscreen.hide();
this.exitFullscreen.show();
} else {
this.unlockedElement.show();
this.lockedElement.hide();
document.exitFullscreen();
this.gotoFullscreen.show();
this.exitFullscreen.hide();
}
this.zoomToFit();
}
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
//console.log("ExcalidrawView.lock(), locked",locked, "reload",reload);
this.textMode = textMode;
if(textMode == TextMode.parsed) {
this.textIsRaw_Element.hide();
this.textIsParsed_Element.show();
} else {
this.textIsRaw_Element.show();
this.textIsParsed_Element.hide();
}
if(reload) {
await this.save(false);
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
}
}
private setupAutosaveTimer() {
public setupAutosaveTimer() {
const timer = async () => {
//console.log("ExcalidrawView.autosaveTimer(), dirty", this.dirty);
if(this.dirty) {
this.dirty = false;
if(this.dirty && (this.dirty == this.file?.path)) {
console.log("autosave",Date.now());
this.dirty = null;
this.autosaving=true;
if(this.excalidrawRef) await this.save();
this.plugin.triggerEmbedUpdates();
this.autosaving=false;
}
}
this.autosaveTimer = setInterval(timer,30000);
if(this.autosaveTimer) clearInterval(this.autosaveTimer); // clear previous timer if one exists
//if(this.plugin.settings.autosave) {
this.autosaveTimer = setInterval(timer,20000);
//}
}
//save current drawing when user closes workspace leaf
async onunload() {
//console.log("ExcalidrawView.onunload()");
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
if(this.autosaveTimer) {
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
//if(this.excalidrawRef) await this.save();
}
@@ -267,9 +341,10 @@ export default class ExcalidrawView extends TextFileView {
if(!this.excalidrawRef) return;
if(!this.file) return;
if(file) this.data = await this.app.vault.read(file);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.isTextLocked);
else await this.excalidrawData.setAllowParse(this.isTextLocked);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
else await this.excalidrawData.setTextMode(this.textMode);
this.loadDrawing(false);
this.dirty = null;
}
// clear the view content
@@ -277,38 +352,60 @@ export default class ExcalidrawView extends TextFileView {
}
async setViewData (data: string, clear: boolean) {
async setViewData (data: string, clear: boolean = false) {
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
this.dirty = null;
this.compatibilityMode = this.file.extension == "excalidraw";
await this.plugin.loadSettings();
this.plugin.opencount++;
if(this.compatibilityMode) {
this.textIsRaw_Element.hide();
this.textIsParsed_Element.hide();
await this.excalidrawData.loadLegacyData(data,this.file);
if (!this.plugin.settings.compatibilityMode) {
new Notice(t("COMPATIBILITY_MODE"),4000);
}
} else {
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
}
if(clear) this.clear();
this.loadDrawing(true)
this.dirty = false;
});
}
private loadDrawing (justloaded:boolean) {
private loadDrawing(justloaded:boolean) {
//console.log("ExcalidrawView.loadDrawing, justloaded", justloaded);
this.justLoaded = justloaded; //a flag to trigger zoom to fit after the drawing has been loaded
const excalidrawData = this.excalidrawData.scene;
if(this.excalidrawRef) {
if(justloaded) {
this.excalidrawRef.current.resetScene();
this.excalidrawRef.current.history.clear();
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it again
}
this.excalidrawRef.current.updateScene({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
commitToHistory: true,
});
} else {
(async() => {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
// scrollToContent: true,
libraryItems: await this.getLibrary(),
});
})();
}
}
//Compatibility mode with .excalidraw files
canAcceptExtension(extension: string) {
return extension == "excalidraw";
}
// gets the title of the document
getDisplayText() {
@@ -328,25 +425,52 @@ export default class ExcalidrawView extends TextFileView {
onMoreOptionsMenu(menu: Menu) {
// Add a menu item to force the board to markdown view
if(!this.compatibilityMode) {
menu
.addItem((item) => {
item
.setTitle(t("OPEN_AS_MD"))
.setIcon("document")
.onClick(async () => {
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
});
})
.addItem((item) => {
item
.setTitle(t("EXPORT_EXCALIDRAW"))
.setIcon(ICON_NAME)
.onClick( async (ev) => {
if(!this.getScene || !this.file) return;
//@ts-ignore
if(this.app.isMobile) {
const prompt = new Prompt(this.app, "Please provide filename",this.file.basename,'filename, leave blank to cancel action');
prompt.openAndGetValue( async (filename:string)=> {
if(!filename) return;
filename = filename + ".excalidraw";
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
this.app.vault.create(fname,JSON.stringify(this.getScene()));
new Notice("Exported to " + fname,6000);
});
return;
}
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
});
});
} else {
menu
.addItem((item) => {
item
.setTitle(t("CONVERT_FILE"))
.onClick(async () => {
await this.save();
this.plugin.openDrawing(await this.plugin.convertSingleExcalidrawToMD(this.file),false);
});
});
}
menu
.addItem((item) => {
item
.setTitle(t("OPEN_AS_MD"))
.setIcon("document")
.onClick(async () => {
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
});
})
.addItem((item) => {
item
.setTitle(t("EXPORT_EXCALIDRAW"))
.setIcon(ICON_NAME)
.onClick( async (ev) => {
if(!this.getScene || !this.file) return;
this.download('data:text/plain;charset=utf-8',encodeURIComponent(this.getScene()), this.file.basename+'.excalidraw');
});
})
.addItem((item) => {
item
.setTitle(t("SAVE_AS_PNG"))
@@ -358,14 +482,14 @@ export default class ExcalidrawView extends TextFileView {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings);
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
let reader = new FileReader();
reader.readAsDataURL(png);
const self = this;
reader.onloadend = function() {
let base64data = reader.result;
self.download(null,base64data,self.file.basename+'.png');
download(null,base64data,self.file.basename+'.png');
}
return;
}
@@ -383,10 +507,10 @@ export default class ExcalidrawView extends TextFileView {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
let svg = ExcalidrawView.getSVG(this.getScene(),exportSettings);
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
this.download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
return;
}
this.saveSVG()
@@ -397,14 +521,14 @@ export default class ExcalidrawView extends TextFileView {
}
async getLibrary() {
const data = JSON.parse(this.plugin.settings.library);
const data = JSON_parse(this.plugin.getStencilLibrary());
return data?.library ? data.library : [];
}
private instantiateExcalidraw(initdata: any) {
//console.log("ExcalidrawView.instantiateExcalidraw()");
this.dirty = false;
this.dirty = null;
const reactElement = React.createElement(() => {
let previousSceneVersion = 0;
let currentPosition = {x:0, y:0};
@@ -495,7 +619,7 @@ export default class ExcalidrawView extends TextFileView {
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
return JSON.stringify({
return {
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
@@ -519,16 +643,14 @@ export default class ExcalidrawView extends TextFileView {
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
gridSize: st.gridSize,
}
});
};
};
this.refresh = () => {
if(!excalidrawRef?.current) return;
excalidrawRef.current.refresh();
};
let timestamp = (new Date()).getTime();
return React.createElement(
React.Fragment,
null,
@@ -539,27 +661,12 @@ export default class ExcalidrawView extends TextFileView {
ref: excalidrawWrapperRef,
key: "abc",
onClick: (e:MouseEvent):any => {
if(this.isTextLocked && (e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
const now = (new Date()).getTime();
if(now-timestamp < 600) { //double click
var event = new MouseEvent('dblclick', {
'view': window,
'bubbles': true,
'cancelable': true,
});
e.target.dispatchEvent(event);
new Notice(t("UNLOCK_TO_EDIT"))
timestamp = now;
return;
}
timestamp = now;
}
//@ts-ignore
if(!(e.ctrlKey||e.metaKey)) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!this.getSelectedId()) return;
this.handleLinkClick(this,e);
},
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
@@ -570,7 +677,9 @@ export default class ExcalidrawView extends TextFileView {
loadScene: false,
saveScene: false,
saveAsScene: false,
export: false
export: { saveFileToDisk: false },
saveAsImage: false,
saveToActiveFile: false,
},
},
initialData: initdata,
@@ -581,25 +690,63 @@ export default class ExcalidrawView extends TextFileView {
onChange: (et:ExcalidrawElement[],st:AppState) => {
if(this.justLoaded) {
this.justLoaded = false;
previousSceneVersion = Excalidraw.getSceneVersion(et);
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
this.zoomToFit();
previousSceneVersion = getSceneVersion(et);
return;
}
if (st.editingElement == null && st.resizingElement == null &&
st.draggingElement == null && st.editingGroupId == null &&
st.editingLinearElement == null ) {
const sceneVersion = Excalidraw.getSceneVersion(et);
const sceneVersion = getSceneVersion(et);
if(sceneVersion != previousSceneVersion) {
previousSceneVersion = sceneVersion;
this.dirty=true;
this.dirty=this.file?.path;
}
}
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
await this.plugin.saveSettings();
})();
},
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
return null;
},
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
if(isDeleted) {
this.excalidrawData.deleteTextElement(textElement.id);
this.dirty=this.file?.path;
this.setupAutosaveTimer();
return;
}
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
if(text!=textElement.text) { //the user made changes to the text
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(textElement, text,async ()=>{
await this.save(false);
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
//thus I only check if TextMode.parsed, text is always != with parseResult
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
this.setupAutosaveTimer();
});
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
this.setupAutosaveTimer();
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
this.excalidrawRef.current.history.clear();
return parseResult;
}
return;
}
this.setupAutosaveTimer();
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
}
})
)
@@ -608,38 +755,44 @@ export default class ExcalidrawView extends TextFileView {
ReactDOM.render(reactElement,(this as any).contentEl);
}
public static getSVG(data:string, exportSettings:ExportSettings):SVGSVGElement {
private zoomToFit() {
const el = this.containerEl;
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
el.querySelector("canvas")?.dispatchEvent(e);
},400)
}
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
try {
const excalidrawData = JSON.parse(data);
return exportToSvg({
elements: excalidrawData.elements,
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (excalidrawData.appState?.theme=="light" ? false : true) : false,
... excalidrawData.appState,},
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
exportPadding:10,
metadata: "Generated by Excalidraw-Obsidian plugin",
});
} catch (error) {
return null;
}
}
public static async getPNG(data:string, exportSettings:ExportSettings) {
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
try {
const excalidrawData = JSON.parse(data);
return await Excalidraw.exportToBlob({
elements: excalidrawData.elements,
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (excalidrawData.appState?.theme=="light" ? false : true) : false,
... excalidrawData.appState,},
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
mimeType: "image/png",
exportWithDarkMode: "true",
metadata: "Generated by Excalidraw-Obsidian plugin",
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
});
} catch (error) {
return null;
}
}
}
}

54
src/MigrationPrompt.ts Normal file
View File

@@ -0,0 +1,54 @@
import { App, Modal } from "obsidian";
import { t } from "./lang/helpers";
import ExcalidrawPlugin from "./main";
export class MigrationPrompt extends Modal {
private plugin: ExcalidrawPlugin;
constructor(app: App, plugin:ExcalidrawPlugin) {
super(app);
this.plugin = plugin;
}
onOpen(): void {
this.titleEl.setText("Welcome to Excalidraw 1.2");
this.createForm();
}
onClose(): void {
this.contentEl.empty();
}
createForm(): void {
const div = this.contentEl.createDiv();
div.addClass("excalidarw-prompt-div");
div.style.maxWidth = "600px";
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
div.createEl('p',{text: ""} , (el) => {
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
});
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
});
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
const bConvert = div.createEl('button', {text: "CONVERT FILES"});
bConvert.onclick = (ev)=>{
this.plugin.convertExcalidrawToMD();
this.close();
};
const bCancel = div.createEl('button', {text: "CANCEL"});
bCancel.onclick = (ev)=>{
this.close();
};
}
}

View File

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

74
src/Utils.ts Normal file
View File

@@ -0,0 +1,74 @@
import { Modal, normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
import { Random } from "roughjs/bin/math";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
* @param filepath
*/
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
let folderpath: string, filename:string;
const lastIndex = filepath.lastIndexOf("/");
return {
folderpath: normalizePath(filepath.substr(0,lastIndex)),
filename: lastIndex==-1 ? filepath : filepath.substr(lastIndex+1),
};
}
/**
* Download data as file from Obsidian, to store on local device
* @param encoding
* @param data
* @param filename
*/
export function download(encoding:string,data:any,filename:string) {
let element = document.createElement('a');
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* Generates the image filename based on the excalidraw filename
* @param excalidrawPath - Full filepath of ExclidrawFile
* @param newExtension - extension of IMG file in ".extension" format
* @returns
*/
export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension:string):string {
const isLegacyFile:boolean = excalidrawPath.endsWith(".excalidraw");
const replaceExtension:string = isLegacyFile ? ".excalidraw" : ".md";
return excalidrawPath.substring(0,excalidrawPath.lastIndexOf(replaceExtension)) + newExtension;
}
/**
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
* @param filename
* @param folderpath
* @returns
*/
export function getNewUniqueFilepath(vault:Vault, filename:string, folderpath:string):string {
let fname = normalizePath(folderpath +'/'+ filename);
let file:TAbstractFile = vault.getAbstractFileByPath(fname);
let i = 0;
while(file) {
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
i++;
file = vault.getAbstractFileByPath(fname);
}
return fname;
}
/**
* Open or create a folderpath if it does not exist
* @param folderpath
*/
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
let folder = vault.getAbstractFileByPath(folderpath);
if(folder && folder instanceof TFolder) return;
await vault.createFolder(folderpath);
}
let random = new Random(Date.now());
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);

View File

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

View File

@@ -1,102 +1,139 @@
// English
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
// English
export default {
// main.ts
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
TOGGLE_MODE: "Toggle between Excalidraw and markdown mode",
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
MIGRATE_TO_2: "MIGRATE to version 2: convert .excalidraw files to .md files",
CREATE_NEW : "New Excalidraw",
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
CREATE_NEW : "New Excalidraw drawing",
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
TRANSCLUDE: "Transclude (embed) an Excalidraw drawing",
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited Excalidraw drawing",
TRANSCLUDE: "Transclude (embed) a drawing",
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed in current document",
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed in current document",
EXPORT_SVG: "Export SVG. Save it next to the current file",
EXPORT_PNG: "Export PNG. Save it next to the current file",
TOGGLE_LOCK: "Toggle text element edit lock/unlock",
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
EXPORT_SVG: "Save as SVG next to the current file",
EXPORT_PNG: "Save as PNG next to the current file",
TOGGLE_LOCK: "Toggle Text Element edit LOCK/UNLOCK",
INSERT_LINK: "Insert link to file",
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
ENTER_LATEX: "Enter a valid LaTeX expression",
//ExcalidrawView.ts
OPEN_AS_MD: "Open as markdown",
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+click to export)",
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+click to export)",
OPEN_LINK: "Open selected text as link\n(SHIFT+click to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to .Excalidraw file",
UNLOCK_TO_EDIT: "Unlock text elements to edit",
LINK_BUTTON_CLICK_NO_TEXT: 'Select a text element containing an internal or external link.\n'+
'SHIFT CLICK this button to open link in a new pane.\n'+
'CTRL/META CLICK on the Text Element on the canvas also works!',
TEXT_ELEMENT_EMPTY: "Text element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
OPEN_AS_MD: "Open as Markdown",
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
'SHIFT CLICK this button to open the link in a new pane.\n'+
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
TEXT_ELEMENT_EMPTY: "Text Element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\  < > : | ?',
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and click link button to create a new file.",
FORCE_SAVE: "Force-save to update transclusions in adjacent panes\n(Please note, that autosave is always on)",
LOCK: "Text Elements are unlocked. Click to lock.",
UNLOCK: "Text Elements are locked. Click to unlock.",
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
RAW: "Text-elements are displayed in RAW mode. Click button to change to PREVIEW mode.",
PARSED: "Text-elements are displayed in PREVIEW mode. Click button to change to RAW mode.",
NOFILE: "Excalidraw (no file)",
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
CONVERT_FILE: "Convert to new format",
//settings.ts
FOLDER_NAME: "Excalidraw folder",
FOLDER_DESC: "Default location for your drawings. If empty, drawings will be created in the Vault root.",
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
TEMPLATE_NAME: "Excalidraw template file",
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
"E.g.: If your template is in the default Excalidraw folder, the setting would be: Excalidraw/Template",
FILENAME_HEAD: "New drawing filename",
FILENAME_DESC: '<p>The auto-generated filename consists of a prefix and a date. ' +
'e.g."Drawing 2021-05-24 12.58.07".</p>'+
'<p>Click this link for the <a href="https://momentjs.com/docs/#/displaying/format/">'+
'date and time format reference</a>.</p>',
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
"Template.md, the setting would be: Excalidraw/Template.md " +
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
"such as Excalidraw/Template.excalidraw.",
AUTOSAVE_NAME: "Autosave",
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
FILENAME_HEAD: "Filename",
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
"date and time format reference</a>.</p>",
FILENAME_SAMPLE: "The current file format is: <b>",
FILENAME_PREFIX_NAME: "Filename prefix",
FILENAME_PREFIX_DESC: "The first part of the filename",
FILENAME_DATE_NAME: "Filename date",
FILENAME_DATE_DESC: "The second part of the filename",
LINKS_HEAD: "Links in drawings",
LINKS_DESC: 'CTRL/META + CLICK on Text Elements to open them as links. ' +
'If the selected text has more than one [[valid Obsidian links]], only the first will be opened. ' +
'If the text starts as a valid web link (i.e. https:// or http://), then ' +
'the plugin will try to open it in a browser. ' +
'When Obsidian files change, the matching [[link]] in your drawings will also change. ' +
'If you don\'t want text accidentally changing in your drawings use [[links|with aliases]].',
LINK_BRACKETS_NAME: "Show [[bracket]] around links",
LINK_BRACKETS_DESC: "When parsing text elements, place brackets around links",
LINK_INDICATOR_NAME:"Link indicator",
LINK_INDICATOR_DESC:"If text element contains a link, precede the text with these characters in preview.",
LINKS_HEAD: "Links",
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
"If the text starts as a valid web link (i.e. https:// or http://), then " +
"the plugin will open it in a browser. " +
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
LINK_BRACKETS_DESC: "In preview (locked) mode, when parsing Text Elements, place brackets around links. " +
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
": true/false' to the file\'s frontmatter.",
LINK_PREFIX_NAME:"Link prefix",
LINK_PREFIX_DESC:"In preview (locked) mode, if the Text Element contains a link, precede the text with these characters. " +
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
': "👉 "\' to the file\'s frontmatter.',
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
LINK_CTRL_CLICK_DESC: 'You can turn this feature off if it interferes with default Excalidraw features you want to use. If ' +
'this is turned off, only the link button in the title bar of the drawing pane will open links.',
EMBED_HEAD: "Embedded image settings",
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
EMBED_HEAD: "Embed & Export",
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
"[[drawing.excalidraw|100x100]] format.",
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
EXPORT_BACKGROUND_NAME: "Export image with background",
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
EXPORT_THEME_NAME: "Export image with theme",
EXPORT_THEME_DESC: 'Export the image matching the dark/light theme of your drawing. If turned off, ' +
'drawings created in drak mode will appear as they would in light mode.',
EXPORT_SVG_NAME: "Auto-export SVG",
EXPORT_SVG_DESC: 'Automatically create an SVG export of your drawing matching the title of your file. ' +
'The plugin will save the .SVG file in the same folder as the drawing. '+
'Embed the .svg file into your documents instead of excalidraw to making you embeds platform independent. ' +
'While the auto export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.',
EXPORT_PNG_NAME: "Auto-export PNG",
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for PNG.",
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
"drawings created in drak mode will appear as they would in light mode.",
EXPORT_HEAD: "Export Settings",
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
EXPORT_SYNC_DESC:'When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. ' +
'The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ',
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
EXPORT_SVG_NAME: "Auto-export SVG",
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
"The plugin will save the *.SVG file in the same folder as the drawing. "+
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
EXPORT_PNG_NAME: "Auto-export PNG",
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
COMPATIBILITY_HEAD: "Compatibility features",
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
"then update the drawing in the .md file based on the .excalidraw file",
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
"when you open a legacy file for editing.",
EXPERIMENTAL_HEAD: "Experimental features",
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
FILETAG_DESC: "The text or emojii to display as type indicator.",
//openDrawings.ts
SELECT_FILE: "Select a file then hit enter.",
SELECT_FILE: "Select a file then press enter.",
NO_MATCH: "No file matches your query.",
SELECT_FILE_TO_LINK: "Select file to inster link for.",
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
TYPE_FILENAME: "Type name of drawing to select.",
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new then hit enter.",
SELECT_TO_EMBED: "Select drawing to insert into document.",
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
};

View File

@@ -1,6 +1,5 @@
import {
TFile,
TFolder,
Plugin,
WorkspaceLeaf,
addIcon,
@@ -15,6 +14,9 @@ import {
Tasks,
MarkdownRenderer,
ViewState,
Notice,
TFolder,
Modal,
} from "obsidian";
import {
@@ -31,12 +33,13 @@ import {
RERENDER_EVENT,
FRONTMATTER_KEY,
FRONTMATTER,
LOCK_ICON,
LOCK_ICON_NAME,
UNLOCK_ICON_NAME,
UNLOCK_ICON
//LOCK_ICON,
TEXT_DISPLAY_PARSED_ICON_NAME,
TEXT_DISPLAY_RAW_ICON_NAME,
//UNLOCK_ICON,
JSON_parse
} from "./constants";
import ExcalidrawView, {ExportSettings} from "./ExcalidrawView";
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
import {getJSON} from "./ExcalidrawData";
import {
ExcalidrawSettings,
@@ -55,18 +58,21 @@ import {
import { Prompt } from "./Prompt";
import { around } from "monkey-around";
import { t } from "./lang/helpers";
import { MigrationPrompt } from "./MigrationPrompt";
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath } from "./Utils";
export default class ExcalidrawPlugin extends Plugin {
public excalidrawFileModes: { [file: string]: string } = {};
private _loaded: boolean = false;
public settings: ExcalidrawSettings;
//public stencilLibrary: any = null;
private openDialog: OpenFileDialog;
private activeExcalidrawView: ExcalidrawView = null;
public lastActiveExcalidrawFilePath: string = null;
private workspaceEventHandlers:Map<string,any> = new Map();
private vaultEventHandlers:Map<string,any> = new Map();
private hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
private observer: MutationObserver;
private fileExplorerObserver: MutationObserver;
public opencount:number = 0;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -77,12 +83,11 @@ export default class ExcalidrawPlugin extends Plugin {
addIcon(DISK_ICON_NAME,DISK_ICON);
addIcon(PNG_ICON_NAME,PNG_ICON);
addIcon(SVG_ICON_NAME,SVG_ICON);
addIcon(LOCK_ICON_NAME,LOCK_ICON);
addIcon(UNLOCK_ICON_NAME,UNLOCK_ICON);
//addIcon(TEXT_DISPLAY_PARSED_ICON_NAME,LOCK_ICON);
//addIcon(TEXT_DISPLAY_RAW_ICON_NAME,UNLOCK_ICON);
await this.loadSettings();
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
await initExcalidrawAutomate(this);
this.registerView(
@@ -90,15 +95,33 @@ export default class ExcalidrawPlugin extends Plugin {
(leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this)
);
this.addMarkdownPostProcessor();
this.registerCommands();
//Compatibility mode with .excalidraw files
this.registerExtensions(["excalidraw"],VIEW_TYPE_EXCALIDRAW);
this.addMarkdownPostProcessor();
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
this.registerCommands();
this.registerEventListeners();
//inspiration taken from kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
//inspiration taken from kanban:
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
if(this.settings.loadCount<1) this.migrationNotice();
}
private migrationNotice(){
const self = this;
this.app.workspace.onLayoutReady(async () => {
self.settings.loadCount++;
self.saveSettings();
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
if(files.length>0) {
const prompt = new MigrationPrompt(self.app, self);
prompt.open();
}
});
}
/**
* Displays a transcluded .excalidraw image in markdown preview mode
*/
@@ -127,7 +150,7 @@ export default class ExcalidrawPlugin extends Plugin {
withBackground: this.settings.exportWithBackground,
withTheme: this.settings.exportWithTheme
}
let svg = ExcalidrawView.getSVG(getJSON(content),exportSettings);
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
const img = createEl("img");
@@ -137,7 +160,7 @@ export default class ExcalidrawPlugin extends Plugin {
if(parts.fheight) img.setAttribute("height",parts.fheight);
img.addClass(parts.style);
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML))));
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll("&nbsp;"," ")))));
return img;
}
@@ -148,13 +171,15 @@ export default class ExcalidrawPlugin extends Plugin {
*/
const markdownPostProcessor = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => {
const drawings = el.querySelectorAll('.internal-embed');
if(drawings.length==0) return;
let attr:imgElementAttributes={fname:"",fheight:"",fwidth:"",style:""};
let alt:string, img:any, parts, div, file:TFile;
for (const drawing of drawings) {
attr.fname = drawing.getAttribute("src");
file = this.app.metadataCache.getFirstLinkpathDest(attr.fname, ctx.sourcePath);
if(file && file instanceof TFile && this.isExcalidrawFile(file)) {
attr.fwidth = drawing.getAttribute("width");
attr.fwidth = drawing.getAttribute("width") ? drawing.getAttribute("width") : this.settings.width;
attr.fheight = drawing.getAttribute("height");
alt = drawing.getAttribute("alt");
if(alt == attr.fname) alt = ""; //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text
@@ -175,8 +200,8 @@ export default class ExcalidrawPlugin extends Plugin {
div = createDiv(attr.style, (el)=>{
el.append(img);
el.setAttribute("src",file.path);
el.setAttribute("w",attr.fwidth);
el.setAttribute("h",attr.fheight);
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
if(attr.fheight) el.setAttribute("h",attr.fheight);
el.onClickEvent((ev)=>{
if(ev.target instanceof Element && ev.target.tagName.toLowerCase() != "img") return;
let src = el.getAttribute("src");
@@ -207,27 +232,30 @@ export default class ExcalidrawPlugin extends Plugin {
* @returns
*/
const hoverEvent = (e:any) => {
//@ts-ignore
if(!(e.event.ctrlKey||e.event.metaKey)) return;
if(!e.linktext) return;
this.hover.linkText = e.linktext;
this.hover.sourcePath = e.sourcePath;
const file = this.app.vault.getAbstractFileByPath(e.linktext);
if(file && file instanceof TFile && !this.isExcalidrawFile(file)) {
this.hover.linkText = null;
return;
if(!e.linktext) {
this.hover.linkText = null;
return;
}
this.hover.linkText = e.linktext;
this.hover.sourcePath = e.sourcePath;
};
this.registerEvent(
//@ts-ignore
this.app.workspace.on('hover-link',hoverEvent)
);
};
//@ts-ignore
this.app.workspace.on('hover-link',hoverEvent);
this.workspaceEventHandlers.set('hover-link',hoverEvent);
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
this.observer = new MutationObserver((m)=>{
if(!this.hover.linkText) return;
if(m.length == 0) return;
if(!this.hover.linkText) return;
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
if(!file || !(file instanceof TFile) || !this.isExcalidrawFile(file)) return
if((file as TFile).extension == "excalidraw") {
observerForLegacyFileFormat(m,file);
return;
}
let i=0;
//@ts-ignore
while(i<m.length && m[i].target?.className!="markdown-preview-sizer markdown-preview-section") i++;
@@ -240,24 +268,96 @@ export default class ExcalidrawPlugin extends Plugin {
//@ts-ignore
if(m[i].addedNodes[0].firstElementChild?.firstElementChild?.className=="excalidraw-svg") return;
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
if(file) {
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
//@ts-ignore
if(!m[i].addedNodes[0].matchParent(".hover-popover")) return;
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
});
m[i].addedNodes[0].insertBefore(div,m[i].addedNodes[0].firstChild)
});
//compatibility: .excalidraw file observer
let observerForLegacyFileFormat = (m:MutationRecord[], file:TFile) => {
if(!this.hover.linkText) return;
if(m.length!=1) return;
if(m[0].addedNodes.length != 1) return;
//@ts-ignore
if(m[0].addedNodes[0].className!="popover hover-popover file-embed is-loaded") return;
const node = m[0].addedNodes[0];
node.empty();
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
});
node.appendChild(div);
}
this.observer.observe(document, {childList: true, subtree: true});
}
public experimentalFileTypeDisplayToggle(enabled: boolean) {
if(enabled) {
this.experimentalFileTypeDisplay();
return;
}
if(this.fileExplorerObserver) this.fileExplorerObserver.disconnect();
this.fileExplorerObserver = null;
}
private experimentalFileTypeDisplay() {
this.fileExplorerObserver = new MutationObserver((m)=>{
const mutationsWithNodes = m.filter((v)=>v.addedNodes.length > 0);
mutationsWithNodes.forEach((mu)=>{
mu.addedNodes.forEach((n)=>{
if(!(n instanceof Element)) return;
n.querySelectorAll(".nav-file-title").forEach((el)=>{
if(el.childElementCount == 1) {
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
}
});
});
m[i].addedNodes[0].insertBefore(div,m[i].addedNodes[0].firstChild)
}
});
});
this.observer.observe(document, {childList: true, subtree: true});
const self = this;
this.app.workspace.onLayoutReady(()=>{
document.querySelectorAll(".nav-file-title").forEach((el)=>{
if(el.childElementCount == 1) {
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
}
});
this.fileExplorerObserver.observe(document.querySelector(".workspace"), {childList: true, subtree: true});
});
}
private registerCommands() {
@@ -267,23 +367,75 @@ export default class ExcalidrawPlugin extends Plugin {
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
});
const fileMenuHandler = (menu: Menu, file: TFile) => {
if (file instanceof TFolder) {
menu.addItem((item: MenuItem) => {
item.setTitle(t("CREATE_NEW"))
.setIcon(ICON_NAME)
.onClick(evt => {
this.createDrawing(this.getNextDefaultFilename(),false,file.path);
})
});
}
const fileMenuHandlerCreateNew = (menu: Menu, file: TFile) => {
menu.addItem((item: MenuItem) => {
item.setTitle(t("CREATE_NEW"))
.setIcon(ICON_NAME)
.onClick(evt => {
let folderpath = file.path;
if(file instanceof TFile) {
folderpath = normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name)));
}
this.createDrawing(this.getNextDefaultFilename(),false,folderpath);
})
});
};
this.registerEvent(
this.app.workspace.on("file-menu", fileMenuHandler)
this.app.workspace.on("file-menu", fileMenuHandlerCreateNew)
);
this.workspaceEventHandlers.set("file-menu",fileMenuHandler);
const fileMenuHandlerConvertKeepExtension = (menu: Menu, file: TFile) => {
if(file instanceof TFile && file.extension == "excalidraw") {
menu.addItem((item: MenuItem) => {
item.setTitle(t("CONVERT_FILE_KEEP_EXT"))
.onClick(evt => {
this.convertSingleExcalidrawToMD(file,false,false);
})
});
}
};
this.registerEvent(
this.app.workspace.on("file-menu", fileMenuHandlerConvertKeepExtension)
);
const fileMenuHandlerConvertReplaceExtension = (menu: Menu, file: TFile) => {
if(file instanceof TFile && file.extension == "excalidraw") {
menu.addItem((item: MenuItem) => {
item.setTitle(t("CONVERT_FILE_REPLACE_EXT"))
.onClick(evt => {
this.convertSingleExcalidrawToMD(file,true,true);
})
});
}
};
this.registerEvent(
this.app.workspace.on("file-menu", fileMenuHandlerConvertReplaceExtension)
);
this.addCommand({
id: "excalidraw-download-lib",
name: t("DOWNLOAD_LIBRARY"),
callback: async () => {
//@ts-ignore
if(this.app.isMobile) {
const prompt = new Prompt(this.app, "Please provide a filename",'my-library','filename, leave blank to cancel action');
prompt.openAndGetValue( async (filename:string)=> {
if(!filename) return;
filename = filename + ".excalidrawlib";
const folderpath = normalizePath(this.settings.folder);
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
this.app.vault.create(fname,this.settings.library);
new Notice("Exported library to " + fname,6000);
});
return;
}
download('data:text/plain;charset=utf-8',encodeURIComponent(this.settings.library), 'my-obsidian-library.excalidrawlib');
},
});
this.addCommand({
id: "excalidraw-open",
@@ -409,15 +561,18 @@ export default class ExcalidrawPlugin extends Plugin {
this.addCommand({
id: "toggle-lock",
hotkeys: [{modifiers:["Ctrl" || "Meta"], key:"e"}],
hotkeys: [{modifiers:["Ctrl" || "Meta","Shift"], key:"e"}],
name: t("TOGGLE_LOCK"),
checkCallback: (checking: boolean) => {
if (checking) {
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
if(this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
return !(this.app.workspace.activeLeaf.view as ExcalidrawView).compatibilityMode;
}
return false;
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
view.lock(!view.isTextLocked);
view.changeTextMode((view.textMode==TextMode.parsed)?TextMode.raw:TextMode.parsed);
return true;
}
else return false;
@@ -427,7 +582,7 @@ export default class ExcalidrawPlugin extends Plugin {
this.addCommand({
id: "insert-link",
hotkeys: [{modifiers:["Ctrl" || "Meta"], key:"k"}],
hotkeys: [{modifiers:["Ctrl" || "Meta","Shift"], key:"k"}],
name: t("INSERT_LINK"),
checkCallback: (checking: boolean) => {
if (checking) {
@@ -453,7 +608,7 @@ export default class ExcalidrawPlugin extends Plugin {
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'');
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'','$\\theta$');
prompt.openAndGetValue( async (formula:string)=> {
if(!formula) return;
const el = createEl('p');
@@ -477,6 +632,9 @@ export default class ExcalidrawPlugin extends Plugin {
const fileIsExcalidraw = this.isExcalidrawFile(activeFile);
if (checking) {
if(this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
return !(this.app.workspace.activeLeaf.view as ExcalidrawView).compatibilityMode;
}
return fileIsExcalidraw;
}
@@ -515,23 +673,48 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
/*1.2 migration command */
this.addCommand({
id: "migrate-to-1.2.x",
name: t("MIGRATE_TO_2"),
callback: async () => {
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
for (const file of files) {
const data = await this.app.vault.read(file);
const fname = this.getNewUniqueFilepath(file.name+'.md',normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
console.log(fname);
await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
this.app.vault.delete(file);
id: "convert-excalidraw",
name: t("CONVERT_EXCALIDRAW"),
checkCallback: (checking) => {
if (checking) {
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
return files.length>0;
}
this.convertExcalidrawToMD()
return true;
}
});
}
public async convertSingleExcalidrawToMD(file: TFile, replaceExtension:boolean = false, keepOriginal:boolean = false):Promise<TFile> {
const data = await this.app.vault.read(file);
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
console.log(fname);
const result = await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
if (this.settings.keepInSync) {
['.svg','.png'].forEach( (ext:string)=>{
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
const imgFile = this.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
if(imgFile && imgFile instanceof TFile) {
const newIMGpath = fname.substr(0,fname.lastIndexOf(".md")) + ext;
this.app.vault.rename(imgFile,newIMGpath);
}
});
}
if (!keepOriginal) this.app.vault.delete(file);
return result;
}
public async convertExcalidrawToMD(replaceExtension:boolean = false, keepOriginal:boolean = false) {
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
for (const file of files) {
this.convertSingleExcalidrawToMD(file,replaceExtension,keepOriginal);
}
new Notice("Converted " + files.length + " files.")
}
private registerMonkeyPatches() {
const self = this;
@@ -627,39 +810,49 @@ export default class ExcalidrawPlugin extends Plugin {
const self = this;
this.app.workspace.onLayoutReady(async () => {
//watch filename change to rename .svg, .png; to sync to .md; to update links
const renameEventHandler = async (file:TAbstractFile,oldPath:string) => {
if(!(file instanceof TFile)) return;
if (!self.isExcalidrawFile(file)) return;
if (!self.settings.keepInSync) return;
['.svg','.png'].forEach(async (ext:string)=>{
const oldIMGpath = oldPath.substring(0,oldPath.lastIndexOf('.md')) + ext;
if(!self.isExcalidrawFile(file)) return;
if(!self.settings.keepInSync) return;
['.svg','.png','.excalidraw'].forEach(async (ext:string)=>{
const oldIMGpath = getIMGPathFromExcalidrawFile(oldPath,ext);
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
if(imgFile && imgFile instanceof TFile) {
const newIMGpath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
const newIMGpath = getIMGPathFromExcalidrawFile(file.path,ext);
await self.app.vault.rename(imgFile,newIMGpath);
}
});
};
self.app.vault.on("rename",renameEventHandler);
this.vaultEventHandlers.set("rename",renameEventHandler);
self.registerEvent(
self.app.vault.on("rename",renameEventHandler)
);
const modifyEventHandler = async (file:TFile) => {
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf:WorkspaceLeaf)=> {
const excalidrawView = (leaf.view as ExcalidrawView);
if(excalidrawView.file && excalidrawView.file.path == file.path) {
excalidrawView.reload(true,file);
if(excalidrawView.file
&& (excalidrawView.file.path == file.path
|| (file.extension=="excalidraw"
&& file.path.substring(0,file.path.lastIndexOf('.excalidraw'))+'.md' == excalidrawView.file.path))) {
excalidrawView.reload(true,excalidrawView.file);
}
});
}
self.app.vault.on("modify",modifyEventHandler);
this.vaultEventHandlers.set("modify",modifyEventHandler);
self.registerEvent(
self.app.vault.on("modify",modifyEventHandler)
)
//watch file delete and delete corresponding .svg
//watch file delete and delete corresponding .svg and .png
const deleteEventHandler = async (file:TFile) => {
if (!(file instanceof TFile)) return;
if (!self.isExcalidrawFile(file)) return;
if (!(file instanceof TFile)) return;
//@ts-ignore
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)>-1)
|| (file.extension=="excalidraw");
if(!isExcalidarwFile) return;
//close excalidraw view where this file is open
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
@@ -671,8 +864,8 @@ export default class ExcalidrawPlugin extends Plugin {
//delete PNG and SVG files as well
if (self.settings.keepInSync) {
['.svg','.png'].forEach(async (ext:string) => {
const imgPath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
['.svg','.png','.excalidraw'].forEach(async (ext:string) => {
const imgPath = getIMGPathFromExcalidrawFile(file.path,ext);
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(imgPath));
if(imgFile && imgFile instanceof TFile) {
await self.app.vault.delete(imgFile);
@@ -680,8 +873,9 @@ export default class ExcalidrawPlugin extends Plugin {
});
}
}
self.app.vault.on("delete",deleteEventHandler);
this.vaultEventHandlers.set("delete",deleteEventHandler);
self.registerEvent(
self.app.vault.on("delete",deleteEventHandler)
);
//save open drawings when user quits the application
const quitEventHandler = (tasks: Tasks) => {
@@ -689,41 +883,46 @@ export default class ExcalidrawPlugin extends Plugin {
for (let i=0;i<leaves.length;i++) {
(leaves[i].view as ExcalidrawView).save();
}
this.settings.drawingOpenCount += this.opencount;
this.settings.loadCount++;
//this.saveSettings();
}
self.app.workspace.on("quit",quitEventHandler);
this.workspaceEventHandlers.set("quit",quitEventHandler);
self.registerEvent(
self.app.workspace.on("quit",quitEventHandler)
);
//save Excalidraw leaf and update embeds when switching to another leaf
const activeLeafChangeEventHandler = async (leaf:WorkspaceLeaf) => {
const activeview:ExcalidrawView = (leaf.view instanceof ExcalidrawView) ? leaf.view as ExcalidrawView : null;
if(self.activeExcalidrawView && self.activeExcalidrawView != activeview) {
//console.log("ExcalidrawPlugin.activeLeafChangeEventHandler()");
await self.activeExcalidrawView.save();
self.triggerEmbedUpdates(self.activeExcalidrawView.file?.path);
const activeExcalidrawView = self.activeExcalidrawView;
const newActiveview:ExcalidrawView = (leaf.view instanceof ExcalidrawView) ? leaf.view as ExcalidrawView : null;
if(activeExcalidrawView && activeExcalidrawView != newActiveview) {
await activeExcalidrawView.save(false);
if(activeExcalidrawView.file) {
self.triggerEmbedUpdates(activeExcalidrawView.file.path);
}
}
self.activeExcalidrawView = activeview;
if(self.activeExcalidrawView) {
self.lastActiveExcalidrawFilePath = self.activeExcalidrawView.file?.path;
self.activeExcalidrawView = newActiveview;
if(newActiveview) {
self.lastActiveExcalidrawFilePath = newActiveview.file?.path;
}
};
self.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler);
this.workspaceEventHandlers.set("active-leaf-change",activeLeafChangeEventHandler);
self.registerEvent(
self.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler)
);
});
}
onunload() {
destroyExcalidrawAutomate();
for(const key of this.vaultEventHandlers.keys())
this.app.vault.off(key,this.vaultEventHandlers.get(key))
for(const key of this.workspaceEventHandlers.keys())
this.app.workspace.off(key,this.workspaceEventHandlers.get(key));
this.observer.disconnect();
if (this.fileExplorerObserver) this.fileExplorerObserver.disconnect();
const excalidrawLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
excalidrawLeaves.forEach((leaf) => {
this.setMarkdownView(leaf);
});
this.settings.drawingOpenCount += this.opencount;
this.settings.loadCount++;
//this.saveSettings();
}
public embedDrawing(data:string) {
@@ -736,7 +935,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
private async loadSettings() {
public async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
@@ -744,6 +943,14 @@ export default class ExcalidrawPlugin extends Plugin {
await this.saveData(this.settings);
}
public getStencilLibrary():string {
return this.settings.library;
}
public setStencilLibrary(library:string) {
this.settings.library = library;
}
public triggerEmbedUpdates(filepath?:string){
const e = document.createEvent("Event")
e.initEvent(RERENDER_EVENT,true,false);
@@ -778,27 +985,34 @@ export default class ExcalidrawPlugin extends Plugin {
}
private getNextDefaultFilename():string {
return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)+'.md';
return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
}
private async getBlankDrawing():Promise<string> {
const template = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.templateFilePath));
const template = this.app.metadataCache.getFirstLinkpathDest(normalizePath(this.settings.templateFilePath),"");
if(template && template instanceof TFile) {
const data = await this.app.vault.read(template);
if (data) return data;
if( (template.extension == "md" && !this.settings.compatibilityMode)
|| (template.extension == "excalidraw" && this.settings.compatibilityMode)) {
const data = await this.app.vault.read(template);
if (data) return data;
}
}
return FRONTMATTER + '\n# Drawing\n'+ BLANK_DRAWING;
if (this.settings.compatibilityMode) {
return BLANK_DRAWING;
}
return FRONTMATTER + '\n# Drawing\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
+ BLANK_DRAWING + '\n'
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
}
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
const folder = this.app.vault.getAbstractFileByPath(folderpath);
if (!(folder && folder instanceof TFolder)) {
await this.app.vault.createFolder(folderpath);
}
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
const fname = this.getNewUniqueFilepath(filename,folderpath);
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
if(initData) {
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
@@ -827,26 +1041,10 @@ export default class ExcalidrawPlugin extends Plugin {
} as ViewState);
}
/**
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
* @param filename
* @param folderpath
* @returns
*/
getNewUniqueFilepath(filename:string, folderpath:string):string {
let fname = normalizePath(folderpath +'/'+ filename);
let file:TAbstractFile = this.app.vault.getAbstractFileByPath(fname);
let i = 0;
while(file) {
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
i++;
file = this.app.vault.getAbstractFileByPath(fname);
}
return fname;
}
isExcalidrawFile(f:TFile) {
if(f.extension=="excalidraw") return true;
const fileCache = this.app.metadataCache.getFileCache(f);
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
}
}
}

View File

@@ -33,7 +33,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
this.inputEl.onkeyup = (e) => {
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
if (this.containerEl.innerText.includes(EMPTY_MESSAGE)) {
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.md', this.onNewPane);
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.excalidraw.md', this.onNewPane);
this.close();
}
}
@@ -62,11 +62,8 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
break;
case(openDialogAction.insertLink):
//TO-DO
//change to this.app.metadataCache.fileToLinktext(file: TFile, sourcePath: string, omitMdExtension?: boolean): string;
//@ts-ignore
const filepath = this.app.metadataCache.getLinkpathDest(item.path,this.drawingPath)[0].path;
this.addText("[["+(filepath.endsWith(".md")?filepath.substr(0,filepath.length-3):filepath)+"]]"); //.md files don't need the extension
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
this.addText("[["+filepath+"]]");
break;
}
}

View File

@@ -1,7 +1,8 @@
import {
App,
PluginSettingTab,
Setting
Setting,
TFile
} from 'obsidian';
import { VIEW_TYPE_EXCALIDRAW } from './constants';
import ExcalidrawView from './ExcalidrawView';
@@ -15,32 +16,48 @@ export interface ExcalidrawSettings {
drawingFilenameDateTime: string,
width: string,
showLinkBrackets: boolean,
linkIndicator: string,
// validLinksOnly: boolean, //valid link as in [[valid Obsidian link]] - how to treat text elements in drawings
linkPrefix: string,
//autosave: boolean;
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
pngExportScale: number,
exportWithTheme: boolean,
exportWithBackground: boolean,
keepInSync: boolean,
autoexportSVG: boolean,
autoexportPNG: boolean,
keepInSync: boolean,
autoexportExcalidraw: boolean,
syncExcalidraw: boolean,
compatibilityMode: boolean,
experimentalFileType: boolean,
experimentalFileTag: string,
loadCount: number, //version 1.2 migration counter
drawingOpenCount: number,
library: string,
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
folder: 'Excalidraw',
templateFilePath: 'Excalidraw/Template',
templateFilePath: 'Excalidraw/Template.excalidraw',
drawingFilenamePrefix: 'Drawing ',
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
width: '400',
linkIndicator: ">> ",
linkPrefix: "📍",
showLinkBrackets: true,
// validLinksOnly: false,
//autosave: false,
allowCtrlClick: true,
pngExportScale: 1,
exportWithTheme: true,
exportWithBackground: true,
keepInSync: false,
autoexportSVG: false,
autoexportPNG: false,
keepInSync: false,
autoexportExcalidraw: false,
syncExcalidraw: false,
experimentalFileType: false,
experimentalFileTag: "✏️",
compatibilityMode: false,
loadCount: 0,
drawingOpenCount: 0,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
}
@@ -78,6 +95,28 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
/* new Setting(containerEl)
.setName(t("AUTOSAVE_NAME"))
.setDesc(t("AUTOSAVE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autosave)
.onChange(async (value) => {
this.plugin.settings.autosave = value;
await this.plugin.saveSettings();
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for(const v of exs) {
if(v.view instanceof ExcalidrawView) {
if(v.view.autosaveTimer) {
clearInterval(v.view.autosaveTimer)
v.view.autosaveTimer = null;
}
if(value) {
v.view.setupAutosaveTimer();
}
}
}
}));*/
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
containerEl.createDiv('',(el) => {
el.innerHTML = t("FILENAME_DESC");
@@ -146,13 +185,13 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName(t("LINK_INDICATOR_NAME"))
.setDesc(t("LINK_INDICATOR_DESC"))
.setName(t("LINK_PREFIX_NAME"))
.setDesc(t("LINK_PREFIX_DESC"))
.addText(text => text
.setPlaceholder('>> ')
.setValue(this.plugin.settings.linkIndicator)
.setPlaceholder('📍')
.setValue(this.plugin.settings.linkPrefix)
.onChange(async (value) => {
this.plugin.settings.linkIndicator = value;
this.plugin.settings.linkPrefix = value;
await this.plugin.saveSettings();
reloadDrawings();
}));
@@ -181,6 +220,26 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.triggerEmbedUpdates();
}));
let scaleText:HTMLDivElement;
new Setting(containerEl)
.setName(t("EXPORT_PNG_SCALE_NAME"))
.setDesc(t("EXPORT_PNG_SCALE_DESC"))
.addSlider(slider => slider
.setLimits(1,5,0.5)
.setValue(this.plugin.settings.pngExportScale)
.onChange(async (value)=> {
scaleText.innerText = " " + value.toString();
this.plugin.settings.pngExportScale = value;
this.plugin.saveSettings();
}))
.settingEl.createDiv('',(el)=>{
scaleText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = " " + this.plugin.settings.pngExportScale.toString();
});
new Setting(containerEl)
.setName(t("EXPORT_BACKGROUND_NAME"))
.setDesc(t("EXPORT_BACKGROUND_DESC"))
@@ -202,7 +261,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
}));
this.containerEl.createEl('h1', {text: t("EXPORT_HEAD")});
new Setting(containerEl)
.setName(t("EXPORT_SYNC_NAME"))
.setDesc(t("EXPORT_SYNC_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.keepInSync)
.onChange(async (value) => {
this.plugin.settings.keepInSync = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("EXPORT_SVG_NAME"))
.setDesc(t("EXPORT_SVG_DESC"))
@@ -213,26 +284,72 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("EXPORT_PNG_NAME"))
.setDesc(t("EXPORT_PNG_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportPNG)
.onChange(async (value) => {
this.plugin.settings.autoexportPNG = value;
await this.plugin.saveSettings();
}));
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportPNG)
.onChange(async (value) => {
this.plugin.settings.autoexportPNG = value;
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
new Setting(containerEl)
.setName(t("EXPORT_SYNC_NAME"))
.setDesc(t("EXPORT_SYNC_DESC"))
.setName(t("COMPATIBILITY_MODE_NAME"))
.setDesc(t("COMPATIBILITY_MODE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.keepInSync)
.setValue(this.plugin.settings.compatibilityMode)
.onChange(async (value) => {
this.plugin.settings.keepInSync = value;
this.plugin.settings.compatibilityMode = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("EXPORT_EXCALIDRAW_NAME"))
.setDesc(t("EXPORT_EXCALIDRAW_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportExcalidraw)
.onChange(async (value) => {
this.plugin.settings.autoexportExcalidraw = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("SYNC_EXCALIDRAW_NAME"))
.setDesc(t("SYNC_EXCALIDRAW_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.syncExcalidraw)
.onChange(async (value) => {
this.plugin.settings.syncExcalidraw = value;
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
this.containerEl.createEl('p', {text: t("EXPERIMENTAL_DESC")});
new Setting(containerEl)
.setName(t("FILETYPE_NAME"))
.setDesc(t("FILETYPE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.experimentalFileType)
.onChange(async (value) => {
this.plugin.settings.experimentalFileType = value;
this.plugin.experimentalFileTypeDisplayToggle(value);
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("FILETAG_NAME"))
.setDesc(t("FILETAG_DESC"))
.addText(text => text
.setPlaceholder('✏️')
.setValue(this.plugin.settings.experimentalFileTag)
.onChange(async (value) => {
this.plugin.settings.experimentalFileTag = value;
await this.plugin.saveSettings();
}));
}
}

View File

@@ -51,6 +51,7 @@ button.ToolIcon_type_button[title="Export"] {
.excalidraw-prompt-div {
display: flex;
max-width: 600px;
}
.excalidraw-prompt-form {
@@ -60,4 +61,32 @@ button.ToolIcon_type_button[title="Export"] {
.excalidraw-prompt-input {
flex-grow: 1;
}
}
li[data-testid] {
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
.excalidraw .context-menu-option-separator {
margin: 4px !important;
}
.excalidraw .popover {
padding: 0 !important;
border-color: transparent !important;
border: 0 !important;
box-shadow: 0 !important;
background-color: transparent !important;
}
/*
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}*/

View File

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

View File

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

View File

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