Lua: tweaks and docs
parent
cbf227fa49
commit
5604f6d8c2
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<string, any> {
|
||||
toJSObject(): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
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<string, any> | any[] {
|
||||
toJS(): Record<string, any> | 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<string, any> = {};
|
||||
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)));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<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({
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
),
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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("");
|
||||
},
|
||||
),
|
||||
});
|
|
@ -25,8 +25,13 @@ export function buildLuaEnv(system: System<any>, scriptEnv: ScriptEnvironment) {
|
|||
|
||||
function exposeSyscalls(env: LuaEnv, system: System<any>) {
|
||||
// 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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}
|
|
@ -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]]}
|
||||
${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"}}
|
|
@ -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"})}
|
|
@ -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}
|
||||
]==])}
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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 <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_TIME`: Specifies the amount of time (in seconds) a client will be blocked until attempting to log back in.
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
Defines some core useful templates for use in [[Space Lua]]
|
||||
```space-lua
|
||||
render = render or {}
|
||||
|
||||
render.page = tpl[==[
|
||||
* [[${name}]]
|
||||
]==]
|
||||
```
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue