import type { System } from "../lib/plugos/system.ts"; import type { ParseTree } from "../plug-api/lib/tree.ts"; import type { ScriptObject } from "../plugs/index/script.ts"; import type { AppCommand, CommandDef } from "$lib/command.ts"; import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill"; import * as syscalls from "@silverbulletmd/silverbullet/syscalls"; import { LuaEnv, LuaNativeJSFunction } from "$common/space_lua/runtime.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; import { parse as parseLua } from "$common/space_lua/parse.ts"; import { evalStatement } from "$common/space_lua/eval.ts"; import { jsToLuaValue } from "$common/space_lua/runtime.ts"; import { LuaBuiltinFunction } from "$common/space_lua/runtime.ts"; import { LuaTable } from "$common/space_lua/runtime.ts"; import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref"; // @ts-ignore: Temporal polyfill Date.prototype.toTemporalInstant = toTemporalInstant; // @ts-ignore: Temporal polyfill globalThis.Temporal = Temporal; // @ts-ignore: Intl polyfill Object.apply(globalThis.Intl, Intl); type FunctionDef = { name: string; }; type AttributeExtractorDef = { tags: string[]; }; type EventListenerDef = { name: string; }; type AttributeExtractorCallback = ( text: string, tree: ParseTree, ) => Record | null | Promise | null>; export class ScriptEnvironment { functions: Record any> = {}; commands: Record = {}; attributeExtractors: Record = {}; eventHandlers: Record any)[]> = {}; // Public API // Register function registerFunction(def: FunctionDef, fn: (...args: any[]) => any): void; // Legacy invocation registerFunction(name: string, fn: (...args: any[]) => any): void; registerFunction( arg: string | FunctionDef, fn: (...args: any[]) => any, ): void { if (typeof arg === "string") { console.warn( "registerFunction with string is deprecated, use `{name: string}` instead", ); arg = { name: arg }; } if (this.functions[arg.name]) { console.warn(`Function ${arg.name} already registered, overwriting`); } this.functions[arg.name] = fn; } registerCommand(command: CommandDef, fn: (...args: any[]) => any) { this.commands[command.name] = { command, run: (...args: any[]) => { return new Promise((resolve) => { // Next tick setTimeout(() => { resolve(fn(...args)); }); }); }, }; } registerAttributeExtractor( def: AttributeExtractorDef, callback: AttributeExtractorCallback, ) { for (const tag of def.tags) { if (!this.attributeExtractors[tag]) { this.attributeExtractors[tag] = []; } this.attributeExtractors[tag].push(callback); } } registerEventListener( def: EventListenerDef, callback: (...args: any[]) => any, ) { if (!this.eventHandlers[def.name]) { this.eventHandlers[def.name] = []; } this.eventHandlers[def.name].push(callback); } // Internal API evalScript(script: string, system: System) { try { const syscallArgs = []; const syscallValues = []; for (const [tl, value] of Object.entries(syscalls)) { syscallArgs.push(tl); syscallValues.push(value); } const fn = Function( "silverbullet", "syscall", ...syscallArgs, script, ); fn.call( {}, this, (name: string, ...args: any[]) => system.syscall({}, name, args), ...syscallValues, ); } catch (e: any) { throw new Error( `Error evaluating script: ${e.message} for script: ${script}`, ); } } async loadFromSystem(system: System) { // Install global syscall function on globalThis (globalThis as any).syscall = (name: string, ...args: any[]) => system.syscall({}, name, args); if (!system.loadedPlugs.has("index")) { console.warn("Index plug not found, skipping loading space scripts"); return; } const allScripts: ScriptObject[] = await system.invokeFunction( "index.queryObjects", ["space-script", {}], ); for (const script of allScripts) { this.evalScript(script.script, system); } return this.loadLuaFromSystem(system); } async loadLuaFromSystem(system: System) { const allScripts: ScriptObject[] = await system.invokeFunction( "index.queryObjects", ["space-lua", {}], ); const env = new LuaEnv(luaBuildStandardEnv()); // Expose all syscalls to Lua for (const [tl, value] of system.registeredSyscalls.entries()) { const [ns, fn] = tl.split("."); if (!env.get(ns)) { env.set(ns, new LuaTable()); } env.get(ns).set( fn, new LuaNativeJSFunction((...args) => { return value.callback({}, ...args); }), ); } const sbApi = new LuaTable(); sbApi.set( "register_command", new LuaBuiltinFunction( (def: LuaTable) => { if (def.get(1) === undefined) { throw new Error("Callback is required"); } this.registerCommand( def.toJSObject() as any, async (...args: any[]) => { try { return await def.get(1).call(...args.map(jsToLuaValue)); } catch (e: any) { console.error("Lua eval exception", e.message, e.context); if (e.context && e.context.ref) { const pageRef = parsePageRef(e.context.ref); await system.localSyscall("editor.flashNotification", [ `Lua error: ${e.message}`, "error", ]); await system.localSyscall("editor.flashNotification", [ `Navigating to the place in the code where this error occurred in ${pageRef.page}`, "info", ]); await system.localSyscall("editor.navigate", [ { page: pageRef.page, pos: pageRef.pos + e.context.from + "```space-lua\n".length, }, ]); } } }, ); }, ), ); sbApi.set( "register_function", new LuaBuiltinFunction((def: LuaTable) => { if (def.get(1) === undefined) { throw new Error("Callback is required"); } this.registerFunction( def.toJSObject() as any, (...args: any[]) => { return def.get(1).call(...args.map(jsToLuaValue)); }, ); }), ); env.set("silverbullet", sbApi); for (const script of allScripts) { try { const ast = parseLua(script.script, { ref: script.ref }); await evalStatement(ast, env); } catch (e: any) { console.error( `Error evaluating script: ${e.message} for script: ${script.script}`, ); } } console.log("Loaded", allScripts.length, "Lua scripts"); } }