281 lines
7.4 KiB
TypeScript
281 lines
7.4 KiB
TypeScript
import {
|
|
jsToLuaValue,
|
|
LuaBuiltinFunction,
|
|
LuaMultiRes,
|
|
LuaRuntimeError,
|
|
LuaTable,
|
|
luaToString,
|
|
} from "../runtime.ts";
|
|
import printf from "./printf.ts";
|
|
|
|
// Bits and pieces borrowed from https://github.com/paulcuth/starlight/blob/master/src/runtime/lib/string.js
|
|
|
|
const ROSETTA_STONE = {
|
|
"([^a-zA-Z0-9%(])-": "$1*?",
|
|
"([^%])-([^a-zA-Z0-9?])": "$1*?$2",
|
|
"([^%])-$": "$1*?",
|
|
"%a": "[a-zA-Z]",
|
|
"%A": "[^a-zA-Z]",
|
|
"%c": "[\x00-\x1f]",
|
|
"%C": "[^\x00-\x1f]",
|
|
"%d": "\\d",
|
|
"%D": "[^\d]",
|
|
"%l": "[a-z]",
|
|
"%L": "[^a-z]",
|
|
"%p": "[\.\,\"'\?\!\;\:\#\$\%\&\(\)\*\+\-\/\<\>\=\@\\[\\]\\\\^\_\{\}\|\~]",
|
|
"%P": "[^\.\,\"'\?\!\;\:\#\$\%\&\(\)\*\+\-\/\<\>\=\@\\[\\]\\\\^\_\{\}\|\~]",
|
|
"%s": "[ \\t\\n\\f\\v\\r]",
|
|
"%S": "[^ \t\n\f\v\r]",
|
|
"%u": "[A-Z]",
|
|
"%U": "[^A-Z]",
|
|
"%w": "[a-zA-Z0-9]",
|
|
"%W": "[^a-zA-Z0-9]",
|
|
"%x": "[a-fA-F0-9]",
|
|
"%X": "[^a-fA-F0-9]",
|
|
"%([^a-zA-Z])": "\\$1",
|
|
};
|
|
|
|
function translatePattern(pattern: string): string {
|
|
pattern = "" + pattern;
|
|
|
|
// Replace single backslash with double backslashes
|
|
pattern = pattern.replace(new RegExp("\\\\", "g"), "\\\\");
|
|
pattern = pattern.replace(new RegExp("\\|", "g"), "\\|");
|
|
|
|
for (const [key, value] of Object.entries(ROSETTA_STONE)) {
|
|
pattern = pattern.replace(new RegExp(key, "g"), value);
|
|
}
|
|
|
|
let l = pattern.length;
|
|
let n = 0;
|
|
|
|
for (let i = 0; i < l; i++) {
|
|
const character = pattern.slice(i, 1);
|
|
if (i && pattern.slice(i - 1, 1) == "\\") {
|
|
continue;
|
|
}
|
|
|
|
let addSlash = false;
|
|
|
|
if (character == "[") {
|
|
if (n) addSlash = true;
|
|
n++;
|
|
} else if (character == "]" && pattern.slice(i - 1, 1) !== "\\") {
|
|
n--;
|
|
if (n) addSlash = true;
|
|
}
|
|
|
|
if (addSlash) {
|
|
pattern = pattern.slice(0, i) + pattern.slice(i++ + 1);
|
|
l++;
|
|
}
|
|
}
|
|
|
|
return pattern;
|
|
}
|
|
|
|
export const stringApi = new LuaTable({
|
|
byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => {
|
|
i = i ?? 1;
|
|
j = j ?? i;
|
|
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) => {
|
|
// Regex
|
|
if (!plain) {
|
|
pattern = translatePattern(pattern);
|
|
const reg = new RegExp(pattern);
|
|
const index = s.slice(init - 1).search(reg);
|
|
|
|
if (index < 0) return null;
|
|
|
|
const match = s.slice(init - 1).match(reg);
|
|
const result = [index + init, index + init + match![0].length - 1];
|
|
|
|
match!.shift();
|
|
return new LuaMultiRes(result.concat(match));
|
|
}
|
|
|
|
// Plain
|
|
const index = s.indexOf(pattern, init - 1);
|
|
return (index === -1)
|
|
? null
|
|
: new LuaMultiRes([index + 1, index + pattern.length]);
|
|
},
|
|
),
|
|
format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => {
|
|
return printf(format, ...args);
|
|
}),
|
|
gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
|
|
pattern = translatePattern(pattern);
|
|
const reg = new RegExp(pattern, "g"),
|
|
matches = s.match(reg);
|
|
return () => {
|
|
if (!matches) {
|
|
return;
|
|
}
|
|
const match = matches.shift();
|
|
if (!match) {
|
|
return;
|
|
}
|
|
const groups = new RegExp(pattern).exec(match) || [];
|
|
|
|
groups.shift();
|
|
return groups.length ? new LuaMultiRes(groups) : match;
|
|
};
|
|
}),
|
|
gsub: new LuaBuiltinFunction(
|
|
async (
|
|
sf,
|
|
s: string,
|
|
pattern: string,
|
|
repl: any, // string or LuaFunction
|
|
n = Infinity,
|
|
) => {
|
|
pattern = translatePattern(pattern);
|
|
const replIsFunction = repl.call;
|
|
|
|
let count = 0,
|
|
result = "",
|
|
str,
|
|
prefix,
|
|
match: any,
|
|
lastMatch;
|
|
|
|
while (
|
|
count < n &&
|
|
s &&
|
|
(match = s.match(pattern))
|
|
) {
|
|
if (replIsFunction) {
|
|
// If no captures, pass in the whole match
|
|
if (match[1] === undefined) {
|
|
str = await repl.call(sf, match[0]);
|
|
} else {
|
|
// Else pass in the captures
|
|
str = await repl.call(sf, ...match.slice(1));
|
|
}
|
|
if (str instanceof LuaMultiRes) {
|
|
str = str.values[0];
|
|
}
|
|
if (str === undefined || str === null) {
|
|
str = match[0];
|
|
}
|
|
} else if (repl instanceof LuaTable) {
|
|
str = repl.get(match[0]);
|
|
} else if (typeof repl === "string") {
|
|
str = repl.replaceAll(/%([0-9]+)/g, (_, i) => match[i]);
|
|
} else {
|
|
throw new LuaRuntimeError(
|
|
"string.gsub replacement argument should be a function, table or string",
|
|
sf,
|
|
);
|
|
}
|
|
|
|
if (match[0].length === 0) {
|
|
if (lastMatch === void 0) {
|
|
prefix = "";
|
|
} else {
|
|
prefix = s.slice(0, 1);
|
|
}
|
|
} else {
|
|
prefix = s.slice(0, match.index);
|
|
}
|
|
|
|
lastMatch = match[0];
|
|
result += `${prefix}${str}`;
|
|
s = s.slice(`${prefix}${lastMatch}`.length);
|
|
|
|
count++;
|
|
}
|
|
|
|
return new LuaMultiRes([`${result}${s}`, 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) => {
|
|
s = s.slice(init - 1);
|
|
const matches = s.match(new RegExp(translatePattern(pattern)));
|
|
|
|
if (!matches) {
|
|
return null;
|
|
} else if (matches[1] === undefined) {
|
|
// No captures
|
|
return matches[0];
|
|
}
|
|
|
|
matches.shift();
|
|
return new LuaMultiRes(matches);
|
|
},
|
|
),
|
|
rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => {
|
|
sep = sep ?? "";
|
|
return s.repeat(n) + sep;
|
|
}),
|
|
reverse: new LuaBuiltinFunction((_sf, s: string) => {
|
|
return s.split("").reverse().join("");
|
|
}),
|
|
sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => {
|
|
j = j ?? s.length;
|
|
if (i < 0) {
|
|
i = s.length + i + 1;
|
|
}
|
|
if (j < 0) {
|
|
j = s.length + j + 1;
|
|
}
|
|
return s.slice(i - 1, j);
|
|
}),
|
|
split: new LuaBuiltinFunction((_sf, s: string, sep: string) => {
|
|
return s.split(sep);
|
|
}),
|
|
|
|
// Non-standard
|
|
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);
|
|
};
|
|
}),
|
|
});
|