Compare commits

...

7 Commits

Author SHA1 Message Date
zsviczian
26812dd297 force to open markdown command palette action, fixed decompress 2024-05-15 22:02:06 +02:00
zsviczian
ae4f4b4f08 2.1.8.1-beta-1 2024-05-14 21:07:42 +02:00
zsviczian
4ac0a4c565 2.1.8 2024-05-13 22:49:32 +02:00
zsviczian
69c9f824a0 Update README.md 2024-05-06 21:24:57 +02:00
zsviczian
6a2220c960 updated slideshow timestamp 2024-05-05 19:16:59 +02:00
zsviczian
aa501c2843 2.1.7 2024-05-05 19:13:30 +02:00
zsviczian
efce44f0a7 2.1.6.1-beta-1 2024-05-01 10:57:50 +02:00
33 changed files with 733 additions and 255 deletions

View File

@@ -3,7 +3,7 @@
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
## Video Walkthrough
<a href="https://youtu.be/P_Q6avJGoWI" target="_blank"><img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/da34bb33-7610-45e6-b36f-cb7a02a9141b" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>

View File

@@ -21,7 +21,7 @@ The script will convert your drawing into a slideshow presentation.
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.23")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -190,7 +190,7 @@ let preventFullscreenExit = true;
const gotoFullscreen = async () => {
if(isFullscreen) return;
preventFullscreenExit = true;
if(app.isMobile) {
if(ea.DEVICE.isMobile) {
ea.viewToggleFullScreen();
} else {
await contentEl.webkitRequestFullscreen();
@@ -206,8 +206,8 @@ const gotoFullscreen = async () => {
const exitFullscreen = async () => {
if(!isFullscreen) return;
preventFullscreenExit = true;
if(!app.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(app.isMobile) ea.viewToggleFullScreen();
if(!ea.DEVICE.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(ea.DEVICE.isMobile) ea.viewToggleFullScreen();
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MAXIMIZE;
await waitForExcalidrawResize();
resetControlPanelElPosition();
@@ -649,7 +649,7 @@ const initializeEventListners = () => {
controlPanelEl.removeEventListener('mouseenter', onMouseEnter, false);
controlPanelEl.removeEventListener('mouseleave', onMouseLeave, false);
controlPanelEl.parentElement?.removeChild(controlPanelEl);
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.removeEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.removeEventListener('fullscreenchange', fullscreenListener);
}
@@ -664,7 +664,7 @@ const initializeEventListners = () => {
return true;
};
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.addEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.addEventListener('fullscreenchange', fullscreenListener);
}
@@ -727,7 +727,7 @@ const exitPresentation = async (openForEdit = false) => {
//Resets pointer offsets. Ugly solution.
//During testing offsets were wrong after presentation, but don't know why.
//This should solve it even if they are wrong.
hostView.refresh();
hostView.refreshCanvasOffset();
excalidrawAPI.setActiveTool({type: "selection"});
})
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-20",
"@zsviczian/excalidraw": "0.17.1-obsidian-21",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",

View File

@@ -10,9 +10,10 @@ const o0 = Decoration.line({ attributes: {class: "ex-opacity-0"} });
export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
class {
view: EditorView;
decorations: DecorationSet;
decorations: DecorationSet;
reExcalidrawData = /^%%(?:\r\n|\r|\n)# Excalidraw Data$/gm;
reTextElements = /^%%(?:\r\n|\r|\n)# Text Elements$/gm;
reDrawing = /^%%(?:\r\n|\r|\n)# Drawing$/gm;
reDrawing = /^%%(?:\r\n|\r|\n)##? Drawing$/gm;
linecount = 0;
isExcalidraw = false;
@@ -32,11 +33,15 @@ export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
const text = doc.toString();
let start = text.search(this.reTextElements);
let start = text.search(this.reExcalidrawData);
if(start == -1) {
start = text.search(this.reTextElements);
}
if(start == -1) {
start = text.search(this.reDrawing);
if(start == -1) return Decoration.none;
}
if(start == -1) return Decoration.none;
const startLine = doc.lineAt(start).number;
const endLine = doc.lines;
let builder = new RangeSetBuilder<Decoration>()

View File

@@ -1,7 +1,7 @@
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
@@ -23,7 +23,7 @@ import { ExportSettings } from "./ExcalidrawView";
import { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "./utils/FileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "./utils/FileUtils";
import {
errorlog,
getDataURL,
@@ -42,6 +42,8 @@ import {
import { ValueOf } from "./types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants/constants";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { PreviewImageType } from "./utils/UtilTypes";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -164,7 +166,7 @@ export class EmbeddedFile {
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
this.plugin = plugin;
this.resetImage(hostPath, imgPath);
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
if(this.file && (this.plugin.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
try {
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
} catch (error) {
@@ -358,22 +360,46 @@ export class EmbeddedFilesLoader {
withTheme: !!forceTheme,
isMask,
};
const svg = replaceSVGColors(
await createSVG(
file?.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
const shouldUseCache = file && imageCache.isReady();
const cacheKey:ImageKey = {
filepath: file.path,
blockref: null,
sectionref: null,
isDark,
previewImageType: PreviewImageType.SVG,
scale: 1,
isTransparent: !exportSettings.withBackground,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasSectionref: false,
linkpartReference: null,
linkpartAlias: null,
}
const maybeSVG = await imageCache.getImageFromCache(cacheKey);
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: replaceSVGColors(
await createSVG(
file?.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
@@ -382,7 +408,8 @@ export class EmbeddedFilesLoader {
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark) {
if (hasSVGwithBitmap && isDark && !Boolean(maybeSVG)) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
@@ -393,6 +420,9 @@ export class EmbeddedFilesLoader {
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
if(shouldUseCache && !Boolean(maybeSVG)) {
imageCache.addImageToCache(cacheKey,"", svg);
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
@@ -526,7 +556,8 @@ export class EmbeddedFilesLoader {
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number
depth:number,
isThemeChange:boolean = false,
) {
if(depth > 7) {
@@ -563,7 +594,8 @@ export class EmbeddedFilesLoader {
}
//files.push(fileData);
}
} /*else if (embeddedFile.isSVGwithBitmap) {
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
//this will reload the image in light/dark mode when switching themes
const fileData = {
mimeType: embeddedFile.mimeType,
id: entry.value[0],
@@ -580,7 +612,7 @@ export class EmbeddedFilesLoader {
catch(e) {
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
}
}*/
}
}
let equation;

View File

@@ -35,10 +35,8 @@ import {
REG_LINKINDEX_INVALIDCHARS,
THEME_FILTER,
mermaidToExcalidraw,
MD_TEXTELEMENTS,
MD_DRAWING,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getListOfTemplateFiles, getNewUniqueFilepath, } from "src/utils/FileUtils";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
import {
arrayToMap,
//debug,
@@ -51,7 +49,6 @@ import {
getSVG,
isMaskFile,
isVersionNewerThanOther,
log,
scaleLoadedImage,
wrapTextAtCharLength,
} from "src/utils/Utils";
@@ -59,7 +56,7 @@ import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsid
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
import { tex2dataURL } from "src/LaTeX";
import { GenericInputPrompt, NewFileActions, Prompt } from "src/dialogs/Prompt";
import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
import { t } from "src/lang/helpers";
import { ScriptEngine } from "src/Scripts";
import { ConnectionPoint, DeviceType } from "src/types";
@@ -81,7 +78,7 @@ import { TInput } from "colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
import { emulateKeysForLinkClick, PaneTarget } from "src/utils/ModifierkeyHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import PolyBool from "polybooljs";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
@@ -91,9 +88,8 @@ import {
extractCodeBlocks as _extractCodeBlocks,
} from "./utils/AIUtils";
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
import { CropImage } from "./utils/CropImage";
import { has } from "./svgToExcalidraw/attributes";
import { getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { log } from "./utils/DebugHelper";
extendPlugins([
HarmonyPlugin,
@@ -134,7 +130,7 @@ export class ExcalidrawAutomate {
public help(target: Function | string) {
if (!target) {
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
return;
}
@@ -153,14 +149,14 @@ export class ExcalidrawAutomate {
}
if(!funcInfo) {
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
return;
}
let isMissing = true;
if (funcInfo.code) {
isMissing = false;
console.log(`Declaration: ${funcInfo.code}`);
log(`Declaration: ${funcInfo.code}`);
}
if (funcInfo.desc) {
isMissing = false;
@@ -171,10 +167,10 @@ export class ExcalidrawAutomate {
.replace(/<a onclick='window\.open\("(.*?)"\)'>(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
console.log(`Description: ${formattedDesc}`, ...styles);
log(`Description: ${formattedDesc}`, ...styles);
}
if (isMissing) {
console.log("Description not available for this function.");
log("Description not available for this function.");
}
}
@@ -252,6 +248,22 @@ export class ExcalidrawAutomate {
return getListOfTemplateFiles(this.plugin);
}
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
if(!excalidrawFile && this.targetView && this.targetView.file) {
excalidrawFile = this.targetView.file;
}
if(!excalidrawFile) {
return [];
}
return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
}
public async getAttachmentFilepath(filename: string): Promise<string> {
if (!this.targetView || !this.targetView?.file) {
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
@@ -706,7 +718,7 @@ export class ExcalidrawAutomate {
const generateMD = ():string => {
const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
let outString = `${MD_TEXTELEMENTS}\n`;
let outString = `# Excalidraw Data\n## Text Elements\n`;
textElements.forEach(te=> {
outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
});
@@ -717,7 +729,7 @@ export class ExcalidrawAutomate {
})
outString += Object.keys(this.imagesDict).length > 0
? "\n# Embedded files\n"
? `\n## Embedded Files\n`
: "";
Object.keys(this.imagesDict).forEach((key: FileId)=> {
@@ -2629,7 +2641,7 @@ export class ExcalidrawAutomate {
importSVG(svgString:string):boolean {
const res:ConversionResult = svgToExcalidraw(svgString);
if(res.hasErrors) {
new Notice (`There were errors while parsing the given SVG:\n${[...res.errors].map((el) => el.innerHTML)}`);
new Notice (`There were errors while parsing the given SVG:\n${res.errors}`);
return false;
}
this.copyViewElementsToEAforEditing(res.content);
@@ -2762,9 +2774,9 @@ async function getTemplate(
textMode,
);
let trimLocation = data.search(new RegExp(`^${MD_TEXTELEMENTS}$`,"m"));
let trimLocation = data.search(/^##? Text Elements$/m);
if (trimLocation == -1) {
trimLocation = data.search(`${MD_DRAWING}\n`);
trimLocation = data.search(/##? Drawing\n/);
}
let scene = excalidrawData.scene;

View File

@@ -18,9 +18,6 @@ import {
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
FRONTMATTER_KEYS,
MD_TEXTELEMENTS,
MD_DRAWING,
MD_ELEMENTLINKS,
} from "./constants/constants";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
@@ -28,7 +25,6 @@ import { TextMode } from "./ExcalidrawView";
import {
addAppendUpdateCustomData,
compress,
debug,
decompress,
//getBakPath,
getBinaryFileFromDataURL,
@@ -51,7 +47,7 @@ import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/exc
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { add } from "@zsviczian/excalidraw/types/excalidraw/ga";
import { debug } from "./utils/DebugHelper";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -118,12 +114,12 @@ export const REGEX_LINK = {
};
//added \n at and of DRAWING_REG: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/357
const DRAWING_REG = /\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
const DRAWING_REG = /\n##? Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n##? Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
export const DRAWING_COMPRESSED_REG =
/(\n# Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
/(\n##? Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_COMPRESSED_REG_FALLBACK =
/(\n# Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
/(\n##? Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const isCompressedMD = (data: string): boolean => {
@@ -205,10 +201,10 @@ export function getMarkdownDrawingSection(
compressed: boolean,
) {
return compressed
? `# Drawing\n\x60\x60\x60compressed-json\n${compress(
? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
jsonString,
)}\n\x60\x60\x60\n%%`
: `# Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
}
/**
@@ -242,33 +238,97 @@ const wrap = (text: string, lineLen: number) =>
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = new RegExp(`^#\n%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = new RegExp(`#\n%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_NOSECTION_OK = new RegExp(`^(%%\n)?${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_WITHSECTION_OK = new RegExp(`^#\n%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_WITHSECTION_NOTOK = new RegExp(`#\n%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_NOSECTION_OK = new RegExp(`^(%%\n)?# Excalidraw Data(?:\n|$)`, "m");
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = new RegExp(`^#\n%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = new RegExp(`#\n%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_NOSECTION_OK = new RegExp(`^(%%\n)?##? Text Elements(?:\n|$)`, "m");
//The issue is that when editing in markdown embeds the user can delete the last enter causing two sections
//to collide. This is particularly problematic when the user is editing the lest section before # Text Elements
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)${MD_TEXTELEMENTS}(?:\n|$)`, "m");
//to collide. This is particularly problematic when the user is editing the last section before # Text Elements
const RE_EXCALIDRAWDATA_FALLBACK_1 = new RegExp(`(.*)%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_FALLBACK_2 = new RegExp(`(.*)# Excalidraw Data(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)##? Text Elements(?:\n|$)`, "m");
const RE_DRAWING = new RegExp(`(%%\n)?${MD_DRAWING}\n`);
const RE_DRAWING = new RegExp(`(%%\n)?##? Drawing\n`);
export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,string][]):string => {
//The base case scenario is at the top, continued with fallbacks in order of likelihood and file structure
//change history for sake of backward compatibility
/* Expected markdown structure:
bla bla bla
#
%%
# Excalidraw Data
*/
let trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
}
/* Expected markdown structure (this happens when the user deletes the last empty line of the last back-of-the-card note):
bla bla bla#
%%
# Excalidraw Data
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_NOTOK);
if(trimLocation > 0) {
shouldFixTrailingHashtag = true;
}
}
/* Expected markdown structure
a)
bla bla bla
%%
# Excalidraw Data
b)
bla bla bla
# Excalidraw Data
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
}
/* Expected markdown structure:
bla bla bla%%
# Excalidraw Data
*/
if(trimLocation === -1) {
const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_1);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla# Excalidraw Data
*/
if(trimLocation === -1) {
const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_2);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla
#
%%
# Text Elements
*/
let trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += 2;
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
if(trimLocation > 0) {
trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
}
}
/* Expected markdown structure:
bla bla bla#
%%
@@ -642,7 +702,28 @@ export class ExcalidrawData {
//link was updated due to filename changes
//The .excalidraw JSON is modified to reflect the MD in case of difference
//Read the text elements into the textElements Map
let position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
let position = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
// ....# Excalidraw Data
// ....
// --------------
// instead of
// --------------
// # back of note section
// ....
// # Excalidraw Data
position = data.search(RE_EXCALIDRAWDATA_FALLBACK_2);
}
if(position === -1) {
// # back of note section
// ....
// # Text Elements
position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
}
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
@@ -662,10 +743,12 @@ export class ExcalidrawData {
return true; //Text Elements header does not exist
}
data = data.slice(position);
const normalMatch = data.match(new RegExp(`^((%%\n)?${MD_TEXTELEMENTS}(?:\n|$))`, "m"));
const normalMatch = data.match(/^((%%\n)?# Excalidraw Data\n## Text Elements(?:\n|$))/m)
??data.match(/^((%%\n)?##? Text Elements(?:\n|$))/m);
const textElementsMatch = normalMatch
? normalMatch[0]
: data.match(new RegExp(`(.*${MD_TEXTELEMENTS}(?:\n|$))`, "m"))[0];
: data.match(/(.*##? Text Elements(?:\n|$))/m)[0];
data = data.slice(textElementsMatch.length);
this.textElementCommentedOut = textElementsMatch.startsWith("%%\n");
@@ -674,9 +757,13 @@ export class ExcalidrawData {
//load element links
const elementLinkMap = new Map<string,string>();
const elementLinksData = data.substring(
data.indexOf(`${MD_ELEMENTLINKS}\n`) + `${MD_ELEMENTLINKS}\n`.length,
);
const indexOfNewElementLinks = data.indexOf("## Element Links\n");
const lengthOfNewElementLinks = 17; //`## Element Links\n`.length
const indexOfOldElementLinks = data.indexOf("# Element Links\n");
const lengthOfOldElementLinks = 16; //`# Element Links\n`.length
const elementLinksData = indexOfNewElementLinks>-1
? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
: data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
//Load Embedded files
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
@@ -747,11 +834,15 @@ export class ExcalidrawData {
}
}
const indexOfEmbeddedFiles = data.indexOf("# Embedded files\n");
if(indexOfEmbeddedFiles>-1) {
data = data.substring(
indexOfEmbeddedFiles + "# Embedded files\n".length,
);
const indexOfNewEmbeddedFiles = data.indexOf("## Embedded Files\n");
const embeddedFilesNewLength = 18; //"## Embedded Files\n".length
const indexOfOldEmbeddedFiles = data.indexOf("# Embedded files\n");
const embeddedFilesOldLength = 17; //"# Embedded files\n".length
if(indexOfNewEmbeddedFiles>-1 || indexOfOldEmbeddedFiles>-1) {
data = indexOfNewEmbeddedFiles>-1
? data.substring(indexOfNewEmbeddedFiles + embeddedFilesNewLength)
: data.substring(indexOfOldEmbeddedFiles + embeddedFilesOldLength);
//Load Embedded files
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
res = data.matchAll(REG_FILEID_FILEPATH);
@@ -904,7 +995,7 @@ export class ExcalidrawData {
container?.type,
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
} catch(e) {
debug({where: "ExcalidrawData.updateSceneTextElements", fn: this.updateSceneTextElements, textElement: te});
debug(`ExcalidrawData.updateSceneTextElements, textElement: ${te?.id}`, te, this.updateSceneTextElements);
}
}
}
@@ -1266,7 +1357,7 @@ export class ExcalidrawData {
disableCompression: boolean = false;
generateMD(deletedElements: ExcalidrawElement[] = []): string {
let outString = this.textElementCommentedOut ? "%%\n" : "";
outString += `${MD_TEXTELEMENTS}\n`;
outString += `# Excalidraw Data\n## Text Elements\n`;
const textElementLinks = new Map<string, string>();
for (const key of this.textElements.keys()) {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
@@ -1282,7 +1373,7 @@ export class ExcalidrawData {
}
if (this.elementLinks.size > 0 || textElementLinks.size > 0) {
outString += `${MD_ELEMENTLINKS}\n`;
outString += `## Element Links\n`;
for (const key of this.elementLinks.keys()) {
outString += `${key}: ${this.elementLinks.get(key)}\n`;
}
@@ -1295,7 +1386,7 @@ export class ExcalidrawData {
// deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
outString +=
this.equations.size > 0 || this.files.size > 0
? "# Embedded files\n"
? "## Embedded Files\n"
: "";
if (this.equations.size > 0) {
for (const key of this.equations.keys()) {

View File

@@ -132,7 +132,7 @@ import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils";
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { nanoid } from "nanoid";
import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, log} from "./utils/DebugHelper";
import { extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { SelectCard } from "./dialogs/SelectCard";
@@ -343,6 +343,7 @@ export default class ExcalidrawView extends TextFileView {
private hoverPreviewTarget: EventTarget = null;
private viewModeEnabled:boolean = false;
private lastMouseEvent: any = null;
private editingTextElementId: string = null; //storing to handle on-screen keyboard hide events
id: string = (this.leaf as any).id;
@@ -680,7 +681,7 @@ export default class ExcalidrawView extends TextFileView {
try {
const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
if(isDebugMode) console.log({allowSave, isDirty: this.isDirty(), autosaving: this.semaphores.autosaving, forcesave});
debug({where: "ExcalidrawView.save", allowSave, isDirty: this.isDirty(), autosaving: this.semaphores.autosaving, forcesave});
if (allowSave) {
const scene = this.getScene();
@@ -799,7 +800,7 @@ export default class ExcalidrawView extends TextFileView {
const header = getExcalidrawMarkdownHeaderSection(this.data, keys);
if (!this.excalidrawData.disableCompression) {
this.excalidrawData.disableCompression =
this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView &&
this.isEditedAsMarkdownInOtherView();
}
const result = header + this.excalidrawData.generateMD(
@@ -1302,8 +1303,13 @@ export default class ExcalidrawView extends TextFileView {
);
const self = this;
app.workspace.onLayoutReady(async () => {
this.canvasNodeFactory.initialize();
this.app.workspace.onLayoutReady(async () => {
debug(`ExcalidrawView.onload app.workspace.onLayoutReady, file: ${self.file?.name}, isActiveLeaf: ${self.app.workspace.activeLeaf === self.leaf}, is activeExcalidrawView set: ${Boolean(self.plugin.activeExcalidrawView)}`);
//implemented to overcome issue that activeLeafChangeEventHandler is not called when view is initialized from a saved workspace, since Obsidian 1.6.0
if (self.app.workspace.activeLeaf === self.leaf) {
self.plugin.activeLeafChangeEventHandler(self.leaf);
}
self.canvasNodeFactory.initialize();
self.contentEl.addClass("excalidraw-view");
//https://github.com/zsviczian/excalibrain/issues/28
await self.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
@@ -1319,7 +1325,7 @@ export default class ExcalidrawView extends TextFileView {
};
self.onKeyDown = (e: KeyboardEvent) => {
this.modifierKeyDown = {
self.modifierKeyDown = {
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
@@ -1395,7 +1401,7 @@ export default class ExcalidrawView extends TextFileView {
self.offsetTop = offsetTop;
}
};
this.parentMoveObserver = isDebugMode
this.parentMoveObserver = this.plugin.settings.isDebugMode
? new CustomMutationObserver(observerFn, "parentMoveObserver")
: new MutationObserver(observerFn)
@@ -1832,7 +1838,11 @@ export default class ExcalidrawView extends TextFileView {
this.lastSaveTimestamp = this.file.stat.mtime;
this.lastLoadedFile = this.file;
data = this.data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
app.workspace.onLayoutReady(async () => {
this.app.workspace.onLayoutReady(async () => {
debug(`ExcalidrawView.setViewData app.workspace.onLayoutReady, file: ${this.file?.name}, isActiveLeaf: ${this.app.workspace.activeLeaf === this.leaf}`);
let counter = 0;
while (!this.file && counter++<50) await sleep(50);
if(!this.file) return;
this.compatibilityMode = this.file.extension === "excalidraw";
await this.plugin.loadSettings();
if (this.compatibilityMode) {
@@ -1960,7 +1970,7 @@ export default class ExcalidrawView extends TextFileView {
public activeLoader: EmbeddedFilesLoader = null;
private nextLoader: EmbeddedFilesLoader = null;
public async loadSceneFiles() {
public async loadSceneFiles(isThemeChange: boolean = false) {
if (!this.excalidrawAPI) {
return;
}
@@ -1996,7 +2006,7 @@ export default class ExcalidrawView extends TextFileView {
return false;
})
}
},0
},0,isThemeChange,
);
};
if (!this.activeLoader) {
@@ -2262,9 +2272,9 @@ export default class ExcalidrawView extends TextFileView {
this.initializeToolsIconPanelAfterLoading();
}
public setDirty(debug?:number) {
public setDirty(location?:number) {
if(this.semaphores.saving) return; //do not set dirty if saving
if(isDebugMode) console.log(debug);
debug(`ExcalidrawView.setDirty location:${location}`);
this.semaphores.dirty = this.file?.path;
this.diskIcon.querySelector("svg").addClass("excalidraw-dirty");
if(!this.semaphores.viewunload && this.toolsPanelRef?.current) {
@@ -2353,7 +2363,7 @@ export default class ExcalidrawView extends TextFileView {
}
public async openAsMarkdown(eState?: any) {
if (this.plugin.settings.compress === true) {
if (this.plugin.settings.compress && this.plugin.settings.decompressForMDView) {
this.excalidrawData.disableCompression = true;
await this.save(true, true);
}
@@ -2956,6 +2966,12 @@ export default class ExcalidrawView extends TextFileView {
api.refresh();
};
// depricated. kept for backward compatibility. e.g. used by the Slideshow plugin
// 2024.05.03
public refresh() {
this.refreshCanvasOffset();
}
private clearHoverPreview() {
if (this.hoverPreviewTarget) {
const event = new MouseEvent("click", {
@@ -3446,7 +3462,7 @@ export default class ExcalidrawView extends TextFileView {
private async onThemeChange (newTheme: string) {
//debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme});
this.excalidrawData.scene.appState.theme = newTheme;
this.loadSceneFiles();
this.loadSceneFiles(true);
this.toolsPanelRef?.current?.setTheme(newTheme);
//Timeout is to allow appState to update
setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling));
@@ -3867,7 +3883,7 @@ export default class ExcalidrawView extends TextFileView {
}
// 1. Set the isEditingText flag to true to prevent autoresize on mobile
// 1500ms is an empirical number, the onscreen keyboard usually disappears in 1-2 seconds
// 1500ms is an empirical number, the on-screen keyboard usually disappears in 1-2 seconds
this.semaphores.isEditingText = true;
if(this.isEditingTextResetTimer) {
clearTimeout(this.isEditingTextResetTimer);
@@ -4540,17 +4556,17 @@ export default class ExcalidrawView extends TextFileView {
}
const json = response.json;
if (isDebugMode) console.log(response);
debug("ExcalidrawView.ttdDialog", response);
if (json?.error) {
console.log(response);
log(response);
return {
error: new Error(json.error.message),
};
}
if(!json?.choices?.[0]?.message?.content) {
console.log(response);
log(response);
return {
error: new Error("Generation failed... see console log for details"),
};
@@ -4559,7 +4575,7 @@ export default class ExcalidrawView extends TextFileView {
let generatedResponse = extractCodeBlocks(json.choices[0]?.message?.content)[0]?.data;
if(!generatedResponse) {
console.log(response);
log(response);
return {
error: new Error("Generation failed... see console log for details"),
};
@@ -4697,7 +4713,7 @@ export default class ExcalidrawView extends TextFileView {
const height = this.contentEl.clientHeight;
if(width === 0 || height === 0) return;
//this is an aweful hack to prevent the keyboard pushing the canvas out of view.
//this is an aweful hack to prevent the on-screen keyboard pushing the canvas out of view.
//The issue is that contrary to Excalidraw.com where the page is simply pushed up, in
//Obsidian the leaf has a fixed top. As a consequence the top of excalidrawWrapperDiv does not get pushed out of view
//but shirnks. But the text area is positioned relative to excalidrawWrapperDiv and consequently does not fit, which
@@ -4708,8 +4724,12 @@ export default class ExcalidrawView extends TextFileView {
//I found that adding and removing this style solves the issue.
//...again, just aweful, but works.
const st = this.excalidrawAPI.getAppState();
const isKeyboardOutEvent = st.editingElement?.type === "text";
const isKeyboardBackEvent = this.semaphores.isEditingText && !isKeyboardOutEvent;
//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;
const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent;
this.editingTextElementId = isKeyboardOutEvent ? st.editingElement.id : null;
if(isKeyboardOutEvent) {
const self = this;
const appToolHeight = (self.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0;

View File

@@ -86,7 +86,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale};
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale, isTransparent: !exportSettings.withBackground};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
@@ -163,7 +163,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1};
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1, isTransparent: !exportSettings.withBackground};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
@@ -220,13 +220,13 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1};
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1, isTransparent: !exportSettings.withBackground};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
let svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
let svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
@@ -714,7 +714,7 @@ const tmpObsidianWYSIWYG = async (
internalEmbedDiv.appendChild(imgDiv);
}, 500);
}
const observer = isDebugMode
const observer = isDebugMode()
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
: new MutationObserver(markdownObserverFn);
observer.observe(internalEmbedDiv, {
@@ -848,7 +848,7 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
node.appendChild(div);
};
export const legacyExcalidrawPopoverObserver = isDebugMode
export const legacyExcalidrawPopoverObserver = isDebugMode()
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
: new MutationObserver(legacyExcalidrawPopoverObserverFn);

View File

@@ -9,13 +9,12 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const MD_TEXTELEMENTS = "# Text Elements";
export const MD_JSON_START = "```json\n";
export const MD_JSON_END = "```";
export const MD_DRAWING = "# Drawing";
export const MD_ELEMENTLINKS = "# Element Links";
export const MD_EMBEDFILES = "# Embed files";
export const MD_EX_SECTIONS = [MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_DRAWING = "## Drawing";
const MD_ELEMENTLINKS = "## Element Links";
const MD_EMBEDFILES = "## Embedded Files";
export const MD_EX_SECTIONS = [MD_EXCALIDRAW, MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";

View File

@@ -6,7 +6,7 @@ If you'd like to learn more, please subscribe to my YouTube channel: [Visual PKM
Thank you & Enjoy!
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/o0exK-xFP3k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`;
@@ -16,6 +16,52 @@ export const RELEASE_NOTES: { [k: string]: string } = {
I develop this plugin as a hobby, spending my free time doing this. If you find it valuable, then please say THANK YOU or...
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.1.8":`
## Fixed
- Fixing issues that surfaced after upgrading to Obsidian 1.6.0
- Fixed Excalidraw hover previews
- Cursor for editing links, text elements, and frame names became virtually invisible if Obsidian was in dark mode and Excalidraw in light mode and vica versa.
- Rendering Excalidraw drawings in Markdown views, right after Obsidian loaded did not work.
- I implemented more graceful fail if you submitted a malformed SVG for SVG to Excalidraw conversation. [#1768](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1768)
## New
- New setting under "Save" in plugin settings to NOT decompress JSON when switching to Markdown view mode. For details see description under "Save" settings. The benefit is smaller file size and fewer results in the Obsidian search. If you want to edit the JSON, you can always manually decompress JSON in markdown view mode with the command palette action "Excalidraw: Decompress JSON".
`,
"2.1.7:":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Updates from Excalidraw.com
- Improved undo management.
- Improved handle to scale images from the side.
- Changed arrow binding behavior.
- Many other minor fixes and improvements.
## New
- Introduced image caching for nested (embedded) Excalidraw drawings on the scene. This enhancement should lead to improved scene loading times, especially when dealing with numerous embedded Excalidraw drawings.
- Added new OCR Command Palette actions. Users can now re-run OCR and run OCR for selected elements.
## Fixed
- Fixed an issue where cropping an embeddable PDF frame in the Excalidraw Scene caused distortion based on the embeddable element's aspect ratio. ([#1756](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1756))
- Removed the listing of ${String.fromCharCode(96)}# Embedded files${String.fromCharCode(96)} section when adding a "Back of the note card". ([#1754](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1754))
- Resolved the issue where closing the on-screen keyboard with the keyboard hide button of your phone, instead of tapping somewhere else on the Excalidraw scene, did not resize the scene correctly. ([#1729](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729))
- Fixed the problem where pasting a text element as text into markdown incorrectly pasted the text to the end of the MD note, with line breaks as rendered on screen in Excalidraw. Also addressed the issue where pasting an image element as an image resulted in it being pasted to the end of the document. ([#1749](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1749))
- Corrected the color inversion of embedded images when changing the theme from light to dark, then back from dark to light, and again from light to dark on the third change.
- Addressed the problem where cropping an image while unlocking and rotating it in the cropper did not reflect the rotation. Note that rotating the image in Cropper required switching to markdown view mode, changing the "locked": true property to false, then switching back to Excalidraw mode. This issue likely impacted only a very few power users. ([#1745](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1745))
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}ts
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[];
${String.fromCharCode(96,96,96)}
`,
"2.1.6":`
## Two minor fixes

View File

@@ -1,6 +1,7 @@
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
import ExcalidrawPlugin from "../main";
import { errorlog, escapeRegExp, log } from "../utils/Utils";
import { errorlog, escapeRegExp } from "../utils/Utils";
import { log } from "src/utils/DebugHelper";
const URL =
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";

View File

@@ -571,6 +571,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
"If no template is set, it returns null.",
after: "",
},
{
field: "getEmbeddedImagesFiletree",
code: "getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[]",
desc: "Retruns the embedded images in the scene recursively. If excalidrawFile is not provided, " +
"the function will use ea.targetView.file",
after: "",
},
{
field: "getAttachmentFilepath",
code: "async getAttachmentFilepath(filename: string): Promise<string>",

View File

@@ -9,6 +9,7 @@ export default {
// main.ts
CONVERT_URL_TO_FILE: "Save image from URL to local file",
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
ZIP_CURRENT_FILE: "Compress current Excalidraw file",
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
@@ -35,6 +36,7 @@ export default {
TRANSCLUDE: "Embed a drawing",
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image",
NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW",
NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB",
NEW_IN_ACTIVE_PANE: "Create new drawing - IN THE CURRENT ACTIVE WINDOW",
@@ -74,7 +76,9 @@ export default {
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
ENTER_LATEX: "Enter a valid LaTeX expression",
READ_RELEASE_NOTES: "Read latest release notes",
RUN_OCR: "OCR: Grab text from freedraw scribble and pictures to clipboard",
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
RERUN_OCR: "OCR full drawing re-run: Grab text from freedraw + images to clipboard and doc.props",
RUN_OCR_ELEMENTS: "OCR selected elements: Grab text from freedraw + images to clipboard",
TRAY_MODE: "Toggle property-panel tray-mode",
SEARCH: "Search for text in drawing",
CROP_IMAGE: "Crop and mask image",
@@ -215,6 +219,14 @@ export default {
"once you switch back to Excalidraw view. " +
"The setting only has effect 'point forward', meaning, existing drawings will not be affected by the setting " +
"until you open them and save them.<br><b><u>Toggle ON:</u></b> Compress drawing JSON<br><b><u>Toggle OFF:</u></b> Leave drawing JSON uncompressed",
DECOMPRESS_FOR_MD_NAME: "Decompress Excalidraw JSON in Markdown View",
DECOMPRESS_FOR_MD_DESC:
"By enabling this feature Excalidraw will automatically decompress the drawing JSON when you switch to Markdown view. " +
"This will allow you to easily read and edit the JSON string. The drawing will be compressed again " +
"once you switch back to Excalidraw view and save the drawing (CTRL+S).<br>" +
"I recommend switching this feature off as it will result in smaller file sizes and avoiding unnecessary results in Obsidian search. " +
"You can always use the 'Excalidraw: Decompress current Excalidraw file' command from the command palette "+
"to manually decompress the drawing JSON when you need to read or edit it.",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "Interval for autosave on Desktop",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"The time interval between saves. Autosave will skip if there are no changes in the drawing. " +
@@ -563,6 +575,9 @@ FILENAME_HEAD: "Filename",
"Double files will be exported both if auto-export SVG or PNG (or both) are enabled, as well as when clicking export on a single image.",
COMPATIBILITY_HEAD: "Compatibility features",
COMPATIBILITY_DESC: "You should only enable these features if you have a strong reason for wanting to work with excalidraw.com files instead of markdown files. Many of the plugin features are not supported on legacy files. Typical usecase would be if you use set your vault up on top of a Visual Studio Code project folder and you have .excalidraw drawings you want to access from Visual Studio Code as well. Another usecase might be using Excalidraw in Logseq and Obsidian in parallel.",
DEBUGMODE_NAME: "Enable debug messages",
DEBUGMODE_DESC: "I recommend restarting Obsidian after enabling/disabling this setting. This enable debug messages in the console. This is useful for troubleshooting issues. " +
"If you are experiencing problems with the plugin, please enable this setting, reproduce the issue, and include the console log in the issue you raise on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/issues'>GitHub</a>",
SLIDING_PANES_NAME: "Sliding panes plugin support",
SLIDING_PANES_DESC:
"Need to restart Obsidian for this change to take effect.<br>" +

View File

@@ -41,7 +41,6 @@ import {
EXPORT_IMG_ICON,
LOCALE,
IMAGE_TYPES,
MD_TEXTELEMENTS,
setExcalidrawPlugin,
DEVICE
} from "./constants/constants";
@@ -96,7 +95,6 @@ import {
import {
getFontDataURL,
errorlog,
log,
setLeftHandedMode,
sleep,
isVersionNewerThanOther,
@@ -105,7 +103,7 @@ import {
decompress,
getImageSize,
} from "./utils/Utils";
import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ScriptEngine } from "./Scripts";
import {
@@ -133,10 +131,11 @@ import { processLinkText } from "./utils/CustomEmbeddableUtils";
import { getEA } from "src";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { CustomMutationObserver, durationTreshold, isDebugMode } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, durationTreshold, log } from "./utils/DebugHelper";
import { carveOutImage, carveOutPDF, createImageCropperFile, CROPPED_PREFIX } from "./utils/CarveOut";
import { ExcalidrawConfig } from "./utils/ExcalidrawConfig";
import { EditorHandler } from "./CodeMirrorExtension/EditorHandler";
import de from "./lang/locale/de";
declare const EXCALIDRAW_PACKAGES:string;
declare const react:any;
@@ -186,6 +185,9 @@ export default class ExcalidrawPlugin extends Plugin {
private stylesManager:StylesManager;
private textMeasureDiv:HTMLDivElement = null;
public editorHandler: EditorHandler;
public activeLeafChangeEventHandler: (leaf: WorkspaceLeaf) => Promise<void>;
//if set, the next time this file is opened it will be opened as markdown
public forceToOpenInMarkdownFilepath: string = null;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -229,7 +231,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
public registerEvent(event: any) {
if(!isDebugMode) {
if(!this.settings.isDebugMode) {
super.registerEvent(event);
return;
}
@@ -320,6 +322,7 @@ export default class ExcalidrawPlugin extends Plugin {
const self = this;
this.app.workspace.onLayoutReady(() => {
debug(`ExcalidrawPlugin.onload.app.workspace.onLayoutReady`);
this.scriptEngine = new ScriptEngine(self);
imageCache.initializeDB(self);
});
@@ -329,6 +332,7 @@ export default class ExcalidrawPlugin extends Plugin {
private setPropertyTypes() {
const app = this.app;
this.app.workspace.onLayoutReady(() => {
debug(`ExcalidrawPlugin.setPropertyTypes app.workspace.onLayoutReady`);
Object.keys(FRONTMATTER_KEYS).forEach((key) => {
if(FRONTMATTER_KEYS[key].depricated === true) return;
const {name, type} = FRONTMATTER_KEYS[key];
@@ -339,6 +343,7 @@ export default class ExcalidrawPlugin extends Plugin {
public initializeFonts() {
this.app.workspace.onLayoutReady(async () => {
debug(`ExcalidrawPlugin.initializeFonts app.workspace.onLayoutReady`);
const font = await getFontDataURL(
this.app,
this.settings.experimantalFourthFont,
@@ -393,16 +398,17 @@ export default class ExcalidrawPlugin extends Plugin {
private switchToExcalidarwAfterLoad() {
const self = this;
this.app.workspace.onLayoutReady(() => {
debug(`ExcalidrawPlugin.switchToExcalidarwAfterLoad app.workspace.onLayoutReady`);
let leaf: WorkspaceLeaf;
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
if (
leaf.view instanceof MarkdownView &&
self.isExcalidrawFile(leaf.view.file) &&
fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)
) {
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
VIEW_TYPE_EXCALIDRAW;
self.setExcalidrawView(leaf);
if ( leaf.view instanceof MarkdownView && self.isExcalidrawFile(leaf.view.file)) {
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)) {
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
VIEW_TYPE_EXCALIDRAW;
self.setExcalidrawView(leaf);
} else {
foldExcalidrawSection(leaf.view);
}
}
}
});
@@ -620,16 +626,19 @@ export default class ExcalidrawPlugin extends Plugin {
* Displays a transcluded .excalidraw image in markdown preview mode
*/
private addMarkdownPostProcessor() {
//Licat: Are you registering your post processors in onLayoutReady? You should register them in onload instead
initializeMarkdownPostProcessor(this);
this.registerMarkdownPostProcessor(markdownPostProcessor);
const self = this;
this.app.workspace.onLayoutReady(() => {
initializeMarkdownPostProcessor(self);
self.registerMarkdownPostProcessor(markdownPostProcessor);
debug(`ExcalidrawPlugin.addMarkdownPostProcessor app.workspace.onLayoutReady`);
// internal-link quick preview
self.registerEvent(self.app.workspace.on("hover-link", hoverEvent));
//only add the legacy file observer if there are legacy files in the vault
if(this.app.vault.getFiles().some(f=>f.extension === "excalidraw")) {
if(self.app.vault.getFiles().some(f=>f.extension === "excalidraw")) {
self.enableLegacyFilePopoverObserver();
}
});
@@ -672,7 +681,7 @@ export default class ExcalidrawPlugin extends Plugin {
});
};
this.themeObserver = isDebugMode
this.themeObserver = this.settings.isDebugMode
? new CustomMutationObserver(themeObserverFn, "themeObserver")
: new MutationObserver(themeObserverFn);
@@ -738,12 +747,13 @@ export default class ExcalidrawPlugin extends Plugin {
});
};
this.fileExplorerObserver = isDebugMode
this.fileExplorerObserver = this.settings.isDebugMode
? new CustomMutationObserver(fileExplorerObserverFn, "fileExplorerObserver")
: new MutationObserver(fileExplorerObserverFn);
const self = this;
this.app.workspace.onLayoutReady(() => {
debug(`ExcalidrawPlugin.experimentalFileTypeDisplay app.workspace.onLayoutReady`);
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
const container = document.querySelector(".nav-files-container");
if (container) {
@@ -868,9 +878,9 @@ export default class ExcalidrawPlugin extends Plugin {
(async () => {
const data = await this.app.vault.read(activeFile);
const parts = data.split("%%\n# Drawing\n```compressed-json\n");
const parts = data.split("\n## Drawing\n```compressed-json\n");
if(parts.length!==2) return;
const header = parts[0] + "%%\n# Drawing\n```json\n";
const header = parts[0] + "\n## Drawing\n```json\n";
const compressed = parts[1].split("\n```\n%%");
if(compressed.length!==2) return;
const decompressed = decompress(compressed[0]);
@@ -1182,7 +1192,51 @@ export default class ExcalidrawPlugin extends Plugin {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return true;
}
this.taskbone.getTextForView(view, false);
this.taskbone.getTextForView(view, {forceReScan: false});
return true;
}
return false;
},
});
this.addCommand({
id: "rerun-ocr",
name: t("RERUN_OCR"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (checking) {
return (
Boolean(view)
);
}
if (view) {
if(!this.settings.taskboneEnabled) {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return true;
}
this.taskbone.getTextForView(view, {forceReScan: true});
return true;
}
return false;
},
});
this.addCommand({
id: "run-ocr-selectedelements",
name: t("RUN_OCR_ELEMENTS"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (checking) {
return (
Boolean(view)
);
}
if (view) {
if(!this.settings.taskboneEnabled) {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return true;
}
this.taskbone.getTextForView(view, {forceReScan: false, selectedElementsOnly: true, addToFrontmatter: false});
return true;
}
return false;
@@ -1531,6 +1585,31 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "flip-image",
name: t("FLIP_IMAGE"),
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) {
return false;
}
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
if(!ef) {
return false;
}
if(!this.isExcalidrawFile(ef.file)) {
return false;
}
if(checking) return true;
this.forceToOpenInMarkdownFilepath = ef.file?.path;
this.openDrawing(ef.file, DEVICE.isMobile ? "new-tab":"popout-window", true);
}
})
this.addCommand({
id: "reset-image-to-100",
name: t("RESET_IMG_TO_100"),
@@ -2227,12 +2306,13 @@ export default class ExcalidrawPlugin extends Plugin {
private registerMonkeyPatches() {
const key = "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues";
this.register(
around(Workspace.prototype, {
getActiveViewOfType(old) {
return dedupe(key, old, function(...args) {
const result = old && old.apply(this, args);
const maybeEAView = app?.workspace?.activeLeaf?.view;
const maybeEAView = self.app?.workspace?.activeLeaf?.view;
if(!maybeEAView || !(maybeEAView instanceof ExcalidrawView)) return result;
const error = new Error();
const stackTrace = error.stack;
@@ -2332,28 +2412,38 @@ export default class ExcalidrawPlugin extends Plugin {
setViewState(next) {
return function (state: ViewState, ...rest: any[]) {
const markdownViewLoaded =
self._loaded && // Don't force excalidraw mode during shutdown
state.type === "markdown" && // If we have a markdown file
state.state?.file;
if (
// Don't force excalidraw mode during shutdown
self._loaded &&
// If we have a markdown file
state.type === "markdown" &&
state.state?.file &&
// And the current mode of the file is not set to markdown
self.excalidrawFileModes[this.id || state.state.file] !==
"markdown"
markdownViewLoaded &&
self.excalidrawFileModes[this.id || state.state.file] !== "markdown"
) {
if (fileShouldDefaultAsExcalidraw(state.state.file,this.app)) {
const file = state.state.file;
if ((self.forceToOpenInMarkdownFilepath !== file) && fileShouldDefaultAsExcalidraw(file,this.app)) {
// If we have it, force the view type to excalidraw
const newState = {
...state,
type: VIEW_TYPE_EXCALIDRAW,
};
self.excalidrawFileModes[state.state.file] =
self.excalidrawFileModes[file] =
VIEW_TYPE_EXCALIDRAW;
return next.apply(this, [newState, ...rest]);
}
self.forceToOpenInMarkdownFilepath = null;
}
if(markdownViewLoaded) {
const leaf = this;
setTimeout(async ()=> {
if(!leaf || !leaf.view || !(leaf.view instanceof MarkdownView) ||
!leaf.view.file || !self.isExcalidrawFile(leaf.view.file)
) return;
foldExcalidrawSection(leaf.view)
},500);
}
return next.apply(this, [state, ...rest]);
@@ -2369,6 +2459,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
const self = this;
this.app.workspace.onLayoutReady(async () => {
debug(`ExcalidrawPlugin.runStartupScript app.workspace.onLayoutReady: ${self.settings?.startupScriptPath}`);
const path = self.settings.startupScriptPath.endsWith(".md")
? self.settings.startupScriptPath
: `${self.settings.startupScriptPath}.md`;
@@ -2391,6 +2482,7 @@ export default class ExcalidrawPlugin extends Plugin {
private registerEventListeners() {
const self: ExcalidrawPlugin = this;
this.app.workspace.onLayoutReady(async () => {
debug("ExcalidrawPlugin.registerEventListeners app.workspace.onLayoutReady");
const onPasteHandler = (
evt: ClipboardEvent,
editor: Editor,
@@ -2419,16 +2511,16 @@ export default class ExcalidrawPlugin extends Plugin {
if(sourceFile && imageFile && imageFile instanceof TFile) {
path = self.app.metadataCache.fileToLinktext(imageFile,sourceFile.path);
}
editor.insertText(getLink(self, {path}));
editorInsertText(editor, getLink(self, {path}));
}
return;
}
if (element.type === "text") {
editor.insertText(element.text);
editorInsertText(editor, element.rawText);
return;
}
if (element.link) {
editor.insertText(`${element.link}`);
editorInsertText(editor, `${element.link}`);
return;
}
} catch (e) {
@@ -2657,7 +2749,7 @@ export default class ExcalidrawPlugin extends Plugin {
const scope = self.app.keymap.getRootScope();
const handler_ctrlEnter = scope.register(["Mod"], "Enter", () => true);
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const handler_ctrlK = scope.register(["Mod"], "k", () => {return true});
const handler_ctrlK = scope.register(["Mod"], "k", () => true);
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const handler_ctrlF = scope.register(["Mod"], "f", () => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
@@ -2687,8 +2779,9 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
};
this.activeLeafChangeEventHandler = activeLeafChangeEventHandler;
self.registerEvent(
app.workspace.on(
this.app.workspace.on(
"active-leaf-change",
activeLeafChangeEventHandler,
),
@@ -2696,7 +2789,7 @@ export default class ExcalidrawPlugin extends Plugin {
self.addFileSaveTriggerEventHandlers();
const metaCache: MetadataCache = app.metadataCache;
const metaCache: MetadataCache = this.app.metadataCache;
//@ts-ignore
metaCache.getCachedFiles().forEach((filename: string) => {
const fm = metaCache.getCache(filename)?.frontmatter;
@@ -2705,7 +2798,7 @@ export default class ExcalidrawPlugin extends Plugin {
filename.match(/\.excalidraw$/)
) {
self.updateFileCache(
app.vault.getAbstractFileByPath(filename) as TFile,
this.app.vault.getAbstractFileByPath(filename) as TFile,
fm,
);
}
@@ -2778,14 +2871,14 @@ export default class ExcalidrawPlugin extends Plugin {
};
if (leftWorkspaceDrawer) {
this.workspaceDrawerLeftObserver = isDebugMode
this.workspaceDrawerLeftObserver = this.settings.isDebugMode
? new CustomMutationObserver(action, "slidingDrawerLeftObserver")
: new MutationObserver(action);
this.workspaceDrawerLeftObserver.observe(leftWorkspaceDrawer, options);
}
if (rightWorkspaceDrawer) {
this.workspaceDrawerRightObserver = isDebugMode
this.workspaceDrawerRightObserver = this.settings.isDebugMode
? new CustomMutationObserver(action, "slidingDrawerRightObserver")
: new MutationObserver(action);
this.workspaceDrawerRightObserver.observe(
@@ -2819,7 +2912,7 @@ export default class ExcalidrawPlugin extends Plugin {
this.activeExcalidrawView.save();
};
this.modalContainerObserver = isDebugMode
this.modalContainerObserver = this.settings.isDebugMode
? new CustomMutationObserver(modalContainerObserverFn, "modalContainerObserver")
: new MutationObserver(modalContainerObserverFn);
this.activeViewDoc = this.activeExcalidrawView.ownerDocument;
@@ -3049,10 +3142,10 @@ export default class ExcalidrawPlugin extends Plugin {
}
let leaf: WorkspaceLeaf;
if(location === "popout-window") {
leaf = app.workspace.openPopoutLeaf();
leaf = this.app.workspace.openPopoutLeaf();
}
if(location === "new-tab") {
leaf = app.workspace.getLeaf('tab');
leaf = this.app.workspace.getLeaf('tab');
}
if(!leaf) {
leaf = this.app.workspace.getLeaf(false);
@@ -3133,7 +3226,7 @@ export default class ExcalidrawPlugin extends Plugin {
const textElements = excalidrawData.elements?.filter(
(el: any) => el.type == "text",
);
let outString = `${MD_TEXTELEMENTS}\n`;
let outString = `# Excalidraw Data\n## Text Elements\n`;
let id: string;
for (const te of textElements) {
id = te.id;
@@ -3212,6 +3305,12 @@ export default class ExcalidrawPlugin extends Plugin {
} as ViewState,
eState ? eState : { focus: true },
);
const mdView = leaf.view;
if(mdView instanceof MarkdownView) {
foldExcalidrawSection(mdView);
}
}
public async setExcalidrawView(leaf: WorkspaceLeaf) {

View File

@@ -381,7 +381,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return;
}
this.props.view.plugin.taskbone.getTextForView(this.props.view, isWinCTRLorMacCMD(e));
this.props.view.plugin.taskbone.getTextForView(this.props.view, {forceReScan: isWinCTRLorMacCMD(e)});
}}
icon={ICONS.ocr}
view={this.props.view}

View File

@@ -1,12 +1,13 @@
import { createPNG, ExcalidrawAutomate } from "../ExcalidrawAutomate";
import { ExcalidrawAutomate, createPNG } from "../ExcalidrawAutomate";
import {Notice, requestUrl} from "obsidian"
import ExcalidrawPlugin from "../main"
import {log} from "../utils/Utils"
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
import FrontmatterEditor from "src/utils/Frontmatter";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { blobToBase64 } from "src/utils/FileUtils";
import { getEA } from "src";
import { log } from "src/utils/DebugHelper";
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
@@ -39,23 +40,9 @@ export default class Taskbone {
return apiKey;
}
public async getTextForView(view: ExcalidrawView, forceReScan: boolean) {
await view.forceSave(true);
const viewElements = view.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement) =>
el.type==="freedraw" ||
( el.type==="image" &&
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
));
if(viewElements.length === 0) {
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
return;
}
const fe = new FrontmatterEditor(view.data);
if(fe.hasKey("taskbone-ocr") && !forceReScan) {
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
return;
}
const bb = this.plugin.ea.getBoundingBox(viewElements);
public async getTextForElements(elements: ExcalidrawElement[], ea: ExcalidrawAutomate): Promise<string> {
ea.copyViewElementsToEAforEditing(elements, true);
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
@@ -79,26 +66,52 @@ export default class Taskbone {
};
const img =
await createPNG(
view.file.path + "#^taskbone",
await ea.createPNG(
null,
scale,
exportSettings,
loader,
"light",
null,
null,
[],
this.plugin,
0
);
)
return await this.getTextForImage(img);
}
const text = await this.getTextForImage(img);
public async getTextForView(view: ExcalidrawView, {
forceReScan,
selectedElementsOnly = false,
addToFrontmatter = true,
}: {
forceReScan: boolean,
selectedElementsOnly?: boolean,
addToFrontmatter?: boolean,
}) {
await view.forceSave(true);
const ea = getEA(view) as ExcalidrawAutomate;
const viewElements = (selectedElementsOnly ? ea.getViewSelectedElements() : ea.getViewElements())
.filter((el:ExcalidrawElement) =>
el.type==="freedraw" ||
( el.type==="image" &&
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
));
if(viewElements.length === 0) {
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
return;
}
const fe = new FrontmatterEditor(view.data);
if(addToFrontmatter && fe.hasKey("taskbone-ocr") && !forceReScan) {
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
return;
}
const text = await this.getTextForElements(viewElements, ea);
if(text) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
if(addToFrontmatter) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
}
window.navigator.clipboard.writeText(text);
new Notice("I placed the recognized in the drawing's frontmatter and onto the system clipboard.");
new Notice(`I placed the recognized text onto the system clipboard${addToFrontmatter?" and to document properties":""}.`);
}
}

View File

@@ -44,6 +44,7 @@ export interface ExcalidrawSettings {
templateFilePath: string;
scriptFolderPath: string;
compress: boolean;
decompressForMDView: boolean;
autosave: boolean;
autosaveInterval: number;
autosaveIntervalDesktop: number;
@@ -188,6 +189,7 @@ export interface ExcalidrawSettings {
areaZoomLimit: number;
longPressDesktop: number;
longPressMobile: number;
isDebugMode: boolean;
}
declare const PLUGIN_VERSION:string;
@@ -200,6 +202,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
templateFilePath: "Excalidraw/Template.excalidraw",
scriptFolderPath: "Excalidraw/Scripts",
compress: false,
decompressForMDView: true,
autosave: true,
autosaveInterval: 15000,
autosaveIntervalDesktop: 15000,
@@ -436,6 +439,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
areaZoomLimit: 1,
longPressDesktop: 500,
longPressMobile: 500,
isDebugMode: false,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -662,6 +666,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DECOMPRESS_FOR_MD_NAME"))
.setDesc(fragWithHTML(t("DECOMPRESS_FOR_MD_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.decompressForMDView)
.onChange(async (value) => {
this.plugin.settings.decompressForMDView = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("AUTOSAVE_INTERVAL_DESKTOP_NAME"))
.setDesc(fragWithHTML(t("AUTOSAVE_INTERVAL_DESKTOP_DESC")))
@@ -2461,6 +2477,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("DEBUGMODE_NAME"))
.setDesc(fragWithHTML(t("DEBUGMODE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.isDebugMode)
.onChange((value) => {
this.plugin.settings.isDebugMode = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SLIDING_PANES_NAME"))
.setDesc(fragWithHTML(t("SLIDING_PANES_DESC")))

View File

@@ -4,37 +4,46 @@ import { createTreeWalker, walk } from "./walker";
export type ConversionResult = {
hasErrors: boolean;
errors: NodeListOf<Element> | null;
errors: string;
content: any; // Serialized Excalidraw JSON
};
export const svgToExcalidraw = (svgString: string): ConversionResult => {
const parser = new DOMParser();
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
try {
const parser = new DOMParser();
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
// was there a parsing error?
const errorsElements = svgDOM.querySelectorAll("parsererror");
const hasErrors = errorsElements.length > 0;
let content = null;
// was there a parsing error?
const errorsElements = svgDOM.querySelectorAll("parsererror");
const hasErrors = errorsElements.length > 0;
let content = null;
if (hasErrors) {
console.error(
"There were errors while parsing the given SVG: ",
[...errorsElements].map((el) => el.innerHTML),
);
} else {
const tw = createTreeWalker(svgDOM);
const scene = new ExcalidrawScene();
const groups: Group[] = [];
if (hasErrors) {
console.error(
"There were errors while parsing the given SVG: ",
[...errorsElements].map((el) => el.innerHTML),
);
} else {
const tw = createTreeWalker(svgDOM);
const scene = new ExcalidrawScene();
const groups: Group[] = [];
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
content = scene.elements; //scene.toExJSON();
content = scene.elements; //scene.toExJSON();
}
return {
hasErrors,
errors: hasErrors ? `${[...errorsElements].map((el) => el.innerHTML)}` : "",
content,
};
} catch (error) {
console.log(error);
return {
hasErrors: true,
errors: `${error}`,
content:[],
};
}
return {
hasErrors,
errors: hasErrors ? errorsElements : null,
content,
};
};

18
src/types.d.ts vendored
View File

@@ -58,9 +58,23 @@ declare module "obsidian" {
},
basePath: string;
}
interface Editor {
insertText(data: string): void;
interface FoldPosition {
from: number;
to: number;
}
interface FoldInfo {
folds: FoldPosition[];
lines: number;
}
interface MarkdownSubView {
applyFoldInfo(foldInfo: FoldInfo): void;
getFoldInfo(): FoldInfo | null;
}
/*interface Editor {
insertText(data: string): void;
}*/
interface MetadataCache {
getBacklinksForFile(file: TFile): any;
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };

View File

@@ -110,7 +110,7 @@ export class CanvasNodeFactory {
}
}
};
const observer = isDebugMode
const observer = isDebugMode()
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);

View File

@@ -69,7 +69,7 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
const targetEA = getEA(sourceEA.targetView) as ExcalidrawAutomate;
const {height, width} = embeddableEl;
let {height, width} = embeddableEl;
if(!height || !width || height === 0 || width === 0) return;
@@ -77,8 +77,6 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
const newImage = targetEA.getElement(imageId) as Mutable<ExcalidrawImageElement>;
newImage.x = 0;
newImage.y = 0;
newImage.width = width;
newImage.height = height;
const angle = embeddableEl.angle;
const fname = pdfFile.basename;

View File

@@ -1,5 +1,15 @@
export const isDebugMode = false;
import { EXCALIDRAW_PLUGIN } from "src/constants/constants";
export const durationTreshold = 0; //0.05; //ms
export const isDebugMode = () => EXCALIDRAW_PLUGIN && EXCALIDRAW_PLUGIN.settings?.isDebugMode;
export const log = console.log.bind(window.console);
export const debug = (...messages: unknown[]) => {
if(isDebugMode()) {
console.log(...messages);
}
};
export class CustomMutationObserver {
private originalCallback: MutationCallback;

View File

@@ -114,7 +114,8 @@ export const setDynamicStyle = (
[`--h2-color`]: str(text),
[`--h3-color`]: str(text),
[`--h4-color`]: str(text),
[`color`]: str(text),
[`color`]: str(text),
['--excalidraw-caret-color']: str(text),
[`--select-highlight-color`]: str(gray1()),
[`--color-gray-80`]: str(isDark?text.darkerBy(40):text.lighterBy(40)), //frame
};

View File

@@ -440,3 +440,38 @@ export const fileShouldDefaultAsExcalidraw = (path:string, app:App):boolean => {
cache.frontmatter[FRONTMATTER_KEYS["plugin"].name] &&
!Boolean(cache.frontmatter[FRONTMATTER_KEYS["open-as-markdown"].name]);
}
export const getExcalidrawEmbeddedFilesFiletree = (sourceFile: TFile, plugin: ExcalidrawPlugin):TFile[] => {
if(!sourceFile || !plugin.isExcalidrawFile(sourceFile)) {
return [];
}
const fileList = new Set<TFile>();
const app = plugin.app;
const addAttachedImages = (f:TFile) => Object
.keys(app.metadataCache.resolvedLinks[f.path])
.forEach(path => {
const file = app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) return;
const isExcalidraw = plugin.isExcalidrawFile(file);
if (
(file.extension === "md" && !isExcalidraw) ||
fileList.has(file) //avoid infinite loops
) {
return;
}
fileList.add(file);
if (isExcalidraw) {
addAttachedImages(file);
}
});
addAttachedImages(sourceFile);
return Array.from(fileList);
}
export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:number, plugin: ExcalidrawPlugin):boolean => {
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
return fileList.some(f=>f.stat.mtime > mtime);
}

View File

@@ -2,6 +2,7 @@ import { App, Notice, TFile } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { convertSVGStringToElement } from "./Utils";
import { FILENAMEPARTS, PreviewImageType } from "./UtilTypes";
import { hasExcalidrawEmbeddedImagesTreeChanged } from "./FileUtils";
//@ts-ignore
const DB_NAME = "Excalidraw " + app.appId;
@@ -19,6 +20,7 @@ export type ImageKey = {
isDark: boolean;
previewImageType: PreviewImageType;
scale: number;
isTransparent: boolean;
} & FILENAMEPARTS;
const getKey = (key: ImageKey): string =>
@@ -29,7 +31,7 @@ const getKey = (key: ImageKey): string =>
: key.previewImageType === PreviewImageType.PNG
? 0
: 2
}#${key.scale}`; //key.isSVG ? 1 : 0
}#${key.scale}${key.isTransparent?"#t":""}`; //key.isSVG ? 1 : 0
class ImageCache {
private dbName: string;
@@ -291,7 +293,10 @@ class ImageCache {
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
if (cachedData && cachedData.mtime >= file.stat.mtime) {
if(hasExcalidrawEmbeddedImagesTreeChanged(file, cachedData.mtime, this.plugin)) {
return undefined;
}
if(cachedData.svg) {
return convertSVGStringToElement(cachedData.svg);
}
@@ -332,7 +337,7 @@ class ImageCache {
} else {
blob = image as Blob;
}
const data: FileCacheData = { mtime: file.stat.mtime, blob, svg};
const data: FileCacheData = { mtime: Date.now(), blob, svg};
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
const store = transaction.objectStore(this.cacheStoreName);
const key = getKey(key_);

View File

@@ -1,13 +1,15 @@
import {
App,
Editor,
FrontMatterCache,
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
import yaml from "js-yaml";
import yaml, { Mark } from "js-yaml";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
@@ -342,4 +344,27 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
const mergedMarkdown = `---\n${mergedFrontmatterYaml}---\n${targetContent}\n\n${templateContent.trim()}\n`;
return mergedMarkdown;
};
};
export const editorInsertText = (editor: Editor, text: string)=> {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
const updatedLine = line.slice(0, cursor.ch) + text + line.slice(cursor.ch);
editor.setLine(cursor.line, updatedLine);
}
export const foldExcalidrawSection = (view: MarkdownView) => {
if(!view || !(view instanceof MarkdownView)) return;
const existingFolds = view.currentMode.getFoldInfo();
const lineCount = view.editor.lineCount();
let i = -1;
while(i++<lineCount && view.editor.getLine(i) !== "# Excalidraw Data");
if(i>-1 && i<lineCount) {
const foldPositions = [
...(existingFolds?.folds ?? []),
...[{from: i, to: lineCount-1}],
];
view.currentMode.applyFoldInfo({folds: foldPositions, lines:lineCount});
}
}

View File

@@ -1,6 +1,7 @@
import { WorkspaceWindow } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { getAllWindowDocuments } from "./ObsidianUtils";
import { debug } from "./DebugHelper";
const STYLE_VARIABLES = ["--background-modifier-cover","--background-primary-alt","--background-secondary","--background-secondary-alt","--background-modifier-border","--text-normal","--text-muted","--text-accent","--text-accent-hover","--text-faint","--text-highlight-bg","--text-highlight-bg-active","--text-selection","--interactive-normal","--interactive-hover","--interactive-accent","--interactive-accent-hover","--scrollbar-bg","--scrollbar-thumb-bg","--scrollbar-active-thumb-bg"];
const EXCALIDRAW_CONTAINER_CLASS = "excalidraw__embeddable__outer";
@@ -16,6 +17,7 @@ export class StylesManager {
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
plugin.app.workspace.onLayoutReady(async () => {
debug("StylesManager.constructor app.workspace.onLayoutReady");
await this.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => {
this.copyPropertiesToTheme(doc);

View File

@@ -27,14 +27,11 @@ import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/elemen
import { ExportSettings } from "../ExcalidrawView";
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
import { FILENAMEPARTS } from "./UtilTypes";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
import { CropImage } from "./CropImage";
import { ExcalidrawData } from "src/ExcalidrawData";
import { ExcalidrawGenericElement } from "lib/svgToExcalidraw/types";
declare const PLUGIN_VERSION:string;
@@ -546,7 +543,7 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
};
export const compress = (data: string): string => {
return LZString.compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
};
export const decompress = (data: string): string => {
@@ -764,8 +761,6 @@ export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(r
export const awaitNextAnimationFrame = async () => new Promise(requestAnimationFrame);
*/
export const log = console.log.bind(window.console);
export const debug = console.log.bind(window.console);
//export const debug = function(){};

View File

@@ -579,4 +579,19 @@ img.excalidraw-cropped-pdfpage,
.ex-opacity-0 {
opacity: 0;
}
.popover .excalidraw-svg {
width: 100%;
max-width: inherit;
height: 100%;
max-height: inherit;
}
root {
--excalidraw-caret-color: initial;
}
textarea.excalidraw-wysiwyg, .excalidraw input {
caret-color: var(--excalidraw-caret-color);
}