From 5604f6d8c220f36eb5110c08d69f91d6914e678b Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Tue, 14 Jan 2025 20:26:47 +0100 Subject: [PATCH] Lua: tweaks and docs --- common/space_lua.ts | 50 +++---- common/space_lua/eval.ts | 10 +- common/space_lua/language.test.ts | 9 +- common/space_lua/language_test.lua | 16 +++ common/space_lua/runtime.ts | 31 ++--- common/space_lua/stdlib.ts | 46 ++++++- common/space_lua/stdlib/space_lua.ts | 122 ++++++++++-------- common/space_lua/stdlib/string.ts | 8 ++ common/space_lua/stdlib/template.ts | 20 +++ common/space_lua_api.ts | 5 + common/template/render.ts | 2 +- website/API.md | 19 +-- .../{Space Lua/stdlib.md => API/global.md} | 15 ++- website/API/space_lua.md | 21 +++ website/API/template.md | 10 ++ website/Federation.md | 4 +- website/HTTP API.md | 16 +++ website/Install/Configuration.md | 2 +- website/Library/Lua Core/Templates.md | 8 ++ website/Space Lua/Lua Integrated Query.md | 2 +- 20 files changed, 284 insertions(+), 132 deletions(-) create mode 100644 common/space_lua/stdlib/template.ts rename website/{Space Lua/stdlib.md => API/global.md} (60%) create mode 100644 website/API/space_lua.md create mode 100644 website/API/template.md create mode 100644 website/HTTP API.md create mode 100644 website/Library/Lua Core/Templates.md diff --git a/common/space_lua.ts b/common/space_lua.ts index d4598f2e..7be72101 100644 --- a/common/space_lua.ts +++ b/common/space_lua.ts @@ -30,33 +30,37 @@ export class SpaceLuaEnvironment { "index.queryObjects", ["space-lua", {}], ); - this.env = buildLuaEnv(system, scriptEnv); - const tl = new LuaEnv(); - for (const script of allScripts) { - try { - console.log("Now evaluating", script.ref); - const ast = parseLua(script.script, { ref: script.ref }); - // We create a local scope for each script - const scriptEnv = new LuaEnv(this.env); - const sf = new LuaStackFrame(tl, ast.ctx); - await evalStatement(ast, scriptEnv, sf); - } catch (e: any) { - if (e instanceof LuaRuntimeError) { - const origin = resolveASTReference(e.sf.astCtx!); - if (origin) { - console.error( - `Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`, - ); - continue; + try { + this.env = buildLuaEnv(system, scriptEnv); + const tl = new LuaEnv(); + for (const script of allScripts) { + try { + console.log("Now evaluating", script.ref); + const ast = parseLua(script.script, { ref: script.ref }); + // We create a local scope for each script + const scriptEnv = new LuaEnv(this.env); + const sf = new LuaStackFrame(tl, ast.ctx); + await evalStatement(ast, scriptEnv, sf); + } catch (e: any) { + if (e instanceof LuaRuntimeError) { + const origin = resolveASTReference(e.sf.astCtx!); + if (origin) { + console.error( + `Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`, + ); + continue; + } } + console.error( + `Error evaluating script: ${e.message} for script: ${script.script}`, + ); } - console.error( - `Error evaluating script: ${e.message} for script: ${script.script}`, - ); } - } - console.log("[Lua] Loaded", allScripts.length, "scripts"); + console.log("[Lua] Loaded", allScripts.length, "scripts"); + } catch (e: any) { + console.error("Error reloading Lua scripts:", e.message); + } } } diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index 1dea3d5c..6790aa18 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -729,9 +729,11 @@ export async function evalStatement( s.expressions.map((e) => evalExpression(e, env, sf)), ), ).flatten(); - const iteratorFunction: ILuaFunction | undefined = - iteratorMultiRes.values[0]; - if (!iteratorFunction?.call) { + let iteratorValue: ILuaFunction | any = iteratorMultiRes.values[0]; + if (Array.isArray(iteratorValue) || iteratorValue instanceof LuaTable) { + iteratorValue = env.get("each").call(sf, iteratorValue); + } + if (!iteratorValue?.call) { console.error("Cannot iterate over", iteratorMultiRes.values[0]); throw new LuaRuntimeError( `Cannot iterate over ${iteratorMultiRes.values[0]}`, @@ -744,7 +746,7 @@ export async function evalStatement( while (true) { const iterResult = new LuaMultiRes( - await luaCall(iteratorFunction, [state, control], s.ctx, sf), + await luaCall(iteratorValue, [state, control], s.ctx, sf), ).flatten(); if ( iterResult.values[0] === null || iterResult.values[0] === undefined diff --git a/common/space_lua/language.test.ts b/common/space_lua/language.test.ts index 655e3f18..c9e88d4f 100644 --- a/common/space_lua/language.test.ts +++ b/common/space_lua/language.test.ts @@ -10,9 +10,12 @@ import { assert } from "@std/assert/assert"; import { fileURLToPath } from "node:url"; Deno.test("Lua language tests", async () => { - // Read the Lua file + await runLuaTest("./language_test.lua"); +}); + +async function runLuaTest(luaPath: string) { const luaFile = await Deno.readTextFile( - fileURLToPath(new URL("./language_test.lua", import.meta.url)), + fileURLToPath(new URL(luaPath, import.meta.url)), ); const chunk = parse(luaFile, {}); const env = new LuaEnv(luaBuildStandardEnv()); @@ -29,4 +32,4 @@ Deno.test("Lua language tests", async () => { } assert(false); } -}); +} diff --git a/common/space_lua/language_test.lua b/common/space_lua/language_test.lua index 58c27cb8..40ca9608 100644 --- a/common/space_lua/language_test.lua +++ b/common/space_lua/language_test.lua @@ -269,6 +269,22 @@ for key, value in pairs({ a = "a", b = "b" }) do assert_equal(key, value) end +-- for in over tables directly +local cnt = 1 +for val in { 1, 2, 3 } do + assert_equal(val, cnt) + cnt = cnt + 1 +end +assert_equal(cnt, 4) + +local cnt = 1 +for val in js.tojs({ 1, 2, 3 }) do + assert_equal(val, cnt) + cnt = cnt + 1 +end +assert_equal(cnt, 4) + + -- type assert(type(1) == "number") assert(type("Hello") == "string") diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 4a501fad..e61b8a6a 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -1,6 +1,6 @@ import type { ASTCtx, LuaFunctionBody } from "./ast.ts"; -import { evalStatement } from "$common/space_lua/eval.ts"; -import { asyncQuickSort, evalPromiseValues } from "$common/space_lua/util.ts"; +import { evalStatement } from "./eval.ts"; +import { asyncQuickSort, evalPromiseValues } from "./util.ts"; export type LuaType = | "nil" @@ -425,7 +425,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable { } } - asJSObject(): Record { + toJSObject(): Record { const result: Record = {}; for (const key of this.keys()) { result[key] = luaValueToJS(this.get(key)); @@ -433,15 +433,15 @@ export class LuaTable implements ILuaSettable, ILuaGettable { return result; } - asJSArray(): any[] { + toJSArray(): any[] { return this.arrayPart.map(luaValueToJS); } - asJS(): Record | any[] { + toJS(): Record | any[] { if (this.length > 0) { - return this.asJSArray(); + return this.toJSArray(); } else { - return this.asJSObject(); + return this.toJSObject(); } } @@ -722,22 +722,7 @@ export function luaValueToJS(value: any): any { return value.then(luaValueToJS); } if (value instanceof LuaTable) { - // We'll go a bit on heuristics here - // If the table has a length > 0 we'll assume it's a pure array - // Otherwise we'll assume it's a pure object - if (value.length > 0) { - const result = []; - for (let i = 0; i < value.length; i++) { - result.push(luaValueToJS(value.get(i + 1))); - } - return result; - } else { - const result: Record = {}; - for (const key of value.keys()) { - result[key] = luaValueToJS(value.get(key)); - } - return result; - } + return value.toJS(); } else if (value instanceof LuaNativeJSFunction) { return (...args: any[]) => { return jsToLuaValue(value.fn(...args.map(luaValueToJS))); diff --git a/common/space_lua/stdlib.ts b/common/space_lua/stdlib.ts index 96f27df0..54727990 100644 --- a/common/space_lua/stdlib.ts +++ b/common/space_lua/stdlib.ts @@ -1,12 +1,13 @@ import { type ILuaFunction, + jsToLuaValue, LuaBuiltinFunction, luaCall, LuaEnv, luaGet, LuaMultiRes, LuaRuntimeError, - type LuaTable, + LuaTable, luaToString, luaTypeOf, type LuaValue, @@ -15,11 +16,15 @@ import { stringApi } from "$common/space_lua/stdlib/string.ts"; 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 { + interpolateLuaString, + spaceLuaApi, +} from "$common/space_lua/stdlib/space_lua.ts"; import type { LuaCollectionQuery, LuaQueryCollection, } from "$common/space_lua/query_collection.ts"; +import { templateApi } from "$common/space_lua/stdlib/template.ts"; const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { console.log("[Lua]", ...(await Promise.all(args))); @@ -58,6 +63,18 @@ const pairsFunction = new LuaBuiltinFunction((sf, t: LuaTable) => { }; }); +export const eachFunction = new LuaBuiltinFunction((sf, ar: LuaTable) => { + let i = 1; + return async () => { + if (i > ar.length) { + return; + } + const result = await luaGet(ar, i, sf); + i++; + return result; + }; +}); + const unpackFunction = new LuaBuiltinFunction(async (sf, t: LuaTable) => { const values: LuaValue[] = []; for (let i = 1; i <= t.length; i++) { @@ -127,6 +144,7 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => { return table.metatable; }); +// Non-standard const tagFunction = new LuaBuiltinFunction( (sf, tagName: LuaValue): LuaQueryCollection => { const global = sf.threadLocal.get("_GLOBAL"); @@ -142,12 +160,29 @@ const tagFunction = new LuaBuiltinFunction( tagName, ], query, - )).asJSArray(); + )).toJSArray(); }, }; }, ); +const tplFunction = 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); + }, + ); + }, +); + export function luaBuildStandardEnv() { const env = new LuaEnv(); // Top-level builtins @@ -168,12 +203,17 @@ export function luaBuildStandardEnv() { env.set("error", errorFunction); env.set("pcall", pcallFunction); env.set("xpcall", xpcallFunction); + // Non-standard env.set("tag", tagFunction); + env.set("tpl", tplFunction); // APIs env.set("string", stringApi); env.set("table", tableApi); env.set("os", osApi); env.set("js", jsApi); + // Non-standard + env.set("each", eachFunction); env.set("space_lua", spaceLuaApi); + env.set("template", templateApi); return env; } diff --git a/common/space_lua/stdlib/space_lua.ts b/common/space_lua/stdlib/space_lua.ts index d9ec5a68..422707b1 100644 --- a/common/space_lua/stdlib/space_lua.ts +++ b/common/space_lua/stdlib/space_lua.ts @@ -28,6 +28,7 @@ function createAugmentedEnv( } const env = new LuaEnv(globalEnv); if (envAugmentation) { + env.setLocal("_", envAugmentation); for (const key of envAugmentation.keys()) { env.setLocal(key, envAugmentation.rawGet(key)); } @@ -35,6 +36,70 @@ function createAugmentedEnv( return env; } +/** + * Interpolates a string with lua expressions and returns the result. + * + * @param sf - The current space_lua state. + * @param template - The template string to interpolate. + * @param envAugmentation - An optional environment to augment the global environment with. + * @returns The interpolated string. + */ +export async function interpolateLuaString( + sf: LuaStackFrame, + template: string, + envAugmentation?: LuaTable, +): Promise { + let result = ""; + let currentIndex = 0; + + while (true) { + const startIndex = template.indexOf("${", currentIndex); + if (startIndex === -1) { + result += template.slice(currentIndex); + break; + } + + result += template.slice(currentIndex, startIndex); + + // Find matching closing brace by counting nesting + let nestLevel = 1; + let endIndex = startIndex + 2; + while (nestLevel > 0 && endIndex < template.length) { + if (template[endIndex] === "{") { + nestLevel++; + } else if (template[endIndex] === "}") { + nestLevel--; + } + if (nestLevel > 0) { + endIndex++; + } + } + + if (nestLevel > 0) { + throw new LuaRuntimeError("Unclosed interpolation expression", sf); + } + + const expr = template.slice(startIndex + 2, endIndex); + try { + const parsedExpr = parseExpressionString(expr); + const env = createAugmentedEnv(sf, envAugmentation); + const luaResult = luaValueToJS( + await evalExpression(parsedExpr, env, sf), + ); + result += luaToString(luaResult); + } catch (e: any) { + throw new LuaRuntimeError( + `Error evaluating "${expr}": ${e.message}`, + sf, + ); + } + + currentIndex = endIndex + 1; + } + + return result; +} + export const spaceLuaApi = new LuaTable({ /** * Parses a lua expression and returns the parsed expression. @@ -64,63 +129,10 @@ export const spaceLuaApi = new LuaTable({ ), /** * Interpolates a string with lua expressions and returns the result. - * - * @param sf - The current space_lua state. - * @param template - The template string to interpolate. - * @param envAugmentation - An optional environment to augment the global environment with. - * @returns The interpolated string. */ interpolate: new LuaBuiltinFunction( - async (sf, template: string, envAugmentation?: LuaTable) => { - let result = ""; - let currentIndex = 0; - - while (true) { - const startIndex = template.indexOf("${", currentIndex); - if (startIndex === -1) { - result += template.slice(currentIndex); - break; - } - - result += template.slice(currentIndex, startIndex); - - // Find matching closing brace by counting nesting - let nestLevel = 1; - let endIndex = startIndex + 2; - while (nestLevel > 0 && endIndex < template.length) { - if (template[endIndex] === "{") { - nestLevel++; - } else if (template[endIndex] === "}") { - nestLevel--; - } - if (nestLevel > 0) { - endIndex++; - } - } - - if (nestLevel > 0) { - throw new LuaRuntimeError("Unclosed interpolation expression", sf); - } - - const expr = template.slice(startIndex + 2, endIndex); - try { - const parsedExpr = parseExpressionString(expr); - const env = createAugmentedEnv(sf, envAugmentation); - const luaResult = luaValueToJS( - await evalExpression(parsedExpr, env, sf), - ); - result += luaToString(luaResult); - } catch (e: any) { - throw new LuaRuntimeError( - `Error evaluating "${expr}": ${e.message}`, - sf, - ); - } - - currentIndex = endIndex + 1; - } - - return result; + (sf, template: string, envAugmentation?: LuaTable) => { + return interpolateLuaString(sf, template, envAugmentation); }, ), }); diff --git a/common/space_lua/stdlib/string.ts b/common/space_lua/stdlib/string.ts index d811eae0..4683e5a0 100644 --- a/common/space_lua/stdlib/string.ts +++ b/common/space_lua/stdlib/string.ts @@ -157,4 +157,12 @@ export const stringApi = new LuaTable({ split: new LuaBuiltinFunction((_sf, s: string, sep: string) => { return s.split(sep); }), + + // Non-standard + startswith: new LuaBuiltinFunction((_sf, s: string, prefix: string) => { + return s.startsWith(prefix); + }), + endswith: new LuaBuiltinFunction((_sf, s: string, suffix: string) => { + return s.endsWith(suffix); + }), }); diff --git a/common/space_lua/stdlib/template.ts b/common/space_lua/stdlib/template.ts new file mode 100644 index 00000000..c6e2cce7 --- /dev/null +++ b/common/space_lua/stdlib/template.ts @@ -0,0 +1,20 @@ +import { + type ILuaFunction, + LuaBuiltinFunction, + LuaTable, +} from "$common/space_lua/runtime.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(""); + }, + ), +}); diff --git a/common/space_lua_api.ts b/common/space_lua_api.ts index b46a50da..5933d708 100644 --- a/common/space_lua_api.ts +++ b/common/space_lua_api.ts @@ -25,8 +25,13 @@ export function buildLuaEnv(system: System, scriptEnv: ScriptEnvironment) { function exposeSyscalls(env: LuaEnv, system: System) { // Expose all syscalls to Lua + // Except... + const exclude = ["template"]; const nativeFs = new LuaStackFrame(env, null); for (const syscallName of system.registeredSyscalls.keys()) { + if (exclude.includes(syscallName)) { + continue; + } const [ns, fn] = syscallName.split("."); if (!env.has(ns)) { env.set(ns, new LuaTable(), nativeFs); diff --git a/common/template/render.ts b/common/template/render.ts index a76d98ac..d0bc9328 100644 --- a/common/template/render.ts +++ b/common/template/render.ts @@ -85,7 +85,7 @@ async function renderExpressionDirective( export function renderExpressionResult(result: any): string { if (result instanceof LuaTable) { - result = result.asJS(); + result = result.toJS(); } if ( Array.isArray(result) && result.length > 0 && typeof result[0] === "object" diff --git a/website/API.md b/website/API.md index 22d2cd96..636dffc4 100644 --- a/website/API.md +++ b/website/API.md @@ -1,16 +1,5 @@ -The server API is relatively small. The client primarily communicates with the server for file “CRUD” (Create, Read, Update, Delete) style operations. +This describes the APIs available in [[Space Lua]] -All API requests from the client will always set the `X-Sync-Mode` request header set to `true`. The server may use this fact to distinguish between requests coming from the client and regular e.g. `GET` requests from the browser (through navigation) and redirect appropriately (for instance to the UI URL associated with a specific `.md` file). - -The API: - -* `GET /index.json` will return a full listing of all files in your space including metadata like when the file was last modified, as well as permissions. This is primarily used for sync purposes with the client. -* `GET /*.*`: _Reads_ and returns the content of the file at the given path. This means that if you `GET /index.md` you will receive the content of your `index` page. If the optional `X-Get-Meta` _request header_ is set, the server does not _need to_ return the body of the file (but it can). The `GET` _response_ will have a few additional SB-specific headers: - * (optional) `X-Last-Modified` the last modified time of the file as a UNIX timestamp in ms since the epoch (as coming from `Data.now()`). This timestamp _has_ to match the `lastModified` listed for this file in `/index.json` otherwise syncing issues may occur. When this header is missing, frequent polling-based sync will be disabled for this file. - * (optional) `X-Created` the created time of the file as a UNIX timestamp in ms since the epoch (as coming from `Data.now()`). - * (optional) `X-Permission`: either `rw` or `ro` which will change whether the editor opens in read-only or edit mode. When missing, `ro` is assumed. - * (optional) `X-Content-Length`: which will be the same as `Content-Length` except if the request was sent with a `X-Get-Meta` header and the body is not returned (then `Content-Length` will be `0` and `X-Content-Length` will be the size of the file) -* `PUT /*.*`: The same as `GET` except that it takes the body of the request and _writes_ it to a file. -* `DELETE /*.*`: Again the same, except this will _delete_ the given file. -* `GET /.client/*`: Retrieve files implementing the client -* `GET /*` and `GET /`: Anything else (any path without a file extension) will serve the SilverBullet UI HTML. +${template.each(query[[ + from tag("page") where string.startswith(name, "API/") +]], render.page)} \ No newline at end of file diff --git a/website/Space Lua/stdlib.md b/website/API/global.md similarity index 60% rename from website/Space Lua/stdlib.md rename to website/API/global.md index a13725a3..975498f4 100644 --- a/website/Space Lua/stdlib.md +++ b/website/API/global.md @@ -30,4 +30,17 @@ Returns a given [[Objects#Tags]] as a query collection, to be queried using [[Sp Example: -${query[[from tag("page") limit 1]]} \ No newline at end of file +${query[[from tag("page") limit 1]]} + +## tpl(template) +Returns a template function that can be used to render a template. Conventionally, a template string is put between `[==[` and `]==]` as string delimiters. + +Example: + +```space-lua +examples = examples or {} + +examples.say_hello = tpl[==[Hello ${name}!]==] +``` + +And its use: ${examples.say_hello {name="Pete"}} diff --git a/website/API/space_lua.md b/website/API/space_lua.md new file mode 100644 index 00000000..d6878636 --- /dev/null +++ b/website/API/space_lua.md @@ -0,0 +1,21 @@ +Space Lua specific functions that are available to all scripts, but are not part of the standard Lua language. + +## space_lua.parse_expression(luaExpression) +Parses a lua expression and returns the parsed expression as an AST. + +Example: + + space_lua.parse_expression("1 + 1") + + +## space_lua.eval_expression(parsedExpr, envAugmentation?) +Evaluates a parsed Lua expression and returns the result. Optionally accepts an environment table to augment the global environment. + +Example: + +${space_lua.eval_expression(space_lua.parse_expression("x + y"), {x = 1, y = 2})} + +## space_lua.interpolate(template, envAugmentation?) +Interpolates a string with lua expressions and returns the result. Expressions are wrapped in ${...} syntax. Optionally accepts an environment table to augment the global environment. + +${space_lua.interpolate("Hello ${name}!", {name="Pete"})} \ No newline at end of file diff --git a/website/API/template.md b/website/API/template.md new file mode 100644 index 00000000..84d4a668 --- /dev/null +++ b/website/API/template.md @@ -0,0 +1,10 @@ +Template functions that use the [[API/global#tpl(template)]] function. + +## template.each(collection, template) +Iterates over a collection and renders a template for each item. + +Example: + +${template.each(query[[from tag "page" limit 3]], tpl[==[ + * ${name} +]==])} \ No newline at end of file diff --git a/website/Federation.md b/website/Federation.md index ff5eab2b..549aab13 100644 --- a/website/Federation.md +++ b/website/Federation.md @@ -2,7 +2,7 @@ Federation enables _browsing_ content from spaces _outside_ the user’s space, This enables a few things: -* **Browsing** other publicly hosted SilverBullet spaces (or websites adhering to its [[API]]) within the comfort of your own SilverBullet client. One use case of this is [[Transclusions|transcluding]] the [[Getting Started]] page in the user’s automatically generated index page when setting up a fresh space. +* **Browsing** other publicly hosted SilverBullet spaces (or websites adhering to its [[HTTP API]]) within the comfort of your own SilverBullet client. One use case of this is [[Transclusions|transcluding]] the [[Getting Started]] page in the user’s automatically generated index page when setting up a fresh space. * **Referencing** other spaces for other purposes, which is leveraged in [[Libraries]]. # How it works @@ -17,7 +17,7 @@ For example: `https://raw.githubusercontent.com/silverbulletmd/silverbullet/main Can be written to federation syntax as follows: `!raw.githubusercontent.com/silverbulletmd/silverbullet/main/README` And used as a link: [[!raw.githubusercontent.com/silverbulletmd/silverbullet/main/README]] -If the target server supports the SilverBullet [[API]] (specifically its `/index.json` endpoint), page completion will be provided as well. +If the target server supports the SilverBullet [[HTTP API]] (specifically its `/index.json` endpoint), page completion will be provided as well. Upon fetching of the page content, a best effort attempt will be made to rewrite any local page links in the page to the appropriate federated paths. diff --git a/website/HTTP API.md b/website/HTTP API.md new file mode 100644 index 00000000..22d2cd96 --- /dev/null +++ b/website/HTTP API.md @@ -0,0 +1,16 @@ +The server API is relatively small. The client primarily communicates with the server for file “CRUD” (Create, Read, Update, Delete) style operations. + +All API requests from the client will always set the `X-Sync-Mode` request header set to `true`. The server may use this fact to distinguish between requests coming from the client and regular e.g. `GET` requests from the browser (through navigation) and redirect appropriately (for instance to the UI URL associated with a specific `.md` file). + +The API: + +* `GET /index.json` will return a full listing of all files in your space including metadata like when the file was last modified, as well as permissions. This is primarily used for sync purposes with the client. +* `GET /*.*`: _Reads_ and returns the content of the file at the given path. This means that if you `GET /index.md` you will receive the content of your `index` page. If the optional `X-Get-Meta` _request header_ is set, the server does not _need to_ return the body of the file (but it can). The `GET` _response_ will have a few additional SB-specific headers: + * (optional) `X-Last-Modified` the last modified time of the file as a UNIX timestamp in ms since the epoch (as coming from `Data.now()`). This timestamp _has_ to match the `lastModified` listed for this file in `/index.json` otherwise syncing issues may occur. When this header is missing, frequent polling-based sync will be disabled for this file. + * (optional) `X-Created` the created time of the file as a UNIX timestamp in ms since the epoch (as coming from `Data.now()`). + * (optional) `X-Permission`: either `rw` or `ro` which will change whether the editor opens in read-only or edit mode. When missing, `ro` is assumed. + * (optional) `X-Content-Length`: which will be the same as `Content-Length` except if the request was sent with a `X-Get-Meta` header and the body is not returned (then `Content-Length` will be `0` and `X-Content-Length` will be the size of the file) +* `PUT /*.*`: The same as `GET` except that it takes the body of the request and _writes_ it to a file. +* `DELETE /*.*`: Again the same, except this will _delete_ the given file. +* `GET /.client/*`: Retrieve files implementing the client +* `GET /*` and `GET /`: Anything else (any path without a file extension) will serve the SilverBullet UI HTML. diff --git a/website/Install/Configuration.md b/website/Install/Configuration.md index a3871dd3..b791fe04 100644 --- a/website/Install/Configuration.md +++ b/website/Install/Configuration.md @@ -10,7 +10,7 @@ Note: these options are primarily useful for [[Install/Deno]] deployments, not s SilverBullet supports basic authentication for a single user. * `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”. -* `SB_AUTH_TOKEN`: Enables `Authorization: Bearer ` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends). +* `SB_AUTH_TOKEN`: Enables `Authorization: Bearer ` style authentication on the [[HTTP API]] (useful for [[Sync]] and remote HTTP storage backends). * `SB_LOCKOUT_LIMIT`: Specifies the number of failed login attempt before locking the user out (for a `SB_LOCKOUT_TIME` specified amount of seconds), defaults to `10` * `SB_LOCKOUT_TIME`: Specifies the amount of time (in seconds) a client will be blocked until attempting to log back in. diff --git a/website/Library/Lua Core/Templates.md b/website/Library/Lua Core/Templates.md new file mode 100644 index 00000000..634e5325 --- /dev/null +++ b/website/Library/Lua Core/Templates.md @@ -0,0 +1,8 @@ +Defines some core useful templates for use in [[Space Lua]] +```space-lua +render = render or {} + +render.page = tpl[==[ +* [[${name}]] +]==] +``` diff --git a/website/Space Lua/Lua Integrated Query.md b/website/Space Lua/Lua Integrated Query.md index 8ff16298..932b586b 100644 --- a/website/Space Lua/Lua Integrated Query.md +++ b/website/Space Lua/Lua Integrated Query.md @@ -19,7 +19,7 @@ Unlike [[Query Language]] which operates on [[Objects]] only, LIQ can operate on For instance, to sort a list of numbers in descending order: ${query[[from n = {1, 2, 3} order by n desc]]} -However, in most cases you’ll use it in conjunction with [[Space Lua/stdlib#tag(name)]]. Here’s an example querying the 3 pages that were last modified: +However, in most cases you’ll use it in conjunction with [[../API/global#tag(name)]]. Here’s an example querying the 3 pages that were last modified: ${query[[ from p = tag "page"