Lua: Reimplement `template` and `config` in Space Lua, distributed with Library/Std

main
Zef Hemel 2025-01-20 23:29:06 +01:00
parent d72983bde5
commit 517cfb209a
9 changed files with 218 additions and 42 deletions

View File

@ -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;
}

View File

@ -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<string> => {
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);
},
);
},
),
});

View File

@ -1,3 +1,4 @@
import type { FileMeta } from "@silverbulletmd/silverbullet/types";
import type { SysCallMapping, System } from "../system.ts";
export default function assetSyscalls(system: System<any>): SysCallMapping {
@ -7,5 +8,28 @@ export default function assetSyscalls(system: System<any>): 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",
};
},
};
}

View File

@ -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<FileMeta[]> {
return await syscall("asset.listFiles", plugName);
}
export async function getFileMeta(
plugName: string,
name: string,
): Promise<FileMeta> {
return await syscall("asset.getFileMeta", plugName, name);
}

View File

@ -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
```

View File

@ -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!!")
```

View File

@ -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:

72
plugs/core/std.ts Normal file
View File

@ -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<FileMeta[]> {
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<FileMeta> {
throw new Error("Writing std files not supported");
}
export function deleteFile(): Promise<void> {
throw new Error("Deleting std files not supported");
}
export function getFileMeta(name: string): Promise<FileMeta> {
return asset.getFileMeta("core", name);
}
const stdLibCacheKey = ["stdLibCache"];
type StdLibCache = Record<string, number>; // 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);
}

View File

@ -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 {