* 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>
201 lines
5.6 KiB
TypeScript
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);
|
|
};
|
|
}),
|
|
});
|