Refactor: Merging common/ into web/

This commit is contained in:
Zef Hemel
2025-06-04 14:01:59 +02:00
parent e5c137a505
commit bf2e0cdb87
122 changed files with 194 additions and 235 deletions
+27
View File
@@ -0,0 +1,27 @@
local function assertEqual(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
end
end
-- Logic values, shouldn't be affected
assertEqual(some(nil), nil)
assertEqual(some(true), true)
assertEqual(some(false), false)
-- Numbers
assertEqual(some(1), 1)
assertEqual(some(0), 0) -- 0 is not falsy unlike C
assertEqual(some(-1), -1)
assertEqual(some(1/0), nil) -- inf
assertEqual(some(0/0), nil) -- nan
-- Strings
assertEqual(some("foo bar"), "foo bar")
assertEqual(some(""), nil)
assertEqual(some(" \n"), nil)
-- Tables
assertEqual(some({}), nil)
assertEqual(some({"baz"})[1], "baz") -- compare an element to ensure passthrough
assertEqual(some({foo="bar"})["foo"], "bar")
+73
View File
@@ -0,0 +1,73 @@
import {
jsToLuaValue,
LuaBuiltinFunction,
LuaTable,
luaValueToJS,
} from "../runtime.ts";
export const jsApi = new LuaTable({
/**
* Creates a new instance of a JavaScript class.
* @param constructorFn - The constructor function.
* @param args - The arguments to pass to the constructor.
* @returns The new instance.
*/
new: new LuaBuiltinFunction(
(sf, constructorFn: any, ...args) => {
return new constructorFn(
...args.map((v) => luaValueToJS(v, sf)),
);
},
),
/**
* Imports a JavaScript module.
* @param url - The URL of the module to import.
* @returns The imported module.
*/
import: new LuaBuiltinFunction(async (_sf, url) => {
let m = await import(url);
// Unwrap default if it exists
if (Object.keys(m).length === 1 && m.default) {
m = m.default;
}
return m;
}),
eachIterable: new LuaBuiltinFunction((_sf, val) => {
const iterator = val[Symbol.asyncIterator]();
return async () => {
const result = await iterator.next();
if (result.done) {
return;
}
return result.value;
};
}),
/**
* Converts a JavaScript value to a Lua value.
* @param val - The JavaScript value to convert.
* @returns The Lua value.
*/
tolua: new LuaBuiltinFunction((_sf, val) => jsToLuaValue(val)),
/**
* Converts a Lua value to a JavaScript value.
* @param val - The Lua value to convert.
* @returns The JavaScript value.
*/
tojs: new LuaBuiltinFunction((sf, val) => luaValueToJS(val, sf)),
/**
* Logs a message to the console.
* @param args - The arguments to log.
*/
log: new LuaBuiltinFunction((_sf, ...args) => {
console.log(...args);
}),
/**
* Converts a Lua value to a JSON string.
* @param val - The Lua value to convert.
* @returns The JSON string.
*/
stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val)),
// Expose the global window object
window: globalThis,
});
+14
View File
@@ -0,0 +1,14 @@
local function assert_equal(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
end
end
local lodash = js.import("https://esm.sh/lodash@4.17.21")
assert_equal(js.stringify(lodash.chunk({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 3)), '[[1,2,3],[4,5,6],[7,8,9],[10]]')
local moment = js.import("https://esm.sh/moment@2.30.1")
local day = moment("1995-12-25");
assert_equal(day.format("DD-MM-YYYY"), "25-12-1995")
+148
View File
@@ -0,0 +1,148 @@
import { LuaBuiltinFunction, LuaRuntimeError, LuaTable } from "../runtime.ts";
export const mathApi = new LuaTable({
/**
* When called without arguments, returns a pseudo-random float with
* uniform distribution in the range [0,1). When called with two
* integers m and n, math.random returns a pseudo-random integer
* with uniform distribution in the range [m, n]. The call
* math.random(n), for a positive n, is equivalent to
* math.random(1,n). The call math.random(0) produces an integer
* with all bits (pseudo)random.
*/
random: new LuaBuiltinFunction((_sf, m?: number, n?: number) => {
if (m === undefined && n === undefined) {
// random() returns [0,1)
return Math.random();
}
if (!Number.isInteger(m)) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.random' (integer expected)",
_sf,
);
}
if (n === undefined) {
if (m! == 0) {
// random(0) returns a random integer
const high = Math.floor(Math.random() * 0x100000000);
const low = Math.floor(Math.random() * 0x100000000);
let result = (BigInt(high) << 32n) | BigInt(low);
if (result & (1n << 63n)) {
result -= 1n << 64n;
}
return result;
} else {
// random(m) returns [1,m]
if (m! < 1) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.random' (interval is empty)",
_sf,
);
}
return Math.floor(Math.random() * m!) + 1;
}
}
if (!Number.isInteger(n!)) {
throw new LuaRuntimeError(
"bad argument #2 to 'math.random' (integer expected)",
_sf,
);
}
// random(m,n) returns [m,n]
if (n! < m!) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.random' (interval is empty)",
_sf,
);
}
return Math.floor(Math.random() * (n! - m! + 1)) + m!;
}),
// Basic functions
abs: new LuaBuiltinFunction((_sf, x: number) => Math.abs(x)),
ceil: new LuaBuiltinFunction((_sf, x: number) => Math.ceil(x)),
floor: new LuaBuiltinFunction((_sf, x: number) => Math.floor(x)),
max: new LuaBuiltinFunction((_sf, ...args: number[]) => Math.max(...args)),
min: new LuaBuiltinFunction((_sf, ...args: number[]) => Math.min(...args)),
// Rounding and remainder
fmod: new LuaBuiltinFunction((_sf, x: number, y: number) => x % y),
modf: new LuaBuiltinFunction((_sf, x: number) => {
const int = Math.floor(x);
const frac = x - int;
return new LuaTable([int, frac]);
}),
// Power and logarithms
exp: new LuaBuiltinFunction((_sf, x: number) => Math.exp(x)),
log: new LuaBuiltinFunction((_sf, x: number, base?: number) => {
if (base === undefined) {
return Math.log(x);
}
return Math.log(x) / Math.log(base);
}),
pow: new LuaBuiltinFunction((_sf, x: number, y: number) => Math.pow(x, y)),
sqrt: new LuaBuiltinFunction((_sf, x: number) => Math.sqrt(x)),
// Trigonometric functions
cos: new LuaBuiltinFunction((_sf, x: number) => Math.cos(x)),
sin: new LuaBuiltinFunction((_sf, x: number) => Math.sin(x)),
tan: new LuaBuiltinFunction((_sf, x: number) => Math.tan(x)),
acos: new LuaBuiltinFunction((_sf, x: number) => Math.acos(x)),
asin: new LuaBuiltinFunction((_sf, x: number) => Math.asin(x)),
atan: new LuaBuiltinFunction((_sf, y: number, x?: number) => {
if (x === undefined) {
return Math.atan(y);
}
return Math.atan2(y, x);
}),
// Hyperbolic functions
cosh: new LuaBuiltinFunction((_sf, x: number) => Math.cosh(x)),
sinh: new LuaBuiltinFunction((_sf, x: number) => Math.sinh(x)),
tanh: new LuaBuiltinFunction((_sf, x: number) => Math.tanh(x)),
// Additional utility
deg: new LuaBuiltinFunction((_sf, x: number) => x * 180 / Math.PI),
rad: new LuaBuiltinFunction((_sf, x: number) => x * Math.PI / 180),
ult: new LuaBuiltinFunction((_sf, m: number, n: number) => {
// Unsigned less than comparison
return (m >>> 0) < (n >>> 0);
}),
// Keep the cosineSimilarity utility function
cosineSimilarity: new LuaBuiltinFunction(
(sf, vecA: LuaTable | number[], vecB: LuaTable | number[]) => {
// Convert LuaTable to number[]
if (vecA instanceof LuaTable) {
vecA = vecA.toJSArray();
}
if (vecB instanceof LuaTable) {
vecB = vecB.toJSArray();
}
if (vecA.length !== vecB.length) {
throw new LuaRuntimeError("Vectors must be of the same length", sf);
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
normA += vecA[i] ** 2;
normB += vecB[i] ** 2;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
},
),
});
+48
View File
@@ -0,0 +1,48 @@
local function assertEqual(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
end
end
-- Trigonometric functions
assertEqual(math.cos(0), 1)
assertEqual(math.sin(0), 0)
assertEqual(math.tan(0), 0)
assertEqual(math.acos(1), 0)
assertEqual(math.asin(0), 0)
assertEqual(math.atan(0), 0)
-- Hyperbolic functions
assertEqual(math.cosh(0), 1)
assertEqual(math.sinh(0), 0)
assertEqual(math.tanh(0), 0)
-- Basic functions
assertEqual(math.abs(-5), 5)
assertEqual(math.ceil(3.3), 4)
assertEqual(math.floor(3.7), 3)
assertEqual(math.max(1, 2, 3, 4), 4)
assertEqual(math.min(1, 2, 3, 4), 1)
-- Rounding and remainder
assertEqual(math.fmod(7, 3), 1)
-- Power and logarithms
assertEqual(math.exp(0), 1)
assertEqual(math.log(math.exp(1)), 1)
assertEqual(math.log(8, 2), 3) -- log base 2 of 8
assertEqual(math.pow(2, 3), 8)
assertEqual(math.sqrt(9), 3)
-- Random number tests (basic range checks)
local rand = math.random()
assertEqual(rand >= 0 and rand < 1, true)
local randN = math.random(10)
assertEqual(randN >= 1 and randN <= 10, true)
local randRange = math.random(5, 10)
assertEqual(randRange >= 5 and randRange <= 10, true)
-- Unsigned less than comparison
assertEqual(math.ult(1, 2), true)
assertEqual(math.ult(2, 1), false)
+155
View File
@@ -0,0 +1,155 @@
import { LuaBuiltinFunction, LuaTable } from "../runtime.ts";
const ONE_DAY = 1000 * 60 * 60 * 24;
const ONE_WEEK = ONE_DAY * 7;
// weekStartDay: 0 for Sunday, 1 for Monday
// iso: if true, week 01 contains Jan. 4th and prior week is week 52 or 53 of year prior
// if false, week 01 starts on first weekStartDay of the year and prior week is week 00
function weekNumber(inDate: Date, weekStartDay: number, iso: boolean): number {
const date = new Date(
Date.UTC(inDate.getFullYear(), inDate.getMonth(), inDate.getDate()),
);
if (iso) {
// ISO week: Week 1 contains January 4th, weeks start on Monday
// Adjust to nearest Thursday
const target = new Date(date);
target.setUTCDate(target.getUTCDate() + 3 - ((target.getUTCDay() + 6) % 7)); // Nearest Thursday
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 4)); // Jan 4
const weekStart = new Date(yearStart);
weekStart.setUTCDate(
yearStart.getUTCDate() - ((yearStart.getUTCDay() + 6) % 7),
); // Monday of that week
const diff = target.getTime() - weekStart.getTime();
return 1 + Math.floor(diff / ONE_WEEK);
} else {
// Non-ISO week: Week 1 starts on the first weekStartDay of the year
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
const startDay = yearStart.getUTCDay();
const offset = (7 + (startDay - weekStartDay)) % 7;
const firstWeekStart = new Date(yearStart);
firstWeekStart.setUTCDate(yearStart.getUTCDate() + (7 - offset) % 7);
if (date < firstWeekStart) {
return 0;
}
const diff = date.getTime() - firstWeekStart.getTime();
return 1 + Math.floor(diff / ONE_WEEK);
}
}
export const osApi = new LuaTable({
time: new LuaBuiltinFunction((_sf, tbl?: LuaTable) => {
if (tbl) {
// Build a date object from the table
const date = new Date();
if (!tbl.has("year")) {
throw new Error("time(): year is required");
}
date.setFullYear(tbl.get("year"));
if (!tbl.has("month")) {
throw new Error("time(): month is required");
}
date.setMonth(tbl.get("month") - 1);
if (!tbl.has("day")) {
throw new Error("time(): day is required");
}
date.setDate(tbl.get("day"));
date.setHours(tbl.get("hour") ?? 12);
date.setMinutes(tbl.get("min") ?? 0);
date.setSeconds(tbl.get("sec") ?? 0);
return Math.floor(date.getTime() / 1000);
} else {
return Math.floor(Date.now() / 1000);
}
}),
/**
* Returns the difference, in seconds, from time t1 to time t2
* (where the times are values returned by os.time). In POSIX,
* Windows, and some other systems, this value is exactly t2-t1.
*/
difftime: new LuaBuiltinFunction((_sf, t2: number, t1: number): number => {
return t2 - t1;
}),
/**
* Returns a string or a table containing date and time, formatted according to the given string format.
* If the time argument is present, this is the time to be formatted (see the os.time function for a description of this value). Otherwise, date formats the current time.
* If format starts with '!', then the date is formatted in Coordinated Universal Time. After this optional character, if format is the string "*t", then date returns a table with the following fields: year, month (1–12), day (1–31), hour (0–23), min (0–59), sec (0–61, due to leap seconds), wday (weekday, 1–7, Sunday is 1), yday (day of the year, 1–366), and isdst (daylight saving flag, a boolean). This last field may be absent if the information is not available.
* If format is not "*t", then date returns the date as a string, formatted according to the same rules as the ISO C function strftime.
* If format is absent, it defaults to "%c", which gives a human-readable date and time representation using the current locale.
*/
date: new LuaBuiltinFunction((_sf, format: string, timestamp?: number) => {
const date = timestamp ? new Date(timestamp * 1000) : new Date();
// Default Lua-like format when no format string is provided
if (!format) {
return date.toDateString() + " " + date.toLocaleTimeString();
}
// Define mappings for Lua-style placeholders
const formatMap: { [key: string]: () => string } = {
// Year
"%Y": () => date.getFullYear().toString(),
"%y": () => (date.getFullYear() % 100).toString().padStart(2, "0"),
// Month
"%m": () => (date.getMonth() + 1).toString().padStart(2, "0"),
"%b": () => date.toLocaleString("en-US", { month: "short" }),
"%B": () => date.toLocaleString("en-US", { month: "long" }),
// Day
"%d": () => date.getDate().toString().padStart(2, "0"),
"%e": () => date.getDate().toString(),
// Hour
"%H": () => date.getHours().toString().padStart(2, "0"),
"%I": () => (date.getHours() % 12 || 12).toString().padStart(2, "0"),
// Minute
"%M": () => date.getMinutes().toString().padStart(2, "0"),
// Second
"%S": () => date.getSeconds().toString().padStart(2, "0"),
// AM/PM
"%p": () => date.getHours() >= 12 ? "PM" : "AM",
// Day of the week
"%A": () => date.toLocaleString("en-US", { weekday: "long" }),
"%a": () => date.toLocaleString("en-US", { weekday: "short" }),
"%w": () => date.getDay().toString(),
// Day of the year
"%j": () => {
const start = new Date(date.getFullYear(), 0, 0);
const diff = date.getTime() - start.getTime();
const dayOfYear = Math.floor(diff / ONE_DAY);
return dayOfYear.toString().padStart(3, "0");
},
// Week
"%U": () => weekNumber(date, 0, false).toString().padStart(2, "0"),
"%W": () => weekNumber(date, 1, false).toString().padStart(2, "0"),
"%V": () => weekNumber(date, 1, true).toString().padStart(2, "0"),
// Time zone
"%Z": () => {
const match = date.toTimeString().match(/\((.*)\)/);
return match ? match[1] : "";
},
"%z": () => {
const offset = -date.getTimezoneOffset();
const sign = offset >= 0 ? "+" : "-";
const absOffset = Math.abs(offset);
const hours = Math.floor(absOffset / 60).toString().padStart(
2,
"0",
);
const minutes = (absOffset % 60).toString().padStart(2, "0");
return `${sign}${hours}${minutes}`;
},
// Literal %
"%%": () => "%",
};
// Replace format placeholders with corresponding values
return format.replace(/%[A-Za-z%]/g, (match) => {
const formatter = formatMap[match];
return formatter ? formatter() : match;
});
}),
});
+22
View File
@@ -0,0 +1,22 @@
local function assert_equal(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
end
end
-- Basic OS functions
assert(os.time() > 0)
assert(os.date("%Y-%m-%d", os.time({ year = 2020, month = 1, day = 1 })) == "2020-01-01")
-- Week calculations
assert(os.date("%U %V %W", os.time({ year = 2051, month = 1, day = 1 })) == "01 52 00")
local t = os.time()
assert(os.difftime(t+10, t) == 10)
assert(os.difftime(t, t+10) == -10)
assert(os.difftime(t, t) == 0)
assert(os.date("%U %V %W", os.time({ year = 2024, month = 1, day = 7 })) == "01 01 01")
assert(os.date("%U %V %W", os.time({ year = 2025, month = 1, day = 5 })) == "01 01 00")
assert(os.date("%U %V %W", os.time({ year = 2025, month = 1, day = 6 })) == "01 02 01")
assert(os.date("%U %V %W", os.time({ year = 2025, month = 1, day = 7 })) == "01 02 01")
assert(os.date("%U %V %W", os.time({ year = 2025, month = 5, day = 3 })) == "17 18 17")
+643
View File
@@ -0,0 +1,643 @@
// Adapted and simplified from https://github.com/adaltas/node-printf
/**
* Tokenizes a string by a regular expression with the ability to capture the delimiters
*/
const tokenize = function (
str: string,
re: RegExp,
parseDelim?: (
...args: any[]
) => any,
instance?: any,
): Array<string | TokenInfo> {
// Split a string by a regular expression with the ability to capture the delimiters
// parseDelim: Each group (excluding the 0 group) is passed as a parameter. If the function returns
// a value, it's added to the list of tokens.
// instance: Used as the "this' instance when calling parseDelim
const tokens: Array<string | TokenInfo> = [];
let match: RegExpExecArray | null;
let content: string;
let lastIndex = 0;
while ((match = re.exec(str))) {
content = str.slice(lastIndex, re.lastIndex - match[0].length);
if (content.length) {
tokens.push(content);
}
if (parseDelim) {
const parsed = parseDelim.apply(
instance,
[...match.slice(1), tokens.length],
);
if (typeof parsed !== "undefined") {
if (parsed.specifier === "%") {
tokens.push("%");
} else {
tokens.push(parsed);
}
}
}
lastIndex = re.lastIndex;
}
content = str.slice(lastIndex);
if (content.length) {
tokens.push(content);
}
return tokens;
};
interface TokenInfo {
mapping?: string;
intmapping?: string;
flags: string;
_minWidth?: string;
period?: string;
_precision?: string;
specifier: string;
arg?: string;
compiled?: boolean;
sign?: string;
zeroPad?: boolean;
rightJustify?: boolean;
alternative?: boolean;
minWidth?: number;
maxWidth?: number;
toUpper?: boolean;
isUnsigned?: boolean;
isInt?: boolean;
isDouble?: boolean;
isObject?: boolean;
precision?: number;
base?: number;
doubleNotation?: string;
setArg?: (token: TokenInfo) => void;
setMaxWidth?: (token: TokenInfo) => void;
extend?: string[];
[key: string]: any; // Allow dynamic property access
}
interface SpecifierInfo {
base?: number;
isInt?: boolean;
extend?: string[];
toUpper?: boolean;
isUnsigned?: boolean;
setArg?: (token: TokenInfo) => void;
setMaxWidth?: (token: TokenInfo) => void;
isDouble?: boolean;
doubleNotation?: string;
isObject?: boolean;
[key: string]: any; // Allow dynamic property access
}
class Formatter {
private _mapped: boolean;
private _format: string;
private _tokens: Array<string | TokenInfo>;
private _zeros10: string = "0000000000";
private _spaces10: string = " ";
private _re: RegExp =
/\%(?:\(([\w_.]+)\)|([1-9]\d*)\$)?([0 +\-\#]*)(\*|\d+)?(?:(\.)(\*|\d+)?)?[hlL]?([\%bscdeEfFgGioOuxX])/g;
private _specifiers: Record<string, SpecifierInfo> = {
b: {
base: 2,
isInt: true,
},
o: {
base: 8,
isInt: true,
},
x: {
base: 16,
isInt: true,
},
X: {
extend: ["x"],
toUpper: true,
},
d: {
base: 10,
isInt: true,
},
i: {
extend: ["d"],
},
u: {
extend: ["d"],
isUnsigned: true,
},
c: {
setArg: function (token: TokenInfo): void {
if (!isNaN(Number(token.arg))) {
const num = parseInt(token.arg as string);
if (num < 0 || num > 127) {
throw new Error("invalid character code passed to %c in printf");
}
token.arg = isNaN(num) ? "" + num : String.fromCharCode(num);
}
},
},
s: {
setMaxWidth: function (token: TokenInfo): void {
token.maxWidth = (token.period === ".") ? token.precision ?? -1 : -1;
},
},
e: {
isDouble: true,
doubleNotation: "e",
},
E: {
extend: ["e"],
toUpper: true,
},
f: {
isDouble: true,
doubleNotation: "f",
},
F: {
extend: ["f"],
},
g: {
isDouble: true,
doubleNotation: "g",
},
G: {
extend: ["g"],
toUpper: true,
},
O: {
isObject: true,
},
};
constructor(format: string) {
this._mapped = false;
this._format = format;
this._tokens = tokenize(format, this._re, this._parseDelim, this);
}
// The old regexp `/\%(?:\(([\w_.]+)\)|([1-9]\d*)\$)?([0 +\-\#]*)(\*|\d+)?(\.)?(\*|\d+)?[hlL]?([\%bscdeEfFgGioOuxX])/` has a cubic worst-case time complexity behavior due to overlapping capture groups `([0 +\-\#]*)(\*|\d+)?(\.)?(\*|\d+)?`. And a pump string of 0 can be consumed by `([0 +\-\#]*), (\*|\d+)?, or (\*|\d+)?`.
// The solution replace the sub-regexp (\*|\d+)?(\.)?(\*|\d+)? with the sub-regexp `(\*|\d+)?(?:(\.)(\*|\d+)?)?`, see the figure in [#32](https://github.com/adaltas/node-printf/pull/32)
// There are also performance improvement, see in [#31](https://github.com/adaltas/node-printf/issues/31#issuecomment-776731490)
private _parseDelim(
mapping?: string,
intmapping?: string,
flags?: string,
minWidth?: string,
period?: string,
precision?: string,
specifier?: string,
): TokenInfo {
if (mapping) {
this._mapped = true;
}
return {
mapping,
intmapping,
flags: flags || "",
_minWidth: minWidth,
period,
_precision: precision,
specifier: specifier || "",
};
}
format(...args: any[]): string {
if (this._mapped && typeof args[0] !== "object") {
throw new Error("format requires a mapping");
}
let str = "";
let position = 0;
for (let i = 0; i < this._tokens.length; i++) {
const token = this._tokens[i];
if (typeof token === "string") {
str += token;
} else {
if (this._mapped) {
// Identify value of property defined in `token.mapping`
const tokens = token.mapping!.split(".");
let value = args[0];
for (let j = 0, c = tokens.length; j < c; j++) {
value = value[tokens[j]];
if (typeof value === "undefined") {
break;
}
}
if (typeof value === "undefined") {
throw new Error("missing key '" + token.mapping + "'");
}
token.arg = String(value);
} else {
if (token.intmapping) {
position = parseInt(token.intmapping) - 1;
}
if (position >= args.length) {
throw new Error(
"got " + args.length +
" printf arguments, insufficient for '" + this._format + "'",
);
}
token.arg = String(args[position++]);
}
if (!token.compiled) {
token.compiled = true;
token.sign = "";
token.zeroPad = false;
token.rightJustify = false;
token.alternative = false;
const flags: Record<string, boolean> = {};
for (let fi = token.flags.length; fi--;) {
const flag = token.flags.charAt(fi);
flags[flag] = true;
switch (flag) {
case " ":
token.sign = " ";
break;
case "+":
token.sign = "+";
break;
case "0":
token.zeroPad = (flags["-"]) ? false : true;
break;
case "-":
token.rightJustify = true;
token.zeroPad = false;
break;
case "#":
token.alternative = true;
break;
default:
throw Error(
"bad formatting flag '" + token.flags.charAt(fi) + "'",
);
}
}
token.minWidth = (token._minWidth) ? parseInt(token._minWidth) : 0;
token.maxWidth = -1;
token.toUpper = false;
token.isUnsigned = false;
token.isInt = false;
token.isDouble = false;
token.isObject = false;
token.precision = 1;
if (token.period === ".") {
if (token._precision) {
token.precision = parseInt(token._precision);
} else {
token.precision = 0;
}
}
const mixins = this._specifiers[token.specifier];
if (typeof mixins === "undefined") {
throw new Error("unexpected specifier '" + token.specifier + "'");
}
if (mixins.extend) {
const s = this._specifiers[mixins.extend[0]];
for (const k in s) {
mixins[k] = s[k];
}
delete mixins.extend;
}
for (const l in mixins) {
token[l] = mixins[l];
}
}
if (typeof token.setArg === "function") {
token.setArg(token);
}
if (typeof token.setMaxWidth === "function") {
token.setMaxWidth(token);
}
if (token._minWidth === "*") {
if (this._mapped) {
throw new Error("* width not supported in mapped formats");
}
token.minWidth = parseInt(String(args[position++]));
if (isNaN(token.minWidth)) {
throw new Error(
"the argument for * width at position " + position +
" is not a number in " + this._format,
);
}
// negative width means rightJustify
if (token.minWidth < 0) {
token.rightJustify = true;
token.minWidth = -token.minWidth;
}
}
if (token._precision === "*" && token.period === ".") {
if (this._mapped) {
throw new Error("* precision not supported in mapped formats");
}
token.precision = parseInt(String(args[position++]));
if (isNaN(token.precision)) {
throw Error(
"the argument for * precision at position " + position +
" is not a number in " + this._format,
);
}
// negative precision means unspecified
if (token.precision < 0) {
token.precision = 1;
token.period = "";
}
}
if (token.isInt) {
// a specified precision means no zero padding
if (token.period === ".") {
token.zeroPad = false;
}
this.formatInt(token);
} else if (token.isDouble) {
if (token.period !== ".") {
token.precision = 6;
}
this.formatDouble(token);
} else if (token.isObject) {
this.formatObject(token);
}
this.fitField(token);
str += "" + token.arg;
}
}
return str;
}
formatInt(token: TokenInfo): void {
let i = parseInt(token.arg as string);
if (!isFinite(i)) { // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
// allow this only if arg is number
if (typeof token.arg !== "number" && typeof token.arg !== "string") {
throw new Error(
"format argument '" + token.arg +
"' not an integer; parseInt returned " + i,
);
}
//return '' + i;
i = 0;
}
// if not base 10, make negatives be positive
// otherwise, (-10).toString(16) is '-a' instead of 'fffffff6'
if (i < 0 && (token.isUnsigned || token.base !== 10)) {
i = 0xffffffff + i + 1;
}
if (i < 0) {
token.arg = (-i).toString(token.base);
this.zeroPad(token);
token.arg = "-" + token.arg;
} else {
token.arg = i.toString(token.base);
// need to make sure that argument 0 with precision==0 is formatted as ''
if (!i && !token.precision) {
token.arg = "";
} else {
this.zeroPad(token);
}
if (token.sign) {
token.arg = token.sign + token.arg;
}
}
if (token.base === 16) {
if (token.alternative) {
token.arg = "0x" + token.arg;
}
token.arg = token.toUpper
? token.arg.toUpperCase()
: token.arg.toLowerCase();
}
if (token.base === 8) {
if (token.alternative && token.arg.charAt(0) !== "0") {
token.arg = "0" + token.arg;
}
}
}
formatDouble(token: TokenInfo): void {
let f = parseFloat(token.arg as string);
if (!isFinite(f)) { // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
// allow this only if arg is number
if (typeof token.arg !== "number" && typeof token.arg !== "string") {
throw new Error(
"format argument '" + token.arg +
"' not a float; parseFloat returned " + f,
);
}
// C99 says that for 'f':
// infinity -> '[-]inf' or '[-]infinity' ('[-]INF' or '[-]INFINITY' for 'F')
// NaN -> a string starting with 'nan' ('NAN' for 'F')
// this is not commonly implemented though.
//return '' + f;
f = 0;
}
switch (token.doubleNotation) {
case "e": {
token.arg = f.toExponential(token.precision ?? 1);
break;
}
case "f": {
token.arg = f.toFixed(token.precision ?? 1);
break;
}
case "g": {
// C says use 'e' notation if exponent is < -4 or is >= prec
// ECMAScript for toPrecision says use exponential notation if exponent is >= prec,
// though step 17 of toPrecision indicates a test for < -6 to force exponential.
if (Math.abs(f) < 0.0001) {
//print('forcing exponential notation for f=' + f);
token.arg = f.toExponential(
(token.precision ?? 1) > 0
? (token.precision ?? 1) - 1
: (token.precision ?? 1),
);
} else {
token.arg = f.toPrecision(token.precision ?? 1);
}
// In C, unlike 'f', 'gG' removes trailing 0s from fractional part, unless alternative format flag ('#').
// But ECMAScript formats toPrecision as 0.00100000. So remove trailing 0s.
if (!token.alternative) {
//print('replacing trailing 0 in \'' + s + '\'');
token.arg = token.arg.replace(/(\..*[^0])0*e/, "$1e");
// if fractional part is entirely 0, remove it and decimal point
token.arg = token.arg.replace(/\.0*e/, "e").replace(/\.0$/, "");
}
break;
}
default:
throw new Error(
"unexpected double notation '" + token.doubleNotation + "'",
);
}
// C says that exponent must have at least two digits.
// But ECMAScript does not; toExponential results in things like '1.000000e-8' and '1.000000e+8'.
// Note that s.replace(/e([\+\-])(\d)/, 'e$10$2') won't work because of the '$10' instead of '$1'.
// And replace(re, func) isn't supported on IE50 or Safari1.
token.arg = token.arg.replace(/e\+(\d)$/, "e+0$1").replace(
/e\-(\d)$/,
"e-0$1",
);
// if alt, ensure a decimal point
if (token.alternative) {
token.arg = token.arg.replace(/^(\d+)$/, "$1.");
token.arg = token.arg.replace(/^(\d+)e/, "$1.e");
}
if (f >= 0 && token.sign) {
token.arg = token.sign + token.arg;
}
token.arg = token.toUpper
? token.arg.toUpperCase()
: token.arg.toLowerCase();
}
formatObject(token: TokenInfo): void {
// Simple object formatting without util.inspect
if (token.arg === null) {
token.arg = "null";
return;
}
if (token.arg === undefined) {
token.arg = "undefined";
return;
}
// Use JSON.stringify with indentation for objects
try {
// Limit depth based on precision if specified
const maxDepth = token.period === "." && token.precision !== undefined
? token.precision
: 2;
// Create a simplified version of the object with limited depth
const simplifiedObj = this.limitObjectDepth(token.arg, maxDepth);
// Format with indentation if alternative flag is set
token.arg = JSON.stringify(
simplifiedObj,
null,
token.alternative ? 2 : 0,
);
} catch {
// Fallback for circular references or other JSON.stringify errors
token.arg = String(token.arg);
}
}
// Helper method to limit object depth for formatObject
private limitObjectDepth(obj: any, maxDepth: number, currentDepth = 0): any {
if (currentDepth >= maxDepth) {
if (Array.isArray(obj)) {
return "[Array]";
} else if (typeof obj === "object" && obj !== null) {
return "[Object]";
}
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) =>
this.limitObjectDepth(item, maxDepth, currentDepth + 1)
);
} else if (typeof obj === "object" && obj !== null) {
const result: Record<string, any> = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = this.limitObjectDepth(
obj[key],
maxDepth,
currentDepth + 1,
);
}
}
return result;
}
return obj;
}
zeroPad(token: TokenInfo, length?: number): void {
length = (arguments.length === 2) ? length : token.precision;
let negative = false;
if (typeof token.arg !== "string") {
token.arg = "" + token.arg;
}
if (token.arg.substr(0, 1) === "-") {
negative = true;
token.arg = token.arg.substr(1);
}
const tenless = (length || 0) - 10;
while (token.arg.length < tenless) {
token.arg = (token.rightJustify)
? token.arg + this._zeros10
: this._zeros10 + token.arg;
}
const pad = (length || 0) - token.arg.length;
token.arg = (token.rightJustify)
? token.arg + this._zeros10.substring(0, pad)
: this._zeros10.substring(0, pad) + token.arg;
if (negative) token.arg = "-" + token.arg;
}
fitField(token: TokenInfo): void {
if (
token.maxWidth !== undefined && token.maxWidth >= 0 &&
token.arg!.length > token.maxWidth
) {
token.arg = token.arg!.substring(0, token.maxWidth);
}
if (token.zeroPad) {
this.zeroPad(token, token.minWidth);
return;
}
this.spacePad(token);
}
spacePad(token: TokenInfo, length?: number): void {
length = (arguments.length === 2) ? length : token.minWidth;
if (typeof token.arg !== "string") {
token.arg = "" + token.arg;
}
const tenless = (length || 0) - 10;
while (token.arg.length < tenless) {
token.arg = (token.rightJustify)
? token.arg + this._spaces10
: this._spaces10 + token.arg;
}
const pad = (length || 0) - token.arg.length;
token.arg = (token.rightJustify)
? token.arg + this._spaces10.substring(0, pad)
: this._spaces10.substring(0, pad) + token.arg;
}
}
/**
* Printf-style string formatting function
* @param format The format string
* @param args Values to be formatted
* @returns Formatted string
*/
export default function (format: string, ...args: any[]): string {
const formatter = new Formatter(format);
return formatter.format(...args);
}
+153
View File
@@ -0,0 +1,153 @@
import { parseExpressionString } from "../parse.ts";
import type { LuaExpression } from "../ast.ts";
import { evalExpression } from "../eval.ts";
import {
LuaBuiltinFunction,
LuaEnv,
LuaRuntimeError,
type LuaStackFrame,
LuaTable,
luaToString,
luaValueToJS,
singleResult,
} from "../runtime.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()) {
env.setLocal(key, envAugmentation.rawGet(key));
}
}
return env;
}
/**
* 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);
const luaResult = await luaValueToJS(
singleResult(await evalExpression(parsedExpr, env, sf)),
sf,
);
result += await luaToString(luaResult);
} catch (e: any) {
throw new LuaRuntimeError(
`Error evaluating "${expr}": ${e.message}`,
sf,
);
}
currentIndex = endIndex + 1;
}
return result;
}
export const spaceluaApi = new LuaTable({
/**
* 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(
(_sf, luaExpression: string) => {
return parseExpressionString(luaExpression);
},
),
/**
* 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(
async (sf, parsedExpr: LuaExpression, envAugmentation?: LuaTable) => {
const env = createAugmentedEnv(sf, envAugmentation);
return luaValueToJS(await evalExpression(parsedExpr, env, sf), sf);
},
),
/**
* Interpolates a string with lua expressions and returns the result.
*/
interpolate: new LuaBuiltinFunction(
(sf, template: string, envAugmentation?: LuaTable) => {
return interpolateLuaString(sf, template, envAugmentation);
},
),
/**
* Returns your SilverBullet instance's base URL, or `undefined` when run on the server
*/
baseUrl: new LuaBuiltinFunction(
() => {
// Deal with Deno
if (typeof location === "undefined") {
return null;
} else {
return location.protocol + "//" + location.host;
}
},
),
});
+8
View File
@@ -0,0 +1,8 @@
local parsedExpr = spacelua.parseExpression("1 + 1")
local evalResult = spacelua.evalExpression(parsedExpr)
assert(evalResult == 2, "Eval should return 2")
-- Slightly more advanced example with augmented environment
local parsedExpr = spacelua.parseExpression("tostring(a + 1)")
local evalResult = spacelua.evalExpression(parsedExpr, { a = 1 })
assert(evalResult == "2", "Eval should return 2 as a string")
+274
View File
@@ -0,0 +1,274 @@
import {
jsToLuaValue,
LuaBuiltinFunction,
LuaMultiRes,
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 {
str = `${repl}`.replace(/%([0-9])/g, (_, i) => match[i]);
}
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);
};
}),
});
+193
View File
@@ -0,0 +1,193 @@
local function assertEqual(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
end
end
-- Basic string functions
assert(string.len("Hello") == 5)
assert(string.byte("Hello", 1) == 72)
assert(string.char(72) == "H")
assert(string.find("Hello", "l") == 3)
assert(string.rep("Hello", 3) == "HelloHelloHello")
assert(string.sub("Hello", 2, 4) == "ell")
assert(string.upper("Hello") == "HELLO")
assert(string.lower("Hello") == "hello")
-- string.sub tests
assertEqual(string.sub("hello", 1), "hello")
assertEqual(string.sub("hello", 1, 1), "h")
-- with negative indeses
assertEqual(string.sub("hello", -1), "o")
assertEqual(string.sub("hello", -2), "lo")
assertEqual(string.sub("hello", -2, -1), "lo")
-- Invoke string metatable methods
assertEqual(("hello"):len(), 5)
assertEqual(("hello"):upper(), "HELLO")
-- Test string.gsub with various replacement types
-- Simple string replacement
local result, count = string.gsub("hello world", "hello", "hi")
assert(result == "hi world", "Basic string replacement failed")
assert(count == 1, "Basic replacement count failed")
-- https://github.com/silverbulletmd/silverbullet/issues/1326
result, count = ("abc|de"):gsub("|", "-")
assert(result == "abc-de", "replacements failed [got: " .. result .. ", expected: abc-de]")
assert(count == 1, "replacement count failed")
result, count = ("abc\\|de|"):gsub("|", "-")
assert(result == "abc\\-de-", "replacements failed [got: " .. result .. ", expected: abc\\-de-]")
assert(count == 2, "replacement count failed")
-- Multiple replacements
result, count = string.gsub("hello hello hello", "hello", "hi")
assert(result == "hi hi hi", "Multiple replacements failed")
assert(count == 3, "Multiple replacement count failed")
-- Limited replacements with n parameter
result, count = string.gsub("hello hello hello", "hello", "hi", 2)
assert(result == "hi hi hello", "Limited replacements failed")
assert(count == 2, "Limited replacement count failed")
-- Function replacement without captures
result = string.gsub("hello world", "hello", function(match)
assert(match == "hello", "Function received incorrect match")
return string.upper(match)
end)
assertEqual(result, "HELLO world", "Function replacement without captures failed")
-- Function replacement with single capture
result = string.gsub("hello world", "(h)ello", function(h)
assertEqual(h, "h", "Function received incorrect capture")
return string.upper(h) .. "i"
end)
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)
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)
assert(result == "HEllo world", "Function replacement with multiple captures failed")
-- Function returning nil (should keep original match)
result = string.gsub("hello world", "hello", function() return nil end)
assert(result == "hello world", "Function returning nil failed")
-- Pattern with multiple matches on same position
result = string.gsub("hello world", "h?e", "X")
assert(result == "Xllo world", "Overlapping matches failed")
-- Empty captures
result = string.gsub("hello", "(h()e)", function(full, empty)
assert(full == "he" and empty == "", "Empty capture handling failed")
return "XX"
end)
assert(result == "XXllo", "Empty capture replacement failed")
-- Patterns with magic characters
result = string.gsub("hello.world", "%.", "-")
assert(result == "hello-world", "Magic character replacement failed")
-- Test string.match
local m1, m2 = string.match("hello world", "(h)(ello)")
assertEqual(m1, "h")
assertEqual(m2, "ello")
-- Test with pattern with character class
assertEqual(string.match("c", "[abc]"), "c")
-- Test match with init position - need to capture the group
local initMatch = string.match("hello world", "(world)", 7)
assertEqual(initMatch, "world")
-- Test string.gmatch
local words = {}
for word in string.gmatch("hello world lua", "%w+") do
table.insert(words, word)
end
assertEqual(words[1], "hello")
assertEqual(words[2], "world")
assertEqual(words[3], "lua")
-- with capture
local captures = {}
for k, v in string.gmatch("from=world, to=Lua", "(%w+)=(%w+)") do
captures[k] = v
end
assertEqual(captures.from, "world")
assertEqual(captures.to, "Lua")
-- Test string.reverse
assertEqual(string.reverse("hello"), "olleh")
assertEqual(string.reverse(""), "")
-- Test string.split
local parts = string.split("a,b,c", ",")
assertEqual(parts[1], "a")
assertEqual(parts[2], "b")
assertEqual(parts[3], "c")
-- Test non-standard string extensions
assertEqual(string.startsWith("hello world", "hello"), true)
assertEqual(string.startsWith("hello world", "world"), false)
assertEqual(string.endsWith("hello world", "world"), true)
assertEqual(string.endsWith("hello world", "hello"), false)
-- Extended string.match tests
-- Basic pattern matching
assertEqual(string.match("hello", "h"), "h")
assertEqual(string.match("hello", "hello"), "hello")
-- Test with no matches
assertEqual(string.match("hello", "x"), nil)
-- Test with captures
local m1, m2 = string.match("hello", "(h)(ello)")
assertEqual(m1, "h")
assertEqual(m2, "ello")
-- Test with init position
local initMatch = string.match("hello world", "(world)", 7)
assertEqual(initMatch, "world")
-- Test init position with no match
assertEqual(string.match("hello world", "hello", 7), nil)
-- Test pattern characters
assertEqual(string.match("123", "%d+"), "123")
assertEqual(string.match("abc123", "%a+"), "abc")
assertEqual(string.match(" abc", "%s+"), " ")
-- Test multiple captures
local year, month, day = string.match("2024-03-14", "(%d+)%-(%d+)%-(%d+)")
assertEqual(year, "2024")
assertEqual(month, "03")
assertEqual(day, "14")
-- Test escaped hyphen at the end of a pattern
assertEqual(string.match("4-", "%d%-"), "4-")
-- Test optional captures
local word = string.match("The quick brown fox", "%s*(%w+)%s*")
assertEqual(word, "The")
-- Test matchRegexAll
local matches = {}
for match in string.matchRegexAll("hellolllbl", "(l+)") do
table.insert(matches, match)
end
assertEqual(#matches, 3)
assertEqual(matches[1][1], "ll")
assertEqual(matches[2][1], "lll")
assertEqual(matches[3][1], "l")
-- https://community.silverbullet.md/t/test-if-string-find-outputs-nil-not-working-as-expected/2342
assert(string.find("a", "b") == nil)
assert(not (string.find("a", "b") ~= nil))
assert(string.match("a", "b") == nil)
assert(not (string.match("a", "b") ~= nil))
+624
View File
@@ -0,0 +1,624 @@
local function assertEqual(a, b, message)
if a ~= b then
if a == nil then
a = "nil"
end
if b == nil then
b = "nil"
end
error("Assertion failed: " .. a .. " is not equal to " .. b .. ". " .. message)
end
end
local function assertTrue(a, message)
if not a then
error("Assertion failed: " .. message)
end
end
--------------------------------------------------------------------------
-- Moonshine - a Lua virtual machine.
--
-- Email: moonshine@gamesys.co.uk
-- http://moonshinejs.org
--
-- Copyright (c) 2013-2015 Gamesys Limited. All rights reserved.
--
-- Permission is hereby granted, free of charge, to any person obtaining
-- a copy of this software and associated documentation files (the
-- "Software"), to deal in the Software without restriction, including
-- without limitation the rights to use, copy, modify, merge, publish,
-- distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to
-- the following conditions:
--
-- The above copyright notice and this permission notice shall be
-- included in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--
-- STRING FUNCTIONS
-- byte
local a, b = string.byte('Mo0')
assertTrue(a == 77, 'string.byte() should return the numerical code for the first character in the first returned item')
assertTrue(b == nil, 'string.byte() should return only one item when only no length is given [1]')
local a, b = string.byte('Mo0', 2)
assertTrue(a == 111,
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [1]')
assertTrue(b == nil, 'string.byte() should return only one item when only no length is given [2]')
local a, b, c = string.byte('Mo0', 2, 3)
assertTrue(a == 111,
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [2]')
assertTrue(b == 48,
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [3]')
assertTrue(c == nil,
'string.byte() should return only the number of items specified in the length argument or the up to the end of the string, whichever is encountered first [1]')
local a, b, c = string.byte('Mo0', 3, 20)
assertTrue(a == 48,
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [4]')
assertTrue(b == nil,
'string.byte() should return only the number of items specified in the length argument or the up to the end of the string, whichever is encountered first [2]')
-- char
local a = string.char()
local b = string.char(116, 101, 115, 116, 105, 99, 108, 101, 115)
assertTrue(a == '', 'string.byte() should return an empty string when called with no arguments')
assertTrue(b == 'testicles',
'string.byte() should return a string comprising of characters representing by the value each of the arguments passed')
-- dump
-- local f = function () end
-- local a = string.dump(f)
-- assertTrue (type(a) == 'string', 'string.dump() should return a string when called with a function')
-- local s = string.dump(function () return 'bar' end)
-- f = loadstring(s)
-- assertTrue (type(f) == 'function', 'loadstring() should create a function from the output of string.dump() [1]')
-- result = f()
-- assertTrue (result == 'bar', 'The result of loadstring(string.dump(f)) should behave the same as f() [1]')
-- function namedFuncWithParams (a, b)
-- return a..b
-- end
-- s = string.dump(namedFuncWithParams)
-- f = loadstring(s)
-- assertTrue (type(f) == 'function', 'loadstring() should create a function from the output of string.dump() [2]')
-- result = f('hel','lo')
-- assertTrue (result == 'hello', 'The result of loadstring(string.dump(f)) should behave the same as f() [2]')
-- find
local a = 'The quick brown fox'
local b = string.find(a, 'quick');
local c = string.find(a, 'fox');
local d = string.find(a, 'kipper');
local e = string.find(a, '');
local f = string.find(a, 'quick', 8);
local g = string.find(a, 'fox', 8);
assertTrue(b == 5,
'string.find() should return the location of the first occurrence of the second argument within the first, if it is present [1]')
assertTrue(c == 17,
'string.find() should return the location of the first occurrence of the second argument within the first, if it is present [2]')
assertTrue(d == nil, 'string.find() should return nil if the second argument is not contained within the first [1]')
assertTrue(e == 1, 'string.find() should return return 1 if the second argument is an empty string')
assertTrue(f == nil,
'string.find() should return nil if the second argument is not contained within the first after the index specified by the third argument')
assertTrue(g == 17,
'string.find() should return the location of the second argument if it is contained within the first after the index specified by the third argument')
local b, c, d, e = string.find(a, 'q(.)(.)');
assertEqual(b, 5,
'string.find() should return the location of the first occurrence of the second argument within the first, if it is present [3]')
assertEqual(c, 7,
'string.find() should return the location of the last character of the first occurrence of the second argument within the first, if it is present')
assertEqual(d, 'u', 'string.find() should return the groups that are specified in the regex. [1]')
assertEqual(e, 'i', 'string.find() should return the groups that are specified in the regex. [2]')
b = string.find('[', '[_%w]')
assertTrue(b == nil, 'string.find() should not return the location of special syntax [ and ].')
-- -- format
-- do
-- local a = string.format("%s %q", "Hello", "Lua user!")
-- local b = string.format("%c%c%c", 76,117,97) -- char
-- local c = string.format("%e, %E", math.pi,math.pi) -- exponent
-- local d1 = string.format("%f", math.pi) -- float
-- local d2 = string.format("%g", math.pi) -- compact float
-- -- issues:
-- local e = string.format("%d, %i, %u", -100,-100,-100) -- signed, signed, unsigned integer
-- local f = string.format("%o, %x, %X", -100,-100,-100) -- octal, hex, hex
-- local g = string.format("%%s", 100)
-- assertTrue (a == 'Hello "Lua user!"', 'string.format() should format %s and %q correctly')
-- assertTrue (b == 'Lua', 'string.format() should format %c correctly')
-- assertTrue (d1 == '3.141593', 'string.format() should format %f correctly')
-- -- assertTrue (e == '-100, -100, 4294967196', 'string.format() should format %d, %i and %u correctly')
-- -- assertTrue (f == '37777777634, ffffff9c, FFFFFF9C', 'string.format() should format %o, %x and %X correctly')
-- -- assertTrue (e == '-100, -100, 18446744073709551516', 'string.format() should format %d, %i and %u correctly')
-- -- assertTrue (f == '1777777777777777777634, ffffffffffffff9c, FFFFFFFFFFFFFF9C', 'string.format() should format %o, %x and %X correctly')
-- assertTrue (g == '%s', 'string.format() should format %% correctly')
-- -- TODO!!!
-- -- assertTrue (c == '3.141593e+00, 3.141593E+00', 'string.format() should format %e and %E correctly')
-- -- assertTrue (d2 == '3.14159', 'string.format() should format %g correctly')
-- a = function () string.format("%*", 100) end
-- b = function () string.format("%l", 100) end
-- c = function () string.format("%L", 100) end
-- d = function () string.format("%n", 100) end
-- e = function () string.format("%p", 100) end
-- f = function () string.format("%h", 100) end
-- assertTrue (not pcall(a), 'string.format() should error when passed %*')
-- assertTrue (not pcall(b), 'string.format() should error when passed %l')
-- assertTrue (not pcall(c), 'string.format() should error when passed %L')
-- assertTrue (not pcall(d), 'string.format() should error when passed %n')
-- assertTrue (not pcall(e), 'string.format() should error when passed %p')
-- assertTrue (not pcall(f), 'string.format() should error when passed %h')
-- a = string.format("%.3f", 5.1)
-- b = "Lua version " .. string.format("%.1f", 5.1)
-- c = string.format("pi = %.4f", math.pi)
-- f = string.format("%.3f", 5)
-- local d, m, y = 5, 11, 1990
-- e = string.format("%02d/%02d/%04d", d, m, y)
-- assertTrue (a == '5.100', 'string.format() should format floating point numbers correctly[1]')
-- assertTrue (b == 'Lua version 5.1', 'string.format() should format floating point numbers correctly[2]')
-- assertTrue (c == 'pi = 3.1416', 'string.format() should format floating point numbers correctly[3]')
-- assertTrue (e == '05/11/1990', 'string.format() should format decimals correctly [0]')
-- assertTrue (f == '5.000', 'string.format() should format floating point numbers correctly[4]')
-- a = function () string.format('%#####s', 'x') end
-- b = function () string.format('%######s', 'x') end
-- assertTrue (pcall(a), 'string.format() should handle five flags')
-- assertTrue (not pcall(b), 'string.format() should not handle six flags')
-- local tag, title = "h1", "a title"
-- a = string.format("<%s>%s</%s>", tag, title, tag)
-- b = string.format("%8s", "Lua")
-- c = string.format("%.8s", "Lua")
-- d = string.format("%.2s", "Lua")
-- e = string.format("%8.2s", "Lua")
-- f = string.format("%+8.2s", "Lua")
-- g = string.format("%-8.2s", "Lua")
-- local h = string.format("%08.2s", "Lua")
-- local i = string.format("%#8.2s", "Lua")
-- local j = string.format("% 8.2s", "Lua")
-- local k = string.format("%+-0# 8.2s", "Lua")
-- local l = string.format("%0.2s", "Lua")
-- assertTrue (a == '<h1>a title</h1>', 'string.format() should format strings correctly[1]')
-- assertTrue (b == ' Lua', 'string.format() should format strings correctly[2]')
-- assertTrue (c == 'Lua', 'string.format() should format strings correctly[3]')
-- assertTrue (d == 'Lu', 'string.format() should format strings correctly[4]')
-- assertTrue (e == ' Lu', 'string.format() should format strings correctly[5]')
-- assertTrue (f == ' Lu', 'string.format() should format strings correctly[6]')
-- assertTrue (g == 'Lu ', 'string.format() should format strings correctly[7]')
-- assertTrue (h == '000000Lu', 'string.format() should format strings correctly[8]')
-- assertTrue (i == ' Lu', 'string.format() should format strings correctly[9]')
-- assertTrue (j == ' Lu', 'string.format() should format strings correctly[10]')
-- assertTrue (k == 'Lu ', 'string.format() should format strings correctly[11]')
-- assertTrue (l == 'Lu', 'string.format() should format strings correctly[12]')
-- a = string.format("%8d", 123.45)
-- b = string.format("%.8d", 123.45)
-- c = string.format("%.2d", 123.45)
-- d = string.format("%8.2d", 123.45)
-- e = string.format("%+8.2d", 123.45)
-- f = string.format("%-8.2d", 123.45)
-- g = string.format("%08.2d", 123.45)
-- h = string.format("%#8.2d", 123.45)
-- i = string.format("% 8.2d", 123.45)
-- j = string.format("%+-0# 8.2d", 123.45)
-- k = string.format("%0.2d", 123.45)
-- l = string.format("%+.8d", 123.45)
-- local m = string.format("%-.8d", 123.45)
-- local n = string.format("%#.8d", 123.45)
-- local o = string.format("%0.8d", 123.45)
-- local p = string.format("% .8d", 123.45)
-- local q = string.format("%+-#0 .8d", 123.45)
-- local r = string.format("%8.5d", 123.45)
-- local s = string.format("%+8.5d", 123.45)
-- local t = string.format("%-8.5d", 123.45)
-- local u = string.format("%-+8.5d", 123.45)
-- local v = string.format("%5d", 12.3e10)
-- local w = string.format("%.d", 123.45)
-- assertTrue (a == ' 123', 'string.format() should format decimals correctly[1]')
-- assertTrue (b == '00000123', 'string.format() should format decimals correctly[2]')
-- assertTrue (c == '123', 'string.format() should format decimals correctly[3]')
-- assertTrue (d == ' 123', 'string.format() should format decimals correctly[4]')
-- assertTrue (e == ' +123', 'string.format() should format decimals correctly[5]')
-- assertTrue (f == '123 ', 'string.format() should format decimals correctly[6]')
-- assertTrue (g == ' 123', 'string.format() should format decimals correctly[7]')
-- assertTrue (h == ' 123', 'string.format() should format decimals correctly[8]')
-- assertTrue (i == ' 123', 'string.format() should format decimals correctly[9]')
-- assertTrue (j == '+123 ', 'string.format() should format decimals correctly[10]')
-- assertTrue (k == '123', 'string.format() should format decimals correctly[11]')
-- assertTrue (l == '+00000123', 'string.format() should format decimals correctly[12]')
-- assertTrue (m == '00000123', 'string.format() should format decimals correctly[13]')
-- assertTrue (n == '00000123', 'string.format() should format decimals correctly[14]')
-- assertTrue (o == '00000123', 'string.format() should format decimals correctly[15]')
-- assertTrue (p == ' 00000123', 'string.format() should format decimals correctly[16]')
-- assertTrue (q == '+00000123', 'string.format() should format decimals correctly[17]')
-- assertTrue (r == ' 00123', 'string.format() should format decimals correctly[18]')
-- assertTrue (s == ' +00123', 'string.format() should format decimals correctly[19]')
-- assertTrue (t == '00123 ', 'string.format() should format decimals correctly[20]')
-- assertTrue (u == '+00123 ', 'string.format() should format decimals correctly[21]')
-- assertTrue (v == '123000000000', 'string.format() should format decimals correctly[22]')
-- assertTrue (w == '123', 'string.format() should format decimals correctly[23]')
-- a = string.format("%8d", -123.45)
-- b = string.format("%.8d", -123.45)
-- c = string.format("%.2d", -123.45)
-- d = string.format("%8.2d", -123.45)
-- e = string.format("%+8.2d", -123.45)
-- f = string.format("%-8.2d", -123.45)
-- g = string.format("%08.2d", -123.45)
-- h = string.format("%#8.2d", -123.45)
-- i = string.format("% 8.2d", -123.45)
-- j = string.format("%+-0# 8.2d", -123.45)
-- k = string.format("%0.2d", -123.45)
-- l = string.format("%+.8d", -123.45)
-- m = string.format("%-.8d", -123.45)
-- n = string.format("%#.8d", -123.45)
-- o = string.format("%0.8d", -123.45)
-- p = string.format("% .8d", -123.45)
-- q = string.format("%+-#0 .8d", -123.45)
-- r = string.format("%8.5d", -123.45)
-- s = string.format("%+8.5d", -123.45)
-- t = string.format("%-8.5d", -123.45)
-- u = string.format("%-+8.5d", -123.45)
-- v = string.format("%5d", -12.3e10)
-- w = string.format("%.d", -123.45)
-- assertTrue (a == ' -123', 'string.format() should format decimals correctly[31]')
-- assertTrue (b == '-00000123', 'string.format() should format decimals correctly[32]')
-- assertTrue (c == '-123', 'string.format() should format decimals correctly[33]')
-- assertTrue (d == ' -123', 'string.format() should format decimals correctly[34]')
-- assertTrue (e == ' -123', 'string.format() should format decimals correctly[35]')
-- assertTrue (f == '-123 ', 'string.format() should format decimals correctly[36]')
-- assertTrue (g == ' -123', 'string.format() should format decimals correctly[37]')
-- assertTrue (h == ' -123', 'string.format() should format decimals correctly[38]')
-- assertTrue (i == ' -123', 'string.format() should format decimals correctly[39]')
-- assertTrue (j == '-123 ', 'string.format() should format decimals correctly[40]')
-- assertTrue (k == '-123', 'string.format() should format decimals correctly[41]')
-- assertTrue (l == '-00000123', 'string.format() should format decimals correctly[42]')
-- assertTrue (m == '-00000123', 'string.format() should format decimals correctly[43]')
-- assertTrue (n == '-00000123', 'string.format() should format decimals correctly[44]')
-- assertTrue (o == '-00000123', 'string.format() should format decimals correctly[45]')
-- assertTrue (p == '-00000123', 'string.format() should format decimals correctly[46]')
-- assertTrue (q == '-00000123', 'string.format() should format decimals correctly[47]')
-- assertTrue (r == ' -00123', 'string.format() should format decimals correctly[48]')
-- assertTrue (s == ' -00123', 'string.format() should format decimals correctly[49]')
-- assertTrue (t == '-00123 ', 'string.format() should format decimals correctly[50]')
-- assertTrue (u == '-00123 ', 'string.format() should format decimals correctly[51]')
-- assertTrue (v == '-123000000000', 'string.format() should format decimals correctly[52]')
-- assertTrue (w == '-123', 'string.format() should format decimals correctly[53]')
-- a = string.format("%+05.d", 123.45)
-- b = string.format("%05d", 123.45)
-- c = string.format("%05d", -123.45)
-- d = string.format("%+05d", 123.45)
-- assertTrue (a == ' +123', 'string.format() should format decimals correctly[60]')
-- assertTrue (b == '00123', 'string.format() should format decimals correctly[61]')
-- assertTrue (c == '-0123', 'string.format() should format decimals correctly[62]')
-- assertTrue (d == '+0123', 'string.format() should format decimals correctly[63]')
-- a = string.format("%8f", 123.45)
-- b = string.format("%.8f", 123.45)
-- c = string.format("%.1f", 123.45)
-- d = string.format("%8.2f", 123.45)
-- e = string.format("%+8.2f", 123.45)
-- f = string.format("%-8.3f", 123.45)
-- g = string.format("%08.3f", 123.45)
-- h = string.format("%#8.3f", 123.45)
-- i = string.format("% 8.3f", 123.45)
-- j = string.format("%+-0# 8.2f", 123.45)
-- k = string.format("%0.2f", 123.45)
-- l = string.format("%+.8f", 123.45)
-- m = string.format("%-.8f", 123.45)
-- n = string.format("%#.8f", 123.45)
-- o = string.format("%9.3f", 123.45)
-- p = string.format("%+9.3f", 123.45)
-- q = string.format("%-9.3f", 123.45)
-- r = string.format("%-+9.3f", 123.45)
-- s = string.format("%.0f", 123.45)
-- t = string.format("%.4f", 123.05)
-- assertTrue (a == '123.450000', 'string.format() should format floats correctly[1]')
-- assertTrue (b == '123.45000000', 'string.format() should format floats correctly[2]')
-- assertTrue (c == '123.5', 'string.format() should format floats correctly[3]')
-- assertTrue (d == ' 123.45', 'string.format() should format floats correctly[4]')
-- assertTrue (e == ' +123.45', 'string.format() should format floats correctly[5]')
-- assertTrue (f == '123.450 ', 'string.format() should format floats correctly[6]')
-- assertTrue (g == '0123.450', 'string.format() should format floats correctly[7]')
-- assertTrue (h == ' 123.450', 'string.format() should format floats correctly[8]')
-- assertTrue (i == ' 123.450', 'string.format() should format floats correctly[9]')
-- assertTrue (j == '+123.45 ', 'string.format() should format floats correctly[10]')
-- assertTrue (k == '123.45', 'string.format() should format floats correctly[11]')
-- assertTrue (l == '+123.45000000', 'string.format() should format floats correctly[12]')
-- assertTrue (m == '123.45000000', 'string.format() should format floats correctly[13]')
-- assertTrue (n == '123.45000000', 'string.format() should format floats correctly[14]')
-- assertTrue (o == ' 123.450', 'string.format() should format floats correctly[15]')
-- assertTrue (p == ' +123.450', 'string.format() should format floats correctly[16]')
-- assertTrue (q == '123.450 ', 'string.format() should format floats correctly[17]')
-- assertTrue (r == '+123.450 ', 'string.format() should format floats correctly[18]')
-- assertTrue (s == '123', 'string.format() should format floats correctly[19]')
-- assertTrue (t == '123.0500', 'string.format() should format floats correctly[20]')
-- a = string.format("%x", 123)
-- b = string.format("%x", 123.45)
-- c = string.format("%x", -123)
-- d = string.format("%4x", 123)
-- e = string.format("%.4x", 123)
-- f = string.format("%8.4x", 123)
-- g = string.format("%+8.4x", 123)
-- h = string.format("%-8.4x", 123)
-- i = string.format("%#8.4x", 123)
-- j = string.format("%08.4x", 123)
-- k = string.format("% 8.4x", 123)
-- l = string.format("%+-#0 8.4x", 123)
-- m = string.format("%08x", 123)
-- n = string.format("% x", 123)
-- assertTrue (a == '7b', 'string.format() should format hex correctly[1]')
-- assertTrue (b == '7b', 'string.format() should format hex correctly[2]')
-- assertTrue (c == 'ffffffffffffff85', 'string.format() should format hex correctly[3]')
-- assertTrue (d == ' 7b', 'string.format() should format hex correctly[4]')
-- assertTrue (e == '007b', 'string.format() should format hex correctly[5]')
-- assertTrue (f == ' 007b', 'string.format() should format hex correctly[6]')
-- assertTrue (g == ' 007b', 'string.format() should format hex correctly[7]')
-- assertTrue (h == '007b ', 'string.format() should format hex correctly[8]')
-- assertTrue (i == ' 0x007b', 'string.format() should format hex correctly[9]')
-- assertTrue (k == ' 007b', 'string.format() should format hex correctly[11]')
-- assertTrue (l == '0x007b ', 'string.format() should format hex correctly[12]')
-- assertTrue (n == '7b', 'string.format() should format hex correctly[14]')
-- a = string.format("%8.2f\n", 1.234)
-- b = string.format("\n%8.2f", 1.234)
-- c = string.format("\n%8.2f\n", 1.234)
-- assertTrue (a == ' 1.23\n', 'string.format() should correctly format patterns that contain new lines.[1]')
-- assertTrue (b == '\n 1.23', 'string.format() should correctly format patterns that contain new lines.[2]')
-- assertTrue (c == '\n 1.23\n', 'string.format() should correctly format patterns that contain new lines.[3]')
-- -- TODO!!!!
-- -- assertTrue (j == ' 007b', 'string.format() should format hex correctly[10]')
-- -- assertTrue (m == '0000007b', 'string.format() should format hex correctly[13]')
-- -- print (c)
-- end
-- gmatch
local s = "from=world, to=Lua"
local x = string.gmatch(s, "(%w+)=(%w+)")
assertTrue(type(x) == 'function', 'string.gmatch() should return an iterator function')
local a, b, c = x()
assertTrue(a == 'from', 'string.gmatch() iterator should return the first group matched in the string [1]')
assertTrue(b == 'world', 'string.gmatch() iterator should return the second group matched in the string [1]')
assertTrue(c == nil, 'string.gmatch() iterator should return nil after all groups are matched [1]')
local a, b, c = x()
assertTrue(a == 'to', 'string.gmatch() iterator should return the first group matched in the string [2]')
assertTrue(b == 'Lua', 'string.gmatch() iterator should return the second group matched in the string [2]')
assertTrue(c == nil, 'string.gmatch() iterator should return nil after all groups are matched [2]')
local a = x()
assertTrue(a == nil, 'string.gmatch() iterator should return nil after all matches have ben returned')
local x = string.gmatch(s, "%w+=%w+")
local a, b = x()
assertTrue(a == 'from=world', 'string.gmatch() iterator should return the first match when no groups are specified')
assertTrue(b == nil,
'string.gmatch() iterator should return nil as second return value when no groups are specified [1]')
local a, b = x()
assertTrue(a == 'to=Lua', 'string.gmatch() iterator should return the second match when no groups are specified')
assertTrue(b == nil,
'string.gmatch() iterator should return nil as second return value when no groups are specified [2]')
do
local x = string.gmatch(';a;', 'a*')
local a, b, c, d, e, f = x(), x(), x(), x(), x(), x();
assertEqual(a, '', 'string.gmatch() iterator should return correct values [1]')
assertEqual(b, 'a', 'string.gmatch() iterator should return correct values [2]')
assertEqual(c, '', 'string.gmatch() iterator should return correct values [3]')
-- assertEqual(d, '', 'string.gmatch() iterator should return correct values [4]')
assertEqual(e, nil, 'string.gmatch() iterator should return correct values [5]')
assertEqual(e, nil, 'string.gmatch() iterator should return correct values [6]')
end
-- gsub
a = '<%?xml version="1.0" encoding="UTF%-8"%?>'
b = '<?xml version="1.0" encoding="UTF-8"?><my-xml></my-xml>'
c = string.gsub(b, a, 'moo')
assertTrue(c == 'moo<my-xml></my-xml>', 'string.gsub() should replace the matched part of the string[1]')
-- Not even scraping the surface
a = '%%1'
b = 'Hello %1'
c = string.gsub(b, a, 'world')
assertTrue(c == 'Hello world', 'string.gsub() should replace the matched part of the string[2]')
a = '%d'
b = 'ab5kfd8scf4lll'
c = function(x)
return '(' .. x .. ')'
end
d = string.gsub(b, a, c, 2)
assertTrue(d == 'ab(5)kfd(8)scf4lll',
'string.gsub() should replace the matched part of the string with the value returned from the given map function')
a = "[^:]+"
b = ":aa:bbb:cccc:ddddd:eee:"
c = function(subStr)
end
d = string.gsub(b, a, c)
assertTrue(d == ':aa:bbb:cccc:ddddd:eee:',
'string.gsub() should not replace the matched part of the string if the value returned from the map function is nil')
c = function(subStr)
return 'X'
end
d = string.gsub(b, a, c)
assertTrue(d == ':X:X:X:X:X:',
'string.gsub() should replace the matched part of the string if the value returned from the map function is not nil')
-- c = string.gsub(';a;', 'a*', 'ITEM')
-- assertTrue(c == 'ITEM;ITEMITEM;ITEM', 'string.gsub() should replace the matched part of the string[2]')
a = 'abc\\def'
b = string.gsub(a, '\\', '\\\\')
assertEqual(b, 'abc\\\\def', 'string.gsub() should allow backslashes')
a = "a = 'a', b = 'b', c = 'c',"
b = string.gsub(a, ",$", "")
assertEqual(b, "a = 'a', b = 'b', c = 'c'", 'string.gsub() should match $ with end of string')
-- len
local a = 'McLaren Mercedes'
local b = string.len('');
local c = string.len(a);
assertTrue(b == 0, 'string.len() should return 0 if passed an empty string')
assertTrue(c == 16, 'string.len() should return the length of the string in the first argument')
-- lower
local a = 'McLaren Mercedes'
local b = string.lower('');
local c = string.lower(a);
assertTrue(b == '', 'string.lower() should return an empty string if passed an empty string')
assertTrue(c == 'mclaren mercedes',
'string.lower() should return the string in the first argument with all character in lower case')
-- match
local a = string.match('20/11/1988', "^%d+%p%d+%p%d%d%d%d$")
assertEqual(a, '20/11/1988', 'string.match() should handle punctuation.')
local a = ('foo@bar.com'):match("^[%w+%.%-_]+@[%w+%.%-_]+%.%a%a+$")
local a = ('test-123_test.2@a-b_c.movie'):match("^[%w+%.%-_]+@[%w+%.%-_]+%.%a%a+$")
assertEqual(a, 'test-123_test.2@a-b_c.movie', 'string.match() should flatten nested groups.')
local a = ('-=[]\';'):match("%W")
assertEqual(a, '-', 'string.match() match non-word chars.')
-- rep
local a = 'Ho'
local b = string.rep(a, 0);
local c = string.rep(a, 1);
local d = string.rep(a, 3);
assertTrue(b == '', 'string.rep() should return an empty string if the second argument is 0')
assertTrue(c == 'Ho', 'string.rep() should return the first argument if the second argument is 1')
assertTrue(d == 'HoHoHo',
'string.rep() should return a string containing the first argument repeated the second argument number of times')
-- reverse
local a = string.reverse('');
local b = string.reverse('x');
local c = string.reverse('tpircSavaJ');
assertTrue(a == '', 'string.reverse() should return an empty string if passed an empty string')
assertTrue(b == 'x', 'string.reverse() should return the first argument if its length is 1')
assertTrue(c == 'JavaScript', 'string.reverse() should return a string containing the first argument reversed')
-- sub
local a = 'Pub Standards'
local b = string.sub(a, 1)
local c = string.sub(a, 5)
local d = string.sub(a, -4)
local e = string.sub(a, 1, 3)
local f = string.sub(a, 7, 9)
local g = string.sub(a, -4, -2)
local h = string.sub(a, 5, -2)
local i = string.sub(a, 0)
assertTrue(b == 'Pub Standards', 'string.sub() should return the first argument if the second argument is 1')
assertTrue(c == 'Standards',
'string.sub() should return a subset of the first argument from the nth character onwards, when n is the second argument and positive')
assertTrue(d == 'ards',
'string.sub() should return the last n characters of the first argument, where n is the absolute value of the second argument and the second argument is negative')
assertTrue(e == 'Pub',
'string.sub() should return the first n characters of the first argument when the second argument is one and n is the third argument')
assertTrue(f == 'and',
'string.sub() should return a subset of the first argument from the nth character to the mth character, when n is the second argument and positive and m is the third argument and negative')
assertTrue(h == 'Standard',
'string.sub() should return a subset of the first argument from the nth character to the last but mth character, when n is the second argument and positive and m is the third argument and negative')
assertTrue(i == 'Pub Standards',
'string.sub() should return a subset of the first argument from the last but nth character to the last but mth character, when n is the second argument and negative and m is the third argument and negative')
-- upper
local a = string.upper('');
local b = string.upper('JavaScript');
assertTrue(a == '', 'string.upper() should return an empty string if passed an empty string')
assertTrue(b == 'JAVASCRIPT', 'string.upper() should return the first argument in uppercase')
-- `string` lib as metatable of strings.
local strMeta = getmetatable('')
assertEqual(strMeta.__index, string, 'String lib should be metamethod of string instances.')
a = ('Hey'):lower()
assertEqual(a, 'hey', 'String lib should be metamethod of string instances.')
+182
View File
@@ -0,0 +1,182 @@
import {
type ILuaFunction,
LuaBuiltinFunction,
type LuaEnv,
luaEquals,
luaGet,
LuaMultiRes,
LuaRuntimeError,
LuaTable,
type LuaValue,
} from "../runtime.ts";
import { asyncQuickSort } from "../util.ts";
export const tableApi = new LuaTable({
/**
* Concatenates the elements of a table into a string, using a separator.
* @param tbl - The table to concatenate.
* @param sep - The separator to use between elements.
* @param i - The start index.
* @param j - The end index.
* @returns The concatenated string.
*/
concat: new LuaBuiltinFunction(
(_sf, tbl: LuaTable | any[], sep?: string, i?: number, j?: number) => {
sep = sep ?? "";
i = i ?? 1;
j = j ?? tbl.length;
if (Array.isArray(tbl)) {
return tbl.slice(i - 1, j).join(sep);
}
const result = [];
for (let k = i; k <= j; k++) {
result.push(tbl.get(k));
}
return result.join(sep);
},
),
/**
* Inserts an element into a table at a specified position.
* @param tbl - The table to insert the element into.
* @param posOrValue - The position or value to insert.
* @param value - The value to insert.
*/
insert: new LuaBuiltinFunction(
(_sf, tbl: LuaTable | any[], posOrValue: number | any, value?: any) => {
if (Array.isArray(tbl)) {
if (value === undefined) {
tbl.push(posOrValue);
} else {
tbl.splice(posOrValue - 1, 0, value);
}
} else if (tbl instanceof LuaTable) {
if (value === undefined) {
value = posOrValue;
posOrValue = tbl.length + 1;
}
tbl.insert(value, posOrValue);
}
},
),
/**
* Removes an element from a table at a specified position.
* @param tbl - The table to remove the element from.
* @param pos - The position of the element to remove.
*/
remove: new LuaBuiltinFunction((_sf, tbl: LuaTable | any[], pos?: number) => {
pos = pos ?? tbl.length;
if (Array.isArray(tbl)) {
tbl.splice(pos - 1, 1);
} else if (tbl instanceof LuaTable) {
tbl.remove(pos);
}
}),
/**
* Sorts a table.
* @param tbl - The table to sort.
* @param comp - The comparison function.
* @returns The sorted table.
*/
sort: new LuaBuiltinFunction(
async (sf, tbl: LuaTable | any[], comp?: ILuaFunction) => {
if (Array.isArray(tbl)) {
tbl = await asyncQuickSort(tbl, async (a, b) => {
if (comp) {
return (await comp.call(sf, a, b)) ?? 0;
} else {
return a - b;
}
});
} else {
await tbl.sort(comp, sf);
}
return tbl;
},
),
/**
* Returns the keys of a table.
* @param tbl - The table to get the keys from.
* @returns The keys of the table.
*/
keys: new LuaBuiltinFunction((_sf, tbl: LuaTable | LuaEnv | any) => {
if (tbl.keys) {
return tbl.keys();
} else {
return Object.keys(tbl);
}
}),
/**
* Checks if a table (used as an array) contains a value.
* @param tbl - The table to check.
* @param value - The value to check for.
* @returns True if the value is in the table, false otherwise.
*/
includes: new LuaBuiltinFunction(
(sf, tbl: LuaTable | any[], value: LuaValue) => {
if (!tbl) {
return false;
}
if (tbl instanceof LuaTable) {
// Iterate over the table
for (const key of tbl.keys()) {
if (luaEquals(tbl.get(key), value)) {
return true;
}
}
return false;
} else if (Array.isArray(tbl)) {
return !!tbl.find((item) => luaEquals(item, value));
} else {
throw new LuaRuntimeError(
`Cannot use includes on a non-table or non-array value`,
sf,
);
}
},
),
pack: new LuaBuiltinFunction((_sf, ...args: any[]) => {
const tbl = new LuaTable();
for (let i = 0; i < args.length; i++) {
tbl.set(i + 1, args[i]);
}
tbl.set("n", args.length);
return tbl;
}),
unpack: new LuaBuiltinFunction(
(_sf, tbl: LuaTable, i?: number, j?: number) => {
i = i ?? 1;
j = j ?? tbl.length;
const result = [];
for (let k = i; k <= j; k++) {
result.push(tbl.get(k));
}
return new LuaMultiRes(result);
},
),
// Non-standard Lua functions
/**
* Finds an element in a table that matches a criteria function. Returns the first matching element.
* @param tbl - The table to search.
* @param criteriaFn - The criteria function.
* @param fromIndex - The index to start searching from.
* @returns Lua multi value of index, value, or nil if no element is found.
*/
find: new LuaBuiltinFunction(
async (
sf,
tbl: LuaTable | any[],
criteriaFn: ILuaFunction,
fromIndex = 1,
) => {
const startIndex = fromIndex < 1 ? 1 : fromIndex;
for (let i = startIndex; i <= tbl.length; i++) {
const val = await luaGet(tbl, i, sf);
if (await criteriaFn.call(sf, val)) {
return new LuaMultiRes([i, val]);
}
}
return null;
},
),
});
+150
View File
@@ -0,0 +1,150 @@
local function assertEqual(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
end
end
-- Basic table operations
local t = { 1, 2, 3 }
table.insert(t, 4)
assertEqual(t[4], 4)
table.remove(t, 1)
table.remove(t)
assertEqual(t[2], 3)
assertEqual(#t, 2)
t = js.tojs({ 1, 2, 3 })
table.insert(t, 4)
assertEqual(t[4], 4)
table.remove(t, 1)
table.remove(t)
assertEqual(t[2], 3)
assertEqual(#t, 2)
-- Test concat
assertEqual(table.concat({ "Hello", "world" }, " "), "Hello world")
assertEqual(table.concat({ "Hello", "world", "three" }, " ", 2, 3), "world three")
-- Test with JavaScript array
assertEqual(table.concat(js.tojs({ "Hello", "world", "three" }), " ", 2, 3), "world three")
-- Table sorting
local t = { 3, 1, 2 }
table.sort(t)
assertEqual(t[1], 1)
assertEqual(t[2], 2)
assertEqual(t[3], 3)
-- Table sorting with custom comparator
table.sort(t, function(a, b)
return a > b
end)
assertEqual(t[1], 3)
assertEqual(t[2], 2)
assertEqual(t[3], 1)
-- Table sorting with complex objects
local data = { { name = "John", age = 30 }, { name = "Jane", age = 25 } }
table.sort(data, function(a, b)
return a.age < b.age
end)
assertEqual(data[1].name, "Jane")
assertEqual(data[2].name, "John")
-- Now the same with js.tojs
local data = js.tojs { 1, 3, 2 }
table.sort(data)
assertEqual(data[1], 1)
assertEqual(data[2], 2)
assertEqual(data[3], 3)
local data = js.tojs { { name = "John", age = 30 }, { name = "Jane", age = 25 } }
table.sort(data, function(a, b)
return a.age < b.age
end)
assertEqual(data[1].name, "Jane")
assertEqual(data[2].name, "John")
-- ipairs tests
local p = ipairs({ 3, 2, 1 })
local idx, value = p()
assert(idx == 1 and value == 3)
idx, value = p()
assert(idx == 2 and value == 2)
idx, value = p()
assert(idx == 3 and value == 1)
idx, value = p()
assert(idx == nil and value == nil)
for index, value in ipairs({ 1, 2, 3 }) do
assert(index == value)
end
-- pairs tests
local p = pairs({ a = 1, b = 2, c = 3 })
local key, value = p()
assert(key == "a" and value == 1)
key, value = p()
assert(key == "b" and value == 2)
key, value = p()
assert(key == "c" and value == 3)
key, value = p()
assert(key == nil and value == nil)
for key, value in pairs({ a = "a", b = "b" }) do
assertEqual(key, value)
end
-- for in over tables directly
local cnt = 1
for val in { 1, 2, 3 } do
assertEqual(val, cnt)
cnt = cnt + 1
end
assertEqual(cnt, 4)
local cnt = 1
for val in js.tojs({ 1, 2, 3 }) do
assertEqual(val, cnt)
cnt = cnt + 1
end
assertEqual(cnt, 4)
-- Table keys tests
local t = { a = 1, b = 2, c = 3 }
local keys = table.keys(t)
assert(table.includes(keys, "a"))
assert(table.includes(keys, "b"))
assert(table.includes(keys, "c"))
-- Table includes tests with different value types
local t = { 1, 2, "three", true }
assert(table.includes(t, 1))
assert(table.includes(t, "three"))
assert(table.includes(t, true))
assert(not table.includes(t, "missing"))
-- Error cases
local success, error = pcall(function()
table.includes("not a table", 1)
end)
assert(not success)
assert(string.find(error, "Cannot use includes"))
-- Test pack and unpack
local t = { 1, 2, 3 }
local packed = table.pack(table.unpack(t))
assertEqual(packed[1], 1)
assertEqual(packed[2], 2)
assertEqual(packed[3], 3)
assertEqual(packed.n, 3)
-- Test table.find
local t = { 0, 1, 2, 3 }
local idx, two = table.find(t, function(v) return v == 2 end)
assertEqual(idx, 3)
assertEqual(two, 2)
assertEqual(table.find(t, function(v) return v == 4 end), nil)