Refactor: Merging common/ into web/
This commit is contained in:
@@ -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")
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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")
|
||||
@@ -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));
|
||||
},
|
||||
),
|
||||
});
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -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")
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
),
|
||||
});
|
||||
@@ -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")
|
||||
@@ -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);
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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))
|
||||
@@ -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.')
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
});
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user