mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
379 lines
10 KiB
TypeScript
379 lines
10 KiB
TypeScript
import {
|
|
App,
|
|
Instruction,
|
|
normalizePath,
|
|
TAbstractFile,
|
|
TFile,
|
|
} from "obsidian";
|
|
import { PLUGIN_ID } from "../constants/constants";
|
|
import ExcalidrawView from "../view/ExcalidrawView";
|
|
import ExcalidrawPlugin from "../core/main";
|
|
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./Dialogs/Prompt";
|
|
import { getIMGFilename } from "../utils/fileUtils";
|
|
import { splitFolderAndFilename } from "../utils/fileUtils";
|
|
import { getEA } from "src/core";
|
|
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
|
import { WeakArray } from "./WeakArray";
|
|
import { getExcalidrawViews } from "../utils/obsidianUtils";
|
|
|
|
export type ScriptIconMap = {
|
|
[key: string]: { name: string; group: string; svgString: string };
|
|
};
|
|
|
|
export class ScriptEngine {
|
|
private plugin: ExcalidrawPlugin;
|
|
private app: App;
|
|
private scriptPath: string;
|
|
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
|
|
public scriptIconMap: ScriptIconMap;
|
|
eaInstances = new WeakArray<ExcalidrawAutomate>();
|
|
|
|
constructor(plugin: ExcalidrawPlugin) {
|
|
this.plugin = plugin;
|
|
this.app = plugin.app;
|
|
this.scriptIconMap = {};
|
|
this.loadScripts();
|
|
this.registerEventHandlers();
|
|
}
|
|
|
|
public removeViewEAs(view: ExcalidrawView) {
|
|
const eas = new Set<ExcalidrawAutomate>();
|
|
this.eaInstances.forEach((ea) => {
|
|
if (ea.targetView === view) {
|
|
eas.add(ea);
|
|
ea.destroy();
|
|
}
|
|
});
|
|
this.eaInstances.removeObjects(eas);
|
|
}
|
|
|
|
public destroy() {
|
|
this.eaInstances.forEach((ea) => ea.destroy());
|
|
this.eaInstances.clear();
|
|
this.eaInstances = null;
|
|
this.scriptIconMap = null;
|
|
this.plugin = null;
|
|
this.scriptPath = null;
|
|
}
|
|
|
|
private handleSvgFileChange (path: string) {
|
|
if (!path.endsWith(".svg")) {
|
|
return;
|
|
}
|
|
const scriptFile = this.app.vault.getAbstractFileByPath(
|
|
getIMGFilename(path, "md"),
|
|
);
|
|
if (scriptFile && scriptFile instanceof TFile) {
|
|
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
|
|
this.loadScript(scriptFile);
|
|
}
|
|
}
|
|
|
|
private async deleteEventHandler (file: TFile) {
|
|
if (!(file instanceof TFile)) {
|
|
return;
|
|
}
|
|
if (!file.path.startsWith(this.scriptPath)) {
|
|
return;
|
|
}
|
|
this.unloadScript(this.getScriptName(file), file.path);
|
|
this.handleSvgFileChange(file.path);
|
|
};
|
|
|
|
private async createEventHandler (file: TFile) {
|
|
if (!(file instanceof TFile)) {
|
|
return;
|
|
}
|
|
if (!file.path.startsWith(this.scriptPath)) {
|
|
return;
|
|
}
|
|
this.loadScript(file);
|
|
this.handleSvgFileChange(file.path);
|
|
};
|
|
|
|
private async renameEventHandler (file: TAbstractFile, oldPath: string) {
|
|
if (!(file instanceof TFile)) {
|
|
return;
|
|
}
|
|
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
|
|
const newFileIsScript = file.path.startsWith(this.scriptPath);
|
|
if (oldFileIsScript) {
|
|
this.unloadScript(this.getScriptName(oldPath), oldPath);
|
|
this.handleSvgFileChange(oldPath);
|
|
}
|
|
if (newFileIsScript) {
|
|
this.loadScript(file);
|
|
this.handleSvgFileChange(file.path);
|
|
}
|
|
}
|
|
|
|
registerEventHandlers() {
|
|
this.plugin.registerEvent(
|
|
this.app.vault.on(
|
|
"delete",
|
|
(file: TFile)=>this.deleteEventHandler(file)
|
|
),
|
|
);
|
|
this.plugin.registerEvent(
|
|
this.app.vault.on(
|
|
"create",
|
|
(file: TFile)=>this.createEventHandler(file)
|
|
),
|
|
);
|
|
this.plugin.registerEvent(
|
|
this.app.vault.on(
|
|
"rename",
|
|
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
|
|
),
|
|
);
|
|
}
|
|
|
|
updateScriptPath() {
|
|
if (this.scriptPath === this.plugin.settings.scriptFolderPath) {
|
|
return;
|
|
}
|
|
if (this.scriptPath) {
|
|
this.unloadScripts();
|
|
}
|
|
this.loadScripts();
|
|
}
|
|
|
|
public getListofScripts(): TFile[] {
|
|
this.scriptPath = this.plugin.settings.scriptFolderPath;
|
|
if(!this.scriptPath) return;
|
|
this.scriptPath = normalizePath(this.scriptPath);
|
|
if (!this.app.vault.getAbstractFileByPath(this.scriptPath)) {
|
|
return;
|
|
}
|
|
return this.app.vault
|
|
.getFiles()
|
|
.filter(
|
|
(f: TFile) =>
|
|
f.path.startsWith(this.scriptPath+"/") && f.extension === "md",
|
|
);
|
|
}
|
|
|
|
loadScripts() {
|
|
this.getListofScripts()?.forEach((f) => this.loadScript(f));
|
|
}
|
|
|
|
public getScriptName(f: TFile | string): string {
|
|
let basename = "";
|
|
let path = "";
|
|
if (f instanceof TFile) {
|
|
basename = f.basename;
|
|
path = f.path;
|
|
} else {
|
|
basename = splitFolderAndFilename(f).basename;
|
|
path = f;
|
|
}
|
|
|
|
const subpath = path.split(`${this.scriptPath}/`)[1];
|
|
if(!subpath) {
|
|
console.warn(`ScriptEngine.getScriptName unexpected basename: ${basename}; path: ${path}`)
|
|
}
|
|
const lastSlash = subpath?.lastIndexOf("/");
|
|
if (lastSlash > -1) {
|
|
return subpath.substring(0, lastSlash + 1) + basename;
|
|
}
|
|
return basename;
|
|
}
|
|
|
|
async addScriptIconToMap(scriptPath: string, name: string) {
|
|
const svgFilePath = getIMGFilename(scriptPath, "svg");
|
|
const file = this.app.vault.getAbstractFileByPath(svgFilePath);
|
|
const svgString: string =
|
|
file && file instanceof TFile
|
|
? await this.app.vault.read(file)
|
|
: null;
|
|
this.scriptIconMap = {
|
|
...this.scriptIconMap,
|
|
};
|
|
const splitname = splitFolderAndFilename(name)
|
|
this.scriptIconMap[scriptPath] = { name:splitname.filename, group: splitname.folderpath === "/" ? "" : splitname.folderpath, svgString };
|
|
this.updateToolPannels();
|
|
}
|
|
|
|
loadScript(f: TFile) {
|
|
if (f.extension !== "md") {
|
|
return;
|
|
}
|
|
const scriptName = this.getScriptName(f);
|
|
this.addScriptIconToMap(f.path, scriptName);
|
|
this.plugin.addCommand({
|
|
id: scriptName,
|
|
name: `(Script) ${scriptName}`,
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
(async()=>{
|
|
const script = await this.app.vault.read(f);
|
|
if(script) {
|
|
//remove YAML frontmatter if present
|
|
this.executeScript(view, script, scriptName,f);
|
|
}
|
|
})()
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
}
|
|
|
|
unloadScripts() {
|
|
const scripts = this.app.vault
|
|
.getFiles()
|
|
.filter((f: TFile) => f.path.startsWith(this.scriptPath));
|
|
scripts.forEach((f) => {
|
|
this.unloadScript(this.getScriptName(f), f.path);
|
|
});
|
|
}
|
|
|
|
unloadScript(basename: string, path: string) {
|
|
if (!path.endsWith(".md")) {
|
|
return;
|
|
}
|
|
delete this.scriptIconMap[path];
|
|
this.scriptIconMap = { ...this.scriptIconMap };
|
|
this.updateToolPannels();
|
|
|
|
const commandId = `${PLUGIN_ID}:${basename}`;
|
|
// @ts-ignore
|
|
if (!this.app.commands.commands[commandId]) {
|
|
return;
|
|
}
|
|
// @ts-ignore
|
|
delete this.app.commands.commands[commandId];
|
|
}
|
|
|
|
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
|
|
if (!view || !script || !title) {
|
|
return;
|
|
}
|
|
//addresses the situation when after paste text element IDs are not updated to 8 characters
|
|
//linked to onPaste save issue with the false parameter
|
|
if(view.getScene().elements.some(el=>!el.isDeleted && el.type === "text" && el.id.length > 8)) {
|
|
await view.save(false, true);
|
|
}
|
|
|
|
script = script.replace(/^---.*?---\n/gs, "");
|
|
const ea = getEA(view);
|
|
this.eaInstances.push(ea);
|
|
ea.activeScript = title;
|
|
|
|
//https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
|
|
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
|
|
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
|
let result = null;
|
|
//try {
|
|
result = await new AsyncFunction("ea", "utils", script)(ea, {
|
|
inputPrompt: (
|
|
header: string,
|
|
placeholder?: string,
|
|
value?: string,
|
|
buttons?: ButtonDefinition[],
|
|
lines?: number,
|
|
displayEditorButtons?: boolean,
|
|
customComponents?: (container: HTMLElement) => void,
|
|
blockPointerInputOutsideModal?: boolean,
|
|
) =>
|
|
ScriptEngine.inputPrompt(
|
|
view,
|
|
this.plugin,
|
|
this.app,
|
|
header,
|
|
placeholder,
|
|
value,
|
|
buttons,
|
|
lines,
|
|
displayEditorButtons,
|
|
customComponents,
|
|
blockPointerInputOutsideModal,
|
|
),
|
|
suggester: (
|
|
displayItems: string[],
|
|
items: any[],
|
|
hint?: string,
|
|
instructions?: Instruction[],
|
|
) =>
|
|
ScriptEngine.suggester(
|
|
this.app,
|
|
displayItems,
|
|
items,
|
|
hint,
|
|
instructions,
|
|
),
|
|
scriptFile: file
|
|
});
|
|
/*} catch (e) {
|
|
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
|
|
errorlog({ script: this.plugin.ea.activeScript, error: e });
|
|
}*/
|
|
return result;
|
|
}
|
|
|
|
private updateToolPannels() {
|
|
const excalidrawViews = getExcalidrawViews(this.app);
|
|
excalidrawViews.forEach(excalidrawView => {
|
|
excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(
|
|
this.scriptIconMap,
|
|
);
|
|
});
|
|
}
|
|
|
|
public static async inputPrompt(
|
|
view: ExcalidrawView,
|
|
plugin: ExcalidrawPlugin,
|
|
app: App,
|
|
header: string,
|
|
placeholder?: string,
|
|
value?: string,
|
|
buttons?: ButtonDefinition[],
|
|
lines?: number,
|
|
displayEditorButtons?: boolean,
|
|
customComponents?: (container: HTMLElement) => void,
|
|
blockPointerInputOutsideModal?: boolean,
|
|
) {
|
|
try {
|
|
return await GenericInputPrompt.Prompt(
|
|
view,
|
|
plugin,
|
|
app,
|
|
header,
|
|
placeholder,
|
|
value,
|
|
buttons,
|
|
lines,
|
|
displayEditorButtons,
|
|
customComponents,
|
|
blockPointerInputOutsideModal,
|
|
);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
public static async suggester(
|
|
app: App,
|
|
displayItems: string[],
|
|
items: any[],
|
|
hint?: string,
|
|
instructions?: Instruction[],
|
|
) {
|
|
try {
|
|
return await GenericSuggester.Suggest(
|
|
app,
|
|
displayItems,
|
|
items,
|
|
hint,
|
|
instructions,
|
|
);
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|