From 517cfb209ad3e14ff5157ea45d4c2f1f2a662241 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Mon, 20 Jan 2025 23:29:06 +0100 Subject: [PATCH] Lua: Reimplement `template` and `config` in Space Lua, distributed with Library/Std --- common/space_lua/stdlib.ts | 3 +- common/space_lua/stdlib/template.ts | 38 --------------- lib/plugos/syscalls/asset.ts | 24 ++++++++++ plug-api/syscalls/asset.ts | 12 +++++ plugs/core/Library/Std/Config.md | 33 +++++++++++++ plugs/core/Library/Std/Template.md | 38 +++++++++++++++ plugs/core/core.plug.yaml | 33 +++++++++++++ plugs/core/std.ts | 72 +++++++++++++++++++++++++++++ web/reducer.ts | 7 ++- 9 files changed, 218 insertions(+), 42 deletions(-) delete mode 100644 common/space_lua/stdlib/template.ts create mode 100644 plugs/core/Library/Std/Config.md create mode 100644 plugs/core/Library/Std/Template.md create mode 100644 plugs/core/std.ts diff --git a/common/space_lua/stdlib.ts b/common/space_lua/stdlib.ts index 8df03cb1..ca3d0ee3 100644 --- a/common/space_lua/stdlib.ts +++ b/common/space_lua/stdlib.ts @@ -16,7 +16,6 @@ import { tableApi } from "$common/space_lua/stdlib/table.ts"; import { osApi } from "$common/space_lua/stdlib/os.ts"; import { jsApi } from "$common/space_lua/stdlib/js.ts"; import { spaceLuaApi } from "$common/space_lua/stdlib/space_lua.ts"; -import { templateApi } from "$common/space_lua/stdlib/template.ts"; import { mathApi } from "$common/space_lua/stdlib/math.ts"; import { parse } from "$common/space_lua/parse.ts"; import { evalStatement } from "$common/space_lua/eval.ts"; @@ -192,6 +191,6 @@ export function luaBuildStandardEnv() { // Non-standard env.set("each", eachFunction); env.set("space_lua", spaceLuaApi); - env.set("template", templateApi); + // env.set("template", templateApi); return env; } diff --git a/common/space_lua/stdlib/template.ts b/common/space_lua/stdlib/template.ts deleted file mode 100644 index f655b70b..00000000 --- a/common/space_lua/stdlib/template.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - type ILuaFunction, - jsToLuaValue, - LuaBuiltinFunction, - LuaTable, -} from "$common/space_lua/runtime.ts"; -import { interpolateLuaString } from "$common/space_lua/stdlib/space_lua.ts"; - -export const templateApi = new LuaTable({ - each: new LuaBuiltinFunction( - async (sf, tbl: LuaTable | any[], fn: ILuaFunction): Promise => { - const result = []; - if (tbl instanceof LuaTable) { - tbl = tbl.toJSArray(); - } - for (const item of tbl) { - result.push(await fn.call(sf, item)); - } - return result.join(""); - }, - ), - new: new LuaBuiltinFunction( - (_sf, template: string): ILuaFunction => { - const lines = template.split("\n").map((line) => - line.replace(/^\s{4}/, "") - ); - const processed = lines.join("\n"); - return new LuaBuiltinFunction( - async (sf, env: LuaTable | any) => { - if (!(env instanceof LuaTable)) { - env = jsToLuaValue(env); - } - return await interpolateLuaString(sf, processed, env); - }, - ); - }, - ), -}); diff --git a/lib/plugos/syscalls/asset.ts b/lib/plugos/syscalls/asset.ts index 8e4e5f0f..cfbe14f8 100644 --- a/lib/plugos/syscalls/asset.ts +++ b/lib/plugos/syscalls/asset.ts @@ -1,3 +1,4 @@ +import type { FileMeta } from "@silverbulletmd/silverbullet/types"; import type { SysCallMapping, System } from "../system.ts"; export default function assetSyscalls(system: System): SysCallMapping { @@ -7,5 +8,28 @@ export default function assetSyscalls(system: System): SysCallMapping { name, ); }, + "asset.listFiles": (_ctx, plugName: string): FileMeta[] => { + const assets = system.loadedPlugs.get(plugName)!.assets!; + const fileNames = assets.listFiles(); + return fileNames.map((name) => ({ + name, + contentType: assets.getMimeType(name), + created: assets.getMtime(name), + lastModified: assets.getMtime(name), + size: -1, + perm: "ro", + })); + }, + "asset.getFileMeta": (_ctx, plugName: string, name: string): FileMeta => { + const assets = system.loadedPlugs.get(plugName)!.assets!; + return { + name, + contentType: assets.getMimeType(name), + created: assets.getMtime(name), + lastModified: assets.getMtime(name), + size: -1, + perm: "ro", + }; + }, }; } diff --git a/plug-api/syscalls/asset.ts b/plug-api/syscalls/asset.ts index 39de4daf..37136f55 100644 --- a/plug-api/syscalls/asset.ts +++ b/plug-api/syscalls/asset.ts @@ -1,5 +1,6 @@ import { base64DecodeDataUrl } from "../../lib/crypto.ts"; import { syscall } from "../syscall.ts"; +import type { FileMeta } from "@silverbulletmd/silverbullet/types"; /** * Reads an asset embedded in a plug (via the `assets` field in the plug manifest). @@ -21,3 +22,14 @@ export async function readAsset( return dataUrl; } } + +export async function listFiles(plugName: string): Promise { + return await syscall("asset.listFiles", plugName); +} + +export async function getFileMeta( + plugName: string, + name: string, +): Promise { + return await syscall("asset.getFileMeta", plugName, name); +} diff --git a/plugs/core/Library/Std/Config.md b/plugs/core/Library/Std/Config.md new file mode 100644 index 00000000..a9aa284d --- /dev/null +++ b/plugs/core/Library/Std/Config.md @@ -0,0 +1,33 @@ +#meta + +Config library for defining and getting config values + +```space-lua +-- priority: 100 +config = {} + +local config_values = {} +local config_schema = {} + +function config.define(key, schema) + config_schema[key] = schema or true +end + +function config.set(key, value) + local schema = config_schema[key] + if schema == nil then + error("Config key not defined: " .. key) + end + if schema != true then + result = jsonschema.validate_object(schema, value) + if result != nil then + error("Validation error (" .. key .. "): " .. result) + end + end + config_values[key] = value +end + +function config.get(key) + return config_values[key] +end +``` \ No newline at end of file diff --git a/plugs/core/Library/Std/Template.md b/plugs/core/Library/Std/Template.md new file mode 100644 index 00000000..07805dcf --- /dev/null +++ b/plugs/core/Library/Std/Template.md @@ -0,0 +1,38 @@ +#meta + +Implements useful template functions + +```space-lua +-- priority: 100 +-- Template library for working with templates and iterables +template = {} + +-- Iterates over a table/array and applies a function to each element, +-- concatenating the results +function template.each(tbl, fn) + local result = {} + for _, item in ipairs(tbl) do + table.insert(result, fn(item)) + end + return table.concat(result) +end + +-- Creates a new template function from a string template +function template.new(template_str) + -- Preprocess: strip indentation + local lines = {} + local split_lines = string.split(template_str, "\n") + for _, line in ipairs(split_lines) do + line = string.gsub(line, "^ ", "") + table.insert(lines, line) + end + template_str = table.concat(lines, "\n") + return function(obj) + return space_lua.interpolate(template_str, obj) + end +end + +print("template loaded!!") + + +``` diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index 89a83d10..b30a4fa2 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -1,4 +1,37 @@ name: core +assets: + - "Library/Std/*" +functions: + init: + path: ./std.ts:init + env: server + events: + - system:ready + readFile: + path: ./std.ts:readFile + pageNamespace: + pattern: "^Library/Std/.+" + operation: readFile + writeFile: + path: ./std.ts:writeFile + pageNamespace: + pattern: "^Library/Std/.+" + operation: writeFile + deleteFile: + path: ./std.ts:deleteFile + pageNamespace: + pattern: "^Library/Std/.+" + operation: deleteFile + getFileMeta: + path: ./std.ts:getFileMeta + pageNamespace: + pattern: "^Library/Std/.+" + operation: getFileMeta + listFiles: + path: ./std.ts:listFiles + pageNamespace: + pattern: "^Library/Std/.+" + operation: listFiles config: # Built-in schemas schema.tag: diff --git a/plugs/core/std.ts b/plugs/core/std.ts new file mode 100644 index 00000000..22235495 --- /dev/null +++ b/plugs/core/std.ts @@ -0,0 +1,72 @@ +import { + asset, + datastore, + mq, + system, +} from "@silverbulletmd/silverbullet/syscalls"; +import type { FileMeta } from "@silverbulletmd/silverbullet/types"; + +export async function listFiles(): Promise { + return await asset.listFiles("core"); +} + +export async function readFile( + name: string, +): Promise<{ data: Uint8Array; meta: FileMeta }> { + const text = await asset.readAsset("core", name, "utf8"); + return { + data: new TextEncoder().encode(text), + meta: await asset.getFileMeta("core", name), + }; +} + +export function writeFile(): Promise { + throw new Error("Writing std files not supported"); +} + +export function deleteFile(): Promise { + throw new Error("Deleting std files not supported"); +} + +export function getFileMeta(name: string): Promise { + return asset.getFileMeta("core", name); +} + +const stdLibCacheKey = ["stdLibCache"]; + +type StdLibCache = Record; // page name -> last modified time + +export async function init() { + let stdLibCache: StdLibCache | undefined = await datastore.get( + stdLibCacheKey, + ); + if (!stdLibCache) { + stdLibCache = {}; + } + // Iterate over the current file listing, check if any new files have been added, removed or modified + const newListing = await listFiles(); + // First check for files that were removed + for (const cachedFile of Object.keys(stdLibCache)) { + if (!newListing.find((f) => f.name === cachedFile)) { + console.log(`Clearing index for removed file ${cachedFile}`); + await system.invokeFunction("index.clearDSIndex", cachedFile); + delete stdLibCache[cachedFile]; + } + } + + // Then check for new/modified files + for (const file of newListing) { + const lastModified = file.lastModified; + + // Check if file is new or modified compared to cache + if (!stdLibCache[file.name] || stdLibCache[file.name] !== lastModified) { + // console.log(`Queuing for indexing ${file.name}`); + await system.invokeFunction("index.clearDSIndex", file.name); + await mq.send("indexQueue", file.name); + stdLibCache[file.name] = lastModified; + } + } + + // Save updated cache + await datastore.set(stdLibCacheKey, stdLibCache); +} diff --git a/web/reducer.ts b/web/reducer.ts index 9060b458..f682398f 100644 --- a/web/reducer.ts +++ b/web/reducer.ts @@ -80,11 +80,14 @@ export default function reducer( currPageMeta = pageMeta; } } - return { + const newState = { ...state, allPages: action.allPages, - currentPageMeta: currPageMeta, }; + if (currPageMeta) { + newState.currentPageMeta = currPageMeta; + } + return newState; } case "start-navigate": { return {