Lua: experimental string interpolation
parent
b6f1977cec
commit
86f31e3a00
|
@ -304,6 +304,8 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
|
|||
const env = new LuaEnv(luaBuildStandardEnv());
|
||||
const threadLocal = new LuaEnv();
|
||||
|
||||
env.setLocal("globalEnv", "GLOBAL");
|
||||
|
||||
// Set up some thread local values
|
||||
threadLocal.setLocal("user", "alice");
|
||||
threadLocal.setLocal("permissions", new LuaTable());
|
||||
|
@ -423,4 +425,25 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
|
|||
|
||||
assertEquals(await evalExpr("errorTest()", env, sf), "caught");
|
||||
assertEquals(threadLocal.get("error"), "caught");
|
||||
|
||||
// Test string interpolation
|
||||
sf.threadLocal.setLocal("_GLOBAL", env);
|
||||
assertEquals(
|
||||
await evalExpr(
|
||||
"interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})",
|
||||
env,
|
||||
sf,
|
||||
),
|
||||
"Hello, GLOBAL and local!",
|
||||
);
|
||||
|
||||
// Some more complex string interpolation with more complex lua expressions, with nested {}
|
||||
assertEquals(
|
||||
await evalExpr(
|
||||
`interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`,
|
||||
env,
|
||||
sf,
|
||||
),
|
||||
`Some JSON {"name":"Pete"}!`,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -75,6 +75,17 @@ do
|
|||
end
|
||||
assert(a == 1)
|
||||
|
||||
-- Async function calling
|
||||
function multiplier(a)
|
||||
-- Anything will be async in practice
|
||||
return function(b)
|
||||
return a * b
|
||||
end
|
||||
end
|
||||
|
||||
local multiplier = multiplier(2)
|
||||
assert(multiplier(3) == 6)
|
||||
|
||||
-- Function definitions in tables
|
||||
ns = { name = "Pete" }
|
||||
function ns.returnOne()
|
||||
|
@ -308,12 +319,10 @@ assert(result == "Hi world", "Function replacement with single capture failed")
|
|||
|
||||
-- Function replacement with multiple captures
|
||||
result = string.gsub("hello world", "(h)(e)(l)(l)o", function(h, e, l1, l2)
|
||||
print("Captures:", h, e, l1, l2) -- Debug what captures we're getting
|
||||
assert(h == "h" and e == "e" and l1 == "l" and l2 == "l",
|
||||
"Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2)
|
||||
return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o"
|
||||
end)
|
||||
print("Result:", result) -- Debug the actual result
|
||||
assert(result == "HEllo world", "Function replacement with multiple captures failed")
|
||||
|
||||
-- Function returning nil (should keep original match)
|
||||
|
|
|
@ -10,11 +10,18 @@ import {
|
|||
luaToString,
|
||||
luaTypeOf,
|
||||
type LuaValue,
|
||||
luaValueToJS,
|
||||
} from "$common/space_lua/runtime.ts";
|
||||
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 { parse } from "$common/space_lua/parse.ts";
|
||||
import type {
|
||||
LuaBlock,
|
||||
LuaFunctionCallStatement,
|
||||
} from "$common/space_lua/ast.ts";
|
||||
import { evalExpression } from "$common/space_lua/eval.ts";
|
||||
|
||||
const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
|
||||
console.log("[Lua]", ...(await Promise.all(args.map(luaToString))));
|
||||
|
@ -122,6 +129,82 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
|
|||
return table.metatable;
|
||||
});
|
||||
|
||||
/**
|
||||
* This is not standard Lua, but it's a useful feature for us
|
||||
*/
|
||||
const interpolateFunction = new LuaBuiltinFunction(
|
||||
async (sf, template: string, expandedEnv?: 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 parsedLua = parse(`_(${expr})`) as LuaBlock;
|
||||
const parsedExpr =
|
||||
(parsedLua.statements[0] as LuaFunctionCallStatement).call
|
||||
.args[0];
|
||||
|
||||
const globalEnv = sf.threadLocal.get("_GLOBAL");
|
||||
if (!globalEnv) {
|
||||
throw new Error("_GLOBAL not defined");
|
||||
}
|
||||
// Create a new env with the global env as the parent, augmented with the expandedEnv
|
||||
const env = new LuaEnv(globalEnv);
|
||||
if (expandedEnv) {
|
||||
// Iterate over the keys in the expandedEnv and set them in the new env
|
||||
for (const key of expandedEnv.keys()) {
|
||||
env.setLocal(key, expandedEnv.rawGet(key));
|
||||
}
|
||||
}
|
||||
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 function luaBuildStandardEnv() {
|
||||
const env = new LuaEnv();
|
||||
// Top-level builtins
|
||||
|
@ -142,6 +225,8 @@ export function luaBuildStandardEnv() {
|
|||
env.set("error", errorFunction);
|
||||
env.set("pcall", pcallFunction);
|
||||
env.set("xpcall", xpcallFunction);
|
||||
// String interpolation
|
||||
env.set("interpolate", interpolateFunction);
|
||||
|
||||
// APIs
|
||||
env.set("string", stringApi);
|
||||
|
|
|
@ -72,7 +72,7 @@ function exposeDefinitions(
|
|||
hide: def.get("hide"),
|
||||
} as CommandDef,
|
||||
async (...args: any[]) => {
|
||||
const tl = await buildThreadLocalEnv(system);
|
||||
const tl = await buildThreadLocalEnv(system, env);
|
||||
const sf = new LuaStackFrame(tl, null);
|
||||
try {
|
||||
return await fn.call(sf, ...args.map(jsToLuaValue));
|
||||
|
@ -102,7 +102,7 @@ function exposeDefinitions(
|
|||
scriptEnv.registerEventListener(
|
||||
{ name: def.get("event") },
|
||||
async (...args: any[]) => {
|
||||
const tl = await buildThreadLocalEnv(system);
|
||||
const tl = await buildThreadLocalEnv(system, env);
|
||||
const sf = new LuaStackFrame(tl, null);
|
||||
try {
|
||||
return await fn.call(sf, ...args.map(jsToLuaValue));
|
||||
|
@ -115,13 +115,14 @@ function exposeDefinitions(
|
|||
);
|
||||
}
|
||||
|
||||
async function buildThreadLocalEnv(system: System<any>) {
|
||||
async function buildThreadLocalEnv(system: System<any>, globalEnv: LuaEnv) {
|
||||
const tl = new LuaEnv();
|
||||
const currentPageMeta = await system.localSyscall(
|
||||
"editor.getCurrentPageMeta",
|
||||
[],
|
||||
);
|
||||
tl.setLocal("pageMeta", currentPageMeta);
|
||||
tl.setLocal("_GLOBAL", globalEnv);
|
||||
return tl;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ export function luaDirectivePlugin(client: Client) {
|
|||
|
||||
const tl = new LuaEnv();
|
||||
tl.setLocal("pageMeta", currentPageMeta);
|
||||
tl.setLocal("_GLOBAL", client.clientSystem.spaceLuaEnv.env);
|
||||
const sf = new LuaStackFrame(tl, expr.ctx);
|
||||
const threadLocalizedEnv = new LuaEnv(
|
||||
client.clientSystem.spaceLuaEnv.env,
|
||||
|
|
Loading…
Reference in New Issue