mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
28 Commits
2.3.0
...
2.4.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
353732f597 | ||
|
|
5599d2507f | ||
|
|
70cf6ffe70 | ||
|
|
61c9277097 | ||
|
|
401052efd3 | ||
|
|
a57a0e797d | ||
|
|
8f48853e2c | ||
|
|
a62148dc07 | ||
|
|
3ae890bd86 | ||
|
|
65a4cd4ba5 | ||
|
|
f63b473bc1 | ||
|
|
859a5ba03a | ||
|
|
832b97b179 | ||
|
|
e98d688d36 | ||
|
|
39318337fe | ||
|
|
f21215be84 | ||
|
|
0690525af8 | ||
|
|
b3176425c5 | ||
|
|
9f2c18b6b6 | ||
|
|
d529a04f48 | ||
|
|
8786c5aa99 | ||
|
|
013279ab60 | ||
|
|
06193b6d49 | ||
|
|
ac6f4af5d6 | ||
|
|
bf148adc68 | ||
|
|
0f9dafb01d | ||
|
|
9fc0452b70 | ||
|
|
83eda9b3f5 |
@@ -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;"/> 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.
|
||||
|
||||
[](https://youtu.be/2poSS-Z91lY)
|
||||
|
||||
[](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
242
ea-scripts/Excalidraw Writing Machine.md
Normal file
242
ea-scripts/Excalidraw Writing Machine.md
Normal 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:
|
||||

|
||||
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 `})`;
|
||||
}
|
||||
|
||||
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);
|
||||
11
ea-scripts/Excalidraw Writing Machine.svg
Normal file
11
ea-scripts/Excalidraw Writing Machine.svg
Normal 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 |
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0-beta-9",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-36",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-42",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colormaster": "^1.2.1",
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
nanoid,
|
||||
THEME_FILTER,
|
||||
FRONTMATTER_KEYS,
|
||||
getFontDefinition,
|
||||
getCSSFontDefinition,
|
||||
} from "./constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
svgToBase64,
|
||||
isMaskFile,
|
||||
getEmbeddedFilenameParts,
|
||||
cropCanvas,
|
||||
} from "./utils/Utils";
|
||||
import { ValueOf } from "./types/types";
|
||||
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
@@ -746,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) => {
|
||||
@@ -766,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;
|
||||
};
|
||||
|
||||
@@ -816,29 +836,29 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
switch (fontName) {
|
||||
case "Virgil":
|
||||
fontDef = await getFontDefinition(1);
|
||||
fontDef = await getCSSFontDefinition(1);
|
||||
break;
|
||||
case "Cascadia":
|
||||
fontDef = await getFontDefinition(3);
|
||||
fontDef = await getCSSFontDefinition(3);
|
||||
break;
|
||||
case "Assistant":
|
||||
case "Helvetica":
|
||||
fontDef = await getFontDefinition(2);
|
||||
fontDef = await getCSSFontDefinition(2);
|
||||
break;
|
||||
case "Excalifont":
|
||||
fontDef = await getFontDefinition(5);
|
||||
fontDef = await getCSSFontDefinition(5);
|
||||
break;
|
||||
case "Nunito":
|
||||
fontDef = await getFontDefinition(6);
|
||||
fontDef = await getCSSFontDefinition(6);
|
||||
break;
|
||||
case "Lilita One":
|
||||
fontDef = await getFontDefinition(7);
|
||||
fontDef = await getCSSFontDefinition(7);
|
||||
break;
|
||||
case "Comic Shanns":
|
||||
fontDef = await getFontDefinition(8);
|
||||
fontDef = await getCSSFontDefinition(8);
|
||||
break;
|
||||
case "Liberation Sans":
|
||||
fontDef = await getFontDefinition(9);
|
||||
fontDef = await getCSSFontDefinition(9);
|
||||
break;
|
||||
case "":
|
||||
fontDef = "";
|
||||
@@ -921,12 +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];
|
||||
@@ -1087,3 +1109,19 @@ export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promi
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise<void> => {
|
||||
const images = divElement.querySelectorAll<HTMLImageElement>('img[src^="blob:app://obsidian.md"]');
|
||||
|
||||
for (let img of images) {
|
||||
const blobUrl = img.src;
|
||||
try {
|
||||
const response = await fetch(blobUrl);
|
||||
const blob = await response.blob();
|
||||
const base64 = await blobToBase64(blob);
|
||||
img.src = `data:${blob.type};base64,${base64}`;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch or convert blob: ${blobUrl}`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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";
|
||||
@@ -716,6 +717,12 @@ export class ExcalidrawAutomate {
|
||||
this.style.roundness ? "round":"sharp",
|
||||
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
|
||||
colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
|
||||
...template?.appState?.frameRendering
|
||||
? {frameRendering: template.appState.frameRendering}
|
||||
: {},
|
||||
...template?.appState?.objectsSnapModeEnabled
|
||||
? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled}
|
||||
: {},
|
||||
},
|
||||
files: template?.files ?? {},
|
||||
};
|
||||
@@ -1026,6 +1033,22 @@ export class ExcalidrawAutomate {
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add elements to frame
|
||||
* @param frameId
|
||||
* @param elementIDs to add
|
||||
* @returns void
|
||||
*/
|
||||
addElementsToFrame(frameId: string, elementIDs: string[]):void {
|
||||
if(!this.getElement(frameId)) return;
|
||||
elementIDs.forEach(elID => {
|
||||
const el = this.getElement(elID);
|
||||
if(el) {
|
||||
el.frameId = frameId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
@@ -1571,6 +1594,26 @@ export class ExcalidrawAutomate {
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the base64 dataURL of the LaTeX equation rendered as an SVG
|
||||
* @param tex The LaTeX equation as string
|
||||
* @param scale of the image, default value is 4
|
||||
* @returns
|
||||
*/
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
|
||||
async tex2dataURL(
|
||||
tex: string,
|
||||
scale: number = 4 // Default scale value, adjust as needed
|
||||
): Promise<{
|
||||
mimeType: MimeType;
|
||||
fileId: FileId;
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
return await tex2dataURL(tex,scale);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objectA
|
||||
@@ -1896,15 +1939,16 @@ export class ExcalidrawAutomate {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param includeFrameChildren
|
||||
* @returns
|
||||
*/
|
||||
getViewSelectedElements(): any[] {
|
||||
getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return [];
|
||||
}
|
||||
return this.targetView.getViewSelectedElements();
|
||||
return this.targetView.getViewSelectedElements(includeFrameChildren);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2396,24 +2440,44 @@ export class ExcalidrawAutomate {
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
|
||||
getElementsInTheSameGroupWithElement(
|
||||
element: ExcalidrawElement,
|
||||
elements: ExcalidrawElement[],
|
||||
includeFrameElements: boolean = false,
|
||||
): ExcalidrawElement[] {
|
||||
if(!element || !elements) return [];
|
||||
const container = (element.type === "text" && element.containerId)
|
||||
? elements.filter(el=>el.id === element.containerId)
|
||||
: [];
|
||||
if(element.groupIds.length === 0) {
|
||||
if(includeFrameElements && element.type === "frame") {
|
||||
return this.getElementsInFrame(element,elements,true);
|
||||
}
|
||||
if(container.length === 1) return [element,container[0]];
|
||||
return [element];
|
||||
}
|
||||
|
||||
if(container.length === 1) {
|
||||
return elements.filter(el=>
|
||||
el.groupIds.some(id=>element.groupIds.includes(id)) ||
|
||||
el === container[0]
|
||||
);
|
||||
const conditionFN = container.length === 1
|
||||
? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
|
||||
: (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
|
||||
|
||||
if(!includeFrameElements) {
|
||||
return elements.filter(el=>conditionFN(el));
|
||||
} else {
|
||||
//I use the set and the filter at the end to preserve scene layer seqeuence
|
||||
//adding frames could potentially mess up the sequence otherwise
|
||||
const elementIDs = new Set<string>();
|
||||
elements
|
||||
.filter(el=>conditionFN(el))
|
||||
.forEach(el=>{
|
||||
if(el.type === "frame") {
|
||||
this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
|
||||
} else {
|
||||
elementIDs.add(el.id);
|
||||
}
|
||||
});
|
||||
return elements.filter(el=>elementIDs.has(el.id));
|
||||
}
|
||||
|
||||
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2502,10 +2566,11 @@ export class ExcalidrawAutomate {
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the size of the image element at 100% (i.e. the original size)
|
||||
* Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available
|
||||
* @param imageElement an image element from the active scene on targetView
|
||||
* @param shouldWaitForImage if true, the function will wait for the image to load before returning the size
|
||||
*/
|
||||
async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}> {
|
||||
async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getOriginalImageSize()");
|
||||
@@ -2521,10 +2586,59 @@ export class ExcalidrawAutomate {
|
||||
return null;
|
||||
}
|
||||
const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
|
||||
const dataURL = ef.getImage(isDark);
|
||||
let dataURL = ef.getImage(isDark);
|
||||
if(!dataURL && !shouldWaitForImage) return;
|
||||
if(!dataURL) {
|
||||
let watchdog = 0;
|
||||
while(!dataURL && watchdog < 50) {
|
||||
await sleep(100);
|
||||
dataURL = ef.getImage(isDark);
|
||||
watchdog++;
|
||||
}
|
||||
if(!dataURL) return;
|
||||
}
|
||||
return await getImageSize(dataURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the image to its original aspect ratio.
|
||||
* If the image is resized then the function returns true.
|
||||
* If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
|
||||
* Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
|
||||
* @param imageElement - the EA image element to be resized
|
||||
* returns true if image was changed, false if image was not changed
|
||||
*/
|
||||
async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean> {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "resetImageAspectRatio()");
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = await this.getOriginalImageSize(imgEl, true);
|
||||
if (size) {
|
||||
const originalArea = imgEl.width * imgEl.height;
|
||||
const originalAspectRatio = size.width / size.height;
|
||||
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
|
||||
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
|
||||
const centerX = imgEl.x + imgEl.width / 2;
|
||||
const centerY = imgEl.y + imgEl.height / 2;
|
||||
|
||||
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
|
||||
if(!this.getElement(imgEl.id)) {
|
||||
this.copyViewElementsToEAforEditing([imgEl]);
|
||||
}
|
||||
const eaEl = this.getElement(imgEl.id);
|
||||
eaEl.width = newWidth;
|
||||
eaEl.height = newHeight;
|
||||
eaEl.x = centerX - newWidth / 2;
|
||||
eaEl.y = centerY - newHeight / 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||
* recommended use:
|
||||
@@ -2738,7 +2852,7 @@ export async function initExcalidrawAutomate(
|
||||
function normalizeLinePoints(
|
||||
points: [x: number, y: number][],
|
||||
//box: { x: number; y: number; w: number; h: number },
|
||||
) {
|
||||
): number[][] {
|
||||
const p = [];
|
||||
const [x, y] = points[0];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
@@ -2747,7 +2861,9 @@ function normalizeLinePoints(
|
||||
return p;
|
||||
}
|
||||
|
||||
function getLineBox(points: [x: number, y: number][]) {
|
||||
function getLineBox(
|
||||
points: [x: number, y: number][]
|
||||
):{x:number, y:number, w: number, h:number} {
|
||||
const [x1, y1, x2, y2] = estimateLineBound(points);
|
||||
return {
|
||||
x: x1,
|
||||
@@ -2757,11 +2873,11 @@ function getLineBox(points: [x: number, y: number][]) {
|
||||
};
|
||||
}
|
||||
|
||||
function getFontFamily(id: number) {
|
||||
getFontFamilyString({fontFamily:id})
|
||||
function getFontFamily(id: number):string {
|
||||
return getFontFamilyString({fontFamily:id})
|
||||
}
|
||||
|
||||
export async function initFonts() {
|
||||
export async function initFonts():Promise<void> {
|
||||
await excalidrawLib.registerFontsInCSS();
|
||||
const fonts = excalidrawLib.getFontFamilies();
|
||||
for(let i=0;i<fonts.length;i++) {
|
||||
@@ -2774,7 +2890,7 @@ export function _measureText(
|
||||
fontSize: number,
|
||||
fontFamily: number,
|
||||
lineHeight: number,
|
||||
) {
|
||||
): {w: number, h:number} {
|
||||
//following odd error with mindmap on iPad while synchornizing with desktop.
|
||||
if (!fontSize) {
|
||||
fontSize = 20;
|
||||
@@ -2873,7 +2989,7 @@ async function getTemplate(
|
||||
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
|
||||
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
|
||||
if(el.length > 0) {
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
|
||||
@@ -2893,6 +3009,7 @@ async function getTemplate(
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
const filehead = data.substring(0, trimLocation);
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
@@ -2900,7 +3017,7 @@ async function getTemplate(
|
||||
hostFile: file,
|
||||
}) : groupElements,
|
||||
appState: scene.appState,
|
||||
frontmatter: data.substring(0, trimLocation),
|
||||
frontmatter: filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead,
|
||||
files: scene.files,
|
||||
hasSVGwithBitmap,
|
||||
};
|
||||
@@ -2932,7 +3049,7 @@ export async function createPNG(
|
||||
depth: number,
|
||||
padding?: number,
|
||||
imagesDict?: any,
|
||||
) {
|
||||
): Promise<Blob> {
|
||||
if (!loader) {
|
||||
loader = new EmbeddedFilesLoader(plugin);
|
||||
}
|
||||
@@ -3016,7 +3133,7 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
|
||||
})
|
||||
}
|
||||
|
||||
function addFilterToForeignObjects(svg:SVGSVGElement) {
|
||||
function addFilterToForeignObjects(svg:SVGSVGElement):void {
|
||||
const foreignObjects = svg.querySelectorAll("foreignObject");
|
||||
foreignObjects.forEach((foreignObject) => {
|
||||
foreignObject.setAttribute("filter", THEME_FILTER);
|
||||
@@ -3165,7 +3282,7 @@ export function repositionElementsToCursor(
|
||||
return restore({elements}, null, null).elements;
|
||||
}
|
||||
|
||||
function errorMessage(message: string, source: string) {
|
||||
function errorMessage(message: string, source: string):void {
|
||||
switch (message) {
|
||||
case "targetView not set":
|
||||
errorlog({
|
||||
@@ -3218,7 +3335,7 @@ export const search = async (view: ExcalidrawView) => {
|
||||
const ea = view.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(view);
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame");
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -3312,6 +3429,62 @@ export const getFrameElementsMatchingQuery = (
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getElementsWithLinkMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.link &&
|
||||
query.some((q) => {
|
||||
const text = el.link.toLowerCase().trim();
|
||||
return exactMatch
|
||||
? (text === q.toLowerCase())
|
||||
: text.match(q.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getImagesMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
excalidrawData: ExcalidrawData,
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: ExcalidrawElement) =>
|
||||
el.type === "image" &&
|
||||
query.some((q) => {
|
||||
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
|
||||
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
|
||||
const text = filename ?? equation;
|
||||
if(!text) return false;
|
||||
return exactMatch
|
||||
? (text === q.toLowerCase())
|
||||
: text.match(q.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
export const cloneElement = (el: ExcalidrawElement):any => {
|
||||
const newEl = JSON.parse(JSON.stringify(el));
|
||||
newEl.version = el.version + 1;
|
||||
|
||||
@@ -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
|
||||
@@ -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]);
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
18
src/ExcalidrawLib.d.ts
vendored
18
src/ExcalidrawLib.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
|
||||
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
|
||||
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
|
||||
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
|
||||
import { AppState, BinaryFiles, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
|
||||
type EmbeddedLink =
|
||||
@@ -27,6 +27,7 @@ declare namespace ExcalidrawLib {
|
||||
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
|
||||
files: BinaryFiles | null;
|
||||
maxWidthOrHeight?: number;
|
||||
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
||||
getDimensions?: (
|
||||
width: number,
|
||||
height: number,
|
||||
@@ -170,11 +171,20 @@ declare namespace ExcalidrawLib {
|
||||
var WelcomeScreen: any;
|
||||
var TTDDialogTrigger: any;
|
||||
var TTDDialog: any;
|
||||
|
||||
var DiagramToCodePlugin: (props: {
|
||||
generate: GenerateDiagramToCode;
|
||||
}) => any;
|
||||
|
||||
function getDataURL(file: Blob | File): Promise<DataURL>;
|
||||
function destroyObsidianUtils(): void;
|
||||
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
|
||||
function getFontFamilies(): string[];
|
||||
function registerFontsInCSS(): Promise<void>;
|
||||
function getFontDefinition(fontFamily: number): Promise<string>;
|
||||
function getCSSFontDefinition(fontFamily: number): Promise<string>;
|
||||
function getTextFromElements (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
separator?: string,
|
||||
): string;
|
||||
function safelyParseJSON (json: string): Record<string, any> | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawMagicFrameElement,
|
||||
ExcalidrawTextElement,
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
@@ -60,7 +61,9 @@ import {
|
||||
ExcalidrawAutomate,
|
||||
getTextElementsMatchingQuery,
|
||||
cloneElement,
|
||||
getFrameElementsMatchingQuery
|
||||
getFrameElementsMatchingQuery,
|
||||
getElementsWithLinkMatchingQuery,
|
||||
getImagesMatchingQuery
|
||||
} from "./ExcalidrawAutomate";
|
||||
import { t } from "./lang/helpers";
|
||||
import {
|
||||
@@ -126,7 +129,7 @@ import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifier
|
||||
import { setDynamicStyle } from "./utils/DynamicStyling";
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { CustomEmbeddable, renderWebView } from "./customEmbeddable";
|
||||
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, openTagSearch, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils";
|
||||
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
|
||||
import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
|
||||
@@ -135,11 +138,12 @@ import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
|
||||
import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
import { nanoid } from "nanoid";
|
||||
import { CustomMutationObserver, DEBUGGING, debug, log} from "./utils/DebugHelper";
|
||||
import { extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
|
||||
import { errorHTML, extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { SelectCard } from "./dialogs/SelectCard";
|
||||
import { Packages } from "./types/types";
|
||||
import React from "react";
|
||||
import { diagramToHTML } from "./utils/matic";
|
||||
|
||||
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
|
||||
const PREVENT_RELOAD_TIMEOUT = 2000;
|
||||
@@ -900,6 +904,90 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
|
||||
async openLaTeXEditor(eqId: string) {
|
||||
const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === eqId && el.type==="image") as ExcalidrawImageElement;
|
||||
if(!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = el.fileId;
|
||||
|
||||
let equation = this.excalidrawData.getEquation(fileId)?.latex;
|
||||
if(!equation) {
|
||||
await this.save(false);
|
||||
equation = this.excalidrawData.getEquation(fileId)?.latex;
|
||||
if(!equation) return;
|
||||
}
|
||||
|
||||
GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => {
|
||||
if (!formula || formula === equation) {
|
||||
return;
|
||||
}
|
||||
this.excalidrawData.setEquation(fileId, {
|
||||
latex: formula,
|
||||
isLoaded: false,
|
||||
});
|
||||
await this.save(false);
|
||||
await updateEquation(
|
||||
formula,
|
||||
fileId,
|
||||
this,
|
||||
addFiles,
|
||||
);
|
||||
this.setDirty(1);
|
||||
});
|
||||
}
|
||||
|
||||
async openEmbeddedLinkEditor(imgId:string) {
|
||||
const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === imgId && el.type==="image") as ExcalidrawImageElement;
|
||||
if(!el) {
|
||||
return;
|
||||
}
|
||||
const fileId = el.fileId;
|
||||
const ef = this.excalidrawData.getFile(fileId);
|
||||
if(!ef) {
|
||||
return
|
||||
}
|
||||
if (!ef.isHyperLink && !ef.isLocalLink && ef.file) {
|
||||
const handler = async (link:string) => {
|
||||
if (!link || ef.linkParts.original === link) {
|
||||
return;
|
||||
}
|
||||
ef.resetImage(this.file.path, link);
|
||||
this.excalidrawData.setFile(fileId, ef);
|
||||
this.setDirty(2);
|
||||
await this.save(false);
|
||||
await sleep(100);
|
||||
if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) {
|
||||
const ea = getEA(this) as ExcalidrawAutomate;
|
||||
let imgEl = this.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as ExcalidrawImageElement;
|
||||
if(!imgEl) {
|
||||
ea.destroy();
|
||||
return;
|
||||
}
|
||||
if(imgEl && await ea.resetImageAspectRatio(imgEl)) {
|
||||
await ea.addElementsToView(false);
|
||||
}
|
||||
ea.destroy();
|
||||
}
|
||||
}
|
||||
GenericInputPrompt.Prompt(
|
||||
this,
|
||||
this.plugin,
|
||||
this.app,
|
||||
t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"),
|
||||
undefined,
|
||||
ef.linkParts.original,
|
||||
[{caption: "✅", action: (x:string)=>{x.replaceAll("\n","").trim()}}],
|
||||
3,
|
||||
false,
|
||||
(container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}),
|
||||
false
|
||||
).then(handler.bind(this),()=>{});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toggleDisableBinding() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleDisableBinding, "ExcalidrawView.toggleDisableBinding");
|
||||
const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour;
|
||||
@@ -1042,6 +1130,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
? this.excalidrawData.getRawText(selectedText.id)
|
||||
: selectedText.text);
|
||||
|
||||
if(linkText.startsWith("#")) {
|
||||
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
|
||||
}
|
||||
|
||||
const maybeObsidianLink = parseObsidianLink(linkText, this.app);
|
||||
if(typeof maybeObsidianLink === "string") {
|
||||
linkText = maybeObsidianLink;
|
||||
@@ -1054,6 +1146,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
|
||||
if(container) {
|
||||
linkText = container.link;
|
||||
|
||||
if(linkText?.startsWith("#")) {
|
||||
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
|
||||
}
|
||||
|
||||
const maybeObsidianLink = parseObsidianLink(linkText, this.app);
|
||||
if(typeof maybeObsidianLink === "string") {
|
||||
linkText = maybeObsidianLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!linkText || partsArray.length === 0) {
|
||||
@@ -1108,30 +1209,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (selectedImage?.id) {
|
||||
const imageElement = this.getScene().elements.find((el:ExcalidrawElement)=>el.id === selectedImage.id) as ExcalidrawImageElement;
|
||||
if (this.excalidrawData.hasEquation(selectedImage.fileId)) {
|
||||
(async () => {
|
||||
await this.save(false);
|
||||
selectedImage.fileId = imageElement.fileId;
|
||||
const equation = this.excalidrawData.getEquation(
|
||||
selectedImage.fileId,
|
||||
).latex;
|
||||
GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => {
|
||||
if (!formula || formula === equation) {
|
||||
return;
|
||||
}
|
||||
this.excalidrawData.setEquation(selectedImage.fileId, {
|
||||
latex: formula,
|
||||
isLoaded: false,
|
||||
});
|
||||
await this.save(false);
|
||||
await updateEquation(
|
||||
formula,
|
||||
selectedImage.fileId,
|
||||
this,
|
||||
addFiles,
|
||||
);
|
||||
this.setDirty(1);
|
||||
});
|
||||
})();
|
||||
this.updateScene({appState: {contextMenu: null}});
|
||||
this.openLaTeXEditor(selectedImage.id);
|
||||
return;
|
||||
}
|
||||
if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) {
|
||||
@@ -1144,38 +1223,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
await this.save(false); //in case pasted images haven't been saved yet
|
||||
if (this.excalidrawData.hasFile(selectedImage.fileId)) {
|
||||
const ef = this.excalidrawData.getFile(selectedImage.fileId);
|
||||
if (!ef.isHyperLink && !ef.isLocalLink && linkClickType === "md-properties") {
|
||||
if (
|
||||
ef.file.extension === "md" &&
|
||||
!this.plugin.isExcalidrawFile(ef.file)
|
||||
) {
|
||||
const handler = async (link:string) => {
|
||||
if (!link || ef.linkParts.original === link) {
|
||||
return;
|
||||
}
|
||||
ef.resetImage(this.file.path, link);
|
||||
this.setDirty(2);
|
||||
await this.save(false);
|
||||
await this.loadSceneFiles();
|
||||
}
|
||||
GenericInputPrompt.Prompt(
|
||||
this,
|
||||
this.plugin,
|
||||
this.app,
|
||||
t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"),
|
||||
undefined,
|
||||
ef.linkParts.original,
|
||||
[{caption: "✅", action: handler}],
|
||||
1,
|
||||
false,
|
||||
(container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}),
|
||||
false
|
||||
).then(handler, () => {});
|
||||
return;
|
||||
}
|
||||
const fileId = selectedImage.fileId;
|
||||
const ef = this.excalidrawData.getFile(fileId);
|
||||
if (!ef.isHyperLink && !ef.isLocalLink && ef.file && linkClickType === "md-properties") {
|
||||
this.updateScene({appState: {contextMenu: null}});
|
||||
this.openEmbeddedLinkEditor(selectedImage.id);
|
||||
return;
|
||||
}
|
||||
|
||||
let secondOrderLinks: string = " ";
|
||||
|
||||
const backlinks = this.app.metadataCache?.getBacklinksForFile(ef.file)?.data;
|
||||
@@ -1315,7 +1369,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
//final fallback to prevent resizing when text element is in edit mode
|
||||
//this is to prevent jumping text due to on-screen keyboard popup
|
||||
if (api.getAppState()?.editingElement?.type === "text") {
|
||||
if (api.getAppState()?.editingTextElement) {
|
||||
return;
|
||||
}
|
||||
this.zoomToFit(false);
|
||||
@@ -1627,8 +1681,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return;
|
||||
}
|
||||
const st = api.getAppState();
|
||||
const isEditing = st.editingElement !== null;
|
||||
const isDragging = st.draggingElement !== null;
|
||||
const isEditingText = st.editingTextElement !== null;
|
||||
const isEditingNewElement = st.newElement !== null;
|
||||
//this will reset positioning of the cursor in case due to the popup keyboard,
|
||||
//or the command palette, or some other unexpected reason the onResize would not fire...
|
||||
this.refreshCanvasOffset();
|
||||
@@ -1638,8 +1692,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
!this.semaphores.forceSaving &&
|
||||
!this.semaphores.autosaving &&
|
||||
!this.semaphores.embeddableIsEditingSelf &&
|
||||
!isEditing &&
|
||||
!isDragging //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
|
||||
!isEditingText &&
|
||||
!isEditingNewElement //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
|
||||
) {
|
||||
//console.log("autosave");
|
||||
this.autosaveTimer = null;
|
||||
@@ -1928,7 +1982,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
state.match &&
|
||||
state.match.content &&
|
||||
state.match.matches &&
|
||||
state.match.matches.length === 1 &&
|
||||
state.match.matches.length >= 1 &&
|
||||
state.match.matches[0].length === 2
|
||||
) {
|
||||
query = [
|
||||
@@ -2023,7 +2077,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
)) {
|
||||
const cleanQuery = cleanSectionHeading(query[0]);
|
||||
const sections = await this.getBackOfTheNoteSections();
|
||||
if(sections.includes(cleanQuery)) {
|
||||
if(sections.includes(cleanQuery) || this.data.includes(query[0])) {
|
||||
this.setMarkdownView(state);
|
||||
return;
|
||||
}
|
||||
@@ -2213,13 +2267,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
});
|
||||
}
|
||||
|
||||
private getGridColor(bgColor: string, st: AppState):{Bold: string, Regular: string, MajorGridFrequency: number} {
|
||||
private getGridColor(bgColor: string, st: AppState):{Bold: string, Regular: string} {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getGridColor, "ExcalidrawView.getGridColor", bgColor, st);
|
||||
const cm = this.plugin.ea.getCM(bgColor);
|
||||
const isDark = cm.isDark();
|
||||
const Regular = (isDark ? cm.lighterBy(7) : cm.darkerBy(7)).stringHEX({alpha: false});
|
||||
const Bold = (isDark ? cm.lighterBy(14) : cm.darkerBy(14)).stringHEX({alpha: false});
|
||||
return {Bold, Regular, MajorGridFrequency:st.gridColor.MajorGridFrequency};
|
||||
return {Bold, Regular};
|
||||
}
|
||||
|
||||
public activeLoader: EmbeddedFilesLoader = null;
|
||||
@@ -3230,6 +3284,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
version: 2,
|
||||
source: GITHUB_RELEASES+PLUGIN_VERSION,
|
||||
elements: el,
|
||||
//see also ExcalidrawAutomate async create(
|
||||
appState: {
|
||||
theme: st.theme,
|
||||
viewBackgroundColor: st.viewBackgroundColor,
|
||||
@@ -3245,15 +3300,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
currentItemTextAlign: st.currentItemTextAlign,
|
||||
currentItemStartArrowhead: st.currentItemStartArrowhead,
|
||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||
currentItemArrowType: st.currentItemArrowType,
|
||||
scrollX: st.scrollX,
|
||||
scrollY: st.scrollY,
|
||||
zoom: st.zoom,
|
||||
currentItemRoundness: st.currentItemRoundness,
|
||||
gridSize: st.gridSize,
|
||||
gridStep: st.gridStep,
|
||||
gridModeEnabled: st.gridModeEnabled,
|
||||
gridColor: st.gridColor,
|
||||
colorPalette: st.colorPalette,
|
||||
currentStrokeOptions: st.currentStrokeOptions,
|
||||
previousGridSize: st.previousGridSize,
|
||||
frameRendering: st.frameRendering,
|
||||
objectsSnapModeEnabled: st.objectsSnapModeEnabled,
|
||||
},
|
||||
@@ -3372,7 +3429,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element);
|
||||
if(!this.lastMouseEvent) return;
|
||||
const st = this.excalidrawAPI?.getAppState();
|
||||
if(st?.editingElement || st?.draggingElement) return; //should not activate hover preview when element is being edited or dragged
|
||||
if(st?.editingTextElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged
|
||||
if(this.semaphores.wheelTimeout) return;
|
||||
//if link text is not provided, try to get it from the element
|
||||
if (!linktext) {
|
||||
@@ -3693,11 +3750,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
st.editingElement === null &&
|
||||
st.editingTextElement === null &&
|
||||
//Removed because of
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565
|
||||
/*st.resizingElement === null &&
|
||||
st.draggingElement === null &&
|
||||
st.newElement === null &&
|
||||
st.editingGroupId === null &&*/
|
||||
st.editingLinearElement === null
|
||||
) {
|
||||
@@ -3766,8 +3823,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]);
|
||||
ea.destroy();
|
||||
} else {
|
||||
const modal = new UniversalInsertFileModal(this.plugin, this);
|
||||
modal.open(file, this.currentPosition);
|
||||
if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
|
||||
const ea = getEA(this) as ExcalidrawAutomate;
|
||||
await ea.addImage(this.currentPosition.x, this.currentPosition.y,link);
|
||||
ea.addElementsToView(false,false).then(()=>ea.destroy());
|
||||
} else {
|
||||
const modal = new UniversalInsertFileModal(this.plugin, this);
|
||||
modal.open(file, this.currentPosition);
|
||||
}
|
||||
}
|
||||
this.setDirty(9);
|
||||
})) {
|
||||
@@ -5008,6 +5071,70 @@ export default class ExcalidrawView extends TextFileView {
|
||||
);
|
||||
};
|
||||
|
||||
private diagramToCode() {
|
||||
return this.packages.react.createElement(
|
||||
this.packages.excalidrawLib.DiagramToCodePlugin,
|
||||
{
|
||||
generate: async ({ frame, children }:
|
||||
{frame: ExcalidrawMagicFrameElement, children: readonly ExcalidrawElement[]}) => {
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
try {
|
||||
const blob = await this.packages.excalidrawLib.exportToBlob({
|
||||
elements: children,
|
||||
appState: {
|
||||
...appState,
|
||||
exportBackground: true,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
},
|
||||
exportingFrame: frame,
|
||||
files: this.excalidrawAPI.getFiles(),
|
||||
mimeType: "image/jpeg",
|
||||
});
|
||||
|
||||
const dataURL = await this.packages.excalidrawLib.getDataURL(blob);
|
||||
const textFromFrameChildren = this.packages.excalidrawLib.getTextFromElements(children);
|
||||
|
||||
const response = await diagramToHTML ({
|
||||
image:dataURL,
|
||||
apiKey: this.plugin.settings.openAIAPIToken,
|
||||
text: textFromFrameChildren,
|
||||
theme: appState.theme,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
const text = json.error?.message || "Unknown error during generation";
|
||||
return {
|
||||
html: errorHTML(text),
|
||||
};
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
if(json.choices[0].message.content == null) {
|
||||
return {
|
||||
html: errorHTML("Nothing generated"),
|
||||
};
|
||||
}
|
||||
|
||||
const message = json.choices[0].message.content;
|
||||
|
||||
const html = message.slice(
|
||||
message.indexOf("<!DOCTYPE html>"),
|
||||
message.indexOf("</html>") + "</html>".length,
|
||||
);
|
||||
|
||||
return { html };
|
||||
} catch (err: any) {
|
||||
return {
|
||||
html: errorHTML("Request failed"),
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private ttdDialogTrigger() {
|
||||
return this.packages.react.createElement(
|
||||
this.packages.excalidrawLib.TTDDialogTrigger,
|
||||
@@ -5256,14 +5383,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//...again, just aweful, but works.
|
||||
const st = api.getAppState();
|
||||
//isEventOnSameElement attempts to solve https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729
|
||||
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingElement is not null
|
||||
const isEventOnSameElement = this.editingTextElementId === st.editingElement?.id;
|
||||
const isKeyboardOutEvent:Boolean = st.editingElement && st.editingElement.type === "text" && !isEventOnSameElement;
|
||||
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingTextElement is not null
|
||||
const isEventOnSameElement = this.editingTextElementId === st.editingTextElement?.id;
|
||||
const isKeyboardOutEvent:Boolean = st.editingTextElement && !isEventOnSameElement;
|
||||
const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent;
|
||||
this.editingTextElementId = isKeyboardOutEvent ? st.editingElement.id : null;
|
||||
this.editingTextElementId = isKeyboardOutEvent ? st.editingTextElement.id : null;
|
||||
if(isKeyboardOutEvent) {
|
||||
const appToolHeight = (this.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0;
|
||||
const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingElement.y}, st).y;
|
||||
const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingTextElement.y}, st).y;
|
||||
const scrollViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:-st.scrollY}, st).y;
|
||||
const delta = editingElViewY - scrollViewY;
|
||||
const isElementAboveKeyboard = height > (delta + appToolHeight*2)
|
||||
@@ -5454,6 +5581,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.renderCustomActionsMenu(),
|
||||
this.renderWelcomeScreen(),
|
||||
this.ttdDialog(),
|
||||
this.diagramToCode(),
|
||||
this.ttdDialogTrigger(),
|
||||
),
|
||||
this.renderToolsPanel(observer),
|
||||
@@ -5634,15 +5762,24 @@ export default class ExcalidrawView extends TextFileView {
|
||||
let match = getTextElementsMatchingQuery(
|
||||
elements.filter((el: ExcalidrawElement) => el.type === "text"),
|
||||
query,
|
||||
exactMatch
|
||||
exactMatch,
|
||||
).concat(getFrameElementsMatchingQuery(
|
||||
elements.filter((el: ExcalidrawElement) => el.type === "frame"),
|
||||
query,
|
||||
exactMatch
|
||||
exactMatch,
|
||||
)).concat(getElementsWithLinkMatchingQuery(
|
||||
elements.filter((el: ExcalidrawElement) => el.link),
|
||||
query,
|
||||
exactMatch,
|
||||
)).concat(getImagesMatchingQuery(
|
||||
elements,
|
||||
query,
|
||||
this.excalidrawData,
|
||||
exactMatch,
|
||||
));
|
||||
|
||||
if (match.length === 0) {
|
||||
new Notice("I could not find a matching text element");
|
||||
new Notice(t("NO_SEARCH_RESULT"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5667,7 +5804,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
const zoomLevel = this.plugin.settings.zoomToFitMaxLevel;
|
||||
if (selectResult) {
|
||||
api.selectElements(elements);
|
||||
api.selectElements(elements, true);
|
||||
}
|
||||
api.zoomToFit(elements, zoomLevel, 0.05);
|
||||
}
|
||||
@@ -5681,9 +5818,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return api.getSceneElements();
|
||||
}
|
||||
|
||||
public getViewSelectedElements(): ExcalidrawElement[] {
|
||||
/**
|
||||
*
|
||||
* @param deepSelect: if set to true, child elements of the selected frame will also be selected
|
||||
* @returns
|
||||
*/
|
||||
public getViewSelectedElements(includFrameChildren: boolean = true): ExcalidrawElement[] {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewSelectedElements, "ExcalidrawView.getViewSelectedElements");
|
||||
const api = this.excalidrawAPI;
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if (!api) {
|
||||
return [];
|
||||
}
|
||||
@@ -5695,6 +5837,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!selectedElementsKeys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const elementIDs = new Set<string>();
|
||||
|
||||
const elements: ExcalidrawElement[] = api
|
||||
.getSceneElements()
|
||||
.filter((e: any) => selectedElementsKeys.includes(e.id));
|
||||
@@ -5712,15 +5857,27 @@ export default class ExcalidrawView extends TextFileView {
|
||||
.map((be) => be.id)[0],
|
||||
);
|
||||
|
||||
const elementIDs = elements
|
||||
.map((el) => el.id)
|
||||
.concat(containerBoundTextElmenetsReferencedInElements);
|
||||
if(includFrameChildren && elements.some(el=>el.type === "frame")) {
|
||||
elements.filter(el=>el.type === "frame").forEach(frameEl => {
|
||||
api.getSceneElements()
|
||||
.filter(el=>el.frameId === frameEl.id)
|
||||
.forEach(el=>elementIDs.add(el.id))
|
||||
})
|
||||
}
|
||||
|
||||
elements.forEach(el=>elementIDs.add(el.id));
|
||||
containerBoundTextElmenetsReferencedInElements.forEach(id=>elementIDs.add(id));
|
||||
|
||||
return api
|
||||
.getSceneElements()
|
||||
.filter((el: ExcalidrawElement) => elementIDs.contains(el.id));
|
||||
.filter((el: ExcalidrawElement) => elementIDs.has(el.id));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param prefix - defines the default button.
|
||||
* @returns
|
||||
*/
|
||||
public async copyLinkToSelectedElementToClipboard(prefix:string) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.copyLinkToSelectedElementToClipboard, "ExcalidrawView.copyLinkToSelectedElementToClipboard", prefix);
|
||||
const elements = this.getViewSelectedElements();
|
||||
@@ -5747,58 +5904,59 @@ export default class ExcalidrawView extends TextFileView {
|
||||
: this.plugin.ea.getLargestElement(elements).id;
|
||||
}
|
||||
|
||||
const isFrame = elements.some(el=>el.id === elementId && el.type==="frame");
|
||||
const frames = elements.filter(el=>el.type==="frame");
|
||||
const hasFrame = frames.length === 1;
|
||||
const hasGroup = elements.some(el=>el.groupIds && el.groupIds.length>0);
|
||||
|
||||
let button = {
|
||||
area: {caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
link: {caption: "Link", action:()=>{prefix="";return}},
|
||||
group: {caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
frame: {caption: "Frame", action:()=>{prefix="frame="; elementId = frames[0].id; return;}},
|
||||
clippedframe: {caption: "Clipped Frame", action:()=>{prefix="clippedframe="; ; elementId = frames[0].id; return;}},
|
||||
}
|
||||
|
||||
let buttons = [];
|
||||
if(isFrame) {
|
||||
switch(prefix) {
|
||||
case "clippedframe=":
|
||||
buttons = [
|
||||
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
];
|
||||
break;
|
||||
case "area=":
|
||||
case "group=":
|
||||
case "frame=":
|
||||
buttons = [
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
buttons = [
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
|
||||
]
|
||||
}
|
||||
|
||||
} else {
|
||||
switch(prefix) {
|
||||
case "area=":
|
||||
buttons = [
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
];
|
||||
break;
|
||||
case "group=":
|
||||
buttons = [
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
buttons = [
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
]
|
||||
}
|
||||
switch(prefix) {
|
||||
case "area=":
|
||||
buttons = [
|
||||
button.area,
|
||||
button.link,
|
||||
...hasGroup ? [button.group] : [],
|
||||
...hasFrame ? [button.frame, button.clippedframe] : [],
|
||||
];
|
||||
break;
|
||||
case "group=":
|
||||
buttons = [
|
||||
...hasGroup ? [button.group] : [],
|
||||
button.link,
|
||||
button.area,
|
||||
...hasFrame ? [button.frame, button.clippedframe] : [],
|
||||
];
|
||||
break;
|
||||
case "frame=":
|
||||
buttons = [
|
||||
...hasFrame ? [button.frame, button.clippedframe] : [],
|
||||
...hasGroup ? [button.group] : [],
|
||||
button.link,
|
||||
button.area,
|
||||
];
|
||||
break;
|
||||
case "clippedframe=":
|
||||
buttons = [
|
||||
...hasFrame ? [button.clippedframe, button.frame] : [],
|
||||
...hasGroup ? [button.group] : [],
|
||||
button.link,
|
||||
button.area,
|
||||
];
|
||||
break;
|
||||
default:
|
||||
buttons = [
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
...hasFrame ? [button.frame, button.clippedframe] : [],
|
||||
]
|
||||
}
|
||||
|
||||
const alias = await ScriptEngine.inputPrompt(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
App,
|
||||
MarkdownPostProcessorContext,
|
||||
MetadataCache,
|
||||
PaneType,
|
||||
@@ -25,7 +26,7 @@ import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./util
|
||||
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
|
||||
import { ImageKey, imageCache } from "./utils/ImageCache";
|
||||
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
|
||||
import { CustomMutationObserver, DEBUGGING } from "./utils/DebugHelper";
|
||||
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
|
||||
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
|
||||
import { linkPrompt } from "./dialogs/Prompt";
|
||||
|
||||
@@ -38,8 +39,11 @@ interface imgElementAttributes {
|
||||
}
|
||||
|
||||
let plugin: ExcalidrawPlugin;
|
||||
let app: App;
|
||||
let vault: Vault;
|
||||
let metadataCache: MetadataCache;
|
||||
const DEBUGGING_MPP = false;
|
||||
|
||||
|
||||
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
|
||||
const width = parseInt(plugin.settings.width);
|
||||
@@ -60,8 +64,9 @@ const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
|
||||
|
||||
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
|
||||
plugin = p;
|
||||
vault = p.app.vault;
|
||||
metadataCache = p.app.metadataCache;
|
||||
app = plugin.app;
|
||||
vault = app.vault;
|
||||
metadataCache = app.metadataCache;
|
||||
};
|
||||
|
||||
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
|
||||
@@ -74,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
}):Promise<HTMLImageElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
|
||||
const width = parseInt(imgAttributes.fwidth);
|
||||
const scale = width >= 2400
|
||||
? 5
|
||||
@@ -140,6 +146,7 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
|
||||
onCanvas: boolean,
|
||||
}
|
||||
) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
|
||||
let style = "";
|
||||
if(imgAttributes.fwidth) {
|
||||
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
|
||||
@@ -171,6 +178,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
}):Promise<HTMLImageElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
|
||||
exportSettings.skipInliningFonts = false;
|
||||
const cacheKey = {
|
||||
...filenameParts,
|
||||
@@ -238,6 +246,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
}):Promise<HTMLDivElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
|
||||
exportSettings.skipInliningFonts = false;
|
||||
const cacheKey = {
|
||||
...filenameParts,
|
||||
@@ -300,6 +309,7 @@ const getIMG = async (
|
||||
imgAttributes: imgElementAttributes,
|
||||
onCanvas: boolean = false,
|
||||
): Promise<HTMLImageElement | HTMLDivElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
|
||||
let file = imgAttributes.file;
|
||||
if (!imgAttributes.file) {
|
||||
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
|
||||
@@ -347,22 +357,23 @@ const getIMG = async (
|
||||
case PreviewImageType.PNG: {
|
||||
const img = createEl("img");
|
||||
setStyle({element:img,imgAttributes,onCanvas});
|
||||
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
|
||||
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
|
||||
}
|
||||
case PreviewImageType.SVGIMG: {
|
||||
const img = createEl("img");
|
||||
setStyle({element:img,imgAttributes,onCanvas});
|
||||
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
|
||||
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
|
||||
}
|
||||
case PreviewImageType.SVG: {
|
||||
const img = createEl("div");
|
||||
setStyle({element:img,imgAttributes,onCanvas});
|
||||
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
|
||||
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
|
||||
const svgString = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
@@ -375,6 +386,7 @@ const createImgElement = async (
|
||||
attr: imgElementAttributes,
|
||||
onCanvas: boolean = false,
|
||||
) :Promise<HTMLElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
|
||||
const imgOrDiv = await getIMG(attr,onCanvas);
|
||||
if(!imgOrDiv) {
|
||||
return null;
|
||||
@@ -502,6 +514,7 @@ const createImageDiv = async (
|
||||
attr: imgElementAttributes,
|
||||
onCanvas: boolean = false
|
||||
): Promise<HTMLDivElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
|
||||
const img = await createImgElement(attr, onCanvas);
|
||||
return createDiv(attr.style.join(" "), (el) => el.append(img));
|
||||
};
|
||||
@@ -510,6 +523,7 @@ const processReadingMode = async (
|
||||
embeddedItems: NodeListOf<Element> | [HTMLElement],
|
||||
ctx: MarkdownPostProcessorContext,
|
||||
) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
|
||||
//We are processing a non-excalidraw file in reading mode
|
||||
//Embedded files will be displayed in an .internal-embed container
|
||||
|
||||
@@ -541,6 +555,7 @@ const processReadingMode = async (
|
||||
};
|
||||
|
||||
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
|
||||
const attr: imgElementAttributes = {
|
||||
fname: "",
|
||||
fheight: "",
|
||||
@@ -577,6 +592,7 @@ const processAltText = (
|
||||
alt:string,
|
||||
attr: imgElementAttributes
|
||||
) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
|
||||
if (alt && !alt.startsWith(fname)) {
|
||||
//2:width, 3:height, 4:style 12 3 4
|
||||
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
|
||||
@@ -596,6 +612,7 @@ const processAltText = (
|
||||
}
|
||||
|
||||
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
@@ -606,7 +623,11 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
const tmpObsidianWYSIWYG = async (
|
||||
el: HTMLElement,
|
||||
ctx: MarkdownPostProcessorContext,
|
||||
isPrinting: boolean,
|
||||
isMarkdownReadingMode: boolean,
|
||||
isHoverPopover: boolean,
|
||||
) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
|
||||
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
|
||||
if(!(file instanceof TFile)) return;
|
||||
if(!plugin.isExcalidrawFile(file)) return;
|
||||
@@ -624,11 +645,11 @@ const tmpObsidianWYSIWYG = async (
|
||||
//@ts-ignore
|
||||
const containerEl = ctx.containerEl;
|
||||
|
||||
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
|
||||
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
|
||||
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -656,14 +677,14 @@ const tmpObsidianWYSIWYG = async (
|
||||
|
||||
|
||||
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
|
||||
const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
|
||||
//const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
|
||||
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
|
||||
if(isHoverPopover && shouldOpenMD) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
|
||||
//const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
|
||||
|
||||
const attr: imgElementAttributes = {
|
||||
fname: ctx.sourcePath,
|
||||
@@ -675,7 +696,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
attr.file = file;
|
||||
|
||||
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
|
||||
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
|
||||
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
|
||||
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
|
||||
if(isPrinting) {
|
||||
internalEmbedDiv = containerEl;
|
||||
@@ -762,6 +783,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
});
|
||||
};
|
||||
|
||||
const docIDs = new Set<string>();
|
||||
/**
|
||||
*
|
||||
* @param el
|
||||
@@ -771,12 +793,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;
|
||||
}
|
||||
|
||||
@@ -785,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;
|
||||
}
|
||||
|
||||
3
src/constants/constSettingsTags.ts
Normal file
3
src/constants/constSettingsTags.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const TAG_PDFEXPORT = "PDFExport";
|
||||
export const TAG_MDREADINGMODE = "MDReadingMode";
|
||||
export const TAG_AUTOEXPORT = "Autoexport";
|
||||
@@ -9,6 +9,11 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
|
||||
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
|
||||
EXCALIDRAW_PLUGIN = plugin;
|
||||
};
|
||||
export const THEME = {
|
||||
LIGHT: "light",
|
||||
DARK: "dark",
|
||||
} as const;
|
||||
|
||||
const MD_EXCALIDRAW = "# Excalidraw Data";
|
||||
const MD_TEXTELEMENTS = "## Text Elements";
|
||||
const MD_ELEMENTLINKS = "## Element Links";
|
||||
@@ -97,7 +102,7 @@ export const {
|
||||
getFontFamilyString,
|
||||
getContainerElement,
|
||||
refreshTextDimensions,
|
||||
getFontDefinition,
|
||||
getCSSFontDefinition,
|
||||
} = excalidrawLib;
|
||||
|
||||
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { t } from "src/lang/helpers";
|
||||
import { ExcalidrawElement, getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
|
||||
import { REGEX_LINK } from "src/ExcalidrawData";
|
||||
import { REGEX_LINK, REGEX_TAGS } from "src/ExcalidrawData";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/ExcalidrawViewUtils";
|
||||
|
||||
@@ -211,15 +211,15 @@ export class GenericInputPrompt extends Modal {
|
||||
}, 30);
|
||||
}
|
||||
|
||||
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback);
|
||||
textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written
|
||||
textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down
|
||||
textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down
|
||||
textComponent.inputEl.addEventListener('input', checkcaret); // Other input events
|
||||
textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions
|
||||
textComponent.inputEl.addEventListener('cut', checkcaret);
|
||||
textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event
|
||||
textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event
|
||||
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback.bind(this));
|
||||
textComponent.inputEl.addEventListener('keyup', checkcaret.bind(this)); // Every character written
|
||||
textComponent.inputEl.addEventListener('pointerup', checkcaret.bind(this)); // Click down
|
||||
textComponent.inputEl.addEventListener('touchend', checkcaret.bind(this)); // Click down
|
||||
textComponent.inputEl.addEventListener('input', checkcaret.bind(this)); // Other input events
|
||||
textComponent.inputEl.addEventListener('paste', checkcaret.bind(this)); // Clipboard actions
|
||||
textComponent.inputEl.addEventListener('cut', checkcaret.bind(this));
|
||||
textComponent.inputEl.addEventListener('select', checkcaret.bind(this)); // Some browsers support this event
|
||||
textComponent.inputEl.addEventListener('selectionchange', checkcaret.bind(this));// Some browsers support this event
|
||||
|
||||
return textComponent;
|
||||
}
|
||||
@@ -272,18 +272,18 @@ export class GenericInputPrompt extends Modal {
|
||||
this.createButton(
|
||||
actionButtonContainer,
|
||||
"✅",
|
||||
this.submitClickCallback,
|
||||
this.submitClickCallback.bind(this),
|
||||
).setCta().buttonEl.style.marginRight = "0";
|
||||
}
|
||||
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL"));
|
||||
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback.bind(this), t("PROMPT_BUTTON_CANCEL"));
|
||||
if(this.displayEditorButtons) {
|
||||
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
|
||||
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
|
||||
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback.bind(this), "Delete");
|
||||
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
|
||||
if(this.view) {
|
||||
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK"));
|
||||
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback.bind(this), t("PROMPT_BUTTON_INSERT_LINK"));
|
||||
}
|
||||
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE"));
|
||||
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback.bind(this), t("PROMPT_BUTTON_UPPERCASE"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,8 +342,13 @@ export class GenericInputPrompt extends Modal {
|
||||
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
|
||||
private submitClickCallback = () => this.submit();
|
||||
private cancelClickCallback = () => this.cancel();
|
||||
private submitClickCallback () {
|
||||
this.submit();
|
||||
}
|
||||
|
||||
private cancelClickCallback () {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
private keyDownCallback = (evt: KeyboardEvent) => {
|
||||
if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) {
|
||||
@@ -668,10 +673,10 @@ export class ConfirmationPrompt extends Modal {
|
||||
buttonContainer.style.display = "flex";
|
||||
buttonContainer.style.justifyContent = "flex-end";
|
||||
|
||||
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback);
|
||||
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback.bind(this));
|
||||
cancelButton.buttonEl.style.marginRight = "0.5rem";
|
||||
|
||||
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback);
|
||||
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback.bind(this));
|
||||
confirmButton.buttonEl.style.marginRight = "0";
|
||||
|
||||
cancelButton.buttonEl.focus();
|
||||
@@ -683,12 +688,12 @@ export class ConfirmationPrompt extends Modal {
|
||||
return button;
|
||||
}
|
||||
|
||||
private cancelClickCallback = () => {
|
||||
private cancelClickCallback() {
|
||||
this.didConfirm = false;
|
||||
this.close();
|
||||
};
|
||||
|
||||
private confirmClickCallback = () => {
|
||||
private confirmClickCallback() {
|
||||
this.didConfirm = true;
|
||||
this.close();
|
||||
};
|
||||
@@ -714,18 +719,28 @@ export async function linkPrompt (
|
||||
view?: ExcalidrawView,
|
||||
message: string = "Select link to open",
|
||||
):Promise<[file:TFile, linkText:string, subpath: string]> {
|
||||
const partsArray = REGEX_LINK.getResList(linkText);
|
||||
const linksArray = REGEX_LINK.getResList(linkText);
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText);
|
||||
let subpath: string = null;
|
||||
let file: TFile = null;
|
||||
let parts = partsArray[0];
|
||||
if (partsArray.length > 1) {
|
||||
let parts = linksArray[0] ?? tagsArray[0];
|
||||
const itemsDisplay = [
|
||||
...linksArray.filter(p=> Boolean(p.value)).map(p => {
|
||||
const alias = REGEX_LINK.getAliasOrLink(p);
|
||||
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
|
||||
}),
|
||||
...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)),
|
||||
];
|
||||
const items = [
|
||||
...linksArray.filter(p=>Boolean(p.value)),
|
||||
...tagsArray.filter(x=> Boolean(x.value)),
|
||||
];
|
||||
|
||||
if (items.length>1) {
|
||||
parts = await ScriptEngine.suggester(
|
||||
app,
|
||||
partsArray.filter(p=>Boolean(p.value)).map(p => {
|
||||
const alias = REGEX_LINK.getAliasOrLink(p);
|
||||
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
|
||||
}),
|
||||
partsArray.filter(p=>Boolean(p.value)),
|
||||
itemsDisplay,
|
||||
items,
|
||||
message,
|
||||
);
|
||||
if(!parts) return;
|
||||
@@ -735,8 +750,8 @@ export async function linkPrompt (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parts.value) {
|
||||
openTagSearch(linkText, app);
|
||||
if (REGEX_TAGS.isTag(parts)) {
|
||||
openTagSearch(REGEX_TAGS.getTag(parts), app);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addElementsToFrame",
|
||||
code: "addElementsToFrame(frameId: string, elementIDs: string[]):void;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addFrame",
|
||||
code: "addFrame(topX: number, topY: number, width: number, height: number, name?: string): string;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addRect",
|
||||
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
|
||||
@@ -311,6 +323,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "tex2dataURL",
|
||||
code: "async tex2dataURL(tex: string, scale: number = 4): Promise<{mimeType: MimeType;fileId: FileId;dataURL: DataURL;created: number;size: { height: number; width: number };}> ",
|
||||
desc: "returns the base64 dataURL of the LaTeX equation rendered as an SVG. tex is the LaTeX equation string",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "connectObjects",
|
||||
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
|
||||
@@ -387,8 +405,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "getViewSelectedElements",
|
||||
code: "getViewSelectedElements(): ExcalidrawElement[];",
|
||||
desc: null,
|
||||
code: "getViewSelectedElements(includeFrameChildren: boolean = true): ExcalidrawElement[];",
|
||||
desc: "If a frame is selected this function will return the frame and all its elements unless includeFrameChildren is set to false",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -489,8 +507,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "getElementsInTheSameGroupWithElement",
|
||||
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];",
|
||||
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
|
||||
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[], includeFrameElements: boolean = false): ExcalidrawElement[];",
|
||||
desc: "Gets all the elements from elements[] that share one or more groupIds with element.<br>" +
|
||||
"If includeFrameElements is true, then if the frame is part of the group all the elements that are in the frame will also be included in the result set",
|
||||
after: ""
|
||||
},
|
||||
{
|
||||
@@ -531,8 +550,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "getOriginalImageSize",
|
||||
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>",
|
||||
desc: "Returns the size of the image element at 100% (i.e. the original size). This is an async function, you need to await the result.",
|
||||
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}>",
|
||||
desc: "Returns the size of the image element at 100% (i.e. the original size) or undefined if the data URL is not available.\n"+
|
||||
"If shouldWaitForImage is true, the function will wait for the view to load the image before returning the size.\n"+
|
||||
"This is an async function, you need to await the result.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "resetImageAspectRatio",
|
||||
code: "async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean>",
|
||||
desc: "Resets the image to its original aspect ratio.\n" +
|
||||
"If the image is resized then the function returns true.\n" +
|
||||
"If the image element is not in EA (only in the view), then if the image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).\n" +
|
||||
"Note you need to run await ea.addElementsToView(false); to add the modified image to the view.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -53,7 +54,7 @@ export default {
|
||||
COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)",
|
||||
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
|
||||
INSERT_LINK_TO_ELEMENT:
|
||||
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link.`,
|
||||
INSERT_LINK_TO_ELEMENT_GROUP:
|
||||
"Copy 'group=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
@@ -79,7 +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",
|
||||
@@ -92,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.",
|
||||
@@ -322,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",
|
||||
@@ -526,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. " +
|
||||
@@ -557,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",
|
||||
@@ -767,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...",
|
||||
@@ -835,4 +847,16 @@ FILENAME_HEAD: "Filename",
|
||||
FRAME_SETTIGNS_NAME: "Display Frame Name",
|
||||
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
|
||||
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
|
||||
|
||||
//InsertPDFModal.ts
|
||||
IPM_PAGES_TO_IMPORT_NAME: "Pages to import",
|
||||
IPM_SELECT_PAGES_TO_IMPORT: "Please select pages to import",
|
||||
IPM_ADD_BORDER_BOX_NAME: "Add border box",
|
||||
IPM_ADD_FRAME_NAME: "Add page to frame",
|
||||
IPM_ADD_FRAME_DESC: "For easier handling I recommend to lock the page inside the frame. " +
|
||||
"If, however, you do lock the page inside the frame then the only way to unlock it is to right-click the frame, select remove elements from frame, then unlock the page.",
|
||||
IPM_GROUP_PAGES_NAME: "Group pages",
|
||||
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
|
||||
IPM_SELECT_PDF: "Please select a PDF file",
|
||||
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
// 简体中文
|
||||
@@ -322,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 的背景笔记)时将笔记导出为 PDF,Excalidraw 绘图是否应该呈现为图像? <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: "允许在触控笔模式下进行双指缩放",
|
||||
@@ -526,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 文档中的绘图。" +
|
||||
@@ -557,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: "将链接作为注释嵌入",
|
||||
@@ -606,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>"+
|
||||
@@ -767,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: "缩放至标题",
|
||||
@@ -835,4 +841,16 @@ FILENAME_HEAD: "文件名",
|
||||
FRAME_SETTIGNS_NAME: "显示框架名称",
|
||||
FRAME_SETTINGS_OUTLINE: "显示框架外边框",
|
||||
FRAME_SETTINGS_CLIP: "启用框架裁剪",
|
||||
};
|
||||
|
||||
//InsertPDFModal.ts
|
||||
IPM_PAGES_TO_IMPORT_NAME: "要导入的页面",
|
||||
IPM_SELECT_PAGES_TO_IMPORT: "请选择页面以进行导入",
|
||||
IPM_ADD_BORDER_BOX_NAME: "添加带边框的盒子容器",
|
||||
IPM_ADD_FRAME_NAME: "添加页面到框架",
|
||||
IPM_ADD_FRAME_DESC: "为了更方便的操作,我建议将页面锁定在框架内。" +
|
||||
"如果,你将锁定页面在框架内,则唯一的解锁方法是右键点击框架,选择‘从框架中移除元素’,然后解锁页面。",
|
||||
IPM_GROUP_PAGES_NAME: "建立页面组",
|
||||
IPM_GROUP_PAGES_DESC: "这将把所有页面建立为一个单独的组。如果您在导入后锁定页面,建议使用此方法,因为这样可以更方便地解锁整个组,而不是逐个解锁。",
|
||||
IPM_SELECT_PDF: "请选择一个 PDF 文件",
|
||||
|
||||
};
|
||||
70
src/main.ts
70
src/main.ts
@@ -1794,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"),
|
||||
|
||||
@@ -365,13 +365,10 @@ export const ICONS = {
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 22h14a2 2 0 0 0 2-2V7.5L14.5 2H6a2 2 0 0 0-2 2v7"/>
|
||||
<polyline
|
||||
points="14 2 14 8 20 8"
|
||||
fill="var(--icon-fill-color)"
|
||||
/>
|
||||
<path d="m10 18 3-3-3-3"/>
|
||||
<path d="M4 18v-1a2 2 0 0 1 2-2h6"/>
|
||||
<path d="M10 12.5 8 15l2 2.5"/>
|
||||
<path d="m14 12.5 2 2.5-2 2.5"/>
|
||||
<path d="M14 2v4a2 2 0 0 0 2 2h4"/>
|
||||
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/>
|
||||
</svg>
|
||||
),
|
||||
//fa-brands fa-markdown
|
||||
|
||||
@@ -37,8 +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 { link } from "fs";
|
||||
import { Rank } from "./menu/ActionIcons";
|
||||
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -78,6 +78,7 @@ export interface ExcalidrawSettings {
|
||||
matchThemeTrigger: boolean;
|
||||
defaultMode: string;
|
||||
defaultPenMode: "never" | "mobile" | "always";
|
||||
penModeDoubleTapEraser: boolean;
|
||||
penModeCrosshairVisible: boolean;
|
||||
renderImageInMarkdownReadingMode: boolean,
|
||||
renderImageInHoverPreviewForMDNotes: boolean,
|
||||
@@ -168,6 +169,7 @@ export interface ExcalidrawSettings {
|
||||
numberOfCustomPens: number;
|
||||
pdfScale: number;
|
||||
pdfBorderBox: boolean;
|
||||
pdfFrame: boolean;
|
||||
pdfGapSize: number;
|
||||
pdfGroupPages: boolean;
|
||||
pdfLockAfterImport: boolean;
|
||||
@@ -243,6 +245,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
matchThemeTrigger: false,
|
||||
defaultMode: "normal",
|
||||
defaultPenMode: "never",
|
||||
penModeDoubleTapEraser: true,
|
||||
penModeCrosshairVisible: true,
|
||||
renderImageInMarkdownReadingMode: false,
|
||||
renderImageInHoverPreviewForMDNotes: false,
|
||||
@@ -339,6 +342,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
numberOfCustomPens: 0,
|
||||
pdfScale: 4,
|
||||
pdfBorderBox: true,
|
||||
pdfFrame: false,
|
||||
pdfGapSize: 20,
|
||||
pdfGroupPages: false,
|
||||
pdfLockAfterImport: true,
|
||||
@@ -1024,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")))
|
||||
@@ -1036,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) =>
|
||||
@@ -1047,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"))
|
||||
@@ -1837,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) =>
|
||||
@@ -1848,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"))
|
||||
@@ -1987,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"))
|
||||
|
||||
@@ -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>`;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
|
||||
import { App, Notice, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { getLinkParts } from "./Utils";
|
||||
@@ -72,24 +72,26 @@ export function getLinkTextFromLink (text: string): string {
|
||||
return linktext;
|
||||
}
|
||||
|
||||
export function openTagSearch (link:string, app: App, view?: ExcalidrawView) {
|
||||
const tags = link
|
||||
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
|
||||
.next();
|
||||
if (!tags.value || tags.value.length < 2) {
|
||||
export function openTagSearch(link: string, app: App, view?: ExcalidrawView) {
|
||||
const tags = REGEX_TAGS.getResList(link);
|
||||
|
||||
if (!tags.length || !tags[0].value || tags[0].value.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const search = app.workspace.getLeavesOfType("search");
|
||||
if (search.length == 0) {
|
||||
if (search.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
search[0].view.setQuery(`tag:${tags.value[1]}`);
|
||||
search[0].view.setQuery(`tag:${tags[0].value[1]}`);
|
||||
app.workspace.revealLeaf(search[0]);
|
||||
|
||||
if (view && view.isFullscreen()) {
|
||||
view.exitFullscreen();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -253,16 +253,16 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
|
||||
private getObjectStore(mode: IDBTransactionMode, storeName: string): IDBObjectStore {
|
||||
const transaction = this.db!.transaction(storeName, mode);
|
||||
return transaction.objectStore(storeName);
|
||||
}
|
||||
|
||||
private async getCacheData(key: string): Promise<FileCacheData | null> {
|
||||
const store = await this.getObjectStore("readonly", this.cacheStoreName);
|
||||
const store = this.getObjectStore("readonly", this.cacheStoreName);
|
||||
const request = store.get(key);
|
||||
|
||||
return new Promise<FileCacheData | null>((resolve, reject) => {
|
||||
return await new Promise<FileCacheData | null>((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as FileCacheData;
|
||||
resolve(result || null);
|
||||
@@ -275,7 +275,7 @@ class ImageCache {
|
||||
}
|
||||
|
||||
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
|
||||
const store = await this.getObjectStore("readonly", this.backupStoreName);
|
||||
const store = this.getObjectStore("readonly", this.backupStoreName);
|
||||
const request = store.get(key);
|
||||
|
||||
return new Promise<BackupData | null>((resolve, reject) => {
|
||||
@@ -308,7 +308,9 @@ class ImageCache {
|
||||
? await this.getCacheData(key)
|
||||
: await Promise.race([
|
||||
this.getCacheData(key),
|
||||
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
|
||||
new Promise<undefined>((_,reject) => setTimeout(() => {
|
||||
reject(undefined);
|
||||
}, 100))
|
||||
]);
|
||||
this.fullyInitialized = true;
|
||||
if(!cachedData) return undefined;
|
||||
|
||||
@@ -409,4 +409,4 @@ export async function closeLeafView(leaf: WorkspaceLeaf) {
|
||||
type: "empty",
|
||||
state: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -922,3 +922,20 @@ export async function getFontMetrics(fontUrl: string, name: string): Promise<Fon
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Thanks https://stackoverflow.com/a/54555834
|
||||
export function cropCanvas(
|
||||
srcCanvas: HTMLCanvasElement,
|
||||
crop: { left: number, top: number, width: number, height: number },
|
||||
output: { width: number, height: number } = { width: crop.width, height: crop.height })
|
||||
{
|
||||
const dstCanvas = createEl('canvas');
|
||||
dstCanvas.width = output.width;
|
||||
dstCanvas.height = output.height;
|
||||
dstCanvas.getContext('2d')!.drawImage(
|
||||
srcCanvas,
|
||||
crop.left, crop.top, crop.width, crop.height,
|
||||
0, 0, output.width, output.height
|
||||
);
|
||||
return dstCanvas;
|
||||
}
|
||||
90
src/utils/matic.ts
Normal file
90
src/utils/matic.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user