532 lines
16 KiB
TypeScript
532 lines
16 KiB
TypeScript
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<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);
|
|
// 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<string, unknown>;
|
|
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",
|
|
}),
|
|
});
|