import type { LuaFunctionInfo } from "../../../plug-api/types/index.ts"; import { renderApiDocumentationMarkdown } from "../api_documentation.ts"; import type { LuaBlock, LuaExpression } from "../ast.ts"; import { evalExpression } from "../eval.ts"; import { parseBlock, parseExpressionString } from "../parse.ts"; import { type PrintOptions, prettyPrintBlock, prettyPrintExpression, } from "../pretty_print.ts"; import { type ILuaFunction, isILuaFunction, jsToLuaValue, LuaBuiltinFunction, LuaEnv, LuaRuntimeError, type LuaStackFrame, LuaTable, luaToString, luaValueToJS, singleResult, } from "../runtime.ts"; import { isSqlNull } from "../sliq_null.ts"; /** * These are Space Lua specific functions that are available to all scripts, but are not part of the standard Lua language. */ /** * Helper function to create an augmented environment */ function createAugmentedEnv( sf: LuaStackFrame, envAugmentation?: LuaTable, ): LuaEnv { const globalEnv = sf.threadLocal.get("_GLOBAL"); if (!globalEnv) { throw new Error("_GLOBAL not defined"); } const env = new LuaEnv(globalEnv); if (envAugmentation) { env.setLocal("_", envAugmentation); for (const key of envAugmentation.keys()) { const v = envAugmentation.rawGet(key); env.setLocal(key, isSqlNull(v) ? null : v); } } return env; } function globalEnv(sf: LuaStackFrame): LuaEnv { const env = sf.threadLocal.get("_GLOBAL"); if (!(env instanceof LuaEnv)) { throw new Error("_GLOBAL not defined"); } return env; } function resolveApiValue( sf: LuaStackFrame, path: string, ): ILuaFunction | LuaTable | LuaEnv | null { let value: any = globalEnv(sf); for (const part of path.split(".")) { if (value instanceof LuaEnv || value instanceof LuaTable) { value = value.get(part, sf); } else { return null; } if (value && typeof value.then === "function") { throw new Error("Cannot describe asynchronously resolved API values"); } if (value === null || value === undefined) return null; } return value; } function functionInfo( value: unknown, resolvedName?: string, ): LuaFunctionInfo | null { if (!isILuaFunction(value)) return null; return { ...(value.info ?? { kind: "builtin" }), name: value.info?.name ?? resolvedName, }; } function describeFunction( value: unknown, resolvedName?: string, ): LuaTable | null { const info = functionInfo(value, resolvedName); return info ? (jsToLuaValue(info) as LuaTable) : null; } function listFunctionInfo( sf: LuaStackFrame, target?: LuaTable | string, ): LuaFunctionInfo[] { let namespace: LuaTable | LuaEnv; let prefix = ""; if (typeof target === "string") { const resolved = resolveApiValue(sf, target); if (!(resolved instanceof LuaTable) && !(resolved instanceof LuaEnv)) { return []; } namespace = resolved; prefix = `${target}.`; } else if (target instanceof LuaTable) { namespace = target; } else { namespace = globalEnv(sf); } const functions: LuaFunctionInfo[] = []; for (const key of [...new Set(namespace.keys())].sort()) { const value = namespace.get(key, sf); if (value && typeof (value as any).then === "function") continue; const info = functionInfo(value, `${prefix}${key}`); if (info) functions.push(info); } return functions; } function functionNamespace(info: LuaFunctionInfo): string | undefined { const separator = info.name?.lastIndexOf(".") ?? -1; return separator > 0 ? info.name!.slice(0, separator) : undefined; } function apiDocumentationTarget( sf: LuaStackFrame, target?: ILuaFunction | LuaTable | string, ): { functions: LuaFunctionInfo[]; context?: string } { if (typeof target === "string") { const resolved = resolveApiValue(sf, target); const info = functionInfo(resolved, target); if (info) { return { functions: [info], context: functionNamespace(info) }; } if (resolved instanceof LuaTable || resolved instanceof LuaEnv) { return { functions: listFunctionInfo(sf, target), context: target }; } return { functions: [], context: target }; } const info = functionInfo(target); if (info) { return { functions: [info], context: functionNamespace(info) }; } if (target instanceof LuaTable) { return { functions: listFunctionInfo(sf, target) }; } return { functions: listFunctionInfo(sf) }; } /** * 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); // Do `luaToString` before `luaValueToJS` to preserve tagged float // formatting. const luaResult = singleResult(await evalExpression(parsedExpr, env, sf)); result += await luaToString(luaResult); } catch (e: any) { throw new LuaRuntimeError(`Error evaluating "${expr}": ${e.message}`, sf); } currentIndex = endIndex + 1; } return result; } /** * Converts an optional Lua options table into a `PrintOptions` object, * keeping only the recognised keys with the expected types. */ function toPrintOptions( sf: LuaStackFrame, opts?: LuaTable, ): PrintOptions | undefined { if (!opts) return undefined; const js = luaValueToJS(opts, sf) as Record; const result: PrintOptions = {}; if (typeof js.indentWidth === "number") result.indentWidth = js.indentWidth; if (js.quote === "double" || js.quote === "single") result.quote = js.quote; if (typeof js.trailingComma === "boolean") { result.trailingComma = js.trailingComma; } return result; } export const spaceluaApi = new LuaTable({ describe: new LuaBuiltinFunction({ callback: (sf, target: ILuaFunction | string) => { const value = typeof target === "string" ? resolveApiValue(sf, target) : target; return describeFunction( value, typeof target === "string" ? target : undefined, ); }, description: "Returns structured documentation for a Lua function value or dotted API name.", parameters: [ { name: "functionOrName", type: "function|string", description: "Function value or dotted API name to inspect.", }, ], returns: [ { type: "table|nil", description: "Structured function metadata, or `nil` when the target is not a function.", }, ], examples: [ { code: 'local info = spacelua.describe(editor.getText)\nprint(info.name, info.kind, info.see)\n\nlocal sameInfo = spacelua.describe("editor.getText")', }, ], see: "API/spacelua", }), listFunctions: new LuaBuiltinFunction({ callback: (sf, target?: LuaTable | string) => jsToLuaValue(listFunctionInfo(sf, target)), description: "Lists documented functions in the global environment or an API namespace.", parameters: [ { name: "namespace", type: "table|string", description: "Namespace table or dotted name; omit for globals.", optional: true, }, ], returns: [{ type: "table", description: "Function metadata records." }], examples: [ { code: 'for info in each(spacelua.listFunctions("editor")) do\n print(info.name, info.description or info.see)\nend', }, ], see: "API/spacelua", }), renderApiDocumentation: new LuaBuiltinFunction({ callback: (sf, target?: ILuaFunction | LuaTable | string): string => { const selection = apiDocumentationTarget(sf, target); return renderApiDocumentationMarkdown( selection.functions, selection.context, ); }, description: "Renders API documentation for a function, namespace, or the global environment as Markdown.", parameters: [ { name: "target", type: "function|table|string", description: "Function value, namespace table, or dotted API name to document; omit for globals.", optional: true, }, ], returns: [{ type: "string", description: "Rendered Markdown." }], examples: [ { code: '${spacelua.renderApiDocumentation("lua")}', description: "Render a namespace as a live API-page directive.", language: "markdown", }, { code: '${spacelua.renderApiDocumentation("editor.getText")}', description: "Render one function by its dotted API name.", language: "markdown", }, ], see: "API/spacelua", }), /** * Parses a lua expression and returns the parsed expression. * * @param sf - The current space_lua state. * @param luaExpression - The lua expression to parse. * @returns The parsed expression. */ parseExpression: new LuaBuiltinFunction({ callback: (_sf, luaExpression: string) => { return parseExpressionString(luaExpression); }, description: "Parses a Lua expression and returns its AST.", parameters: [ { name: "luaExpression", type: "string", description: "Lua expression to parse.", }, ], returns: [{ type: "table", description: "Parsed expression AST." }], examples: [ { code: 'local parsed = spacelua.parseExpression("1 + 1")', }, ], see: "API/spacelua", }), /** * Parses a lua chunk (block) and returns the parsed AST block. * * @param sf - The current space_lua state. * @param code - The lua code to parse. * @returns The parsed block. */ parseBlock: new LuaBuiltinFunction({ callback: (_sf, code: string): LuaBlock => { return parseBlock(code); }, description: "Parses a Lua chunk and returns its AST. Blocks retain comments in source order with their exact text, kind, and source range.", parameters: [ { name: "code", type: "string", description: "Lua code to parse." }, ], returns: [{ type: "table", description: "Parsed block AST." }], examples: [ { code: 'local parsed = spacelua.parseBlock("local x = 1\\nreturn x + 2")', }, ], see: "API/spacelua", }), /** * Pretty-prints a parsed lua block AST back to formatted source. * * @param sf - The current space_lua state. * @param block - The parsed lua block. * @param opts - Optional formatting options. * @returns The formatted lua source. */ prettyPrintBlock: new LuaBuiltinFunction({ callback: (sf, block: LuaBlock, opts?: LuaTable): string => { return prettyPrintBlock(block, toPrintOptions(sf, opts)); }, description: "Pretty-prints a parsed Lua block AST. Comments are preserved while their placement and indentation are normalized.", parameters: [ { name: "block", type: "table", description: "Parsed block AST." }, { name: "options", type: "table", description: "Formatting options: `indentWidth`, `quote`, and `trailingComma`.", optional: true, }, ], returns: [{ type: "string", description: "Formatted Lua source." }], examples: [ { code: 'local formatted = spacelua.prettyPrintBlock(spacelua.parseBlock("if a then return 1 end"))\nprint(formatted)', }, ], see: "API/spacelua", }), /** * Pretty-prints a parsed lua expression AST back to formatted source. * * @param sf - The current space_lua state. * @param expr - The parsed lua expression. * @param opts - Optional formatting options. * @returns The formatted lua source. */ prettyPrintExpression: new LuaBuiltinFunction({ callback: (sf, expr: LuaExpression, opts?: LuaTable): string => { return prettyPrintExpression(expr, toPrintOptions(sf, opts)); }, description: "Pretty-prints a parsed Lua expression AST.", parameters: [ { name: "parsedExpr", type: "table", description: "Parsed expression AST.", }, { name: "options", type: "table", description: "Formatting options: `indentWidth`, `quote`, and `trailingComma`.", optional: true, }, ], returns: [{ type: "string", description: "Formatted Lua source." }], examples: [ { code: 'local parsed = spacelua.parseExpression("{a=1,b=2}")\nprint(spacelua.prettyPrintExpression(parsed))', }, ], see: "API/spacelua", }), /** * Evaluates a parsed lua expression and returns the result. * * @param sf - The current space_lua state. * @param parsedExpr - The parsed lua expression to evaluate. * @param envAugmentation - An optional environment to augment the global environment with. * @returns The result of the evaluated expression. */ evalExpression: new LuaBuiltinFunction({ callback: async ( sf, parsedExpr: LuaExpression, envAugmentation?: LuaTable, ) => { const env = createAugmentedEnv(sf, envAugmentation); return luaValueToJS(await evalExpression(parsedExpr, env, sf), sf); }, description: "Evaluates a parsed Lua expression, optionally with additional environment values.", parameters: [ { name: "parsedExpr", type: "table", description: "Parsed expression AST.", }, { name: "envAugmentation", type: "table", description: "Values added to the expression environment.", optional: true, }, ], returns: [{ description: "Evaluated result." }], examples: [ { code: 'local parsed = spacelua.parseExpression("x + y")\nlocal result = spacelua.evalExpression(parsed, {x = 1, y = 2})\nprint(result)', }, ], see: "API/spacelua", }), /** * Interpolates a string with lua expressions and returns the result. */ interpolate: new LuaBuiltinFunction({ callback: (sf, template: string, envAugmentation?: LuaTable | any) => { if (envAugmentation && !(envAugmentation instanceof LuaTable)) { envAugmentation = jsToLuaValue(envAugmentation); } return interpolateLuaString(sf, template, envAugmentation); }, description: "Interpolates `${...}` Lua expressions in a string, optionally with additional environment values.", parameters: [ { name: "template", type: "string", description: "Template containing `${...}` expressions.", }, { name: "envAugmentation", type: "table", description: "Values added to the interpolation environment.", optional: true, }, ], returns: [{ type: "string", description: "Interpolated string." }], examples: [ { code: 'local greeting = spacelua.interpolate("Hello ${name}!", {name = "Pete"})\nprint(greeting)', }, ], see: "API/spacelua", }), /** * Returns your SilverBullet instance's base URL */ baseUrl: new LuaBuiltinFunction({ callback: () => { //NOTE: Removing trailing slash to stay compatible with original code: `location.protocol + "//" + location.host;` return document.baseURI.replace(/\/*$/, ""); }, description: "Returns the SilverBullet instance's base URL, or `nil` when run on the server.", returns: [{ type: "string|nil" }], examples: [{ code: "local url = spacelua.baseUrl()\nprint(url)" }], see: "API/spacelua", }), });