2.4.0-beta-10

This commit is contained in:
zsviczian
2024-08-26 22:40:45 +02:00
parent 200d39c408
commit ffdb054291
13 changed files with 274 additions and 185 deletions

View File

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

View File

@@ -68,7 +68,6 @@
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2",

View File

@@ -4,7 +4,6 @@ import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import typescript2 from "rollup-plugin-typescript2";
//import webWorker from "rollup-plugin-web-worker-loader";
import fs from 'fs';
import LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
@@ -100,7 +99,6 @@ const BUILD_CONFIG = {
},
plugins: getRollupPlugins(
{tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"},
//webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
...(isProd ? [
terser({ toplevel: false, compress: { passes: 2 } }),
//!postprocess - the version available on npmjs does not work, need this update:

View File

@@ -278,11 +278,11 @@ export class ExcalidrawAutomate {
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
}
public compressToBase64(str:string):string {
public compressToBase64(str:string): string {
return LZString.compressToBase64(str);
}
public decompressFromBase64(str:string):string {
public decompressFromBase64(str:string): string {
return LZString.decompressFromBase64(str);
}

View File

@@ -35,6 +35,7 @@ import {
updateFrontmatterInString,
wrapTextAtCharLength,
arrayToMap,
compressAsync,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
@@ -219,10 +220,22 @@ export function getJSON(data: string): { scene: string; pos: number } {
return { scene: data, pos: parts.value ? parts.value.index : 0 };
}
export function getMarkdownDrawingSection(
export async function getMarkdownDrawingSectionAsync (
jsonString: string,
compressed: boolean,
) {
const result = compressed
? `## Drawing\n\x60\x60\x60compressed-json\n${await compressAsync(
jsonString,
)}\n\x60\x60\x60\n%%`
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
return result;
}
export function getMarkdownDrawingSection(
jsonString: string,
compressed: boolean,
): string {
const result = compressed
? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
jsonString,
@@ -1395,7 +1408,7 @@ export class ExcalidrawData {
* @returns markdown string
*/
disableCompression: boolean = false;
generateMD(deletedElements: ExcalidrawElement[] = []): string {
generateMDBase(deletedElements: ExcalidrawElement[] = []) {
let outString = this.textElementCommentedOut ? "%%\n" : "";
outString += `# Excalidraw Data\n## Text Elements\n`;
if (this.plugin.settings.addDummyTextElement) {
@@ -1462,13 +1475,31 @@ export class ExcalidrawData {
appState: this.scene.appState,
files: this.scene.files
}, null, "\t");
return { outString, sceneJSONstring };
}
async generateMDAsync(deletedElements: ExcalidrawElement[] = []): Promise<string> {
const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
getMarkdownDrawingSection(
(await getMarkdownDrawingSectionAsync(
sceneJSONstring,
this.disableCompression ? false : this.plugin.settings.compress,
)
))
);
return result;
}
generateMDSync(deletedElements: ExcalidrawElement[] = []): string {
const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
(getMarkdownDrawingSection(
sceneJSONstring,
this.disableCompression ? false : this.plugin.settings.compress,
))
);
return result;
}

View File

@@ -144,6 +144,7 @@ import { SelectCard } from "./dialogs/SelectCard";
import { Packages } from "./types/types";
import React from "react";
import { diagramToHTML } from "./utils/matic";
import { IS_WORKER_SUPPORTED } from "./workers/compression-worker";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -765,6 +766,7 @@ export default class ExcalidrawView extends TextFileView {
//added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back
//can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607
if(this.semaphores?.viewunload) {
await this.prepareGetViewData();
const d = this.getViewData();
const plugin = this.plugin;
const file = this.file;
@@ -775,6 +777,7 @@ export default class ExcalidrawView extends TextFileView {
return;
}
await this.prepareGetViewData();
await super.save();
if (process.env.NODE_ENV === 'development') {
if (DEBUGGING) {
@@ -840,16 +843,23 @@ export default class ExcalidrawView extends TextFileView {
// get the new file content
// if drawing is in Text Element Edit Lock, then everything should be parsed and in sync
// if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
getViewData() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewData, "ExcalidrawView.getViewData");
/**
* I moved the logic from getViewData to prepareGetViewData because getViewData is Sync and prepareGetViewData is async
* prepareGetViewData is async because of moving compression to a worker thread in 2.4.0
*/
private viewSaveData: string = "";
async prepareGetViewData(): Promise<void> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.prepareGetViewData, "ExcalidrawView.prepareGetViewData");
if (!this.excalidrawAPI || !this.excalidrawData.loaded) {
return this.data;
this.viewSaveData = this.data;
return;
}
const scene = this.getScene();
if(!scene) {
return this.data;
this.viewSaveData = this.data;
return;
}
//include deleted elements in save in case saving in markdown mode
@@ -881,17 +891,29 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView &&
this.isEditedAsMarkdownInOtherView();
}
const result = header + this.excalidrawData.generateMD(
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
) + tail;
const result = IS_WORKER_SUPPORTED
? (header + (await this.excalidrawData.generateMDAsync(
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
)) + tail)
: (header + (this.excalidrawData.generateMDSync(
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
)) + tail)
this.excalidrawData.disableCompression = false;
return result;
this.viewSaveData = result;
return;
}
if (this.compatibilityMode) {
return JSON.stringify(scene, null, "\t");
this.viewSaveData = JSON.stringify(scene, null, "\t");
return;
}
return this.data;
this.viewSaveData = this.data;
return;
}
getViewData() {
return this.viewSaveData ?? this.data;
}
private hiddenMobileLeaves:[WorkspaceLeaf,string][] = [];
@@ -2097,6 +2119,7 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear");
this.viewSaveData = "";
this.canvasNodeFactory.purgeNodes();
this.embeddableRefs.clear();
this.embeddableLeafRefs.clear();
@@ -3787,6 +3810,7 @@ export default class ExcalidrawView extends TextFileView {
private onPaste (data: ClipboardData, event: ClipboardEvent | null) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event);
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const ea = this.getHookServer();
if(data && ea.onPasteHook) {
const res = ea.onPasteHook({
@@ -3849,7 +3873,6 @@ export default class ExcalidrawView extends TextFileView {
const quoteWithRef = obsidianPDFQuoteWithRef(data.text);
if(quoteWithRef) {
const ea = getEA(this) as ExcalidrawAutomate;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const st = api.getAppState();
const strokeC = st.currentItemStrokeColor;
const viewC = st.viewBackgroundColor;
@@ -3877,6 +3900,77 @@ export default class ExcalidrawView extends TextFileView {
if (data.elements) {
window.setTimeout(() => this.save(), 30); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements
}
//process pasted text after it was processed into elements by Excalidraw
//I let Excalidraw handle the paste first, e.g. to split text by lines
//Only process text if it includes links or embeds that need to be parsed
if(data && data.text && data.text.match(/(\[\[[^\]]*]])|(\[[^\]]*]\([^)]*\))/gm)) {
const prevElements = api.getSceneElements().filter(el=>el.type === "text").map(el=>el.id);
window.setTimeout(async ()=>{
const sceneElements = api.getSceneElementsIncludingDeleted() as Mutable<ExcalidrawElement>[];
const newElements = sceneElements.filter(el=>el.type === "text" && !el.isDeleted && !prevElements.includes(el.id)) as ExcalidrawTextElement[];
//collect would-be image elements and their corresponding files and links
const imageElementsMap = new Map<ExcalidrawTextElement, [string, TFile]>();
let element: ExcalidrawTextElement;
const callback = (link: string, file: TFile) => {
imageElementsMap.set(element, [link, file]);
}
newElements.forEach((el:ExcalidrawTextElement)=>{
element = el;
isTextImageTransclusion(el.originalText,this,callback);
});
//if there are no image elements, save and return
//Save will ensure links and embeds are parsed
if(imageElementsMap.size === 0) {
this.save(false); //saving because there still may be text transclusions
return;
};
//if there are image elements
//first delete corresponding "old" text elements
for(const [el, [link, file]] of imageElementsMap) {
const clone = cloneElement(el);
clone.isDeleted = true;
this.excalidrawData.deleteTextElement(clone.id);
sceneElements[sceneElements.indexOf(el)] = clone;
}
this.updateScene({elements: sceneElements, storeAction: "update"});
//then insert images and embeds
//shift text elements down to make space for images and embeds
const ea:ExcalidrawAutomate = getEA(this);
let offset = 0;
for(const el of newElements) {
const topleft = {x: el.x, y: el.y+offset};
if(imageElementsMap.has(el)) {
const [link, file] = imageElementsMap.get(el);
if(IMAGE_TYPES.contains(file.extension)) {
const id = await insertImageToView (ea, topleft, file, undefined, false);
offset += ea.getElement(id).height - el.height;
} else if(file.extension !== "pdf") {
//isTextImageTransclusion will not return text only markdowns, this is here
//for the future when we may want to support other embeddables
const id = await insertEmbeddableToView (ea, topleft, file, link, false);
offset += ea.getElement(id).height - el.height;
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, topleft);
}
} else {
if(offset !== 0) {
ea.copyViewElementsToEAforEditing([el]);
ea.getElement(el.id).y = topleft.y;
}
}
}
await ea.addElementsToView(false,true);
ea.selectElementsInView(newElements.map(el=>el.id));
ea.destroy();
},200) //parse transclusion and links after paste
}
return true;
}

View File

@@ -121,7 +121,7 @@ export const DEVICE: DeviceType = {
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
isWindows: document.body.hasClass("mod-windows"),
isIOS: document.body.hasClass("is-ios"),
isAndroid: document.body.hasClass("is-android")
isAndroid: document.body.hasClass("is-android"),
};
export const ROOTELEMENTSIZE = (() => {

View File

@@ -138,6 +138,7 @@ import { showFrameSettings } from "./dialogs/FrameSettings";
import { ExcalidrawLib } from "./ExcalidrawLib";
import { Rank, SwordColors } from "./menu/ActionIcons";
import { RankMessage } from "./dialogs/RankMessage";
import { initCompressionWorker, terminateCompressionWorker } from "./workers/compression-worker";
declare let EXCALIDRAW_PACKAGES:string;
declare let react:any;
@@ -311,6 +312,7 @@ export default class ExcalidrawPlugin extends Plugin {
}*/
async onload() {
initCompressionWorker();
this.loadTimestamp = Date.now();
addIcon(ICON_NAME, EXCALIDRAW_ICON);
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
@@ -3316,6 +3318,7 @@ export default class ExcalidrawPlugin extends Plugin {
react = null;
reactDOM = null;
excalidrawLib = null;
terminateCompressionWorker();
}
public async embedDrawing(file: TFile) {

View File

@@ -22,7 +22,7 @@ export type DeviceType = {
isMacOS: boolean,
isWindows: boolean,
isIOS: boolean,
isAndroid: boolean
isAndroid: boolean,
};
declare global {

View File

@@ -5,7 +5,7 @@ import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
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";
import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./Utils";
import { cleanSectionHeading } from "./ObsidianUtils";
import { getEA } from "src";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
@@ -18,8 +18,9 @@ export async function insertImageToView(
position: { x: number, y: number },
file: TFile | string,
scale?: boolean,
shouldInsertToView: boolean = true,
):Promise<string> {
ea.clear();
if(shouldInsertToView) {ea.clear();}
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
const api = ea.getExcalidrawAPI();
@@ -30,7 +31,7 @@ export async function insertImageToView(
file,
scale,
);
await ea.addElementsToView(false, true, true);
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
return id;
}
@@ -39,12 +40,13 @@ export async function insertEmbeddableToView (
position: { x: number, y: number },
file?: TFile,
link?: string,
shouldInsertToView: boolean = true,
):Promise<string> {
ea.clear();
if(shouldInsertToView) {ea.clear();}
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
return await insertImageToView(ea, position, link??file);
return await insertImageToView(ea, position, link??file, undefined, shouldInsertToView);
} else {
const id = ea.addEmbeddable(
position.x,
@@ -54,7 +56,7 @@ export async function insertEmbeddableToView (
link,
file,
);
await ea.addElementsToView(false, true, true);
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
return id;
}
}
@@ -369,6 +371,9 @@ export function isTextImageTransclusion (
const link = match.value[1] ?? match.value[2];
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
if(view.file === file) {
if(link?.split("#")[1] && !isImagePartRef(getEmbeddedFilenameParts(link))) {
return false;
}
new Notice(t("RECURSIVE_INSERT_ERROR"));
return false;
}

View File

@@ -28,6 +28,7 @@ import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./Obsidia
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
import { CropImage } from "./CropImage";
import opentype from 'opentype.js';
import { runCompressionWorker } from "src/workers/compression-worker";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -528,9 +529,12 @@ export function getLinkParts (fname: string, file?: TFile): LinkParts {
};
};
export async function compressAsync (data: string): Promise<string> {
return await runCompressionWorker(data, "compress");
}
export function compress (data: string): string {
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
@@ -540,7 +544,11 @@ export function compress (data: string): string {
return result.trim();
};
export function decompress (data: string): string {
export async function decompressAsync (data: string): Promise<string> {
return await runCompressionWorker(data, "decompress");
};
export function decompress (data: string, isAsync:boolean = false): string {
let cleanedData = '';
const length = data.length;
@@ -765,6 +773,10 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
}
}
export function isImagePartRef (parts: FILENAMEPARTS): boolean {
return (parts.hasGroupref || parts.hasArearef || parts.hasFrameref || parts.hasClippedFrameref);
}
export function fragWithHTML (html: string) {
return createFragment((frag) => (frag.createDiv().innerHTML = html));
}

View File

@@ -0,0 +1,100 @@
function createWorkerBlob(jsCode:string) {
// Create a new Blob with the JavaScript code
const blob = new Blob([jsCode], { type: 'text/javascript' });
// Create a URL for the Blob
const url = URL.createObjectURL(blob);
return url;
}
const workerCode = `
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
self.onmessage = function(e) {
const { data, action } = e.data;
try {
switch (action) {
case 'compress':
if (!data) throw new Error("No input string provided for compression.");
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
result += compressed.slice(i, i + chunkSize) + '\\n\\n';
}
self.postMessage({ compressed: result.trim() });
break;
case 'decompress':
if (!data) throw new Error("No input string provided for decompression.");
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\\n' && char !== '\\r') {
cleanedData += char;
}
}
const decompressed = LZString.decompressFromBase64(cleanedData);
self.postMessage({ decompressed });
break;
default:
throw new Error("Unknown action.");
}
} catch (error) {
// Post the error message back to the main thread
self.postMessage({ error: error.message });
}
};
`;
let worker:Worker | null = null;
export function initCompressionWorker() {
if(!worker) {
worker = new Worker(createWorkerBlob(workerCode));
}
}
export async function runCompressionWorker(data:string, action: 'compress' | 'decompress'): Promise<string> {
return new Promise((resolve, reject) => {
worker.onmessage = function(e) {
const { compressed, decompressed, error } = e.data;
if (error) {
reject(new Error(error));
} else if (compressed || decompressed) {
resolve(compressed || decompressed);
} else {
reject(new Error('Unexpected response from worker'));
}
};
// Set up the worker's error handler
worker.onerror = function(error) {
reject(new Error(error.message));
};
// Post the message to the worker
worker.postMessage({ data, action });
});
}
export function terminateCompressionWorker() {
worker.terminate();
worker = null;
}
export let IS_WORKER_SUPPORTED = false;
function canCreateWorkerFromBlob() {
try {
const blob = new Blob(["self.onmessage = function() {}"]);
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.terminate();
URL.revokeObjectURL(url);
IS_WORKER_SUPPORTED = true;
} catch (e) {
IS_WORKER_SUPPORTED = false;
}
}
canCreateWorkerFromBlob();

View File

@@ -1,153 +0,0 @@
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import ExcalidrawView from "src/ExcalidrawView";
import { debug, DEBUGGING, ts } from "src/utils/DebugHelper";
import { imageCache } from "src/utils/ImageCache";
interface SaveWorkerMessageData {
superSave: () => Promise<void>;
view: ExcalidrawView;
forcesave: boolean;
preventReload: boolean;
}
onmessage = async function (e: MessageEvent<SaveWorkerMessageData>) {
const { superSave, view, forcesave, preventReload } = e.data;
try {
(process.env.NODE_ENV === 'development') && ts("allow save",1);
const scene = view.getScene();
if (view.compatibilityMode) {
await view.excalidrawData.syncElements(scene);
} else if (
await view.excalidrawData.syncElements(scene, view.excalidrawAPI.getAppState().selectedElementIds)
&& !view.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout
) {
(process.env.NODE_ENV === 'development') && ts("ExcalidrawView.beforeLoadDrawing",1);
await view.loadDrawing(
false,
view.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted)
);
(process.env.NODE_ENV === 'development') && ts("ExcalidrawView.afterLoadDrawing",1);
}
(process.env.NODE_ENV === 'development') && ts("after sync elements",1);
//reload() is triggered indirectly when saving by the modifyEventHandler in main.ts
//prevent reload is set here to override reload when not wanted: typically when the user is editing
//and we do not want to interrupt the flow by reloading the drawing into the canvas.
view.clearDirty();
view.clearPreventReloadTimer();
view.semaphores.preventReload = preventReload;
//added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back
//can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607
if(view.semaphores?.viewunload) {
const d = view.getViewData();
const plugin = view.plugin;
const file = view.file;
window.setTimeout(async ()=>{
await plugin.app.vault.modify(file,d);
await imageCache.addBAKToCache(file.path,d);
},200)
return;
}
(process.env.NODE_ENV === 'development') && ts("before super.save",1);
await superSave();
(process.env.NODE_ENV === 'development') && ts("after super.save",1);
if (process.env.NODE_ENV === 'development') {
if (DEBUGGING) {
debug(self.onmessage, `ExcalidrawView.save, super.save finished`, view.file);
console.trace();
}
}
//saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted.
const path = view.file.path;
//@ts-ignore
const data = view.lastSavedData;
window.setTimeout(()=>imageCache.addBAKToCache(path,data),50);
const triggerReload = (view.lastSaveTimestamp === view.file.stat.mtime) &&
!preventReload && forcesave;
view.lastSaveTimestamp = view.file.stat.mtime;
//view.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629
//there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image
//did not update the open drawing
if(preventReload) {
view.setPreventReload();
}
postMessage({ success: true, triggerReload });
} catch (error) {
postMessage({ success: false, error: (error as Error).message });
}
};
/* ExcalidrawView
import SaveWorker from "web-worker:./workers/save-worker.ts";
//...
try {
if (allowSave) {
const worker = new SaveWorker({name: "Excalidraw File Save Worker"});
const promise = new Promise<{success:boolean, error?:Error, triggerReload?: boolean}>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<{ success: boolean; error?: Error; triggerReload?: boolean }>) => {
resolve(e.data);
};
worker.onerror = (e: ErrorEvent) => {
reject(new Error(e.message));
};
worker.postMessage({
superSave: super.save.bind(this),
view: this,
preventReload,
forcesave,
});
});
const { success, error, triggerReload: tr } = await promise;
worker.terminate();
if(error) {
throw error;
}
if(typeof tr !== "undefined") {
triggerReload = tr;
}
}
// !triggerReload means file has not changed. No need to re-export
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition)
if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) {
const autoexportPreference = this.excalidrawData.autoexportPreference;
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg
) {
this.saveSVG();
}
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png
) {
this.savePNG();
}
if (
!this.compatibilityMode &&
this.plugin.settings.autoexportExcalidraw
) {
this.saveExcalidraw();
}
}
} catch (e) {
errorlog({
where: "ExcalidrawView.save",
fn: this.save,
error: e,
});
warningUnknowSeriousError();
}
*/