mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
335 lines
9.0 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|