Files
plainleaf/client/space_lua/stdlib/string.ts
T
Matouš Jan FialkaandGitHub e0182b3bb4 [Space Lua] Implement native Lua pattern matching engine (#1838)
* Add Lua *pattern matching engine* (`pattern.ts`) that directly ports
  `lstrlib.c` semantics.

* Integrate the new engine into the `string` API for `find`, `match`,
  `gmatch`, and `gsub` functions.

* Add a pattern test suite (`pattern_test.lua`) covering character
  classes, repetition quantifiers, edge cases as well as error
  conditions.

* Refactor `string_test.lua` to remove duplicated pattern tests that
  are now covered by the pattern test suite.

* Fix `string.rep` separator support.

* Fix `string.sub` index handling.

* Remove the obsolete `string_test2.lua` file.

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
2026-02-13 14:18:55 +01:00

201 lines
5.6 KiB
TypeScript

import {
jsToLuaValue,
LuaBuiltinFunction,
LuaMultiRes,
LuaRuntimeError,
LuaTable,
luaToString,
} from "../runtime.ts";
import { untagNumber } from "../numeric.ts";
import { luaFormat } from "./format.ts";
import {
type CaptureResult,
type GsubCallbacks,
patternFind,
patternGmatch,
patternGsub,
patternMatch,
} from "./pattern.ts";
function capturesToLua(caps: CaptureResult[]): any {
if (caps.length === 0) {return null;}
if (caps.length === 1) {
const c = caps[0];
return "s" in c ? c.s : c.position;
}
return new LuaMultiRes(
caps.map((c) => ("s" in c ? c.s : c.position)),
);
}
export const stringApi = new LuaTable({
byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => {
i = i ?? 1;
j = j ?? i;
if (j > s.length) {j = s.length;}
if (i < 1) {i = 1;}
const result = [];
for (let k = i; k <= j; k++) {
result.push(s.charCodeAt(k - 1));
}
return new LuaMultiRes(result);
}),
char: new LuaBuiltinFunction((_sf, ...args: number[]) => {
return String.fromCharCode(...args);
}),
find: new LuaBuiltinFunction(
(_sf, s: string, pattern: string, init = 1, plain = false) => {
const r = patternFind(s, pattern, init, plain);
if (!r) {return null;}
const result: any[] = [r.start, r.end];
for (const c of r.captures) {
result.push("s" in c ? c.s : c.position);
}
return new LuaMultiRes(result);
},
),
format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => {
for (let i = 0; i < args.length; i++) {
args[i] = untagNumber(args[i]);
}
return luaFormat(format, ...args);
}),
gmatch: new LuaBuiltinFunction(
(_sf, s: string, pattern: string, init = 1) => {
const iter = patternGmatch(s, pattern, init);
return () => {
const caps = iter();
if (!caps) {return;}
return capturesToLua(caps);
};
},
),
gsub: new LuaBuiltinFunction(
async (
sf,
s: string,
pattern: string,
repl: any,
n?: number,
) => {
const callbacks: GsubCallbacks = {};
if (typeof repl === "string") {
callbacks.replString = repl;
} else if (repl instanceof LuaTable) {
callbacks.replTable = (key: string) => {
const v = repl.get(key);
if (v === null || v === undefined || v === false) {return null;}
return String(v);
};
} else if (repl.call) {
callbacks.replFunction = async (...caps: CaptureResult[]) => {
const args = caps.map((c) => ("s" in c ? c.s : c.position));
let result = await repl.call(sf, ...args);
if (result instanceof LuaMultiRes) {
result = result.values[0];
}
if (result === null || result === undefined || result === false) {
return null;
}
return luaToString(result);
};
} else {
throw new LuaRuntimeError(
"string.gsub replacement argument should be a function, table or string",
sf,
);
}
const [result, count] = await patternGsub(s, pattern, callbacks, n);
return new LuaMultiRes([result, count]);
},
),
len: new LuaBuiltinFunction((_sf, s: string) => {
return s.length;
}),
lower: new LuaBuiltinFunction((_sf, s: string) => {
return luaToString(s.toLowerCase());
}),
upper: new LuaBuiltinFunction((_sf, s: string) => {
return luaToString(s.toUpperCase());
}),
match: new LuaBuiltinFunction(
(_sf, s: string, pattern: string, init = 1) => {
const caps = patternMatch(s, pattern, init);
if (!caps) {return null;}
return capturesToLua(caps);
},
),
rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => {
if (n <= 0) {return "";}
sep = sep ?? "";
const parts: string[] = [];
for (let i = 0; i < n; i++) {
parts.push(s);
}
return parts.join(sep);
}),
reverse: new LuaBuiltinFunction((_sf, s: string) => {
return s.split("").reverse().join("");
}),
sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => {
const len = s.length;
let start: number;
if (i > 0) {
start = i;
} else if (i < -len) {
start = 1;
} else {
start = i === 0 ? 1 : len + i + 1;
}
let end: number;
if (j === undefined || j === null || j > len) {
end = len;
} else if (j >= 0) {
end = j;
} else if (j < -len) {
end = 0;
} else {
end = len + j + 1;
}
if (start <= end) {
return s.substring(start - 1, end);
}
return "";
}),
split: new LuaBuiltinFunction((_sf, s: string, sep: string) => {
return s.split(sep);
}),
// Non-standard extensions
startsWith: new LuaBuiltinFunction((_sf, s: string, prefix: string) => {
return s.startsWith(prefix);
}),
endsWith: new LuaBuiltinFunction((_sf, s: string, suffix: string) => {
return s.endsWith(suffix);
}),
trim: new LuaBuiltinFunction((_sf, s: string) => {
return s.trim();
}),
trimStart: new LuaBuiltinFunction((_sf, s: string) => {
return s.trimStart();
}),
trimEnd: new LuaBuiltinFunction((_sf, s: string) => {
return s.trimEnd();
}),
matchRegex: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
const regex = new RegExp(pattern);
const result = s.match(regex);
return jsToLuaValue(result);
}),
matchRegexAll: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
const regex = new RegExp(pattern, "g");
return () => {
const match = regex.exec(s);
if (!match) {
return;
}
return jsToLuaValue(match);
};
}),
});