Files
obsidian-excalidraw-plugin/src/Scripts.ts
zsviczian 131294464e 2.1.4
2024-04-13 09:44:15 +02:00

335 lines
9.0 KiB
TypeScript

import {
App,
Instruction,
TAbstractFile,
TFile,
WorkspaceLeaf,
} from "obsidian";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
import { getIMGFilename } from "./utils/FileUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { getEA } from "src";
export type ScriptIconMap = {
[key: string]: { name: string; group: string; svgString: string };
};
export class ScriptEngine {
private plugin: ExcalidrawPlugin;
private scriptPath: string;
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
public scriptIconMap: ScriptIconMap;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.scriptIconMap = {};
this.loadScripts();
this.registerEventHandlers();
}
registerEventHandlers() {
const handleSvgFileChange = (path: string) => {
if (!path.endsWith(".svg")) {
return;
}
const scriptFile = app.vault.getAbstractFileByPath(
getIMGFilename(path, "md"),
);
if (scriptFile && scriptFile instanceof TFile) {
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
this.loadScript(scriptFile);
}
};
const deleteEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.unloadScript(this.getScriptName(file), file.path);
handleSvgFileChange(file.path);
};
this.plugin.registerEvent(
app.vault.on("delete", deleteEventHandler),
);
const createEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.loadScript(file);
handleSvgFileChange(file.path);
};
this.plugin.registerEvent(
app.vault.on("create", createEventHandler),
);
const renameEventHandler = async (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);
handleSvgFileChange(oldPath);
}
if (newFileIsScript) {
this.loadScript(file);
handleSvgFileChange(file.path);
}
};
this.plugin.registerEvent(
app.vault.on("rename", renameEventHandler),
);
}
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 (!app.vault.getAbstractFileByPath(this.scriptPath)) {
//this.scriptPath = null;
return;
}
return 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];
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 = app.vault.getAbstractFileByPath(svgFilePath);
const svgString: string =
file && file instanceof TFile
? await 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(app.workspace.getActiveViewOfType(ExcalidrawView));
}
const view = app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
(async()=>{
const script = await app.vault.read(f);
if(script) {
//remove YAML frontmatter if present
this.executeScript(view, script, scriptName,f);
}
})()
return true;
}
return false;
},
});
}
unloadScripts() {
const scripts = 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.plugin.app.commands.commands[commandId]) {
return;
}
// @ts-ignore
delete this.plugin.app.commands.commands[commandId];
}
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
if (!view || !script || !title) {
return;
}
script = script.replace(/^---.*?---\n/gs, "");
const ea = getEA(view);
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.plugin.app,
header,
placeholder,
value,
buttons,
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
),
suggester: (
displayItems: string[],
items: any[],
hint?: string,
instructions?: Instruction[],
) =>
ScriptEngine.suggester(
app,
displayItems,
items,
hint,
instructions,
),
scriptFile: file
});
/*} catch (e) {
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
errorlog({ script: this.plugin.ea.activeScript, error: e });
}*/
//ea.activeScript = null;
return result;
}
private updateToolPannels() {
const leaves =
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf: WorkspaceLeaf) => {
const excalidrawView = leaf.view as 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;
}
}
}