Compare commits

...

59 Commits

Author SHA1 Message Date
zsviczian
353732f597 2.4.0-beta-9 2024-08-24 13:59:49 +02:00
zsviczian
5599d2507f update writing machine data 2024-08-23 12:43:27 +02:00
zsviczian
70cf6ffe70 Support embeddables 2024-08-23 12:41:33 +02:00
zsviczian
61c9277097 writing machine 2024-08-22 21:59:06 +02:00
zsviczian
401052efd3 svg icon take 2 2024-08-22 20:36:00 +02:00
zsviczian
a57a0e797d update writing machine svg 2024-08-22 20:33:52 +02:00
zsviczian
8f48853e2c updated writing machine script 2024-08-22 17:58:42 +02:00
zsviczian
a62148dc07 publish Excalidraw Writing Machine 2024-08-21 21:53:59 +02:00
zsviczian
3ae890bd86 2.4.0-beta-8 2024-08-19 15:57:23 +02:00
zsviczian
65a4cd4ba5 Merge pull request #1939 from dmscode/master
Update zh-cn.ts 2.4.0-beta-7
2024-08-19 15:38:56 +02:00
dmscode
f63b473bc1 Update zh-cn.ts to the commit "fixed getTemplate, migrade gridSize, gridStep" 2024-08-19 09:42:37 +08:00
稻米鼠
859a5ba03a Merge branch 'zsviczian:master' into master 2024-08-19 09:37:07 +08:00
zsviczian
832b97b179 fixed getTemplate, migrade gridSize, gridStep, 2024-08-18 17:15:03 +02:00
dmscode
e98d688d36 Update zh-cn.ts 2.4.0-beta-7 2024-08-16 15:53:39 +08:00
zsviczian
39318337fe updated set stroke width script 2024-08-16 08:13:55 +02:00
zsviczian
f21215be84 2.4.0-beta-7 2024-08-15 17:45:49 +02:00
zsviczian
0690525af8 Merge pull request #1937 from dmscode/master
Update zh-cn.ts to 2.4.0-beta-6
2024-08-15 17:41:32 +02:00
dmscode
b3176425c5 Merge branch 'master' of https://github.com/dmscode/obsidian-excalidraw-plugin 2024-08-15 18:56:30 +08:00
dmscode
9f2c18b6b6 Update zh-cn.ts to 2.4.0-beta-6 2024-08-15 18:56:20 +08:00
zsviczian
d529a04f48 2.4.0-beta-6, pdf++ crop support, double tap eraser disable 2024-08-14 20:02:41 +02:00
zsviczian
8786c5aa99 2.4.0-beta-5 2024-08-14 10:07:55 +02:00
zsviczian
013279ab60 2.2.4-beta-4, markdown post processor, PDF frames, selectFrameElements 2024-08-13 22:53:03 +02:00
zsviczian
06193b6d49 2.4.0-beta-3 2024-08-11 09:22:13 +02:00
zsviczian
ac6f4af5d6 Merge pull request #1928 from mProjectsCode/patch-1
Fix `authorUrl` in manifest
2024-08-11 08:42:11 +02:00
zsviczian
bf148adc68 2.4.0-beta-2 2024-08-09 23:36:20 +02:00
Moritz Jung
0f9dafb01d Fix authorUrl in manifest 2024-08-09 22:16:07 +02:00
zsviczian
9fc0452b70 2.4.0-beta-1 2024-08-08 23:27:57 +02:00
zsviczian
83eda9b3f5 Update README.md 2024-08-04 11:01:12 +02:00
zsviczian
9bfbf47963 2.3.0 2024-08-04 10:37:06 +02:00
zsviczian
252bf411b1 2.3.0-beta-1 2024-08-03 21:26:47 +02:00
zsviczian
5622c019dd rebuild view DEVICE.isDesktop 2024-08-02 17:55:23 +02:00
zsviczian
b32fab7865 2.2.13-1 2024-08-01 22:18:16 +02:00
zsviczian
cafdad1f7a onWindowMigrated 2024-07-30 18:54:54 +02:00
zsviczian
9da40944ab 2.2.13 2024-07-29 18:13:13 +02:00
zsviczian
f678203a64 Merge pull request #1908 from dmscode/master
Update zh-cn.ts to 2.2.11
2024-07-29 17:32:14 +02:00
zsviczian
a9572e08e9 Merge branch 'master' into master 2024-07-29 17:31:58 +02:00
dmscode
ee9b042cdf Update zh-cn.ts to 2.2.11 2024-07-28 20:42:41 +08:00
zsviczian
bc138fa78a fixed storeAction, replaced "none" with "update" #1906 2024-07-28 10:06:31 +02:00
zsviczian
67dbe256f7 2.2.12 2024-07-28 06:55:27 +02:00
zsviczian
8b066d46e2 fix rename and skip inline fonts to cache key 2024-07-28 06:24:30 +02:00
zsviczian
1769a65a82 2.2.11 2024-07-27 21:25:23 +02:00
zsviczian
941eb56769 updated ExcaliAI 2024-07-27 07:09:19 +02:00
zsviczian
a317613ef4 Merge pull request #1859 from Saik0s/patch-1
Update ExcaliAI.md to fix incorrect request json for openai api
2024-07-27 07:05:58 +02:00
zsviczian
173571846f 2.2.10-2 2024-07-26 20:11:20 +02:00
zsviczian
ab1078d393 Fourth Font 2.2.10-1 2024-07-25 21:59:42 +02:00
zsviczian
681321a595 2.2.10-1 fontpicker beta 2024-07-25 21:52:47 +02:00
zsviczian
fcd50d4bc2 Merge pull request #1897 from dmscode/master
Updated zh-cn.ts to 2.2.10
2024-07-23 17:49:00 +02:00
dmscode
7e214e5aaa Updated zh-cn.ts to 2.2.10 2024-07-23 09:54:43 +08:00
zsviczian
de39053857 2.2.10 2024-07-22 22:01:36 +02:00
zsviczian
f543e3218e clippedframe, framesettings 2024-07-21 17:54:00 +02:00
zsviczian
e668aea214 Merge pull request #1890 from dmscode/master
Fixed link path
2024-07-20 08:33:09 +02:00
zsviczian
d810daa735 bump excalidraw version 2024-07-19 12:20:21 +02:00
zsviczian
b94b3118eb improved error message when getPNG fails 2024-07-19 12:19:48 +02:00
dmscode
06cbd0c92d Fixed link path 2024-07-19 12:44:50 +08:00
zsviczian
96ebbbf11d tolerate ! for embedded files 2024-07-17 21:05:01 +02:00
zsviczian
fc1467b05b Support for area, group, frame references in image embeds 2024-07-17 20:31:36 +02:00
zsviczian
53c27f2a59 2.2.9 2024-07-13 17:34:19 +02:00
zsviczian
db80f5c715 allows opacity 0% 2024-07-09 16:45:06 +02:00
Igor Tarasenko
d3aeedb9f6 Update ExcaliAI.md to fix incorrect request json for openai api 2024-06-28 21:03:58 +02:00
60 changed files with 2994 additions and 998 deletions

View File

@@ -63,6 +63,13 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Eraser, left-handed mode, improved filename configuration</a><br>
</details>
### Beta testing
The plugin follows a monthly release schedule. If you want to receive more frequent updates with new features (e.g. shiny new stuff available on excalidraw.com, but not yet in Obsidian) and minor bug fixes, then join the beta community.
[![Thumbnail - 20240803 Excalidraw Release Approach (Custom)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw Plugin Release Strategy (Phone)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## Features

View File

@@ -6,7 +6,7 @@ import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types";
import { ConnectionPoint, DeviceType } from "src/types/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -123,7 +123,7 @@ export declare class ExcalidrawAutomate {
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
* @returns
*/
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "LocalFont";
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "Local Font";
/**
* @param val //0:"light", 1:"dark"
* @returns

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 611 B

View File

@@ -103,7 +103,7 @@ const selectAttributesToCopy = () => {
attributes.forEach(attr => {
const attrValue = elements[0][attr.key];
if(attrValue || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
if((typeof attrValue !== "undefined" && attrValue !== null) || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
let description = '';
switch(attr.key) {
@@ -190,7 +190,9 @@ const selectAttributesToCopy = () => {
configModal.onClose = () => {
setTimeout(()=>delete configModal);
setTimeout(()=>{
delete configModal
});
}
configModal.open();

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,7 @@ If you want to modify scripts, I recommend moving them to the `Excalidraw Automa
I would love to include your contribution in the script library. If you have a script of your own that you would like to share with the community, please open a [PR](https://github.com/zsviczian/obsidian-excalidraw-plugin/pulls) on GitHub. Be sure to include the following in your pull request
- The [script file](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts) with a self explanetory name. The name of the file will be the name of the script in the Command Palette.
- An [image](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/images) explaining the scripts purpose. Remember a picture speaks thousand words!
- An update to this file [ea-scripts/index.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index.md)
- An update to this file [ea-scripts/index-new.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index-new.md)
---
@@ -119,6 +119,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.svg"/></div>|[[#Custom Zoom]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.svg"/></div>|[[#Excalidraw Writing Machine]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
@@ -389,6 +390,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Excalidraw Writing Machine
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 861 KiB

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-30",
"@zsviczian/excalidraw": "0.17.1-obsidian-42",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
@@ -32,7 +32,9 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roughjs": "^4.5.2",
"js-yaml": "^4.1.0"
"js-yaml": "^4.1.0",
"opentype.js": "^1.3.4",
"woff2sfnt-sfnt2woff": "^1.0.0"
},
"devDependencies": {
"dotenv": "^16.4.5",
@@ -52,6 +54,7 @@
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/js-yaml": "^4.0.9",
"@types/opentype.js": "^1.3.8",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"cssnano": "^6.0.2",

View File

@@ -15,7 +15,7 @@ import cssnano from 'cssnano';
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);
@@ -107,7 +107,7 @@ const BUILD_CONFIG = {
babel({
presets: [['@babel/preset-env', {
targets: {
esmodules: true,
ios: "15", // ios Compatibility //esmodules: true,
},
}]],
exclude: "node_modules/**",

View File

@@ -4,11 +4,6 @@
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "./constants/constFonts";
import {
DEFAULT_MD_EMBED_CSS,
fileid,
@@ -16,6 +11,7 @@ import {
nanoid,
THEME_FILTER,
FRONTMATTER_KEYS,
getCSSFontDefinition,
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
@@ -38,13 +34,14 @@ import {
LinkParts,
svgToBase64,
isMaskFile,
embedFontsInSVG,
getEmbeddedFilenameParts,
cropCanvas,
} from "./utils/Utils";
import { ValueOf } from "./types";
import { ValueOf } from "./types/types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants/constants";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { PreviewImageType } from "./utils/UtilTypes";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -145,8 +142,6 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
return svg;
}
export class EmbeddedFile {
public file: TFile = null;
public isSVGwithBitmap: boolean = false;
@@ -157,6 +152,7 @@ export class EmbeddedFile {
public mimeType: MimeType = "application/octet-stream";
public size: Size = { height: 0, width: 0 };
public linkParts: LinkParts;
public filenameparts: FILENAMEPARTS
private hostPath: string;
public attemptCounter: number = 0;
public isHyperLink: boolean = false;
@@ -204,7 +200,7 @@ export class EmbeddedFile {
if (!this.linkParts.height) {
this.linkParts.height = this.plugin.settings.mdSVGmaxHeight;
}
this.file = app.metadataCache.getFirstLinkpathDest(
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
hostPath,
);
@@ -215,6 +211,9 @@ export class EmbeddedFile {
5000,
);
}
} else {
this.filenameparts = getEmbeddedFilenameParts(imgPath);
this.filenameparts.filepath = this.file.path;
}
}
@@ -360,26 +359,36 @@ export class EmbeddedFilesLoader {
: false,
withTheme: !!forceTheme,
isMask,
skipInliningFonts: false,
};
const hasColorMap = Boolean(inFile instanceof EmbeddedFile ? inFile.colorMap : null);
const shouldUseCache = !hasColorMap && this.plugin.settings.allowImageCacheInScene && file && imageCache.isReady();
const hasFilenameParts = Boolean((inFile instanceof EmbeddedFile) && inFile.filenameparts);
const filenameParts = hasFilenameParts ? (inFile as EmbeddedFile).filenameparts : null;
const cacheKey:ImageKey = {
filepath: file.path,
blockref: null,
sectionref: null,
...hasFilenameParts? {
...filenameParts,
inlineFonts: !exportSettings.skipInliningFonts,
}: {
filepath: file.path,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
hasSectionref: false,
inlineFonts: !exportSettings.skipInliningFonts,
blockref: null,
sectionref: null,
linkpartReference: null,
linkpartAlias: null,
},
isDark,
previewImageType: PreviewImageType.SVG,
scale: 1,
isTransparent: !exportSettings.withBackground,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasSectionref: false,
linkpartReference: null,
linkpartAlias: null,
}
const maybeSVG = shouldUseCache
@@ -390,9 +399,17 @@ export class EmbeddedFilesLoader {
? maybeSVG
: replaceSVGColors(
await createSVG(
file?.path,
hasFilenameParts
? (filenameParts.hasGroupref || filenameParts.hasBlockref ||
filenameParts.hasSectionref || filenameParts.hasFrameref ||
filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path)
: file?.path,
false, //false
exportSettings,
hasFilenameParts && filenameParts.hasClippedFrameref
? {...exportSettings, frameRendering: {enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
this,
forceTheme,
null,
@@ -429,16 +446,16 @@ export class EmbeddedFilesLoader {
//see svgWithFont below
imageCache.addImageToCache(cacheKey,"", svg);
}
const svgWithFont = embedFontsInSVG(svg, this.plugin);
if(!svgWithFont.hasAttribute("width") && svgWithFont.hasAttribute("viewBox")){
if(!svg.hasAttribute("width") && svg.hasAttribute("viewBox")){
//2024.06.09
//this addresses backward compatibility issues where the cache does not have the width and height attributes
//this should be removed in the future
const vb = svgWithFont.getAttr("viewBox").split(" ");
Boolean(vb[2]) && svgWithFont.setAttribute("width", vb[2]);
Boolean(vb[3]) && svgWithFont.setAttribute("height", vb[3]);
const vb = svg.getAttr("viewBox").split(" ");
Boolean(vb[2]) && svg.setAttribute("width", vb[2]);
Boolean(vb[3]) && svg.setAttribute("height", vb[3]);
}
const dURL = svgToBase64(svgWithFont.outerHTML) as DataURL;
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
@@ -556,7 +573,8 @@ export class EmbeddedFilesLoader {
return {
mimeType,
fileId: await generateIdFromFile(
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab,
inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined
),
dataURL,
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
@@ -729,6 +747,8 @@ export class EmbeddedFilesLoader {
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
const scale = this.plugin.settings.pdfScale;
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
// Render the page
const renderPage = async (num:number) => {
@@ -749,6 +769,23 @@ export class EmbeddedFilesLoader {
};
await page.render(renderCtx).promise;
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
return cropCanvas(canvas, crop);
}
}
return canvas;
};
@@ -799,13 +836,29 @@ export class EmbeddedFilesLoader {
}
switch (fontName) {
case "Virgil":
fontDef = VIRGIL_FONT;
fontDef = await getCSSFontDefinition(1);
break;
case "Cascadia":
fontDef = CASCADIA_FONT;
fontDef = await getCSSFontDefinition(3);
break;
case "Assistant":
fontDef = ASSISTANT_FONT;
case "Assistant":
case "Helvetica":
fontDef = await getCSSFontDefinition(2);
break;
case "Excalifont":
fontDef = await getCSSFontDefinition(5);
break;
case "Nunito":
fontDef = await getCSSFontDefinition(6);
break;
case "Lilita One":
fontDef = await getCSSFontDefinition(7);
break;
case "Comic Shanns":
fontDef = await getCSSFontDefinition(8);
break;
case "Liberation Sans":
fontDef = await getCSSFontDefinition(9);
break;
case "":
fontDef = "";
@@ -888,12 +941,14 @@ export class EmbeddedFilesLoader {
mdDIV.style.display = "block";
mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial";
await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
//await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin);
mdDIV
.querySelectorAll(":scope > *[class^='frontmatter']")
.forEach((el) => mdDIV.removeChild(el));
await replaceBlobWithBase64(mdDIV); //because image cache returns a blob
const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']"))
for(let i=0;i<internalEmbeds.length;i++) {
const el = internalEmbeds[i];
@@ -1005,7 +1060,7 @@ const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Pro
return svgToBase64(svg) as DataURL;
};
export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
let id: FileId;
try {
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
@@ -1020,4 +1075,53 @@ export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> =>
id = fileid() as FileId;
}
return id;
};*/
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
let id: FileId;
try {
// Convert the file ArrayBuffer to a Uint8Array
const fileArray = new Uint8Array(file);
// If a key is provided, concatenate it to the file data
let dataToHash: Uint8Array;
if (key) {
const encoder = new TextEncoder();
const keyArray = encoder.encode(key);
dataToHash = new Uint8Array(fileArray.length + keyArray.length);
dataToHash.set(fileArray);
dataToHash.set(keyArray, fileArray.length);
} else {
dataToHash = fileArray;
}
// Hash the combined data (file and key, if provided)
const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash);
id =
// Convert buffer to byte array
Array.from(new Uint8Array(hashBuffer))
// Convert to hex string
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("") as FileId;
} catch (error) {
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
id = fileid() as FileId;
}
return id;
};
const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise<void> => {
const images = divElement.querySelectorAll<HTMLImageElement>('img[src^="blob:app://obsidian.md"]');
for (let img of images) {
const blobUrl = img.src;
try {
const response = await fetch(blobUrl);
const blob = await response.blob();
const base64 = await blobToBase64(blob);
img.src = `data:${blob.type};base64,${base64}`;
} catch (error) {
console.error(`Failed to fetch or convert blob: ${blobUrl}`, error);
}
}
};

View File

@@ -13,6 +13,7 @@ import {
ExcalidrawFrameElement,
ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { MimeType } from "./EmbeddedFileLoader";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
@@ -27,7 +28,7 @@ import {
GITHUB_RELEASES,
determineFocusDistance,
getCommonBoundingBox,
getDefaultLineHeight,
getLineHeight,
getMaximumGroups,
intersectElementWithLine,
measureText,
@@ -37,11 +38,11 @@ import {
THEME_FILTER,
mermaidToExcalidraw,
refreshTextDimensions,
getFontFamilyString,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
import {
//debug,
embedFontsInSVG,
errorlog,
getEmbeddedFilenameParts,
getImageSize,
@@ -61,7 +62,7 @@ import { tex2dataURL } from "src/LaTeX";
import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
import { t } from "src/lang/helpers";
import { ScriptEngine } from "src/Scripts";
import { ConnectionPoint, DeviceType } from "src/types";
import { ConnectionPoint, DeviceType } from "src/types/types";
import CM, { ColorMaster, extendPlugins } from "colormaster";
import HarmonyPlugin from "colormaster/plugins/harmony";
import MixPlugin from "colormaster/plugins/mix"
@@ -92,6 +93,7 @@ import {
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { log } from "./utils/DebugHelper";
import { ExcalidrawLib } from "./ExcalidrawLib";
extendPlugins([
HarmonyPlugin,
@@ -111,6 +113,7 @@ extendPlugins([
declare const PLUGIN_VERSION:string;
declare var LZString: any;
declare const excalidrawLib: typeof ExcalidrawLib;
const GAP = 4;
@@ -386,7 +389,7 @@ export class ExcalidrawAutomate {
opacity: number;
strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
roundness: null | { type: RoundnessType; value?: number };
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font
fontSize: number;
textAlign: string; //"left"|"right"|"center"
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
@@ -714,6 +717,12 @@ export class ExcalidrawAutomate {
this.style.roundness ? "round":"sharp",
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
...template?.appState?.frameRendering
? {frameRendering: template.appState.frameRendering}
: {},
...template?.appState?.objectsSnapModeEnabled
? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled}
: {},
},
files: template?.files ?? {},
};
@@ -1024,6 +1033,22 @@ export class ExcalidrawAutomate {
return id;
};
/**
* Add elements to frame
* @param frameId
* @param elementIDs to add
* @returns void
*/
addElementsToFrame(frameId: string, elementIDs: string[]):void {
if(!this.getElement(frameId)) return;
elementIDs.forEach(elID => {
const el = this.getElement(elID);
if(el) {
el.frameId = frameId;
}
});
}
/**
*
* @param topX
@@ -1191,7 +1216,7 @@ export class ExcalidrawAutomate {
element.text,
element.fontSize,
element.fontFamily,
getDefaultLineHeight(element.fontFamily)
getLineHeight(element.fontFamily)
);
element.width = w;
element.height = h;
@@ -1236,7 +1261,7 @@ export class ExcalidrawAutomate {
text,
this.style.fontSize,
this.style.fontFamily,
getDefaultLineHeight(this.style.fontFamily)
getLineHeight(this.style.fontFamily)
);
const width = formatting?.width ? formatting.width : w;
const height = formatting?.height ? formatting.height : h;
@@ -1294,7 +1319,7 @@ export class ExcalidrawAutomate {
containerId: isContainerBound ? boxId : null,
originalText: isContainerBound ? originalText : text,
rawText: isContainerBound ? originalText : text,
lineHeight: getDefaultLineHeight(this.style.fontFamily),
lineHeight: getLineHeight(this.style.fontFamily),
autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
};
if (boxId && formatting?.box === "blob") {
@@ -1569,6 +1594,26 @@ export class ExcalidrawAutomate {
return id;
};
/**
* returns the base64 dataURL of the LaTeX equation rendered as an SVG
* @param tex The LaTeX equation as string
* @param scale of the image, default value is 4
* @returns
*/
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
async tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
};
/**
*
* @param objectA
@@ -1894,15 +1939,16 @@ export class ExcalidrawAutomate {
/**
*
* @param includeFrameChildren
* @returns
*/
getViewSelectedElements(): any[] {
getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getViewSelectedElements()");
return [];
}
return this.targetView.getViewSelectedElements();
return this.targetView.getViewSelectedElements(includeFrameChildren);
};
/**
@@ -1995,7 +2041,7 @@ export class ExcalidrawAutomate {
appState: {
viewModeEnabled: !isFullscreen,
},
storeAction: "none",
storeAction: "update",
});
this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
}
@@ -2014,7 +2060,7 @@ export class ExcalidrawAutomate {
return;
}
const view = this.targetView as ExcalidrawView;
view.updateScene({appState:{viewModeEnabled: enabled}});
view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"});
view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled);
}
@@ -2040,7 +2086,7 @@ export class ExcalidrawAutomate {
return;
}
if (!Boolean(scene.storeAction)) {
scene.storeAction = scene.commitToHistory ? "capture" : "none";
scene.storeAction = scene.commitToHistory ? "capture" : "update";
}
this.targetView.updateScene({
@@ -2394,35 +2440,61 @@ export class ExcalidrawAutomate {
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInTheSameGroupWithElement(
element: ExcalidrawElement,
elements: ExcalidrawElement[],
includeFrameElements: boolean = false,
): ExcalidrawElement[] {
if(!element || !elements) return [];
const container = (element.type === "text" && element.containerId)
? elements.filter(el=>el.id === element.containerId)
: [];
if(element.groupIds.length === 0) {
if(includeFrameElements && element.type === "frame") {
return this.getElementsInFrame(element,elements,true);
}
if(container.length === 1) return [element,container[0]];
return [element];
}
if(container.length === 1) {
return elements.filter(el=>
el.groupIds.some(id=>element.groupIds.includes(id)) ||
el === container[0]
);
const conditionFN = container.length === 1
? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
: (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
if(!includeFrameElements) {
return elements.filter(el=>conditionFN(el));
} else {
//I use the set and the filter at the end to preserve scene layer seqeuence
//adding frames could potentially mess up the sequence otherwise
const elementIDs = new Set<string>();
elements
.filter(el=>conditionFN(el))
.forEach(el=>{
if(el.type === "frame") {
this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
} else {
elementIDs.add(el.id);
}
});
return elements.filter(el=>elementIDs.has(el.id));
}
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
}
/**
* Gets all the elements from elements[] that are contained in the frame.
* @param element
* @param elements - typically all the non-deleted elements in the scene
* @param frameElement - the frame element for which to get the elements
* @param elements - typically all the non-deleted elements in the scene
* @param shouldIncludeFrame - if true, the frame element will be included in the returned array
* this is useful when generating an image in which you want the frame to be clipped
* @returns
*/
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInFrame(
frameElement: ExcalidrawElement,
elements: ExcalidrawElement[],
shouldIncludeFrame: boolean = false,
): ExcalidrawElement[] {
if(!frameElement || !elements || frameElement.type !== "frame") return [];
return elements.filter(el=>el.frameId === frameElement.id);
return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
}
/**
@@ -2488,16 +2560,17 @@ export class ExcalidrawAutomate {
text,
this.style.fontSize,
this.style.fontFamily,
getDefaultLineHeight(this.style.fontFamily),
getLineHeight(this.style.fontFamily),
);
return { width: size.w ?? 0, height: size.h ?? 0 };
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available
* @param imageElement an image element from the active scene on targetView
* @param shouldWaitForImage if true, the function will wait for the image to load before returning the size
*/
async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}> {
async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getOriginalImageSize()");
@@ -2513,10 +2586,59 @@ export class ExcalidrawAutomate {
return null;
}
const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
const dataURL = ef.getImage(isDark);
let dataURL = ef.getImage(isDark);
if(!dataURL && !shouldWaitForImage) return;
if(!dataURL) {
let watchdog = 0;
while(!dataURL && watchdog < 50) {
await sleep(100);
dataURL = ef.getImage(isDark);
watchdog++;
}
if(!dataURL) return;
}
return await getImageSize(dataURL);
}
/**
* Resets the image to its original aspect ratio.
* If the image is resized then the function returns true.
* If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
* Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
* @param imageElement - the EA image element to be resized
* returns true if image was changed, false if image was not changed
*/
async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "resetImageAspectRatio()");
return null;
}
const size = await this.getOriginalImageSize(imgEl, true);
if (size) {
const originalArea = imgEl.width * imgEl.height;
const originalAspectRatio = size.width / size.height;
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
const centerX = imgEl.x + imgEl.width / 2;
const centerY = imgEl.y + imgEl.height / 2;
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
if(!this.getElement(imgEl.id)) {
this.copyViewElementsToEAforEditing([imgEl]);
}
const eaEl = this.getElement(imgEl.id);
eaEl.width = newWidth;
eaEl.height = newHeight;
eaEl.x = centerX - newWidth / 2;
eaEl.y = centerY - newHeight / 2;
return true;
}
}
return false;
}
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
@@ -2730,7 +2852,7 @@ export async function initExcalidrawAutomate(
function normalizeLinePoints(
points: [x: number, y: number][],
//box: { x: number; y: number; w: number; h: number },
) {
): number[][] {
const p = [];
const [x, y] = points[0];
for (let i = 0; i < points.length; i++) {
@@ -2739,7 +2861,9 @@ function normalizeLinePoints(
return p;
}
function getLineBox(points: [x: number, y: number][]) {
function getLineBox(
points: [x: number, y: number][]
):{x:number, y:number, w: number, h:number} {
const [x1, y1, x2, y2] = estimateLineBound(points);
return {
x: x1,
@@ -2749,27 +2873,16 @@ function getLineBox(points: [x: number, y: number][]) {
};
}
function getFontFamily(id: number) {
switch (id) {
case 1:
return "Virgil, Segoe UI Emoji";
case 2:
return "Helvetica, Segoe UI Emoji";
case 3:
return "Cascadia, Segoe UI Emoji";
case 4:
return "LocalFont";
}
function getFontFamily(id: number):string {
return getFontFamilyString({fontFamily:id})
}
export async function initFonts(doc: Document = document) {
for (let i = 1; i <= 3; i++) {
await (doc as any).fonts.load(`20px ${getFontFamily(i)}`);
}
await (doc as any).fonts.load("400 20px Assistant");
await (doc as any).fonts.load("500 20px Assistant");
await (doc as any).fonts.load("600 20px Assistant");
await (doc as any).fonts.load("700 20px Assistant");
export async function initFonts():Promise<void> {
await excalidrawLib.registerFontsInCSS();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
};
}
export function _measureText(
@@ -2777,14 +2890,14 @@ export function _measureText(
fontSize: number,
fontFamily: number,
lineHeight: number,
) {
): {w: number, h:number} {
//following odd error with mindmap on iPad while synchornizing with desktop.
if (!fontSize) {
fontSize = 20;
}
if (!fontFamily) {
fontFamily = 1;
lineHeight = getDefaultLineHeight(fontFamily);
lineHeight = getLineHeight(fontFamily);
}
const metrics = measureText(
newText,
@@ -2876,18 +2989,17 @@ async function getTemplate(
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
if(el.length > 0) {
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
}
}
if(filenameParts.hasFrameref) {
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
if(el) {
groupElements = plugin.ea.getElementsInFrame(el,scene.elements)
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
}
}
if(filenameParts.hasTaskbone) {
groupElements = groupElements.filter( el =>
el.type==="freedraw" ||
@@ -2897,6 +3009,7 @@ async function getTemplate(
}
excalidrawData.destroy();
const filehead = data.substring(0, trimLocation);
return {
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
@@ -2904,7 +3017,7 @@ async function getTemplate(
hostFile: file,
}) : groupElements,
appState: scene.appState,
frontmatter: data.substring(0, trimLocation),
frontmatter: filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead,
files: scene.files,
hasSVGwithBitmap,
};
@@ -2936,7 +3049,7 @@ export async function createPNG(
depth: number,
padding?: number,
imagesDict?: any,
) {
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
@@ -2965,6 +3078,7 @@ export async function createPNG(
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
@@ -3019,7 +3133,7 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
})
}
function addFilterToForeignObjects(svg:SVGSVGElement) {
function addFilterToForeignObjects(svg:SVGSVGElement):void {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
@@ -3044,6 +3158,9 @@ export async function createSVG(
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
if(typeof exportSettings.skipInliningFonts === "undefined") {
exportSettings.skipInliningFonts = !embedFont;
}
const template = templatePath
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
: null;
@@ -3060,6 +3177,7 @@ export async function createSVG(
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
const filenameParts = getEmbeddedFilenameParts(templatePath);
const svg = await getSVG(
{
//createAndOpenDrawing
@@ -3071,6 +3189,7 @@ export async function createSVG(
theme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
@@ -3079,6 +3198,9 @@ export async function createSVG(
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme,
isMask: exportSettings?.isMask ?? false,
...filenameParts?.hasClippedFrameref
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
: {},
},
padding,
null,
@@ -3086,9 +3208,8 @@ export async function createSVG(
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
const filenameParts = getEmbeddedFilenameParts(templatePath);
if(
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
(filenameParts.hasBlockref || filenameParts.hasSectionref)
) {
let el = filenameParts.hasSectionref
@@ -3110,7 +3231,7 @@ export async function createSVG(
if (template?.hasSVGwithBitmap) {
svg.setAttribute("hasbitmap", "true");
}
return embedFont ? embedFontsInSVG(svg, plugin) : svg;
return svg;
}
function estimateLineBound(points: any): [number, number, number, number] {
@@ -3161,7 +3282,7 @@ export function repositionElementsToCursor(
return restore({elements}, null, null).elements;
}
function errorMessage(message: string, source: string) {
function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({
@@ -3214,7 +3335,7 @@ export const search = async (view: ExcalidrawView) => {
const ea = view.plugin.ea;
ea.reset();
ea.setView(view);
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame");
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
if (elements.length === 0) {
return;
}
@@ -3308,6 +3429,62 @@ export const getFrameElementsMatchingQuery = (
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getElementsWithLinkMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: any) =>
el.link &&
query.some((q) => {
const text = el.link.toLowerCase().trim();
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getImagesMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
excalidrawData: ExcalidrawData,
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: ExcalidrawElement) =>
el.type === "image" &&
query.some((q) => {
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
const text = filename ?? equation;
if(!text) return false;
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
export const cloneElement = (el: ExcalidrawElement):any => {
const newEl = JSON.parse(JSON.stringify(el));
newEl.version = el.version + 1;

View File

@@ -11,7 +11,7 @@ import {
fileid,
DEVICE,
EMBEDDABLE_THEME_FRONTMATTER_VALUES,
getDefaultLineHeight,
getLineHeight,
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
FRONTMATTER_KEYS,
@@ -68,6 +68,27 @@ export enum AutoexportPreference {
inherit
}
export const REGEX_TAGS = {
// #[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+
// 1
EXPR: /(#[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu,
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_TAGS.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
const resultList = [];
while (!(parts = res.next()).done) {
resultList.push(parts);
}
return resultList;
},
getTag: (parts: IteratorResult<RegExpMatchArray, any>): string => {
return parts.value[1];
},
isTag: (parts: IteratorResult<RegExpMatchArray, any>): boolean => {
return parts.value[1]?.startsWith("#")
},
};
export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
// 1 2 3 4 5 67 8 9
@@ -549,7 +570,7 @@ export class ExcalidrawData {
}
if (el.type === "text" && !el.hasOwnProperty("lineHeight")) {
el.lineHeight = getDefaultLineHeight(el.fontFamily);
el.lineHeight = getLineHeight(el.fontFamily);
}
if (el.type === "image" && !el.hasOwnProperty("roundness")) {
@@ -720,6 +741,24 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
//girdSize, gridStep, previousGridSize, gridModeEnabled migration
if(this.scene.appState.hasOwnProperty("previousGridSize")) { //if previousGridSize was present this is legacy data
if(this.scene.appState.gridSize === null) {
this.scene.appState.gridSize = this.scene.appState.previousGridSize;
this.scene.appState.gridModeEnabled = false;
} else {
this.scene.appState.gridModeEnabled = true;
}
delete this.scene.appState.previousGridSize;
}
if(this.scene.appState?.gridColor?.hasOwnProperty("MajorGridFrequency")) { //if this is present, this is legacy data
if(this.scene.appState.gridColor.MajorGridFrequency>1) {
this.scene.gridStep = this.scene.appState.gridColor.MajorGridFrequency;
}
delete this.scene.appState.gridColor.MajorGridFrequency;
}
//once off migration of legacy scenes
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
const prompt = new ConfirmationPrompt(
@@ -810,7 +849,7 @@ export class ExcalidrawData {
? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
: data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
//Load Embedded files
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
const RE_ELEMENT_LINKS = /^(.{8}):\s*(.*)$/gm;
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
while (!(parts = linksRes.next()).done) {
elementLinkMap.set(parts.value[1], parts.value[2]);
@@ -887,7 +926,7 @@ export class ExcalidrawData {
? data.substring(indexOfNewEmbeddedFiles + embeddedFilesNewLength)
: data.substring(indexOfOldEmbeddedFiles + embeddedFilesOldLength);
//Load Embedded files
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s*(\{[^}]*})?\n/gm;
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\!?\[\[([^\]]*)]]\s*(\{[^}]*})?\n/gm;
res = data.matchAll(REG_FILEID_FILEPATH);
while (!(parts = res.next()).done) {
const embeddedFile = new EmbeddedFile(
@@ -1043,7 +1082,7 @@ export class ExcalidrawData {
return (
el.type !== "text" &&
el.link &&
el.link.startsWith("[[") &&
//el.link.startsWith("[[") &&
!this.elementLinks.has(el.id)
);
});
@@ -1134,8 +1173,8 @@ export class ExcalidrawData {
(el: any) =>
el.type !== "text" &&
el.id === key &&
el.link &&
el.link.startsWith("[["),
el.link, //&&
//el.link.startsWith("[["),
);
if (el.length === 0) {
this.elementLinks.delete(key); //if no longer in the scene, delete the text element
@@ -1376,10 +1415,10 @@ export class ExcalidrawData {
const element = this.scene.elements.filter((el:any)=>el.id===key);
let elementString = this.textElements.get(key).raw;
if(element && element.length===1 && element[0].link && element[0].rawText === element[0].originalText) {
if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
//if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
textElementLinks.set(key, element[0].link);
//elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
}
//}
}
outString += `${elementString} ^${key}\n\n`;
}

View File

@@ -1,8 +1,9 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { AppState, BinaryFiles, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
@@ -26,6 +27,7 @@ declare namespace ExcalidrawLib {
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
exportingFrame?: ExcalidrawFrameLikeElement | null;
getDimensions?: (
width: number,
height: number,
@@ -46,6 +48,7 @@ declare namespace ExcalidrawLib {
exportPadding?: number;
exportingFrame: ExcalidrawFrameElement | null | undefined;
renderEmbeddables?: boolean;
skipInliningFonts?: boolean;
}): Promise<SVGSVGElement>;
function sceneCoordsToViewportCoords(
@@ -116,8 +119,7 @@ declare namespace ExcalidrawLib {
lineHeight: number,
): { width: number; height: number; };
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
function getLineHeight (fontFamily: FontFamilyValues):number;
function wrapText(text: string, font: FontString, maxWidth: number): string;
function getFontString({
@@ -128,6 +130,13 @@ declare namespace ExcalidrawLib {
fontFamily: FontFamilyValues;
}): FontString;
function getFontFamilyString ({
fontFamily,
}: {
fontFamily: number;
}): string;
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
function exportToBlob(
@@ -162,6 +171,20 @@ declare namespace ExcalidrawLib {
var WelcomeScreen: any;
var TTDDialogTrigger: any;
var TTDDialog: any;
var DiagramToCodePlugin: (props: {
generate: GenerateDiagramToCode;
}) => any;
function getDataURL(file: Blob | File): Promise<DataURL>;
function destroyObsidianUtils(): void;
}
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
function getFontFamilies(): string[];
function registerFontsInCSS(): Promise<void>;
function getCSSFontDefinition(fontFamily: number): Promise<string>;
function getTextFromElements (
elements: readonly ExcalidrawElement[],
separator?: string,
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
@@ -12,7 +13,6 @@ import { ExportSettings } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {getIMGFilename,} from "./utils/FileUtils";
import {
embedFontsInSVG,
getEmbeddedFilenameParts,
getExportTheme,
getQuickImagePreview,
@@ -26,7 +26,7 @@ import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./util
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, DEBUGGING } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
@@ -39,8 +39,11 @@ interface imgElementAttributes {
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = false;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
@@ -61,8 +64,9 @@ const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
app = plugin.app;
vault = app.vault;
metadataCache = app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -75,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
@@ -86,7 +91,14 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale, isTransparent: !exportSettings.withBackground};
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.PNG,
scale,
isTransparent: !exportSettings.withBackground,
inlineFonts: true, //though for PNG this makes no difference, but the key requires it
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
@@ -105,11 +117,13 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
const png =
quickPNG ??
(await createPNG(
(filenameParts.hasGroupref || filenameParts.hasFrameref)
(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref)
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
filenameParts.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -132,6 +146,7 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
onCanvas: boolean,
}
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
let style = "";
if(imgAttributes.fwidth) {
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
@@ -163,7 +178,17 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1, isTransparent: !exportSettings.withBackground};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVGIMG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
@@ -182,13 +207,15 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
}
}
let svg = convertSVGStringToElement((
const svg = convertSVGStringToElement((
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
true,
exportSettings,
filenameParts?.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -204,7 +231,6 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
return null;
}
svg = embedFontsInSVG(svg, plugin, false);
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
@@ -220,20 +246,31 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1, isTransparent: !exportSettings.withBackground};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
let svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
false,
exportSettings,
filenameParts.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -254,7 +291,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
if(!Boolean(maybeSVG)) {
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
}
svg = embedFontsInSVG(svg, plugin, true);
svg.removeAttribute("width");
svg.removeAttribute("height");
containerElement.append(svg);
@@ -272,6 +309,7 @@ const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLImageElement | HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -319,22 +357,23 @@ const getIMG = async (
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
};
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
@@ -347,6 +386,7 @@ const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
@@ -474,6 +514,7 @@ const createImageDiv = async (
attr: imgElementAttributes,
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
@@ -482,6 +523,7 @@ const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
@@ -513,6 +555,7 @@ const processReadingMode = async (
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
const attr: imgElementAttributes = {
fname: "",
fheight: "",
@@ -549,6 +592,7 @@ const processAltText = (
alt:string,
attr: imgElementAttributes
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
@@ -568,17 +612,22 @@ const processAltText = (
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
(fnameParts.hasBlockref || fnameParts.hasSectionref)
}
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
isPrinting: boolean,
isMarkdownReadingMode: boolean,
isHoverPopover: boolean,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
@@ -596,11 +645,11 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
return;
}
@@ -628,14 +677,14 @@ const tmpObsidianWYSIWYG = async (
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
//const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
if(isHoverPopover && shouldOpenMD) {
return;
}
}
const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
//const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
@@ -647,7 +696,7 @@ const tmpObsidianWYSIWYG = async (
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
@@ -661,7 +710,7 @@ const tmpObsidianWYSIWYG = async (
} else {
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
if(warningEl) {
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=)[^ ]*)/)?.[1];
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
if(ref) {
attr.fname = file.path + ref;
areaPreview = true;
@@ -734,6 +783,7 @@ const tmpObsidianWYSIWYG = async (
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -743,12 +793,41 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
if(isPrinting && el.hasClass("mod-frontmatter")) {
return;
}
//@ts-ignore
const containerEl = ctx.containerEl;
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el);
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
//if yes, then there should be no .internal-embed containers
const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view"));
const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover"));
const isPreview = (isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes);
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if(isPrinting && plugin.settings.renderImageInMarkdownToPDF) {
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
if (!isPreview && embeddedItems.length === 0) {
if(el.hasClass("mod-frontmatter")) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId) && !el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
const isAreaGroupFrameRef = el.querySelectorAll('[data-heading^="Unable to find"]').length === 1;
if(!isAreaGroupFrameRef) {
return;
}
}
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
@@ -757,8 +836,7 @@ export const markdownPostProcessor = async (
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
const isPrinting = Boolean(document.body.querySelectorAll("body > .print"));
if (excalidrawFile && !isPrinting) {
if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) {
el.style.display = "none";
return;
}

View File

@@ -33,8 +33,20 @@ export class ScriptEngine {
this.registerEventHandlers();
}
public removeViewEAs(view: ExcalidrawView) {
const eas = new Set<ExcalidrawAutomate>();
this.eaInstances.forEach((ea) => {
if (ea.targetView === view) {
eas.add(ea);
ea.destroy();
}
});
this.eaInstances.removeObjects(eas);
}
public destroy() {
this.eaInstances.forEach((ea) => ea.destroy());
this.eaInstances.clear();
this.eaInstances = null;
this.scriptIconMap = null;
this.plugin = null;

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,5 +1,5 @@
import { customAlphabet } from "nanoid";
import { DeviceType } from "../types";
import { DeviceType } from "../types/types";
import { ExcalidrawLib } from "../ExcalidrawLib";
import { moment } from "obsidian";
import ExcalidrawPlugin from "src/main";
@@ -9,6 +9,11 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const THEME = {
LIGHT: "light",
DARK: "dark",
} as const;
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_ELEMENTLINKS = "## Element Links";
@@ -85,7 +90,7 @@ export const {
getCommonBoundingBox,
getMaximumGroups,
measureText,
getDefaultLineHeight,
getLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
@@ -94,10 +99,14 @@ export const {
mutateElement,
restore,
mermaidToExcalidraw,
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getCSSFontDefinition,
} = excalidrawLib;
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
export function JSON_parse(x: string): any {
return JSON.parse(x.replaceAll("&#91;", "["));
}

View File

@@ -223,6 +223,7 @@ function RenderObsidianView(
} else {
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
rootSplit.containerEl.addClass("mod-visible");
containerRef.current.appendChild(rootSplit.containerEl);
setColors(containerRef.current, element, mdProps, canvasColor);
}

View File

@@ -184,7 +184,7 @@ export class EmbeddableSettings extends Modal {
new Notice("File rename failed. A file with this name already exists.\n"+newPath,10000);
} else {
try {
await this.app.vault.rename(this.file,newPath);
await this.app.fileManager.renameFile(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
@@ -226,7 +226,7 @@ export class EmbeddableSettings extends Modal {
(async() => {
await this.ea.addElementsToView();
//@ts-ignore
this.ea.viewUpdateScene({appState: {}});
this.ea.viewUpdateScene({appState: {}, storeAction: "update"});
this.close(); //close should only run once update scene is done
})();
} else {

View File

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

View File

@@ -46,7 +46,9 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
}
onClose(): void {
this.addText = null;
window.setTimeout(()=>{
this.addText = null;
}) //onChooseItem must run first
super.onClose();
}
}

View File

@@ -58,7 +58,9 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
}
onClose(): void {
this.addText = null;
window.setTimeout(()=>{
this.addText = null
}); //make sure this happens after onChooseItem runs
super.onClose();
}

View File

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

View File

@@ -17,6 +17,73 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.3.0": `
I am moving to a new release approach aiming to publish one update per month to the Obsidian script store. If you want to continue to receive more frequent updates with new features and minor bug fixes, then join the beta testing team. [#1912](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1912)
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/2poSS-Z91lY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Elbow connectors: https://x.com/excalidraw/status/1819084086222393554
## Fixed
- Convert Markdown to Excalidraw did not work correctly when there was ${String.fromCharCode(96)}---${String.fromCharCode(96)} anywhere in the file, but no frontmatter (e.g. a table)
- Fixed Obsidian move tab to new window
- Fixed duplicating bound arrows without its bound elements throwing error [#8315](https://github.com/excalidraw/excalidraw/issues/8315)
`,
"2.2.13": `
## Fixed
- Could not undo element after pasting [#1906](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1906)
- Links broke after renaming an Excalidraw file using the F2 shortcut [#1907](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1907)
- Unable to open or convert very large ${String.fromCharCode(96)}.excalidraw${String.fromCharCode(96)} file, e.g. BoaPs you can download from [here](https://ko-fi.com/zsolt/shop)
`,
"2.2.12": `
## Fixed
- Rename moved files to root folder [#1905](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1905)
- Fonts not displaying correctly in cached image previews
`,
"2.2.11": `
<img alt="badges" src="https://github.com/user-attachments/assets/7591b523-6bc6-46ff-b552-5c3492139e4c" referrerpolicy="no-referrer" style="width: 100%;">
## New
- Font picker with additional fonts (not yet fully configurable, but that will come in due time) [#8012](https://github.com/excalidraw/excalidraw/pull/8012)
- Introducing Visual Thinking Badges. The more you use Excalidraw the higher your rank will be. Levels are: Bronze, Silver, Gold and Platinum.
## Fixed
- Embedded PDF was not visible on phones [#1904](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1904)
- F2 does not rename files in Excalidraw View [#1900](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1900)
- Wireframe to Code now honors the GPT model settings in plugin settings. [#1901](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1901)
- Updated ExcaliAI to support gpt-4o for vision. [#1859](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1859) 🙏@Saik0s
- Minor fixes from excalidraw.com [#8287](https://github.com/excalidraw/excalidraw/pull/8287), [#8285](https://github.com/excalidraw/excalidraw/pull/8285)
`,
"2.2.10": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/sjZfdqpxqsg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Drastically degraded rendering performance when zoomed in and when arrows with labels are used. [#8267](https://github.com/excalidraw/excalidraw/pull/8267), [#8266](https://github.com/excalidraw/excalidraw/pull/8266), [#1893](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1893)
- Frame title font in exports.
## New
- Area, Group, Frame, and Clipped-Frame references to images now also work when pasting images to Excalidraw.
- The new reference type ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} works in the same way as ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} but will display the elements clipped by the frame. ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} will always display the image with zero padding.
- New command palette action: ${String.fromCharCode(96)}Frame Settings${String.fromCharCode(96)} gives you fine-grained control over how frames are rendered. Frame settings will also be reflected in image exports. For example, if you hide the frame name or outline, then in exports they will not be visible.
`,
"2.2.9": `
## New
- Improved the "Open the back-of-the-note of the selected Excalidraw image" action. It now works with grouped elements and keeps the popout window within the visible screen area when elements are close to the top of the canvas. Note: Due to an Obsidian bug, I do not recommend using this feature with versions 1.6.0 - 1.6.6, if you have Obsidian Sync enabled, because Obsidian may freeze when closing the popout window. It functions properly in Obsidian versions before 1.6.0 and from 1.6.7 onwards.
## Fixed
- Drag and drop from a local folder (outside Obsidian) resulted in duplicate images.
- Insert Link Action did not work [#1873](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1873)
- Insert Obsidian Command Action did not work
- Element link for text element got deleted when editing the text. [#1878](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1878)
- When back-of-the-drawing Section Headings have spaces in them, clicking the link opens the drawing side not the markdown side. [#1877](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1877)
- obsidian:// links did not work as expected. [#1872](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1872)
- copying and moving a rectangle with text, moves the text unexpectedly. The issue should now be resolved (at least much less likely to occur) [#1867](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1867)
`,
"2.2.8": `
While this release may appear modest with no new features, it represents nearly 50 hours of dedicated work. Here's what's been improved:

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -100,7 +100,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "style.fontFamily",
code: "[number]",
desc: "1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont",
desc: "1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font, 5: Excalifont, 6: Nunito, 7: Lilita One, 8: Comic Shanns, 9: Liberation Sans",
after: "",
},
{
@@ -233,6 +233,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addElementsToFrame",
code: "addElementsToFrame(frameId: string, elementIDs: string[]):void;",
desc: null,
after: "",
},
{
field: "addFrame",
code: "addFrame(topX: number, topY: number, width: number, height: number, name?: string): string;",
desc: null,
after: "",
},
{
field: "addRect",
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
@@ -311,6 +323,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
field: "tex2dataURL",
code: "async tex2dataURL(tex: string, scale: number = 4): Promise<{mimeType: MimeType;fileId: FileId;dataURL: DataURL;created: number;size: { height: number; width: number };}> ",
desc: "returns the base64 dataURL of the LaTeX equation rendered as an SVG. tex is the LaTeX equation string",
after: "",
},
{
field: "connectObjects",
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
@@ -387,8 +405,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getViewSelectedElements",
code: "getViewSelectedElements(): ExcalidrawElement[];",
desc: null,
code: "getViewSelectedElements(includeFrameChildren: boolean = true): ExcalidrawElement[];",
desc: "If a frame is selected this function will return the frame and all its elements unless includeFrameChildren is set to false",
after: "",
},
{
@@ -442,7 +460,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "getExportSettings",
code: "getExportSettings(withBackground: boolean, withTheme: boolean,): ExportSettings;",
desc: "Utility function to generate ExportSettings object",
desc: "Utility function to generate ExportSettings object\n" +
"export interface ExportSettings {\n" +
" withBackground: boolean;\n" +
" withTheme: boolean;\n" +
" isMask: boolean; //if true elements will be processed as mask, clipping, etc.\n" +
" frameRendering?: { //optional, overrides relevant appState settings for rendering the frame\n" +
" enabled: boolean;\n" +
" name: boolean;\n" +
" outline: boolean;\n" +
" clip: boolean;\n" +
" };\n" +
" skipInliningFonts?: boolean;\n" +
"}",
after: "",
},
{
@@ -477,10 +507,17 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getElementsInTheSameGroupWithElement",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[], includeFrameElements: boolean = false): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.<br>" +
"If includeFrameElements is true, then if the frame is part of the group all the elements that are in the frame will also be included in the result set",
after: ""
},
{
field: "getElementsInFrame",
code: " getElementsInFrame(frameElement: ExcalidrawElement,elements: ExcalidrawElement[],shouldIncludeFrame: boolean = false,): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that are inside the frameElement. If shouldIncludeFrame is true, the frameElement will also be included in the result.",
after: "",
},
{
field: "activeScript",
code: "activeScript: string;",
@@ -513,8 +550,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getOriginalImageSize",
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>",
desc: "Returns the size of the image element at 100% (i.e. the original size). This is an async function, you need to await the result.",
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}>",
desc: "Returns the size of the image element at 100% (i.e. the original size) or undefined if the data URL is not available.\n"+
"If shouldWaitForImage is true, the function will wait for the view to load the image before returning the size.\n"+
"This is an async function, you need to await the result.",
after: "",
},
{
field: "resetImageAspectRatio",
code: "async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean>",
desc: "Resets the image to its original aspect ratio.\n" +
"If the image is resized then the function returns true.\n" +
"If the image element is not in EA (only in the view), then if the image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).\n" +
"Note you need to run await ea.addElementsToView(false); to add the modified image to the view.",
after: "",
},
{

View File

@@ -2,6 +2,7 @@ import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// English
@@ -50,17 +51,20 @@ export default {
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
COPY_ELEMENT_LINK: "Copy markdown link for selected element(s)",
COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)",
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
INSERT_LINK_TO_ELEMENT:
`Copy markdown link for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link.`,
INSERT_LINK_TO_ELEMENT_GROUP:
"Copy 'group=' markdown link for selected element to clipboard.",
"Copy 'group=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_AREA:
"Copy 'area=' markdown link for selected element to clipboard.",
"Copy 'area=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_FRAME:
"Copy 'frame=' markdown link for selected element to clipboard.",
"Copy 'frame=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
"Copy 'clippedframe=' ![[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_NORMAL:
"Copy markdown link for selected element to clipboard.",
"Copy [[link]] for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
INSERT_LINK: "Insert link to file",
@@ -76,7 +80,7 @@ export default {
ERROR_TRY_AGAIN: "Please try again.",
PASTE_CODEBLOCK: "Paste code block",
INSERT_LATEX:
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}).`,
ENTER_LATEX: "Enter a valid LaTeX expression",
READ_RELEASE_NOTES: "Read latest release notes",
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
@@ -89,14 +93,20 @@ export default {
ANNOTATE_IMAGE : "Annotate image in Excalidraw",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert active PDF page as image",
RESET_IMG_TO_100: "Set selected image element size to 100% of original",
RESET_IMG_ASPECT_RATIO: "Reset selected image element aspect ratio",
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
//ExcalidrawView.ts
NO_SEARCH_RESULT: "Didn't find a matching element in the drawing",
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
LINKLIST_SECOND_ORDER_LINK: "Second Order Link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the Embedded File link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>" +
"For markdown-page images follow this format when editing your link: <mark>filename#^blockref|WIDTHxMAXHEIGHT</mark><br>" +
"You can anchor Excalidraw images to 100% of their size by adding <code>|100%</code> to the end of the link.<br>" +
"You can change the PDF page by changing <code>#page=1</code> to <code>#page=2</code> etc.<br>" +
"PDF rect crop values are: <code>left, bottom, right, top</code>. Eg.: <code>#rect=0,0,500,500</code><br>",
FRAME_CLIPPING_ENABLED: "Frame Rendering: Enabled",
FRAME_CLIPPING_DISABLED: "Frame Rendering: Disabled",
ARROW_BINDING_INVERSE_MODE: "Inverted Mode: Default arrow binding is now disabled. Use CTRL/CMD to temporarily enable binding when needed.",
@@ -136,6 +146,7 @@ export default {
ERROR_SAVING_IMAGE: "Unknown error occurred while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
RECURSIVE_INSERT_ERROR: "You may not recursively insert part of an image into the same image as it would create an infinite loop",
CONVERT_TO_MARKDOWN: "Convert to file...",
SELECT_TEXTELEMENT_ONLY: "Select text element only (not container)",
REMOVE_LINK: "Remove text element link",
@@ -226,7 +237,7 @@ export default {
"The default OpenAI API URL. This is a freetext field, so you can enter any valid OpenAI API compatible URL. " +
"Excalidraw will use this URL when posting API requests to OpenAI. I am not doing any error handling on this field, so make sure you enter a valid URL and only change this if you know what you are doing. ",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4-vision-preview",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4o",
SAVING_HEAD: "Saving",
SAVING_DESC: "In the 'Saving' section of Excalidraw Settings, you can configure how your drawings are saved. This includes options for compressing Excalidraw JSON in Markdown, setting autosave intervals for both desktop and mobile, defining filename formats, and choosing whether to use the .excalidraw.md or .md file extension. ",
COMPRESS_NAME: "Compress Excalidraw JSON in Markdown",
@@ -318,24 +329,29 @@ FILENAME_HEAD: "Filename",
DEFAULT_PEN_MODE_NAME: "Pen mode",
DEFAULT_PEN_MODE_DESC:
"Should pen mode be automatically enabled when opening Excalidraw?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
"The effect depends on the device. Crosshair is typically visible on drawing tablets, MS Surface, but not on iOS.",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render image in hover preview for MD files",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render Excalidraw file as an image in hover preview...",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"This setting effects files that have the <b>excalidraw-open-md: true</b> frontmatter key.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
"markdown side of the document.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render as image when in markdown reading mode of an Excalidraw file",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"Must close the active excalidraw/markdown file and reopen it for this change to take effect.<br>When you are in markdown reading mode (aka. reading the back side of the drawing), should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode, when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <b>PDF Export</b> under 'Embedding and Exporting' further below.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render image when EXPORT TO PDF in markdown mode",
"When you are in markdown reading mode (aka. reading the back side of the drawing) should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <a href='#"+TAG_PDFEXPORT+"'>PDF Export</a> under 'Embedding and Exporting' further below.</li></ul><br>" +
"You must close the active excalidraw/markdown file and reopen it for this change to take effect.",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render the file as an image when exporting an Excalidraw file to PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"Must close the active excalidraw/markdown file and reopen for this change to take effect.<br>When you are printing the markdown side of the note to PDF (aka. the back side of the drawing), should the Excalidraw drawing be rendered as an image?<br><ul>" +
"<li>See other related setting for <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
"This setting controls the behavior of Excalidraw when exporting an Excalidraw file to PDF in markdown view mode using Obsidian's <b>Export to PDF</b> feature.<br>" +
"<ul><li>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -522,7 +538,7 @@ FILENAME_HEAD: "Filename",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"If found, use the already exported image for preview",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"This setting works in conjunction with the Auto-export SVG/PNG setting. If an exported image that matches the file name of the drawing " +
"This setting works in conjunction with the <a href='#"+TAG_AUTOEXPORT+"'>Auto-export SVG/PNG</a> setting. If an exported image that matches the file name of the drawing " +
"is available, use that image instead of generating a preview image on the fly. This will result in faster previews especially when you have many embedded objects in the drawing, however, " +
"it may happen that your latest changes are not displayed and that the image will not automatically match your Obsidian theme in case you have changed the " +
"Obsidian theme since the export was created. This setting only applies to embedding images into markdown documents. " +
@@ -553,7 +569,7 @@ FILENAME_HEAD: "Filename",
EMBED_TYPE_NAME: "Type of file to insert into the document",
EMBED_TYPE_DESC:
"When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original Excalidraw file " +
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"or a PNG or an SVG copy. You need to enable <a href='#"+TAG_AUTOEXPORT+"'>auto-export PNG / SVG</a> (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
@@ -571,7 +587,7 @@ FILENAME_HEAD: "Filename",
"If turned off, the exported image will be transparent.",
EXPORT_PADDING_NAME: "Image Padding",
EXPORT_PADDING_DESC:
"The padding (in pixels) around the exported SVG or PNG image. " +
"The padding (in pixels) around the exported SVG or PNG image. Padding is set to 0 for clippedFrame references." +
"If you have curved lines close to the edge of the image they might get cropped during image export. You can increase this value to avoid cropping. " +
"You can also override this setting at a file level by adding the <code>excalidraw-export-padding: 5<code> frontmatter key.",
EXPORT_THEME_NAME: "Export image with theme",
@@ -643,7 +659,7 @@ FILENAME_HEAD: "Filename",
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a fourth font option, which adds a fourth font button to the properties panel for text elements. `,
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a local font option, which adds a local font to the list of fonts on the element properties panel for text elements. `,
RENDER_TWEAK_HEAD: "Rendering tweaks",
MAX_IMAGE_ZOOM_IN_NAME: "Maximum image zoom in resolution",
MAX_IMAGE_ZOOM_IN_DESC: "To save on memory and because Apple Safari (Obsidian on iOS) has some hard-coded limitations, Excalidraw.com limits the max resolution of images and large objects when zooming in. You can override this limitation using a multiplicator. " +
@@ -693,16 +709,16 @@ FILENAME_HEAD: "Filename",
"Enabling this feature simplifies the use of Excalidraw front matter properties, allowing you to leverage many powerful settings. If you prefer not to load these properties automatically, " +
"you can disable this feature, but you will need to manually remove any unwanted properties from the suggester. " +
"Note that turning on this setting requires restarting the plugin as properties are loaded at startup.",
CUSTOM_FONT_HEAD: "Fourth font",
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
CUSTOM_FONT_HEAD: "Local font",
ENABLE_FOURTH_FONT_NAME: "Enable local font option",
ENABLE_FOURTH_FONT_DESC:
"By turning this on, you will see a fourth font button on the properties panel for text elements. " +
"Files that use this fourth font will (partly) lose their platform independence. " +
"By turning this on, you will see a local font in the font list on the properties panel for text elements. " +
"Files that use this local font will (partly) lose their platform independence. " +
"Depending on the custom font set in settings, they will look differently when loaded in another vault, or at a later time. " +
"Also the 4th font will display as system default font on excalidraw.com, or other Excalidraw versions.",
FOURTH_FONT_NAME: "Fourth font file",
FOURTH_FONT_NAME: "Local font file",
FOURTH_FONT_DESC:
"Select a .ttf, .woff or .woff2 font file from your vault to use as the fourth font. " +
"Select a .ttf, .woff or .woff2 font file from your vault to use as the local font. " +
"If no file is selected, Excalidraw will use the Virgil font by default.",
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
@@ -763,7 +779,7 @@ FILENAME_HEAD: "Filename",
TOGGLE_FRAME_RENDERING: "Toggle frame rendering",
TOGGLE_FRAME_CLIPPING: "Toggle frame clipping",
OPEN_LINK_CLICK: "Open Link",
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
OPEN_LINK_PROPS: "Open the image-link or LaTeX-formula editor",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "Narrow to heading...",
@@ -824,4 +840,23 @@ FILENAME_HEAD: "Filename",
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
PANE_TARGET: "Link click behavior",
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
//FrameSettings.ts
FRAME_SETTINGS_TITLE: "Frame Settings",
FRAME_SETTINGS_ENABLE: "Enable Frames",
FRAME_SETTIGNS_NAME: "Display Frame Name",
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "Pages to import",
IPM_SELECT_PAGES_TO_IMPORT: "Please select pages to import",
IPM_ADD_BORDER_BOX_NAME: "Add border box",
IPM_ADD_FRAME_NAME: "Add page to frame",
IPM_ADD_FRAME_DESC: "For easier handling I recommend to lock the page inside the frame. " +
"If, however, you do lock the page inside the frame then the only way to unlock it is to right-click the frame, select remove elements from frame, then unlock the page.",
IPM_GROUP_PAGES_NAME: "Group pages",
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
IPM_SELECT_PDF: "Please select a PDF file",
};

View File

@@ -2,6 +2,7 @@ import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// 简体中文
@@ -50,15 +51,18 @@ export default {
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LOCK: "文本元素原文模式RAW⟺ 预览模式PREVIEW",
DELETE_FILE: "从库中删除所选图像(或以图像形式嵌入绘图中的 Markdown的源文件",
COPY_ELEMENT_LINK: "复制所选元素的 Markdown 链接",
COPY_ELEMENT_LINK: "复制所选元素的链接(形如 [[file#^id]]]",
COPY_DRAWING_LINK: "复制绘图的嵌入链接(形如 ![[darwing]]",
INSERT_LINK_TO_ELEMENT:
`复制所选元素为内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组为内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )。\n按住 ${labelALT()} 可观看视频演示。`,
INSERT_LINK_TO_ELEMENT_GROUP:
"复制所选元素所在分组为内部链接(形如 [[file#^group=id]] ",
"复制所选元素所在分组为嵌入链接(形如 ![[file#^group=id]] ",
INSERT_LINK_TO_ELEMENT_AREA:
"复制所选元素所在区域为内部链接(形如 [[file#^area=id]] ",
"复制所选元素所在区域为嵌入链接(形如 ![[file#^area=id]] ",
INSERT_LINK_TO_ELEMENT_FRAME:
"复制所选框架为内部链接(形如 [[file#^frame=id]] ",
"复制所选框架为嵌入链接(形如 ![[file#^frame=id]] ",
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
"复制所选框架(内容)为嵌入链接(形如 ![[file#^clippedframe=id]] ",
INSERT_LINK_TO_ELEMENT_NORMAL:
"复制所选元素为内部链接(形如 [[file#^id]] ",
INSERT_LINK_TO_ELEMENT_ERROR: "未选择画布里的单个元素",
@@ -136,6 +140,7 @@ export default {
ERROR_SAVING_IMAGE: "获取图像时发生未知错误。可能是由于某种原因,图像不可用或拒绝了 Obsidian 的获取请求。",
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
USE_INSERT_FILE_MODAL: "使用“插入任意文件”功能来嵌入 Markdown 文档",
RECURSIVE_INSERT_ERROR: "你不能将图像的一部分嵌入到此图像中,因为这可能导致无限循环。",
CONVERT_TO_MARKDOWN: "转存为 Markdown 文档(并嵌入为 MD-Embeddable",
SELECT_TEXTELEMENT_ONLY: "只选择文本元素(非容器)",
REMOVE_LINK: "移除文字元素链接",
@@ -226,7 +231,7 @@ export default {
"默认的 OpenAI API URL。请填写有效的 OpenAI API URL。" +
"Excalidraw 会通过该 URL 发送 API 请求给 OpenAI。我没有对此选项做任何错误处理请谨慎修改。",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI 图像生成 API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4-vision-preview",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "输入你的默认 AI 模型名称例如gpt-4o",
SAVING_HEAD: "保存",
SAVING_DESC: "包括:压缩,自动保存的时间间隔,文件的命名格式和扩展名等的设置。",
COMPRESS_NAME: "压缩 Excalidraw JSON",
@@ -318,24 +323,29 @@ FILENAME_HEAD: "文件名",
DEFAULT_PEN_MODE_NAME: "触控笔模式Pen mode",
DEFAULT_PEN_MODE_DESC:
"打开绘图时,是否自动开启触控笔模式?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "启用手写模式下的双击橡皮擦功能",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在 Markdown 文件的悬停预览中渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"这个设置影响 frontmatter 中具有 <b>excalidraw-open-md: true</b> 的文件。",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "在 Markdown 文件阅读模式下渲染为图片",
"...即使文件具有 `<b>excalidraw-open-md: true</b>` frontmatter 属性。<br>" +
"当此设置关闭且文件默认设置为以 md 格式打开时,悬停预览将显示文档的 Markdown 部分(背景笔记)。" +
"",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Excalidraw 文件在 Markdown 阅读模式下渲染为图片",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时 Excalidraw 绘图是否应该呈现为图像? " +
"此设置不会影响您处于 Excalidraw 模式的绘图显示,也不会影响将绘图嵌入 Markdown 文档或在渲染悬停预览时的显示。<br><ul>" +
"<li>看下面的“嵌入和导出”中的 <b>PDF 导出</b>的其他相关设置。</li>" +
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在 Markdown 模式下导出为 PDF 时渲染为图",
"当您处于 Markdown 阅读模式(即查看绘图的背景笔记)时Excalidraw 绘图是否应该渲染为图像" +
"此设置不会影响您 Excalidraw 模式的绘图显示,或者在将绘图嵌入 Markdown 文档时,或在渲染悬停预览时。<br><ul>" +
"<li>请参阅下面‘嵌入和导出’部分的 <a href='#"+TAG_PDFEXPORT+"'>PDF 导出</a> 相关设置。</li></ul><br>" +
"您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在将 Excalidraw 文件导出为 PDF 时将文件渲染为图",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时将笔记导出为 PDFExcalidraw 绘图是否应该呈现为图像? <br><ul>" +
"<li>查看上面“外观和行为”下的 <b>Markdown 阅读模式</b>的其他相关设置。</li>" +
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
"处于 Markdown 视图模式时,此设置控制 Excalidraw 在使用 Obsidian 的 <b>导出为 PDF</b> 功能时,将 Excalidraw 文件导出为 PDF 的行为。<br>" +
"<ul><li>当 <b>启用</b> 时PDF 将仅显示 Excalidraw 绘图;</li>" +
"<li>当 <b>禁用</b> 时PDF 将显示文档的 Markdown 部分(背景笔记)。</li></ul>" +
"请参阅上面‘外观和行为’部分的 <<a href='#"+TAG_MDREADINGMODE+"'>>Markdown 阅读模式</a> 相关设置。" +
"⚠️ 注意,您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。⚠️",
THEME_HEAD: "主题和样式",
ZOOM_HEAD: "缩放",
DEFAULT_PINCHZOOM_NAME: "允许在触控笔模式下进行双指缩放",
@@ -522,7 +532,7 @@ FILENAME_HEAD: "文件名",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"将之前已导出的图像作为预览图",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"该选项与自动导出 SVG/PNG 副本选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项与<a href='#"+TAG_AUTOEXPORT+"'>自动导出 SVG/PNG 副本</a>选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项能够提高 Markdown 文档的打开速度,尤其是当嵌入到 Markdown 文档中的绘图文件中含有大量图像或 MD-Embed 时。" +
"但是,该选项也可能导致预览图无法立即响应你对绘图文件或者 Obsidian 主题风格的修改。<br>" +
"该选项仅作用于嵌入到 Markdown 文档中的绘图。" +
@@ -553,7 +563,7 @@ FILENAME_HEAD: "文件名",
EMBED_TYPE_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令的源文件类型",
EMBED_TYPE_DESC:
"在命令面板中执行“嵌入绘图到当前 Markdown 文档中”系列命令时,要嵌入绘图文件本身,还是嵌入其 PNG 或 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的<a href='#"+TAG_AUTOEXPORT+"'>自动导出 PNG / SVG 副本</a>。<br>" +
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
@@ -571,7 +581,7 @@ FILENAME_HEAD: "文件名",
"如果关闭,将导出透明背景的图像。",
EXPORT_PADDING_NAME: "导出的图像的空白边距",
EXPORT_PADDING_DESC:
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。<br>" +
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。对于裁剪框架引用,间距被设置为 0。<br>" +
"增加该值,可以避免在导出图像时,靠近图像边缘的图形被裁掉。<br>" +
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>excalidraw-export-padding: 5<code> 的键值对。",
EXPORT_THEME_NAME: "导出的图像匹配主题",
@@ -602,7 +612,7 @@ FILENAME_HEAD: "文件名",
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "若开启Excalidraw 将导出两个文件filename.dark.png或 filename.dark.svg和 filename.light.png或 filename.light.svg。<br>"+
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
COMPATIBILITY_HEAD: "兼容性设置",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 Markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "代码格式化Linting兼容性",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw 对 <code># Excalidraw Data</code> 下的文件结构非常敏感。文档的自动代码格式化linting可能会在 Excalidraw 数据中造成错误。" +
"虽然我已经努力使数据加载对自动代码格式化linting变更具有一定的抗性但这种解决方案并非万无一失。<br>"+
@@ -642,7 +652,7 @@ FILENAME_HEAD: "文件名",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
LATEX_DEFAULT_DESC: "允许留空。允许使用类似 <code>\\color{white}</code> 的格式化表达式。",
NONSTANDARD_HEAD: "非 Excalidraw.com 官方支持的特性",
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果 Excalidraw.com 格式导出绘图,这些特性将会发生不可预知的变化。
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果 Excalidraw.com 导入绘图,这些特性将会发生不可预知的变化。
包括:自定义画笔工具的数量,自定义字体等。`,
RENDER_TWEAK_HEAD: "渲染优化",
MAX_IMAGE_ZOOM_IN_NAME: "最大图像放大倍数",
@@ -693,7 +703,7 @@ FILENAME_HEAD: "文件名",
"启用此功能简化了 Excalidraw 前置属性的使用,使您能够利用许多强大的设置。如果您不希望自动加载这些属性," +
"您可以禁用此功能,但您将需要手动从自动提示中移除任何不需要的属性。" +
"请注意,启用此设置需要重启插件,因为属性是在启动时加载的。",
CUSTOM_FONT_HEAD: "自定义字体",
CUSTOM_FONT_HEAD: "本地字体",
ENABLE_FOURTH_FONT_NAME: "为文本元素启用本地字体",
ENABLE_FOURTH_FONT_DESC:
"开启此项后,文本元素的属性面板里会多出一个本地字体按钮。<br>" +
@@ -763,7 +773,7 @@ FILENAME_HEAD: "文件名",
TOGGLE_FRAME_RENDERING: "开启或关闭框架渲染",
TOGGLE_FRAME_CLIPPING: "开启或关闭框架裁剪",
OPEN_LINK_CLICK: "打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "编辑所选 MD-Embed 的内部链接,或者打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "打开 markdown-embed 属性或 LaTeX 编辑器,或在新窗口中打开链接",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "缩放至标题",
@@ -824,4 +834,23 @@ FILENAME_HEAD: "文件名",
INTERNAL_DRAG_ACTION: "在 Obsidian 内部拖放时",
PANE_TARGET: "点击链接时",
DEFAULT_ACTION_DESC: "无修饰键时的行为:",
};
//FrameSettings.ts
FRAME_SETTINGS_TITLE: "框架设置",
FRAME_SETTINGS_ENABLE: "启用框架",
FRAME_SETTIGNS_NAME: "显示框架名称",
FRAME_SETTINGS_OUTLINE: "显示框架外边框",
FRAME_SETTINGS_CLIP: "启用框架裁剪",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "要导入的页面",
IPM_SELECT_PAGES_TO_IMPORT: "请选择页面以进行导入",
IPM_ADD_BORDER_BOX_NAME: "添加带边框的盒子容器",
IPM_ADD_FRAME_NAME: "添加页面到框架",
IPM_ADD_FRAME_DESC: "为了更方便的操作,我建议将页面锁定在框架内。" +
"如果,你将锁定页面在框架内,则唯一的解锁方法是右键点击框架,选择‘从框架中移除元素’,然后解锁页面。",
IPM_GROUP_PAGES_NAME: "建立页面组",
IPM_GROUP_PAGES_DESC: "这将把所有页面建立为一个单独的组。如果您在导入后锁定页面,建议使用此方法,因为这样可以更方便地解锁整个组,而不是逐个解锁。",
IPM_SELECT_PDF: "请选择一个 PDF 文件",
};

View File

@@ -43,13 +43,9 @@ import {
IMAGE_TYPES,
setExcalidrawPlugin,
DEVICE,
sceneCoordsToViewportCoords
} from "./constants/constants";
import {
VIRGIL_FONT,
VIRGIL_DATAURL,
sceneCoordsToViewportCoords,
FONTS_STYLE_ID,
} from "./constants/constFonts";
} from "./constants/constants";
import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
import {
changeThemeOfExcalidrawMD,
@@ -103,8 +99,9 @@ import {
decompress,
getImageSize,
versionUpdateCheckTimer,
getFontMetrics,
} from "./utils/Utils";
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf, setExcalidrawView } from "./utils/ObsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ScriptEngine } from "./Scripts";
import {
@@ -116,7 +113,7 @@ import {
import { FieldSuggester } from "./dialogs/FieldSuggester";
import { ReleaseNotes } from "./dialogs/ReleaseNotes";
import { Packages } from "./types";
import { Packages } from "./types/types";
import { PreviewImageType } from "./utils/UtilTypes";
import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
import Taskbone from "./ocr/Taskbone";
@@ -137,14 +134,19 @@ import { carveOutImage, carveOutPDF, createImageCropperFile } from "./utils/Carv
import { ExcalidrawConfig } from "./utils/ExcalidrawConfig";
import { EditorHandler } from "./CodeMirrorExtension/EditorHandler";
import { clearMathJaxVariables } from "./LaTeX";
import { showFrameSettings } from "./dialogs/FrameSettings";
import { ExcalidrawLib } from "./ExcalidrawLib";
import { Rank, SwordColors } from "./menu/ActionIcons";
import { RankMessage } from "./dialogs/RankMessage";
declare let EXCALIDRAW_PACKAGES:string;
declare let react:any;
declare let reactDOM:any;
declare let excalidrawLib: any;
declare let excalidrawLib: typeof ExcalidrawLib;
declare let PLUGIN_VERSION:string;
export default class ExcalidrawPlugin extends Plugin {
public fourthFontLoaded: boolean = false;
public excalidrawConfig: ExcalidrawConfig;
public taskbone: Taskbone;
private excalidrawFiles: Set<TFile> = new Set<TFile>();
@@ -177,7 +179,6 @@ export default class ExcalidrawPlugin extends Plugin {
public equationsMaster: Map<FileId, string> = null; //fileId, formula
public mermaidsMaster: Map<FileId, string> = null; //fileId, mermaidText
public scriptEngine: ScriptEngine;
public fourthFontDef: string = VIRGIL_FONT;
private packageMap: Map<Window,Packages> = new Map<Window,Packages>();
public leafChangeTimeout: number = null;
private forceSaveCommand:Command;
@@ -186,8 +187,9 @@ export default class ExcalidrawPlugin extends Plugin {
public editorHandler: EditorHandler;
//if set, the next time this file is opened it will be opened as markdown
public forceToOpenInMarkdownFilepath: string = null;
private slob:string;
//private slob:string;
private ribbonIcon:HTMLElement;
public loadTimestamp:number;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -199,9 +201,9 @@ export default class ExcalidrawPlugin extends Plugin {
this.equationsMaster = new Map<FileId, string>();
this.mermaidsMaster = new Map<FileId, string>();
setExcalidrawPlugin(this);
if((process.env.NODE_ENV === 'development')) {
/*if((process.env.NODE_ENV === 'development')) {
this.slob = new Array(200 * 1024 * 1024 + 1).join('A'); // Create a 200MB blob
}
}*/
}
get locale() {
@@ -309,14 +311,23 @@ export default class ExcalidrawPlugin extends Plugin {
}*/
async onload() {
this.loadTimestamp = Date.now();
addIcon(ICON_NAME, EXCALIDRAW_ICON);
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON);
await this.loadSettings({reEnableAutosave:true});
const updateSettings = !this.settings.onceOffCompressFlagReset || !this.settings.onceOffGPTVersionReset;
if(!this.settings.onceOffCompressFlagReset) {
this.settings.compress = true;
this.settings.onceOffCompressFlagReset = true;
}
if(!this.settings.onceOffGPTVersionReset) {
if(this.settings.openAIDefaultVisionModel === "gpt-4-vision-preview") {
this.settings.openAIDefaultVisionModel = "gpt-4o";
}
}
if(updateSettings) {
await this.saveSettings();
}
this.excalidrawConfig = new ExcalidrawConfig(this);
@@ -368,11 +379,10 @@ export default class ExcalidrawPlugin extends Plugin {
this.switchToExcalidarwAfterLoad();
const self = this;
this.app.workspace.onLayoutReady(() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.onload,"ExcalidrawPlugin.onload > app.workspace.onLayoutReady");
this.scriptEngine = new ScriptEngine(self);
imageCache.initializeDB(self);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload,"ExcalidrawPlugin.onload > app.workspace.onLayoutReady");
this.scriptEngine = new ScriptEngine(this);
imageCache.initializeDB(this);
});
this.taskbone = new Taskbone(this);
}
@@ -380,9 +390,8 @@ export default class ExcalidrawPlugin extends Plugin {
private setPropertyTypes() {
if(!this.settings.loadPropertySuggestions) return;
const app = this.app;
const self = this;
this.app.workspace.onLayoutReady(() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes > app.workspace.onLayoutReady`);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes > app.workspace.onLayoutReady`);
Object.keys(FRONTMATTER_KEYS).forEach((key) => {
if(FRONTMATTER_KEYS[key].depricated === true) return;
const {name, type} = FRONTMATTER_KEYS[key];
@@ -392,28 +401,51 @@ export default class ExcalidrawPlugin extends Plugin {
}
public initializeFonts() {
const self = this;
this.app.workspace.onLayoutReady(async () => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.initializeFonts,`ExcalidrawPlugin.initializeFonts > app.workspace.onLayoutReady`);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeFonts, `ExcalidrawPlugin.initializeFonts > app.workspace.onLayoutReady`);
const font = await getFontDataURL(
this.app,
this.settings.experimantalFourthFont,
"",
"LocalFont",
"Local Font",
);
const fourthFontDataURL =
font.dataURL === "" ? VIRGIL_DATAURL : font.dataURL;
this.fourthFontDef = font.fontDef;
this.getOpenObsidianDocuments().forEach((ownerDocument) => {
this.addFonts([
`@font-face{font-family:'LocalFont';src:url("${fourthFontDataURL}");font-display: swap;`,
],ownerDocument);
})
if(font.dataURL === "") {
this.fourthFontLoaded = true;
return;
}
const fourthFontDataURL = font.dataURL;
const f = this.app.metadataCache.getFirstLinkpathDest(this.settings.experimantalFourthFont, "");
// Call getFontMetrics with the fourthFontDataURL
let fontMetrics = f.extension.startsWith("woff") ? undefined : await getFontMetrics(fourthFontDataURL, "Local Font");
if (!fontMetrics) {
console.log("Font Metrics not found, using default");
fontMetrics = {
unitsPerEm: 1000,
ascender: 750,
descender: -250,
lineHeight: 1.2,
fontName: "Local Font",
}
}
this.packageMap.forEach(({excalidrawLib}) => {
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any, icon: null}, fourthFontDataURL);
});
// Add fonts to open Obsidian documents
for(const ownerDocument of this.getOpenObsidianDocuments()) {
await this.addFonts([
`@font-face{font-family:'Local Font';src:url("${fourthFontDataURL}");font-display: swap;font-weight: 400;`,
], ownerDocument);
};
if(!this.fourthFontLoaded) setTimeout(()=>{this.fourthFontLoaded = true},100);
});
}
public addFonts(declarations: string[],ownerDocument:Document = document) {
public async addFonts(declarations: string[],ownerDocument:Document = document) {
// replace the old local font <style> element with the one we just created
const newStylesheet = ownerDocument.createElement("style");
newStylesheet.id = FONTS_STYLE_ID;
@@ -423,7 +455,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (oldStylesheet) {
ownerDocument.head.removeChild(oldStylesheet);
}
ownerDocument.fonts.load('20px LocalFont');
await ownerDocument.fonts.load('20px Local Font');
}
public removeFonts() {
@@ -448,16 +480,15 @@ export default class ExcalidrawPlugin extends Plugin {
}
private switchToExcalidarwAfterLoad() {
const self = this;
this.app.workspace.onLayoutReady(() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.switchToExcalidarwAfterLoad, `ExcalidrawPlugin.switchToExcalidarwAfterLoad > app.workspace.onLayoutReady`);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.switchToExcalidarwAfterLoad, `ExcalidrawPlugin.switchToExcalidarwAfterLoad > app.workspace.onLayoutReady`);
let leaf: WorkspaceLeaf;
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
if ( leaf.view instanceof MarkdownView && self.isExcalidrawFile(leaf.view.file)) {
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)) {
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
if ( leaf.view instanceof MarkdownView && this.isExcalidrawFile(leaf.view.file)) {
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, this.app)) {
this.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
VIEW_TYPE_EXCALIDRAW;
self.setExcalidrawView(leaf);
setExcalidrawView(leaf);
} else {
foldExcalidrawSection(leaf.view);
}
@@ -682,16 +713,15 @@ export default class ExcalidrawPlugin extends Plugin {
initializeMarkdownPostProcessor(this);
this.registerMarkdownPostProcessor(markdownPostProcessor);
const self = this;
this.app.workspace.onLayoutReady(() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.addMarkdownPostProcessor, `ExcalidrawPlugin.addMarkdownPostProcessor > app.workspace.onLayoutReady`);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addMarkdownPostProcessor, `ExcalidrawPlugin.addMarkdownPostProcessor > app.workspace.onLayoutReady`);
// internal-link quick preview
self.registerEvent(self.app.workspace.on("hover-link", hoverEvent));
this.registerEvent(this.app.workspace.on("hover-link", hoverEvent));
//only add the legacy file observer if there are legacy files in the vault
if(self.app.vault.getFiles().some(f=>f.extension === "excalidraw")) {
self.enableLegacyFilePopoverObserver();
if(this.app.vault.getFiles().some(f=>f.extension === "excalidraw")) {
this.enableLegacyFilePopoverObserver();
}
});
}
@@ -722,10 +752,9 @@ export default class ExcalidrawPlugin extends Plugin {
const darkClass = bodyClassList.contains('theme-dark');
if (mutation?.oldValue?.includes('theme-dark') === darkClass) return;
const self = this;
setTimeout(()=>{ //run async to avoid blocking the UI
const theme = isObsidianThemeDark() ? "dark" : "light";
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf: WorkspaceLeaf) => {
const excalidrawView = leaf.view as ExcalidrawView;
if (excalidrawView.file && excalidrawView.excalidrawAPI) {
@@ -807,13 +836,12 @@ export default class ExcalidrawPlugin extends Plugin {
? new CustomMutationObserver(fileExplorerObserverFn, "fileExplorerObserver")
: new MutationObserver(fileExplorerObserverFn);
const self = this;
this.app.workspace.onLayoutReady(() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay > app.workspace.onLayoutReady`);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay > app.workspace.onLayoutReady`);
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
const container = document.querySelector(".nav-files-container");
if (container) {
self.fileExplorerObserver.observe(container, {
this.fileExplorerObserver.observe(container, {
childList: true,
subtree: true,
});
@@ -1377,6 +1405,42 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "frame-settings",
name: t("FRAME_SETTINGS_TITLE"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
);
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
showFrameSettings(getEA(view));
return true;
}
return false;
},
});
this.addCommand({
id: "copy-link-to-drawing",
name: t("COPY_DRAWING_LINK"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
);
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
navigator.clipboard.writeText(`![[${view.file.path}]]`);
return true;
}
return false;
},
});
this.addCommand({
id: "disable-frameclipping",
name: t("TOGGLE_FRAME_CLIPPING"),
@@ -1586,7 +1650,7 @@ export default class ExcalidrawPlugin extends Plugin {
name: t("INSERT_LINK_TO_ELEMENT_FRAME"),
checkCallback: (checking: boolean) => {
if (checking) {
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
@@ -1597,6 +1661,22 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "insert-link-to-element-frame-clipped",
name: t("INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED"),
checkCallback: (checking: boolean) => {
if (checking) {
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
view.copyLinkToSelectedElementToClipboard("clippedframe=");
return true;
}
return false;
},
});
this.addCommand({
id: "insert-link-to-element-area",
name: t("INSERT_LINK_TO_ELEMENT_AREA"),
@@ -1647,29 +1727,35 @@ export default class ExcalidrawPlugin extends Plugin {
id: "flip-image",
name: t("FLIP_IMAGE"),
checkCallback: (checking:boolean) => {
if (!DEVICE.isDesktop) return;
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if(!view) return false;
if(!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
const els = view
.getViewSelectedElements()
.filter(el=>{
if(el.type==="image") {
const ef = view.excalidrawData.getFile(el.fileId);
if(!ef) {
return false;
}
return this.isExcalidrawFile(ef.file);
}
return false;
});
if(els.length !== 1) {
return false;
}
if(checking) return true;
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
if(!ef) {
return false;
}
if(!this.isExcalidrawFile(ef.file)) {
return false;
}
if(checking) return true;
this.forceToOpenInMarkdownFilepath = ef.file?.path;
const appState = view.excalidrawAPI.getAppState();
const {x:centerX,y:centerY} = sceneCoordsToViewportCoords({sceneX:el.x+el.width/2,sceneY:el.y+el.height/2},appState);
const {width, height} = {width:600, height:600};
const {x,y} = {
x:centerX - width/2 + view.ownerWindow.screenX,
y:centerY - height/2 + view.ownerWindow.screenY,
x:Math.max(0,centerX - width/2 + view.ownerWindow.screenX),
y:Math.max(0,centerY - height/2 + view.ownerWindow.screenY),
}
this.openDrawing(ef.file, DEVICE.isMobile ? "new-tab":"popout-window", true, undefined, false, {x,y,width,height});
@@ -1708,12 +1794,80 @@ export default class ExcalidrawPlugin extends Plugin {
const eaEl = ea.getElement(el.id);
//@ts-ignore
eaEl.width = size.width; eaEl.height = size.height;
ea.addElementsToView(false,false,false);
await ea.addElementsToView(false,false,false);
}
ea.destroy();
})()
}
})
this.addCommand({
id: "reset-image-ar",
name: t("RESET_IMG_ASPECT_RATIO"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (!view) return false;
if (!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el => el.type === "image");
if (els.length !== 1) {
if (checking) return false;
new Notice("Select a single image element and try again");
return false;
}
if (checking) return true;
(async () => {
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
if (!ef) {
await view.forceSave();
let ef = view.excalidrawData.getFile(el.fileId);
new Notice("Select a single image element and try again");
return false;
}
const ea = new ExcalidrawAutomate(this, view);
if (await ea.resetImageAspectRatio(el)) {
await ea.addElementsToView(false, false, false);
}
ea.destroy();
})();
}
});
this.addCommand({
id: "open-link-props",
name: t("OPEN_LINK_PROPS"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (!view) return false;
if (!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el => el.type === "image");
if (els.length !== 1) {
if (checking) return false;
new Notice("Select a single image element and try again");
return false;
}
if (checking) return true;
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
let eq = view.excalidrawData.getEquation(el.fileId);
if (!ef && !eq) {
view.forceSave();
new Notice("Please try again.");
return false;
}
if(ef) {
view.openEmbeddedLinkEditor(el.id);
}
if(eq) {
view.openLaTeXEditor(el.id);
}
}
});
this.addCommand({
id: "convert-card-to-file",
name: t("CONVERT_CARD_TO_FILE"),
@@ -2291,7 +2445,7 @@ export default class ExcalidrawPlugin extends Plugin {
const activeLeaf = markdownView.leaf;
this.excalidrawFileModes[(activeLeaf as any).id || activeFile.path] =
VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(activeLeaf);
setExcalidrawView(activeLeaf);
})()
return;
}
@@ -2326,7 +2480,7 @@ export default class ExcalidrawPlugin extends Plugin {
activeFile,
mergedTarget,
);
this.setExcalidrawView(activeView.leaf);
setExcalidrawView(activeView.leaf);
})();
},
});
@@ -2364,7 +2518,7 @@ export default class ExcalidrawPlugin extends Plugin {
log(fname);
const result = await this.app.vault.create(
fname,
FRONTMATTER + (await this.exportSceneToMD(data)),
FRONTMATTER + (await this.exportSceneToMD(data, false)),
);
if (this.settings.keepInSync) {
EXPORT_TYPES.forEach((ext: string) => {
@@ -2452,7 +2606,7 @@ export default class ExcalidrawPlugin extends Plugin {
await view.save();
//@ts-ignore
this.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(leaf);
setExcalidrawView(leaf);
}));
},
),
@@ -2477,7 +2631,7 @@ export default class ExcalidrawPlugin extends Plugin {
await view.save();
//@ts-ignore
this.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(leaf);
setExcalidrawView(leaf);
})});
//@ts-ignore
menu.items.unshift(menu.items.pop());
@@ -2555,21 +2709,20 @@ export default class ExcalidrawPlugin extends Plugin {
if(!this.settings.startupScriptPath || this.settings.startupScriptPath === "") {
return;
}
const self = this;
this.app.workspace.onLayoutReady(async () => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.runStartupScript, `ExcalidrawPlugin.runStartupScript > app.workspace.onLayoutReady, scriptPath:${self.settings?.startupScriptPath}`);
const path = self.settings.startupScriptPath.endsWith(".md")
? self.settings.startupScriptPath
: `${self.settings.startupScriptPath}.md`;
const f = self.app.vault.getAbstractFileByPath(path);
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.runStartupScript, `ExcalidrawPlugin.runStartupScript > app.workspace.onLayoutReady, scriptPath:${this.settings?.startupScriptPath}`);
const path = this.settings.startupScriptPath.endsWith(".md")
? this.settings.startupScriptPath
: `${this.settings.startupScriptPath}.md`;
const f = this.app.vault.getAbstractFileByPath(path);
if (!f || !(f instanceof TFile)) {
new Notice(`Startup script not found: ${path}`);
return;
}
const script = await self.app.vault.read(f);
const script = await this.app.vault.read(f);
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
try {
await new AsyncFunction("ea", script)(self.ea);
await new AsyncFunction("ea", script)(this.ea);
} catch (e) {
new Notice(`Error running startup script: ${e}`);
}
@@ -2619,7 +2772,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (previouslyActiveEV.leaf !== leaf) {
//if loading new view to same leaf then don't save. Excalidarw view will take care of saving anyway.
//avoid double saving
if(previouslyActiveEV.semaphores?.dirty && !previouslyActiveEV.semaphores?.viewunload) {
if(previouslyActiveEV?.isDirty() && !previouslyActiveEV.semaphores?.viewunload) {
await previouslyActiveEV.save(true); //this will update transclusions in the drawing
}
}
@@ -2817,7 +2970,7 @@ export default class ExcalidrawPlugin extends Plugin {
await inData.loadData(data,file,getTextMode(data));
excalidrawView.synchronizeWithData(inData);
inData.destroy();
if(excalidrawView.semaphores?.dirty) {
if(excalidrawView?.isDirty()) {
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
clearTimeout(excalidrawView.autosaveTimer);
}
@@ -2874,16 +3027,6 @@ export default class ExcalidrawPlugin extends Plugin {
};
this.registerEvent(this.app.vault.on("delete", (file:TFile) => deleteEventHandler(file)));
//save open drawings when user quits the application
//Removing because it is not guaranteed to run, and frequently gets terminated mid flight, causing file consistency issues
/*const quitEventHandler = async () => {
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for (let i = 0; i < leaves.length; i++) {
await (leaves[i].view as ExcalidrawView).save(true);
}
};
self.registerEvent(app.workspace.on("quit", quitEventHandler));*/
//save Excalidraw leaf and update embeds when switching to another leaf
this.registerEvent(
this.app.workspace.on(
@@ -2922,7 +3065,7 @@ export default class ExcalidrawPlugin extends Plugin {
const onClickEventSaveActiveDrawing = (e: PointerEvent) => {
if (
!this.activeExcalidrawView ||
!this.activeExcalidrawView.semaphores?.dirty ||
!this.activeExcalidrawView?.isDirty() ||
//@ts-ignore
e.target && (e.target.className === "excalidraw__canvas" ||
//@ts-ignore
@@ -2941,7 +3084,7 @@ export default class ExcalidrawPlugin extends Plugin {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(onFileMenuEventSaveActiveDrawing,`ExcalidrawPlugin.onFileMenuEventSaveActiveDrawing`);
if (
!this.activeExcalidrawView ||
!this.activeExcalidrawView.semaphores?.dirty
!this.activeExcalidrawView?.isDirty()
) {
return;
}
@@ -2965,7 +3108,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (
m[0].oldValue !== "display: none;" ||
!this.activeExcalidrawView ||
!this.activeExcalidrawView.semaphores?.dirty
!this.activeExcalidrawView?.isDirty()
) {
return;
}
@@ -3012,7 +3155,8 @@ export default class ExcalidrawPlugin extends Plugin {
(m[0].type !== "childList") ||
(m[0].addedNodes.length !== 1) ||
(!this.activeExcalidrawView) ||
(!this.activeExcalidrawView.semaphores?.dirty)
this.activeExcalidrawView?.semaphores?.viewunload ||
(!this.activeExcalidrawView?.isDirty())
) {
return;
}
@@ -3410,7 +3554,7 @@ export default class ExcalidrawPlugin extends Plugin {
* @param {string} data - Excalidraw scene JSON string
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
*/
public async exportSceneToMD(data: string): Promise<string> {
public async exportSceneToMD(data: string, compressOverride?: boolean): Promise<string> {
if (!data) {
return "";
}
@@ -3435,7 +3579,9 @@ export default class ExcalidrawPlugin extends Plugin {
outString +
getMarkdownDrawingSection(
JSON.stringify(JSON_parse(data), null, "\t"),
this.settings.compress,
typeof compressOverride === "undefined"
? this.settings.compress
: compressOverride,
)
);
}
@@ -3465,6 +3611,21 @@ export default class ExcalidrawPlugin extends Plugin {
errorlog({file, error: "new drawing not recognized as an excalidraw file", fn: this.createDrawing});
}
if(Date.now() - this.loadTimestamp > 1){//2000) {
const filecount = this.app.vault.getFiles().filter(f=>this.isExcalidrawFile(f)).length;
const rank:Rank = filecount < 200 ? "Bronze" : filecount < 750 ? "Silver" : filecount < 2000 ? "Gold" : "Platinum";
const {grip, decoration, blade} = SwordColors[rank];
if(this.settings.rank !== rank) {
//in case the message was already displayed on another device and it was synced in the mean time
await this.loadSettings();
if(this.settings.rank !== rank) {
this.settings.rank = rank;
await this.saveSettings();
new RankMessage(this.app, this, filecount, rank, decoration, blade, grip).open();
}
}
}
return file;
}
@@ -3505,14 +3666,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
public async setExcalidrawView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawView,`ExcalidrawPlugin.setExcalidrawView`, leaf);
await leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
public isExcalidrawFile(f: TFile) {
if(!f) return false;

File diff suppressed because one or more lines are too long

View File

@@ -64,14 +64,13 @@ export class EmbeddableMenu {
};
private handleMouseLeave () {
const self = this;
this.menuFadeTimeout = window.setTimeout(() => {
self.containerRef.current?.style.setProperty("opacity", "0.2");
this.containerRef.current?.style.setProperty("opacity", "0.2");
}, 5000);
};
private async actionMarkdownSelection (file: TFile, isExcalidrawFile: boolean, subpath: string, element: ExcalidrawEmbeddableElement) {
this.view.updateScene({appState: {activeEmbeddable: null}});
this.view.updateScene({appState: {activeEmbeddable: null}, storeAction: "update"});
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
@@ -99,7 +98,7 @@ export class EmbeddableMenu {
private async actionMarkdownBlock (file: TFile, subpath: string, element: ExcalidrawEmbeddableElement) {
if(!file) return;
this.view.updateScene({appState: {activeEmbeddable: null}});
this.view.updateScene({appState: {activeEmbeddable: null}, storeAction: "update"});
const paragraphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node &&

View File

@@ -34,7 +34,8 @@ export function setPen (pen: PenStyle, api: any) {
currentItemRoughness: st.currentItemRoughness,
}}
: null,
}
},
storeAction: "update",
})
}
@@ -50,7 +51,8 @@ export function resetStrokeOptions (resetCustomPen:any, api: ExcalidrawImperativ
}: null,
resetCustomPen: null,
...clearCurrentStrokeOptions ? {currentStrokeOptions: null} : null,
}
},
storeAction: "update",
});
}

View File

@@ -2,6 +2,7 @@ import {
App,
ButtonComponent,
DropdownComponent,
getIcon,
normalizePath,
PluginSettingTab,
Setting,
@@ -13,7 +14,7 @@ import ExcalidrawView from "./ExcalidrawView";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
import { PenStyle } from "./PenTypes";
import { DynamicStyle } from "./types";
import { DynamicStyle } from "./types/types";
import { PreviewImageType } from "./utils/UtilTypes";
import { setDynamicStyle } from "./utils/DynamicStyling";
import {
@@ -36,6 +37,8 @@ import { ModifierKeySettingsComponent } from "./dialogs/ModifierKeySettings";
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./utils/CarveOut";
import { EDITOR_FADEOUT } from "./CodeMirrorExtension/EditorHandler";
import { setDebugging } from "./utils/DebugHelper";
import { Rank } from "./menu/ActionIcons";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
export interface ExcalidrawSettings {
folder: string;
@@ -47,6 +50,7 @@ export interface ExcalidrawSettings {
compress: boolean;
decompressForMDView: boolean;
onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0
onceOffGPTVersionReset: boolean; //used to reset GPT version in 2.2.11
autosave: boolean;
autosaveIntervalDesktop: number;
autosaveIntervalMobile: number;
@@ -74,6 +78,7 @@ export interface ExcalidrawSettings {
matchThemeTrigger: boolean;
defaultMode: string;
defaultPenMode: "never" | "mobile" | "always";
penModeDoubleTapEraser: boolean;
penModeCrosshairVisible: boolean;
renderImageInMarkdownReadingMode: boolean,
renderImageInHoverPreviewForMDNotes: boolean,
@@ -164,6 +169,7 @@ export interface ExcalidrawSettings {
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfFrame: boolean;
pdfGapSize: number;
pdfGroupPages: boolean;
pdfLockAfterImport: boolean;
@@ -196,6 +202,7 @@ export interface ExcalidrawSettings {
longPressDesktop: number;
longPressMobile: number;
isDebugMode: boolean;
rank: Rank;
}
declare const PLUGIN_VERSION:string;
@@ -210,6 +217,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
compress: true,
decompressForMDView: false,
onceOffCompressFlagReset: false,
onceOffGPTVersionReset: false,
autosave: true,
autosaveIntervalDesktop: 15000,
autosaveIntervalMobile: 10000,
@@ -237,6 +245,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
matchThemeTrigger: false,
defaultMode: "normal",
defaultPenMode: "never",
penModeDoubleTapEraser: true,
penModeCrosshairVisible: true,
renderImageInMarkdownReadingMode: false,
renderImageInHoverPreviewForMDNotes: false,
@@ -333,6 +342,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfFrame: false,
pdfGapSize: 20,
pdfGroupPages: false,
pdfLockAfterImport: true,
@@ -360,7 +370,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
startupScriptPath: "",
openAIAPIToken: "",
openAIDefaultTextModel: "gpt-3.5-turbo-1106",
openAIDefaultVisionModel: "gpt-4-vision-preview",
openAIDefaultVisionModel: "gpt-4o",
openAIDefaultImageGenerationModel: "dall-e-3",
openAIURL: "https://api.openai.com/v1/chat/completions",
openAIImageGenerationURL: "https://api.openai.com/v1/images/generations",
@@ -451,6 +461,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
longPressDesktop: 500,
longPressMobile: 500,
isDebugMode: false,
rank: "Bronze",
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -539,6 +550,46 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
},
});
coffeeImg.height = 45;
const iconLinks = [
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>`,
href: "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues",
aria: "Report bugs and raise feature requsts on the plugin's GitHub page",
text: "Bugs and Feature Requests",
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19c-2.3 0-6.4-.2-8.1-.6-.7-.2-1.2-.7-1.4-1.4-.3-1.1-.5-3.4-.5-5s.2-3.9.5-5c.2-.7.7-1.2 1.4-1.4C5.6 5.2 9.7 5 12 5s6.4.2 8.1.6c.7.2 1.2.7 1.4 1.4.3 1.1.5 3.4.5 5s-.2 3.9-.5 5c-.2.7-.7 1.2-1.4 1.4-1.7.4-5.8.6-8.1.6 0 0 0 0 0 0z"></path><polygon points="10 15 15 12 10 9"></polygon></svg>`,
href: "https://www.youtube.com/@VisualPKM",
aria: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
text: "Visual PKM on YouTube",
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" strokeWidth="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
href: "https://discord.gg/DyfAXFwUHc",
aria: "Join the Visual Thinking Workshop Discord Server",
text: "Community on Discord",
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>`,
href: "https://twitter.com/zsviczian",
aria: "Follow me on Twitter",
text: "Follow me on Twitter",
},
{
icon: getIcon("graduation-cap").outerHTML,
href: "https://visual-thinking-workshop.com",
aria: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
text: "Join the Visual Thinking Workshop",
}
];
const linksEl = containerEl.createDiv("setting-item-description excalidraw-settings-links-container");
iconLinks.forEach(({ icon, href, aria, text }) => {
linksEl.createEl("a",{href, attr: { "aria-label": aria }}, (a)=> {
a.innerHTML = icon + text;
});
});
// ------------------------------------------------
// Saving
@@ -977,6 +1028,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
@@ -989,7 +1051,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
const readingModeEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC")))
.addToggle((toggle) =>
@@ -1000,6 +1062,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
readingModeEl.nameEl.setAttribute("id",TAG_MDREADINGMODE);
new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME"))
@@ -1790,7 +1853,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
});
addIframe(detailsEl, "wTtaXmRJ7wg",171);
new Setting(detailsEl)
const pdfExportEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC")))
.addToggle((toggle) =>
@@ -1801,6 +1864,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
pdfExportEl.nameEl.setAttribute("id",TAG_PDFEXPORT);
new Setting(detailsEl)
.setName(t("EXPORT_EMBED_SCENE_NAME"))
@@ -1940,6 +2004,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text: t("EXPORT_HEAD"),
cls: "excalidraw-setting-h4",
});
detailsEl.setAttribute("id",TAG_AUTOEXPORT);
new Setting(detailsEl)
.setName(t("EXPORT_SYNC_NAME"))
@@ -2137,7 +2202,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
d.addOption("Assistant", "Assistant");
this.app.vault
.getFiles()
.filter((f) => ["ttf", "woff", "woff2"].contains(f.extension))
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension))
.forEach((f: TFile) => {
d.addOption(f.path, f.name);
});

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import { Notice } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate, cloneElement } from "src/ExcalidrawAutomate";
import { ExportSettings } from "src/ExcalidrawView";
import { embedFontsInSVG } from "./Utils";
import { nanoid } from "src/constants/constants";
export class CropImage {
@@ -92,7 +91,7 @@ export class CropImage {
isMask: false,
}
const maskSVG = await this.maskEA.createSVG(null,false,exportSettings,null,null,0);
const maskSVG = await this.maskEA.createSVG(null,true,exportSettings,null,null,0);
const defs = maskSVG.querySelector("defs");
const styleEl = maskSVG.querySelector("style");
const style = styleEl ? styleEl.outerHTML : "";
@@ -138,7 +137,7 @@ export class CropImage {
async getCroppedPNG(): Promise<Blob> {
//@ts-ignore
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"];
const svg = embedFontsInSVG(await this.buildSVG(), PLUGIN);
const svg = await this.buildSVG();
return new Promise((resolve, reject) => {
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement('canvas');

View File

@@ -10,7 +10,8 @@ export let DEBUGGING = false;
export const log = console.log.bind(window.console);
export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => {
console.log(fnName,fn,...messages);
//console.log(fnName,fn,...messages);
console.log(fnName, ...messages);
};
export class CustomMutationObserver {

View File

@@ -2,7 +2,7 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/
import { ColorMaster } from "colormaster";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { DynamicStyle } from "src/types";
import { DynamicStyle } from "src/types/types";
import { cloneElement } from "src/ExcalidrawAutomate";
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { addAppendUpdateCustomData } from "./Utils";
@@ -16,7 +16,7 @@ export const setDynamicStyle = (
) => {
if(dynamicStyle === "none") {
view.excalidrawContainer?.removeAttribute("style");
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}}));
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}, storeAction: "update"}));
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
@@ -74,6 +74,7 @@ export const setDynamicStyle = (
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
const styleObject:{[x: string]: string;} = {
['backgroundColor']: str(cmBG()),
[`--color-primary`]: str(accent()),
[`--color-surface-low`]: str(gray1()),
[`--color-surface-mid`]: str(gray1()),
@@ -167,7 +168,8 @@ export const setDynamicStyle = (
appState:{
frameColor,
dynamicStyle: styleObject
}
},
storeAction: "update",
});
view = null;
ea = null;

View File

@@ -1,8 +1,8 @@
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
import { App, TFile, WorkspaceLeaf } from "obsidian";
import { App, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData";
import ExcalidrawView from "src/ExcalidrawView";
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getLinkParts } from "./Utils";
@@ -11,13 +11,14 @@ import { getEA } from "src";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddableMDCustomProps } from "src/dialogs/EmbeddableSettings";
import { nanoid } from "nanoid";
import { t } from "src/lang/helpers";
export const insertImageToView = async (
export async function insertImageToView(
ea: ExcalidrawAutomate,
position: { x: number, y: number },
file: TFile | string,
scale?: boolean,
):Promise<string> => {
):Promise<string> {
ea.clear();
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
@@ -33,17 +34,17 @@ export const insertImageToView = async (
return id;
}
export const insertEmbeddableToView = async (
export async function insertEmbeddableToView (
ea: ExcalidrawAutomate,
position: { x: number, y: number },
file?: TFile,
link?: string,
):Promise<string> => {
):Promise<string> {
ea.clear();
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
return await insertImageToView(ea, position, file);
return await insertImageToView(ea, position, link??file);
} else {
const id = ea.addEmbeddable(
position.x,
@@ -58,7 +59,7 @@ export const insertEmbeddableToView = async (
}
}
export const getLinkTextFromLink = (text: string): string => {
export function getLinkTextFromLink (text: string): string {
if (!text) return;
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
@@ -71,42 +72,93 @@ export const getLinkTextFromLink = (text: string): string => {
return linktext;
}
export const openTagSearch = (link:string, app: App, view?: ExcalidrawView) => {
const tags = link
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
.next();
if (!tags.value || tags.value.length < 2) {
export function openTagSearch(link: string, app: App, view?: ExcalidrawView) {
const tags = REGEX_TAGS.getResList(link);
if (!tags.length || !tags[0].value || tags[0].value.length < 2) {
return;
}
const search = app.workspace.getLeavesOfType("search");
if (search.length == 0) {
if (search.length === 0) {
return;
}
//@ts-ignore
search[0].view.setQuery(`tag:${tags.value[1]}`);
search[0].view.setQuery(`tag:${tags[0].value[1]}`);
app.workspace.revealLeaf(search[0]);
if (view && view.isFullscreen()) {
view.exitFullscreen();
}
return;
}
export const openExternalLink = (link:string, app: App, element?: ExcalidrawElement):boolean => {
function getLinkFromMarkdownLink(link: string): string {
const result = /^\[[^\]]*]\(([^\)]*)\)/.exec(link);
return result ? result[1] : link;
}
export function openExternalLink (link:string, app: App, element?: ExcalidrawElement):boolean {
link = getLinkFromMarkdownLink(link);
if (link.match(/^cmd:\/\/.*/)) {
const cmd = link.replace("cmd://", "");
//@ts-ignore
app.commands.executeCommandById(cmd);
return true;
}
if (link.match(REG_LINKINDEX_HYPERLINK)) {
window.open(link, "_blank");
if (!link.startsWith("obsidian://") && link.match(REG_LINKINDEX_HYPERLINK)) {
window.open(link, "_blank");
return true;
}
return false;
}
export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, secondOrderLinksSet: Set<string>):string => {
/**
*
* @param link
* @param app
* @param returnWikiLink
* @returns
* false if the link is not an obsidian link,
* true if the link is an obsidian link and it was opened (i.e. it is a link to another Vault or not a file link e.g. plugin link), or
* the link to the file path. By default as a wiki link, or as a file path if returnWikiLink is false.
*/
export function parseObsidianLink(link: string, app: App, returnWikiLink: boolean = true): boolean | string {
link = getLinkFromMarkdownLink(link);
if (!link.startsWith("obsidian://")) {
return false;
}
const url = new URL(link);
const action = url.pathname.slice(2); // Remove leading '//'
const props: {[key: string]: string} = {};
url.searchParams.forEach((value, key) => {
props[key] = decodeURIComponent(value);
});
if (action === "open" && props.vault === app.vault.getName()) {
const file = props.file;
const f = app.metadataCache.getFirstLinkpathDest(file, "");
if (f && f instanceof TFile) {
if (returnWikiLink) {
return `[[${f.path}]]`;
} else {
return f.path;
}
}
}
window.open(link, "_blank");
return true;
}
export function getExcalidrawFileForwardLinks (
app: App, excalidrawFile: TFile,
secondOrderLinksSet: Set<string>,
):string {
let secondOrderLinks = "";
const forwardLinks = app.metadataCache.getLinks()[excalidrawFile.path];
if(forwardLinks && forwardLinks.length > 0) {
@@ -125,7 +177,10 @@ export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, s
return secondOrderLinks;
}
export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: ExcalidrawElement[]): ExcalidrawFrameElement | null => {
export function getFrameBasedOnFrameNameOrId(
frameName: string,
elements: ExcalidrawElement[],
): ExcalidrawFrameElement | null {
const frames = elements
.filter((el: ExcalidrawElement)=>el.type==="frame")
.map((el: ExcalidrawFrameElement, idx: number)=>{
@@ -136,7 +191,13 @@ export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: Excali
return frames.length === 1 ? frames[0] : null;
}
export const addBackOfTheNoteCard = async (view: ExcalidrawView, title: string, activate: boolean = true, cardBody?: string, embeddableCustomData?: EmbeddableMDCustomProps):Promise<string> => {
export async function addBackOfTheNoteCard(
view: ExcalidrawView,
title: string,
activate: boolean = true,
cardBody?: string,
embeddableCustomData?: EmbeddableMDCustomProps,
):Promise<string> {
const data = view.data;
const header = getExcalidrawMarkdownHeaderSection(data);
const body = data.split(header)[1];
@@ -177,8 +238,8 @@ export const addBackOfTheNoteCard = async (view: ExcalidrawView, title: string,
const el = ea.getViewElements().find(el=>el.id === id);
api.selectElements([el]);
if(activate) {
setTimeout(()=>{
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}});
window.setTimeout(()=>{
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}, storeAction: "update"});
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
});
}
@@ -186,7 +247,12 @@ export const addBackOfTheNoteCard = async (view: ExcalidrawView, title: string,
return el.id;
}
export const renderContextMenuAction = (React: any, label: string, action: Function, onClose: (callback?: () => void) => void) => {
export function renderContextMenuAction(
React: any,
label: string,
action: Function,
onClose: (callback?: () => void) => void,
) {
return React.createElement (
"li",
{
@@ -218,7 +284,7 @@ export const renderContextMenuAction = (React: any, label: string, action: Funct
);
}
export const tmpBruteForceCleanup = (view: ExcalidrawView) => {
export function tmpBruteForceCleanup (view: ExcalidrawView) {
window.setTimeout(()=>{
if(!view) return;
// const cleanupHTMLElement = (el: Element) => {
@@ -282,4 +348,38 @@ export const tmpBruteForceCleanup = (view: ExcalidrawView) => {
delete view[key];
});
}, 500);
}
/**
* Check if the text matches the transclusion pattern and if so,
* check if the link in the transclusion can be resolved to a file in the vault.
* if yes, call the callback function with the link and the file.
* @param text
* @param callback
* @returns true if text is a transclusion and the link can be resolved to a file in the vault, false otherwise.
*/
export function isTextImageTransclusion (
text: string,
view: ExcalidrawView,
callback: (link: string, file: TFile)=>void
): boolean {
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
const match = text.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
if(match?.value?.[0]) {
const link = match.value[1] ?? match.value[2];
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
if(view.file === file) {
new Notice(t("RECURSIVE_INSERT_ERROR"));
return false;
}
if(file && file instanceof TFile) {
if (file.extension !== "md" || view.plugin.isExcalidrawFile(file)) {
callback(link, file);
return true;
} else {
new Notice(t("USE_INSERT_FILE_MODAL"),5000);
}
}
}
return false;
}

View File

@@ -11,6 +11,7 @@ import { getAttachmentsFolderAndFilePath } from "./ObsidianUtils";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
* @param filepath
* @returns folderpath will be normalized. This means "/" for root folder and no trailing "/" for other folders
*/
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;

View File

@@ -21,11 +21,13 @@ export type ImageKey = {
previewImageType: PreviewImageType;
scale: number;
isTransparent: boolean;
inlineFonts: boolean;
} & FILENAMEPARTS;
const getKey = (key: ImageKey): string =>
`${key.filepath}#${key.blockref??""}#${key.sectionref??""}#${key.isDark ? 1 : 0}#${
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasSectionref}#${
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasClippedFrameref}#${
key.hasSectionref}#${key.inlineFonts}#${
key.previewImageType === PreviewImageType.SVGIMG
? 1
: key.previewImageType === PreviewImageType.PNG
@@ -172,7 +174,7 @@ class ImageCache {
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
if(cursor) {
const key = cursor.key as string;
const isLegacyKey = key.replaceAll(/[^#]/g,"").length < 9; // introduced hasGroupref, etc. in 1.9.28
const isLegacyKey = key.split("#").length-1 < 12; // introduced hasGroupref, etc. in 1.9.28 // introduced hasClippedFrameref in 2.2.10 //introduced inlineFonts 2.2.11
const filepath = key.split("#")[0];
const fileExists = files.some((f: TFile) => f.path === filepath);
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
@@ -251,16 +253,16 @@ class ImageCache {
});
}
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
private getObjectStore(mode: IDBTransactionMode, storeName: string): IDBObjectStore {
const transaction = this.db!.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly", this.cacheStoreName);
const store = this.getObjectStore("readonly", this.cacheStoreName);
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
return await new Promise<FileCacheData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as FileCacheData;
resolve(result || null);
@@ -273,7 +275,7 @@ class ImageCache {
}
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
const store = await this.getObjectStore("readonly", this.backupStoreName);
const store = this.getObjectStore("readonly", this.backupStoreName);
const request = store.get(key);
return new Promise<BackupData | null>((resolve, reject) => {
@@ -306,7 +308,9 @@ class ImageCache {
? await this.getCacheData(key)
: await Promise.race([
this.getCacheData(key),
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
new Promise<undefined>((_,reject) => setTimeout(() => {
reject(undefined);
}, 100))
]);
this.fullyInitialized = true;
if(!cachedData) return undefined;

View File

@@ -3,13 +3,14 @@ import {
Editor,
FrontMatterCache,
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import yaml, { Mark } from "js-yaml";
import { debug, DEBUGGING } from "./DebugHelper";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
@@ -300,7 +301,7 @@ export const openLeaf = ({
return {leaf, promise};
}
export const mergeMarkdownFiles = (template: string, target: string): string => {
export function mergeMarkdownFiles (template: string, target: string): string {
// Extract frontmatter from the template
const templateFrontmatterEnd = template.indexOf('---', 4); // Find end of frontmatter
const templateFrontmatter = template.substring(4, templateFrontmatterEnd).trim();
@@ -312,8 +313,8 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
// Extract frontmatter from the target if it exists
let targetFrontmatterObj: FrontMatterCache = {};
let targetContent = '';
if (target.includes('---')) {
const targetFrontmatterEnd = target.indexOf('---', 4); // Find end of frontmatter
if (target.startsWith('---\n') && target.indexOf('---\n', 4) > 0) {
const targetFrontmatterEnd = target.indexOf('---\n', 4); // Find end of frontmatter
const targetFrontmatter = target.substring(4, targetFrontmatterEnd).trim();
targetContent = target.substring(targetFrontmatterEnd + 3); // Skip frontmatter and ---
@@ -392,3 +393,20 @@ export const foldExcalidrawSection = (view: MarkdownView) => {
view.currentMode.applyFoldInfo({ folds: foldPositions, lines: lineCount });
}
};
export async function setExcalidrawView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
export async function closeLeafView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: "empty",
state: {},
});
}

View File

@@ -37,23 +37,22 @@ export class StylesManager {
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
const self = this;
plugin.app.workspace.onLayoutReady(async () => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(undefined, "StylesManager.constructor > app.workspace.onLayoutReady", self);
await self.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => self.copyPropertiesToTheme(doc));
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(undefined, "StylesManager.constructor > app.workspace.onLayoutReady", this);
await this.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => this.copyPropertiesToTheme(doc));
//initialize
plugin.registerEvent(
plugin.app.workspace.on("css-change", ()=>self.onCSSChange()),
plugin.app.workspace.on("css-change", ()=>this.onCSSChange()),
)
plugin.registerEvent(
plugin.app.workspace.on("window-open", (win)=>self.onWindowOpen(win)),
plugin.app.workspace.on("window-open", (win)=>this.onWindowOpen(win)),
)
plugin.registerEvent(
plugin.app.workspace.on("window-close", (win)=>self.onWindowClose(win)),
plugin.app.workspace.on("window-close", (win)=>this.onWindowClose(win)),
)
});
}

View File

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

View File

@@ -1,18 +1,12 @@
import {
App,
Notice,
request,
requestUrl,
request,requestUrl,
TFile,
TFolder,
} from "obsidian";
import { Random } from "roughjs/bin/math";
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/excalidraw/types";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "src/constants/constFonts";
import {
exportToSvg,
exportToBlob,
@@ -33,7 +27,7 @@ import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
import { CropImage } from "./CropImage";
import opentype from 'opentype.js';
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -195,7 +189,35 @@ export async function getFontDataURL (
const f = app.metadataCache.getFirstLinkpathDest(fontFileName, sourcePath);
if (f) {
const ab = await app.vault.readBinary(f);
const mimeType = f.extension.startsWith("woff")
let mimeType = "";
let format = "";
switch (f.extension) {
case "woff":
mimeType = "application/font-woff";
format = "woff";
break;
case "woff2":
mimeType = "font/woff2";
format = "woff2";
break;
case "ttf":
mimeType = "font/ttf";
format = "truetype";
break;
case "otf":
mimeType = "font/otf";
format = "opentype";
break;
default:
mimeType = "application/octet-stream"; // Fallback if file type is unexpected
}
fontName = name ?? f.basename;
dataURL = await getDataURL(ab, mimeType);
const split = dataURL.split(";base64,", 2);
dataURL = `${split[0]};charset=utf-8;base64,${split[1]}`;
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}") format("${format}")}`;
/* const mimeType = f.extension.startsWith("woff")
? "application/font-woff"
: "font/truetype";
fontName = name ?? f.basename;
@@ -203,7 +225,7 @@ export async function getFontDataURL (
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
const split = fontDef.split(";base64,", 2);
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
}
return { fontDef, fontName, dataURL };
};
@@ -273,16 +295,20 @@ export async function getSVG (
svg = await exportToSvg({
elements: elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
appState: {
...scene.appState,
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme !== "light"
: false,
...scene.appState,
...exportSettings.frameRendering
? {frameRendering: exportSettings.frameRendering}
: {},
},
files: scene.files,
exportPadding: padding,
exportPadding: exportSettings.frameRendering ? 0 : padding,
exportingFrame: null,
renderEmbeddables: true,
skipInliningFonts: exportSettings.skipInliningFonts,
});
}
if(svg) {
@@ -327,14 +353,17 @@ export async function getPNG (
return await exportToBlob({
elements: scene.elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
appState: {
...scene.appState,
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme !== "light"
: false,
...scene.appState,
...exportSettings.frameRendering
? {frameRendering: exportSettings.frameRendering}
: {},
},
files: filterFiles(scene.files),
exportPadding: padding,
exportPadding: exportSettings.frameRendering ? 0 : padding,
mimeType: "image/png",
getDimensions: (width: number, height: number) => ({
width: width * scale,
@@ -343,6 +372,7 @@ export async function getPNG (
}),
});
} catch (error) {
new Notice("Error exporting PNG - PNG too large, try a smaller resolution");
errorlog({ where: "Utils.getPNG", error });
return null;
}
@@ -369,34 +399,6 @@ export async function getQuickImagePreview (
}
};
export function embedFontsInSVG(
svg: SVGSVGElement,
plugin: ExcalidrawPlugin,
localOnly: boolean = false,
): SVGSVGElement {
//replace font references with base64 fonts)
const includesVirgil = !localOnly &&
svg.querySelector("text[font-family^='Virgil']") !== null;
const includesCascadia = !localOnly &&
svg.querySelector("text[font-family^='Cascadia']") !== null;
const includesAssistant = !localOnly &&
svg.querySelector("text[font-family^='Assistant']") !== null;
const includesLocalFont =
svg.querySelector("text[font-family^='LocalFont']") !== null;
const defs = svg.querySelector("defs");
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
let style = defs.querySelector("style");
if (!style) {
style = document.createElement("style");
defs.appendChild(style);
}
style.innerHTML = `${includesVirgil ? VIRGIL_FONT : ""}${
includesCascadia ? CASCADIA_FONT : ""}${
includesAssistant ? ASSISTANT_FONT : ""
}${includesLocalFont ? plugin.fourthFontDef : ""}`;
}
return svg;
};
export async function getImageSize (
src: string,
@@ -712,7 +714,7 @@ export function isVersionNewerThanOther (version: string, otherVersion: string):
export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
// 0 1 23 4 5 6 7 8 9
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|clippedframe=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|clippedframe=|taskbone)?([^\^\|]*))(.*)/);
if(!parts) {
return {
filepath: fname,
@@ -721,6 +723,7 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
blockref: "",
hasSectionref: false,
sectionref: "",
@@ -735,6 +738,7 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
hasTaskbone: (parts[4]==="taskbone") || (parts[7]==="taskbone"),
hasArearef: (parts[4]==="area=") || (parts[7]==="area="),
hasFrameref: (parts[4]==="frame=") || (parts[7]==="frame="),
hasClippedFrameref: (parts[4]==="clippedframe=") || (parts[7]==="clippedframe="),
blockref: parts[5],
hasSectionref: Boolean(parts[6]),
sectionref: parts[8],
@@ -887,4 +891,51 @@ export function addIframe (containerEl: HTMLElement, link:string, startAt?: numb
sandbox: "allow-forms allow-presentation allow-same-origin allow-scripts allow-modals",
},
});
}
export interface FontMetrics {
unitsPerEm: number;
ascender: number;
descender: number;
lineHeight: number;
fontName: string;
}
export async function getFontMetrics(fontUrl: string, name: string): Promise<FontMetrics | null> {
try {
const font = await opentype.load(fontUrl);
const unitsPerEm = font.unitsPerEm;
const ascender = font.ascender;
const descender = font.descender;
const lineHeight = (ascender - descender) / unitsPerEm;
const fontName = font.names.fontFamily.en ?? name;
return {
unitsPerEm,
ascender,
descender,
lineHeight,
fontName,
};
} catch (error) {
console.error('Error loading font:', error);
return null;
}
}
// Thanks https://stackoverflow.com/a/54555834
export function cropCanvas(
srcCanvas: HTMLCanvasElement,
crop: { left: number, top: number, width: number, height: number },
output: { width: number, height: number } = { width: crop.width, height: crop.height })
{
const dstCanvas = createEl('canvas');
dstCanvas.width = output.width;
dstCanvas.height = output.height;
dstCanvas.getContext('2d')!.drawImage(
srcCanvas,
crop.left, crop.top, crop.width, crop.height,
0, 0, output.width, output.height
);
return dstCanvas;
}

View File

@@ -15,4 +15,15 @@ export class WeakArray<T extends object> {
}
});
}
removeObjects(objectsToRemove: Set<T>) {
this.weakArray = this.weakArray.filter((ref) => {
const obj = ref.deref();
return obj && !objectsToRemove.has(obj);
});
}
clear() {
this.weakArray = [];
}
}

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

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

View File

@@ -594,4 +594,35 @@ root {
textarea.excalidraw-wysiwyg, .excalidraw input {
caret-color: var(--excalidraw-caret-color);
}
.excalidraw-settings-links-container {
display: flex; /* Align SVG and text horizontally */
align-items: center; /* Center SVG and text vertically */
text-decoration: none; /* Remove underline from links */
color: inherit; /* Inherit text color */
text-align: center;
}
.excalidraw-settings-links-container a {
display: flex; /* Align children horizontally */
align-items: center; /* Center items vertically */
text-align: left;
}
.excalidraw-settings-links-container svg {
margin-right: 8px;
height: 30px;
width: 30px;
}
.excalidraw-rank {
text-align: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.excalidraw-rank svg {
height: 8rem;
width: 8rem;
}

View File

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

View File

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