mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 353732f597 | |||
| 5599d2507f | |||
| 70cf6ffe70 | |||
| 61c9277097 | |||
| 401052efd3 | |||
| a57a0e797d | |||
| 8f48853e2c | |||
| a62148dc07 | |||
| 3ae890bd86 | |||
| 65a4cd4ba5 | |||
| f63b473bc1 | |||
| 859a5ba03a | |||
| 832b97b179 | |||
| e98d688d36 | |||
| 39318337fe |
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.4.0-beta-7",
|
||||
"version": "2.4.0-beta-9",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-40",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-42",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colormaster": "^1.2.1",
|
||||
|
||||
+29
-11
@@ -11,7 +11,7 @@ import {
|
||||
nanoid,
|
||||
THEME_FILTER,
|
||||
FRONTMATTER_KEYS,
|
||||
getFontDefinition,
|
||||
getCSSFontDefinition,
|
||||
} from "./constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
@@ -836,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 = "";
|
||||
@@ -941,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];
|
||||
@@ -1107,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);
|
||||
}
|
||||
}
|
||||
};
|
||||
+118
-5
@@ -717,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 ?? {},
|
||||
};
|
||||
@@ -2560,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()");
|
||||
@@ -2579,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:
|
||||
@@ -2953,6 +3009,7 @@ async function getTemplate(
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
const filehead = data.substring(0, trimLocation);
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
@@ -2960,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,
|
||||
};
|
||||
@@ -3278,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;
|
||||
}
|
||||
@@ -3372,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;
|
||||
|
||||
+45
-6
@@ -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`;
|
||||
}
|
||||
|
||||
Vendored
+14
-4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+210
-79
@@ -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.newElement !== 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?.newElement) 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,7 +3750,7 @@ 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 &&
|
||||
@@ -5014,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,
|
||||
@@ -5262,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)
|
||||
@@ -5460,6 +5581,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.renderCustomActionsMenu(),
|
||||
this.renderWelcomeScreen(),
|
||||
this.ttdDialog(),
|
||||
this.diagramToCode(),
|
||||
this.ttdDialogTrigger(),
|
||||
),
|
||||
this.renderToolsPanel(observer),
|
||||
@@ -5640,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;
|
||||
}
|
||||
|
||||
@@ -5673,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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
+46
-31
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -550,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: "",
|
||||
},
|
||||
{
|
||||
|
||||
+11
-5
@@ -54,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:
|
||||
@@ -80,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",
|
||||
@@ -93,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.",
|
||||
@@ -773,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...",
|
||||
|
||||
@@ -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";
|
||||
|
||||
// 简体中文
|
||||
@@ -336,14 +337,14 @@ FILENAME_HEAD: "文件名",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
|
||||
"当您处于 Markdown 阅读模式(即查看绘图的背景笔记)时,Excalidraw 绘图是否应该渲染为图像?" +
|
||||
"此设置不会影响您在 Excalidraw 模式下的绘图显示,或者在将绘图嵌入 Markdown 文档时,或在渲染悬停预览时。<br><ul>" +
|
||||
"<li>请参阅下面‘嵌入和导出’部分的 <b>PDF 导出</b> 相关设置。</li></ul><br>" +
|
||||
"<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:
|
||||
"处于 Markdown 视图模式时,此设置控制 Excalidraw 在使用 Obsidian 的 <b>导出为 PDF</b> 功能时,将 Excalidraw 文件导出为 PDF 的行为。<br>" +
|
||||
"<ul><li>当 <b>启用</b> 时,PDF 将仅显示 Excalidraw 绘图;</li>" +
|
||||
"<li>当 <b>禁用</b> 时,PDF 将显示文档的 Markdown 部分(背景笔记)。</li></ul>" +
|
||||
"请参阅上面‘外观和行为’部分的 <b>Markdown 阅读模式</b> 相关设置。" +
|
||||
"请参阅上面‘外观和行为’部分的 <<a href='#"+TAG_MDREADINGMODE+"'>>Markdown 阅读模式</a> 相关设置。" +
|
||||
"⚠️ 注意,您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。⚠️",
|
||||
THEME_HEAD: "主题和样式",
|
||||
ZOOM_HEAD: "缩放",
|
||||
@@ -531,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 文档中的绘图。" +
|
||||
@@ -562,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: "将链接作为注释嵌入",
|
||||
@@ -772,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: "缩放至标题",
|
||||
|
||||
+69
-1
@@ -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
|
||||
|
||||
+12
-1
@@ -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>`;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -409,4 +409,4 @@ export async function closeLeafView(leaf: WorkspaceLeaf) {
|
||||
type: "empty",
|
||||
state: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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