Lua: tweaks and docs

pull/1212/head
Zef Hemel 2025-01-14 20:26:47 +01:00
parent cbf227fa49
commit 5604f6d8c2
20 changed files with 284 additions and 132 deletions

View File

@ -30,33 +30,37 @@ export class SpaceLuaEnvironment {
"index.queryObjects", "index.queryObjects",
["space-lua", {}], ["space-lua", {}],
); );
this.env = buildLuaEnv(system, scriptEnv); try {
const tl = new LuaEnv(); this.env = buildLuaEnv(system, scriptEnv);
for (const script of allScripts) { const tl = new LuaEnv();
try { for (const script of allScripts) {
console.log("Now evaluating", script.ref); try {
const ast = parseLua(script.script, { ref: script.ref }); console.log("Now evaluating", script.ref);
// We create a local scope for each script const ast = parseLua(script.script, { ref: script.ref });
const scriptEnv = new LuaEnv(this.env); // We create a local scope for each script
const sf = new LuaStackFrame(tl, ast.ctx); const scriptEnv = new LuaEnv(this.env);
await evalStatement(ast, scriptEnv, sf); const sf = new LuaStackFrame(tl, ast.ctx);
} catch (e: any) { await evalStatement(ast, scriptEnv, sf);
if (e instanceof LuaRuntimeError) { } catch (e: any) {
const origin = resolveASTReference(e.sf.astCtx!); if (e instanceof LuaRuntimeError) {
if (origin) { const origin = resolveASTReference(e.sf.astCtx!);
console.error( if (origin) {
`Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`, console.error(
); `Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`,
continue; );
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);
}
} }
} }

View File

@ -729,9 +729,11 @@ export async function evalStatement(
s.expressions.map((e) => evalExpression(e, env, sf)), s.expressions.map((e) => evalExpression(e, env, sf)),
), ),
).flatten(); ).flatten();
const iteratorFunction: ILuaFunction | undefined = let iteratorValue: ILuaFunction | any = iteratorMultiRes.values[0];
iteratorMultiRes.values[0]; if (Array.isArray(iteratorValue) || iteratorValue instanceof LuaTable) {
if (!iteratorFunction?.call) { iteratorValue = env.get("each").call(sf, iteratorValue);
}
if (!iteratorValue?.call) {
console.error("Cannot iterate over", iteratorMultiRes.values[0]); console.error("Cannot iterate over", iteratorMultiRes.values[0]);
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Cannot iterate over ${iteratorMultiRes.values[0]}`, `Cannot iterate over ${iteratorMultiRes.values[0]}`,
@ -744,7 +746,7 @@ export async function evalStatement(
while (true) { while (true) {
const iterResult = new LuaMultiRes( const iterResult = new LuaMultiRes(
await luaCall(iteratorFunction, [state, control], s.ctx, sf), await luaCall(iteratorValue, [state, control], s.ctx, sf),
).flatten(); ).flatten();
if ( if (
iterResult.values[0] === null || iterResult.values[0] === undefined iterResult.values[0] === null || iterResult.values[0] === undefined

View File

@ -10,9 +10,12 @@ import { assert } from "@std/assert/assert";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
Deno.test("Lua language tests", async () => { 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( 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 chunk = parse(luaFile, {});
const env = new LuaEnv(luaBuildStandardEnv()); const env = new LuaEnv(luaBuildStandardEnv());
@ -29,4 +32,4 @@ Deno.test("Lua language tests", async () => {
} }
assert(false); assert(false);
} }
}); }

View File

@ -269,6 +269,22 @@ for key, value in pairs({ a = "a", b = "b" }) do
assert_equal(key, value) assert_equal(key, value)
end 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 -- type
assert(type(1) == "number") assert(type(1) == "number")
assert(type("Hello") == "string") assert(type("Hello") == "string")

View File

@ -1,6 +1,6 @@
import type { ASTCtx, LuaFunctionBody } from "./ast.ts"; import type { ASTCtx, LuaFunctionBody } from "./ast.ts";
import { evalStatement } from "$common/space_lua/eval.ts"; import { evalStatement } from "./eval.ts";
import { asyncQuickSort, evalPromiseValues } from "$common/space_lua/util.ts"; import { asyncQuickSort, evalPromiseValues } from "./util.ts";
export type LuaType = export type LuaType =
| "nil" | "nil"
@ -425,7 +425,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
} }
} }
asJSObject(): Record<string, any> { toJSObject(): Record<string, any> {
const result: Record<string, any> = {}; const result: Record<string, any> = {};
for (const key of this.keys()) { for (const key of this.keys()) {
result[key] = luaValueToJS(this.get(key)); result[key] = luaValueToJS(this.get(key));
@ -433,15 +433,15 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return result; return result;
} }
asJSArray(): any[] { toJSArray(): any[] {
return this.arrayPart.map(luaValueToJS); return this.arrayPart.map(luaValueToJS);
} }
asJS(): Record<string, any> | any[] { toJS(): Record<string, any> | any[] {
if (this.length > 0) { if (this.length > 0) {
return this.asJSArray(); return this.toJSArray();
} else { } else {
return this.asJSObject(); return this.toJSObject();
} }
} }
@ -722,22 +722,7 @@ export function luaValueToJS(value: any): any {
return value.then(luaValueToJS); return value.then(luaValueToJS);
} }
if (value instanceof LuaTable) { if (value instanceof LuaTable) {
// We'll go a bit on heuristics here return value.toJS();
// 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<string, any> = {};
for (const key of value.keys()) {
result[key] = luaValueToJS(value.get(key));
}
return result;
}
} else if (value instanceof LuaNativeJSFunction) { } else if (value instanceof LuaNativeJSFunction) {
return (...args: any[]) => { return (...args: any[]) => {
return jsToLuaValue(value.fn(...args.map(luaValueToJS))); return jsToLuaValue(value.fn(...args.map(luaValueToJS)));

View File

@ -1,12 +1,13 @@
import { import {
type ILuaFunction, type ILuaFunction,
jsToLuaValue,
LuaBuiltinFunction, LuaBuiltinFunction,
luaCall, luaCall,
LuaEnv, LuaEnv,
luaGet, luaGet,
LuaMultiRes, LuaMultiRes,
LuaRuntimeError, LuaRuntimeError,
type LuaTable, LuaTable,
luaToString, luaToString,
luaTypeOf, luaTypeOf,
type LuaValue, 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 { tableApi } from "$common/space_lua/stdlib/table.ts";
import { osApi } from "$common/space_lua/stdlib/os.ts"; import { osApi } from "$common/space_lua/stdlib/os.ts";
import { jsApi } from "$common/space_lua/stdlib/js.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 { import type {
LuaCollectionQuery, LuaCollectionQuery,
LuaQueryCollection, LuaQueryCollection,
} from "$common/space_lua/query_collection.ts"; } from "$common/space_lua/query_collection.ts";
import { templateApi } from "$common/space_lua/stdlib/template.ts";
const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
console.log("[Lua]", ...(await Promise.all(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 unpackFunction = new LuaBuiltinFunction(async (sf, t: LuaTable) => {
const values: LuaValue[] = []; const values: LuaValue[] = [];
for (let i = 1; i <= t.length; i++) { for (let i = 1; i <= t.length; i++) {
@ -127,6 +144,7 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
return table.metatable; return table.metatable;
}); });
// Non-standard
const tagFunction = new LuaBuiltinFunction( const tagFunction = new LuaBuiltinFunction(
(sf, tagName: LuaValue): LuaQueryCollection => { (sf, tagName: LuaValue): LuaQueryCollection => {
const global = sf.threadLocal.get("_GLOBAL"); const global = sf.threadLocal.get("_GLOBAL");
@ -142,12 +160,29 @@ const tagFunction = new LuaBuiltinFunction(
tagName, tagName,
], ],
query, 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() { export function luaBuildStandardEnv() {
const env = new LuaEnv(); const env = new LuaEnv();
// Top-level builtins // Top-level builtins
@ -168,12 +203,17 @@ export function luaBuildStandardEnv() {
env.set("error", errorFunction); env.set("error", errorFunction);
env.set("pcall", pcallFunction); env.set("pcall", pcallFunction);
env.set("xpcall", xpcallFunction); env.set("xpcall", xpcallFunction);
// Non-standard
env.set("tag", tagFunction); env.set("tag", tagFunction);
env.set("tpl", tplFunction);
// APIs // APIs
env.set("string", stringApi); env.set("string", stringApi);
env.set("table", tableApi); env.set("table", tableApi);
env.set("os", osApi); env.set("os", osApi);
env.set("js", jsApi); env.set("js", jsApi);
// Non-standard
env.set("each", eachFunction);
env.set("space_lua", spaceLuaApi); env.set("space_lua", spaceLuaApi);
env.set("template", templateApi);
return env; return env;
} }

View File

@ -28,6 +28,7 @@ function createAugmentedEnv(
} }
const env = new LuaEnv(globalEnv); const env = new LuaEnv(globalEnv);
if (envAugmentation) { if (envAugmentation) {
env.setLocal("_", envAugmentation);
for (const key of envAugmentation.keys()) { for (const key of envAugmentation.keys()) {
env.setLocal(key, envAugmentation.rawGet(key)); env.setLocal(key, envAugmentation.rawGet(key));
} }
@ -35,6 +36,70 @@ function createAugmentedEnv(
return env; 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<string> {
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({ export const spaceLuaApi = new LuaTable({
/** /**
* Parses a lua expression and returns the parsed expression. * 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. * 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( interpolate: new LuaBuiltinFunction(
async (sf, template: string, envAugmentation?: LuaTable) => { (sf, template: string, envAugmentation?: LuaTable) => {
let result = ""; return interpolateLuaString(sf, template, envAugmentation);
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;
}, },
), ),
}); });

View File

@ -157,4 +157,12 @@ export const stringApi = new LuaTable({
split: new LuaBuiltinFunction((_sf, s: string, sep: string) => { split: new LuaBuiltinFunction((_sf, s: string, sep: string) => {
return s.split(sep); 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);
}),
}); });

View File

@ -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<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("");
},
),
});

View File

@ -25,8 +25,13 @@ export function buildLuaEnv(system: System<any>, scriptEnv: ScriptEnvironment) {
function exposeSyscalls(env: LuaEnv, system: System<any>) { function exposeSyscalls(env: LuaEnv, system: System<any>) {
// Expose all syscalls to Lua // Expose all syscalls to Lua
// Except...
const exclude = ["template"];
const nativeFs = new LuaStackFrame(env, null); const nativeFs = new LuaStackFrame(env, null);
for (const syscallName of system.registeredSyscalls.keys()) { for (const syscallName of system.registeredSyscalls.keys()) {
if (exclude.includes(syscallName)) {
continue;
}
const [ns, fn] = syscallName.split("."); const [ns, fn] = syscallName.split(".");
if (!env.has(ns)) { if (!env.has(ns)) {
env.set(ns, new LuaTable(), nativeFs); env.set(ns, new LuaTable(), nativeFs);

View File

@ -85,7 +85,7 @@ async function renderExpressionDirective(
export function renderExpressionResult(result: any): string { export function renderExpressionResult(result: any): string {
if (result instanceof LuaTable) { if (result instanceof LuaTable) {
result = result.asJS(); result = result.toJS();
} }
if ( if (
Array.isArray(result) && result.length > 0 && typeof result[0] === "object" Array.isArray(result) && result.length > 0 && typeof result[0] === "object"

View File

@ -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). ${template.each(query[[
from tag("page") where string.startswith(name, "API/")
The API: ]], render.page)}
* `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.

View File

@ -30,4 +30,17 @@ Returns a given [[Objects#Tags]] as a query collection, to be queried using [[Sp
Example: Example:
${query[[from tag("page") limit 1]]} ${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"}}

21
website/API/space_lua.md Normal file
View File

@ -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"})}

10
website/API/template.md Normal file
View File

@ -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}
]==])}

View File

@ -2,7 +2,7 @@ Federation enables _browsing_ content from spaces _outside_ the users space,
This enables a few things: 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 users 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 users automatically generated index page when setting up a fresh space.
* **Referencing** other spaces for other purposes, which is leveraged in [[Libraries]]. * **Referencing** other spaces for other purposes, which is leveraged in [[Libraries]].
# How it works # 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` 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]] 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. 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.

16
website/HTTP API.md Normal file
View File

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

View File

@ -10,7 +10,7 @@ Note: these options are primarily useful for [[Install/Deno]] deployments, not s
SilverBullet supports basic authentication for a single user. 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_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 <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends). * `SB_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` 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_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. * `SB_LOCKOUT_TIME`: Specifies the amount of time (in seconds) a client will be blocked until attempting to log back in.

View File

@ -0,0 +1,8 @@
Defines some core useful templates for use in [[Space Lua]]
```space-lua
render = render or {}
render.page = tpl[==[
* [[${name}]]
]==]
```

View File

@ -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: For instance, to sort a list of numbers in descending order:
${query[[from n = {1, 2, 3} order by n desc]]} ${query[[from n = {1, 2, 3} order by n desc]]}
However, in most cases youll use it in conjunction with [[Space Lua/stdlib#tag(name)]]. Heres an example querying the 3 pages that were last modified: However, in most cases youll use it in conjunction with [[../API/global#tag(name)]]. Heres an example querying the 3 pages that were last modified:
${query[[ ${query[[
from p = tag "page" from p = tag "page"