Merge branch 'main' of github.com:silverbulletmd/silverbullet into to-node

This commit is contained in:
Zef Hemel
2026-02-14 22:10:42 +01:00
8 changed files with 1430 additions and 953 deletions
-1
View File
@@ -48,7 +48,6 @@ test("[Lua] String to number tests", async () => {
test("[Lua] String tests", async () => {
await runLuaTest("./stdlib/string_test.lua");
// await runLuaTest("./stdlib/string_test2.lua");
});
test("[Lua] Space Lua tests", async () => {
+5
View File
@@ -365,4 +365,9 @@ export const osApi = new LuaTable({
return luaFormatTime(fmt, d, utc);
},
),
// Returns an approximation of CPU time used by the program in seconds.
clock: new LuaBuiltinFunction((_sf): number => {
return performance.now() / 1000.0;
}),
});
+12
View File
@@ -168,3 +168,15 @@ assert(type(err) == "string")
-- Repeated specifiers
assertEquals(os.date("!%Y%Y", ts), "20062006")
-- os.clock
-- Returns a number (seconds)
assert(type(os.clock()) == "number")
-- Must be non-negative
assert(os.clock() >= 0)
-- Monotonically non-decreasing
local c1 = os.clock()
local c2 = os.clock()
assert(c2 >= c1)
+703
View File
@@ -0,0 +1,703 @@
// Lua Optimized Pattern Matching Engine Implementation
const CH_ESC = 37; // '%'
const CH_OPEN = 40; // '('
const CH_CLOSE = 41; // ')'
const CH_DOT = 46; // '.'
const CH_DOLLAR = 36; // '$'
const CH_LBRACKET = 91; // '['
const CH_RBRACKET = 93; // ']'
const CH_CARET = 94; // '^'
const CH_DASH = 45; // '-'
const CH_STAR = 42; // '*'
const CH_PLUS = 43; // '+'
const CH_QUESTION = 63; // '?'
const CH_0 = 48; // '0'
const CH_9 = 57; // '9'
const SPECIALS_SET = new Set<number>([
CH_CARET,
CH_DOLLAR,
CH_STAR,
CH_PLUS,
CH_QUESTION,
CH_DOT,
CH_OPEN,
CH_LBRACKET,
CH_ESC,
CH_DASH,
]);
const MAX_CAPTURES = 32;
const MAX_MATCH_DEPTH = 200;
const CAP_UNFINISHED = -1;
const CAP_POSITION = -2;
interface Capture {
init: number;
len: number;
}
export interface MatchState {
src: string; // original source string (for substring extraction)
s: Uint8Array; // source bytes
slen: number;
p: Uint8Array; // pattern bytes
plen: number;
level: number;
capture: Capture[]; // pre-allocated, length `MAX_CAPTURES`
matchdepth: number;
}
function toBytes(s: string): Uint8Array {
const len = s.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = s.charCodeAt(i) & 0xFF;
}
return arr;
}
function matchClass(c: number, cl: number): boolean {
const lcl = cl | 32;
let res: boolean;
switch (lcl) {
case 97: // 'a'
res = (c >= 65 && c <= 90) || (c >= 97 && c <= 122);
break;
case 99: // 'c'
res = c < 32 || c === 127;
break;
case 100: // 'd'
res = c >= 48 && c <= 57;
break;
case 103: // 'g'
res = c > 32 && c < 127;
break;
case 108: // 'l'
res = c >= 97 && c <= 122;
break;
case 112: // 'p'
res = (c >= 33 && c <= 47) || (c >= 58 && c <= 64) ||
(c >= 91 && c <= 96) || (c >= 123 && c <= 126);
break;
case 115: // 's'
res = c === 32 || (c >= 9 && c <= 13);
break;
case 117: // 'u'
res = c >= 65 && c <= 90;
break;
case 119: // 'w'
res = (c >= 65 && c <= 90) || (c >= 97 && c <= 122) ||
(c >= 48 && c <= 57);
break;
case 120: // 'x'
res = (c >= 48 && c <= 57) || (c >= 65 && c <= 70) ||
(c >= 97 && c <= 102);
break;
case 122: // 'z'
res = c === 0;
break;
default:
return cl === c;
}
return (cl >= 97 && cl <= 122) ? res : !res;
}
function classEnd(p: Uint8Array, plen: number, pi: number): number {
const ch = p[pi];
pi++;
if (ch === CH_ESC) {
if (pi >= plen) {
throw new Error("malformed pattern (ends with '%')");
}
return pi + 1;
}
if (ch === CH_LBRACKET) {
if (pi < plen && p[pi] === CH_CARET) pi++;
do {
if (pi >= plen) {
throw new Error("malformed pattern (missing ']')");
}
if (p[pi] === CH_ESC && pi + 1 < plen) {
pi++;
}
pi++;
} while (pi < plen && p[pi] !== CH_RBRACKET);
if (pi >= plen) {
throw new Error("malformed pattern (missing ']')");
}
return pi + 1;
}
return pi;
}
function matchBracketClass(
c: number,
p: Uint8Array,
pi: number,
ec: number,
): boolean {
let sig = true;
if (p[pi + 1] === CH_CARET) {
sig = false;
pi++;
}
pi++;
while (pi < ec) {
const pch = p[pi];
if (pch === CH_ESC) {
pi++;
if (matchClass(c, p[pi])) return sig;
} else if (
pi + 2 < ec && p[pi + 1] === CH_DASH
) {
pi += 2;
if (pch <= c && c <= p[pi]) return sig;
} else if (pch === c) {
return sig;
}
pi++;
}
return !sig;
}
function singleMatch(
ms: MatchState,
si: number,
pi: number,
ep: number,
): boolean {
if (si >= ms.slen) return false;
const c = ms.s[si];
const pch = ms.p[pi];
if (pch === CH_DOT) return true;
if (pch === CH_ESC) {
return matchClass(c, ms.p[pi + 1]);
}
if (pch === CH_LBRACKET) {
return matchBracketClass(c, ms.p, pi, ep - 1);
}
return pch === c;
}
function matchBalance(
ms: MatchState,
si: number,
pi: number,
): number {
if (pi >= ms.plen - 1) {
throw new Error("malformed pattern (missing arguments to '%b')");
}
if (si >= ms.slen || ms.s[si] !== ms.p[pi]) {
return -1;
}
const b = ms.p[pi];
const e = ms.p[pi + 1];
let cont = 1;
si++;
while (si < ms.slen) {
const sc = ms.s[si];
if (sc === e) {
if (--cont === 0) return si + 1;
} else if (sc === b) {
cont++;
}
si++;
}
return -1;
}
function maxExpand(
ms: MatchState,
si: number,
pi: number,
ep: number,
): number {
let i = 0;
while (singleMatch(ms, si + i, pi, ep)) i++;
while (i >= 0) {
const res = match(ms, si + i, ep + 1);
if (res >= 0) return res;
i--;
}
return -1;
}
function minExpand(
ms: MatchState,
si: number,
pi: number,
ep: number,
): number {
for (;;) {
const res = match(ms, si, ep + 1);
if (res >= 0) return res;
if (singleMatch(ms, si, pi, ep)) {
si++;
} else {
return -1;
}
}
}
function checkCapture(ms: MatchState, l: number): number {
l -= CH_0 + 1;
if (l < 0 || l >= ms.level || ms.capture[l].len === CAP_UNFINISHED) {
throw new Error(`invalid capture index %${l + 1}`);
}
return l;
}
function captureToClose(ms: MatchState): number {
for (let level = ms.level - 1; level >= 0; level--) {
if (ms.capture[level].len === CAP_UNFINISHED) return level;
}
throw new Error("invalid pattern capture");
}
function startCapture(
ms: MatchState,
si: number,
pi: number,
what: number,
): number {
const level = ms.level;
if (level >= MAX_CAPTURES) throw new Error("too many captures");
ms.capture[level].init = si;
ms.capture[level].len = what;
ms.level = level + 1;
const res = match(ms, si, pi);
if (res < 0) ms.level--;
return res;
}
function endCapture(ms: MatchState, si: number, pi: number): number {
const l = captureToClose(ms);
const savedLen = ms.capture[l].len;
ms.capture[l].len = si - ms.capture[l].init;
const res = match(ms, si, pi);
if (res < 0) ms.capture[l].len = savedLen;
return res;
}
function matchCapture(ms: MatchState, si: number, l: number): number {
const idx = checkCapture(ms, l);
const cap = ms.capture[idx];
if (cap.len === CAP_POSITION) {
throw new Error(`invalid capture index %${idx + 1}`);
}
const len = cap.len;
if (ms.slen - si < len) return -1;
for (let k = 0; k < len; k++) {
if (ms.s[cap.init + k] !== ms.s[si + k]) return -1;
}
return si + len;
}
function match(ms: MatchState, si: number, pi: number): number {
if (ms.matchdepth-- <= 0) {
throw new Error("pattern too complex");
}
while (true) {
if (pi >= ms.plen) {
ms.matchdepth++;
return si;
}
const pch = ms.p[pi];
if (pch === CH_OPEN) {
if (pi + 1 < ms.plen && ms.p[pi + 1] === CH_CLOSE) {
si = startCapture(ms, si, pi + 2, CAP_POSITION);
} else {
si = startCapture(ms, si, pi + 1, CAP_UNFINISHED);
}
ms.matchdepth++;
return si;
}
if (pch === CH_CLOSE) {
si = endCapture(ms, si, pi + 1);
ms.matchdepth++;
return si;
}
if (pch === CH_DOLLAR && pi + 1 === ms.plen) {
ms.matchdepth++;
return si === ms.slen ? si : -1;
}
if (pch === CH_ESC && pi + 1 < ms.plen) {
const next = ms.p[pi + 1];
if (next === 98) { // 'b'
si = matchBalance(ms, si, pi + 2);
if (si >= 0) {
pi += 4;
continue;
}
ms.matchdepth++;
return -1;
}
if (next === 102) { // 'f'
pi += 2;
if (pi >= ms.plen || ms.p[pi] !== CH_LBRACKET) {
throw new Error("missing '[' after '%f' in pattern");
}
const ep = classEnd(ms.p, ms.plen, pi);
const previous = si === 0 ? 0 : ms.s[si - 1];
const current = si < ms.slen ? ms.s[si] : 0;
if (
!matchBracketClass(previous, ms.p, pi, ep - 1) &&
matchBracketClass(current, ms.p, pi, ep - 1)
) {
pi = ep;
continue;
}
ms.matchdepth++;
return -1;
}
if (next >= CH_0 && next <= CH_9) {
si = matchCapture(ms, si, next);
if (si >= 0) {
pi += 2;
continue;
}
ms.matchdepth++;
return -1;
}
}
// default: class[*+?-]?
const ep = classEnd(ms.p, ms.plen, pi);
const matched = singleMatch(ms, si, pi, ep);
if (!matched) {
if (ep < ms.plen) {
const suffix = ms.p[ep];
if (
suffix === CH_STAR || suffix === CH_QUESTION || suffix === CH_DASH
) {
pi = ep + 1;
continue;
}
}
ms.matchdepth++;
return -1;
}
if (ep < ms.plen) {
const suffix = ms.p[ep];
if (suffix === CH_QUESTION) {
const res = match(ms, si + 1, ep + 1);
if (res >= 0) {
ms.matchdepth++;
return res;
}
pi = ep + 1;
continue;
}
if (suffix === CH_PLUS) {
si++;
const res = maxExpand(ms, si, pi, ep);
ms.matchdepth++;
return res;
}
if (suffix === CH_STAR) {
const res = maxExpand(ms, si, pi, ep);
ms.matchdepth++;
return res;
}
if (suffix === CH_DASH) {
const res = minExpand(ms, si, pi, ep);
ms.matchdepth++;
return res;
}
}
si++;
pi = ep;
continue;
}
}
function createMatchState(s: string, p: string): MatchState {
const sb = toBytes(s);
const pb = toBytes(p);
const capture: Capture[] = new Array(MAX_CAPTURES);
for (let i = 0; i < MAX_CAPTURES; i++) {
capture[i] = { init: 0, len: 0 };
}
return {
src: s,
s: sb,
slen: sb.length,
p: pb,
plen: pb.length,
level: 0,
capture,
matchdepth: MAX_MATCH_DEPTH,
};
}
function resetMatchState(ms: MatchState): void {
ms.level = 0;
ms.matchdepth = MAX_MATCH_DEPTH;
}
function noSpecials(p: string): boolean {
for (let i = 0; i < p.length; i++) {
if (SPECIALS_SET.has(p.charCodeAt(i))) return false;
}
return true;
}
// Public API
export type CaptureResult = { s: string } | { position: number };
interface RawCapture {
kind: 0 | 1; // 0 = string slice, 1 = position
start: number;
len: number; // kind = 0: substring length; kind = 1: 1-based position
}
function getOneRawCapture(
ms: MatchState,
i: number,
matchStart: number,
matchEnd: number,
): RawCapture {
if (i >= ms.level) {
if (i !== 0) {
throw new Error(`invalid capture index %${i + 1}`);
}
return { kind: 0, start: matchStart, len: matchEnd - matchStart };
}
const cap = ms.capture[i];
if (cap.len === CAP_UNFINISHED) {
throw new Error("unfinished capture");
}
if (cap.len === CAP_POSITION) {
return { kind: 1, start: cap.init + 1, len: 0 };
}
return { kind: 0, start: cap.init, len: cap.len };
}
function rawToResult(ms: MatchState, raw: RawCapture): CaptureResult {
if (raw.kind === 1) {
return { position: raw.start };
}
return { s: ms.src.substring(raw.start, raw.start + raw.len) };
}
function getCaptures(
ms: MatchState,
matchStart: number,
matchEnd: number,
): CaptureResult[] {
const nlevels = ms.level === 0 ? 1 : ms.level;
const result: CaptureResult[] = [];
for (let i = 0; i < nlevels; i++) {
result.push(rawToResult(ms, getOneRawCapture(ms, i, matchStart, matchEnd)));
}
return result;
}
function getRawCaptureString(ms: MatchState, raw: RawCapture): string {
if (raw.kind === 1) {
return raw.start.toString();
}
return ms.src.substring(raw.start, raw.start + raw.len);
}
export function patternFind(
s: string,
pattern: string,
init: number = 1,
plain: boolean = false,
): { start: number; end: number; captures: CaptureResult[] } | null {
if (init < 1) init = 1;
if (init > s.length + 1) return null;
const si0 = init - 1;
if (plain || noSpecials(pattern)) {
const idx = s.indexOf(pattern, si0);
if (idx < 0) return null;
return { start: idx + 1, end: idx + pattern.length, captures: [] };
}
let p = pattern;
let anchor = false;
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
anchor = true;
p = p.substring(1);
}
const ms = createMatchState(s, p);
for (let si = si0; si <= ms.slen; si++) {
resetMatchState(ms);
const res = match(ms, si, 0);
if (res >= 0) {
const caps = ms.level === 0 ? [] : getCaptures(ms, si, res);
return { start: si + 1, end: res, captures: caps };
}
if (anchor) break;
}
return null;
}
export function patternMatch(
s: string,
pattern: string,
init: number = 1,
): CaptureResult[] | null {
if (init < 1) init = 1;
if (init > s.length + 1) return null;
const si0 = init - 1;
let p = pattern;
let anchor = false;
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
anchor = true;
p = p.substring(1);
}
const ms = createMatchState(s, p);
for (let si = si0; si <= ms.slen; si++) {
resetMatchState(ms);
const res = match(ms, si, 0);
if (res >= 0) {
return getCaptures(ms, si, res);
}
if (anchor) break;
}
return null;
}
export function patternGmatch(
s: string,
pattern: string,
init: number = 1,
): () => CaptureResult[] | null {
if (init < 1) init = 1;
let p = pattern;
let anchor = false;
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
anchor = true;
p = p.substring(1);
}
const ms = createMatchState(s, p);
let src = init - 1;
let lastMatch: number | null = null;
return () => {
while (src <= ms.slen) {
resetMatchState(ms);
const e = match(ms, src, 0);
if (e >= 0 && e !== lastMatch) {
const captures = getCaptures(ms, src, e);
src = e;
lastMatch = e;
return captures;
}
src++;
if (anchor) break;
}
return null;
};
}
function expandReplacementString(
repl: string,
ms: MatchState,
matchStart: number,
matchEnd: number,
): string {
const parts: string[] = [];
let i = 0;
while (i < repl.length) {
const ch = repl.charCodeAt(i);
if (ch === CH_ESC) {
i++;
if (i >= repl.length) {
throw new Error("invalid use of '%' in replacement string");
}
const rc = repl.charCodeAt(i);
if (rc === CH_ESC) {
parts.push("%");
} else if (rc === CH_0) {
parts.push(ms.src.substring(matchStart, matchEnd));
} else if (rc >= 49 && rc <= CH_9) {
parts.push(getRawCaptureString(
ms,
getOneRawCapture(ms, rc - 49, matchStart, matchEnd),
));
} else {
throw new Error("invalid use of '%' in replacement string");
}
} else {
// Collect consecutive literal characters
let j = i + 1;
while (j < repl.length && repl.charCodeAt(j) !== CH_ESC) j++;
parts.push(repl.substring(i, j));
i = j;
continue;
}
i++;
}
return parts.join("");
}
export interface GsubCallbacks {
replString?: string;
replFunction?: (
...captures: CaptureResult[]
) => Promise<string | null | undefined> | string | null | undefined;
replTable?: (key: string) => string | null | undefined;
}
export async function patternGsub(
s: string,
pattern: string,
callbacks: GsubCallbacks,
maxN?: number,
): Promise<[string, number]> {
const max_s = maxN !== undefined ? maxN : s.length + 1;
let p = pattern;
let anchor = false;
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
anchor = true;
p = p.substring(1);
}
const ms = createMatchState(s, p);
let src = 0;
let lastMatch: number | null = null;
let n = 0;
const resultParts: string[] = [];
while (n < max_s) {
resetMatchState(ms);
const e = match(ms, src, 0);
if (e >= 0 && e !== lastMatch) {
n++;
let replStr: string | null | undefined;
if (callbacks.replString !== undefined) {
replStr = expandReplacementString(callbacks.replString, ms, src, e);
} else if (callbacks.replFunction) {
const caps = getCaptures(ms, src, e);
replStr = await callbacks.replFunction(...caps);
if (replStr === null || replStr === undefined) {
replStr = ms.src.substring(src, e);
}
} else if (callbacks.replTable) {
const raw = getOneRawCapture(ms, 0, src, e);
const key = getRawCaptureString(ms, raw);
replStr = callbacks.replTable(key);
if (replStr === null || replStr === undefined) {
replStr = ms.src.substring(src, e);
}
}
resultParts.push(replStr!);
src = e;
lastMatch = e;
} else if (src < ms.slen) {
resultParts.push(s[src]);
src++;
lastMatch = null;
} else {
break;
}
if (anchor) break;
}
if (src < s.length) {
resultParts.push(s.substring(src));
}
return [resultParts.join(""), n];
}
+557
View File
@@ -0,0 +1,557 @@
local function assertEqual(a, b)
if a ~= b then
error("Assertion failed: " .. tostring(a) .. " ~= " .. tostring(b))
end
end
local function assertError(f, expectedMsg)
local ok, err = pcall(f)
assert(not ok, "Expected error but call succeeded")
local msg = tostring(err)
-- Strip stack trace prefix if present (e.g. "LuaRuntimeError: ...")
local clean = msg:match(":%s*(.+)$") or msg
assert(clean:find(expectedMsg, 1, true),
"Expected error containing '" .. expectedMsg .. "' but got: " .. msg)
end
-- 1. Character classes
-- %a / %A
assertEqual(string.match("hello123", "%a+"), "hello")
assertEqual(string.match("123hello", "%A+"), "123")
-- %d / %D
assertEqual(string.match("abc42xyz", "%d+"), "42")
assertEqual(string.match("42abc", "%D+"), "abc")
-- %l / %L (lowercase)
assertEqual(string.match("Hello", "%l+"), "ello")
assertEqual(string.match("hello", "%L+"), nil)
-- %u / %U (uppercase)
assertEqual(string.match("helloWORLD", "%u+"), "WORLD")
assertEqual(string.match("HELLO", "%U+"), nil)
-- %w / %W (alphanumeric)
assertEqual(string.match("hello world", "%w+"), "hello")
assertEqual(string.match("hello world", "%W+"), " ")
-- %s / %S (whitespace)
assertEqual(string.match("hello world", "%s+"), " ")
assertEqual(string.match(" hello", "%S+"), "hello")
-- %p (punctuation)
assertEqual(string.match("hello, world!", "%p+"), ",")
-- %g (printable, non-space)
assertEqual(string.match(" abc ", "%g+"), "abc")
-- %x (hex digits)
assertEqual(string.match("ghABCDij", "%x+"), "ABCD")
-- %. (dot matches any)
assertEqual(string.match("abc", "."), "a")
assertEqual(string.match("abc", ".."), "ab")
-- 2. Anchors
assertEqual(string.match("hello", "^hello$"), "hello")
assertEqual(string.match("hello world", "^hello$"), nil)
assertEqual(string.match("hello", "^h"), "h")
assertEqual(string.match("hello", "o$"), "o")
assertEqual(string.match("hello", "x$"), nil)
-- 3. Repetition: *, +, -, ?
-- * (greedy, 0 or more)
assertEqual(string.match("aaa", "a*"), "aaa")
assertEqual(string.match("bbb", "a*"), "")
-- + (greedy, 1 or more)
assertEqual(string.match("aaa", "a+"), "aaa")
assertEqual(string.match("bbb", "a+"), nil)
-- - (lazy, 0 or more)
assertEqual(string.match("aaa", "a-b"), nil)
assertEqual(string.match("aab", "a-b"), "aab")
-- lazy match takes shortest prefix
local cap = string.match("<tag>content</tag>", "<(.-)>")
assertEqual(cap, "tag")
-- ? (optional)
assertEqual(string.match("colour", "colou?r"), "colour")
assertEqual(string.match("color", "colou?r"), "color")
-- 4. Escaping magic characters with %
assertEqual(string.match("100%", "(%d+)%%"), "100")
assertEqual(string.match("hello.world", "(%a+)%.(%a+)"), "hello")
assertEqual(string.match("a+b", "a%+b"), "a+b")
assertEqual(string.match("a*b", "a%*b"), "a*b")
assertEqual(string.match("(x)", "%(x%)"), "(x)")
-- Non-magic chars after % are literal
assertEqual(string.match("*", "%*"), "*")
assertEqual(string.match("?", "%?"), "?")
-- 5. Character sets [...]
assertEqual(string.match("cat", "[abc]"), "c")
assertEqual(string.match("dog", "[abc]"), nil)
-- Ranges
assertEqual(string.match("m", "[a-z]"), "m")
assertEqual(string.match("M", "[a-z]"), nil)
assertEqual(string.match("5", "[0-9]"), "5")
-- Negated set
assertEqual(string.match("x", "[^abc]"), "x")
assertEqual(string.match("a", "[^abc]"), nil)
-- Classes inside sets
assertEqual(string.match("3", "[%d]"), "3")
assertEqual(string.match("a", "[%d]"), nil)
-- 6. Captures
-- Single capture
local y, m, d = string.match("2024-03-14", "(%d+)-(%d+)-(%d+)")
assertEqual(y, "2024")
assertEqual(m, "03")
assertEqual(d, "14")
-- No captures returns whole match
assertEqual(string.match("hello", "%a+"), "hello")
-- Nested captures
local outer, inner = string.match("hello world", "(h(ello))")
assertEqual(outer, "hello")
assertEqual(inner, "ello")
-- Captures with literal parens
local content = string.match("((test))", "%((%(%a+%))%)")
assertEqual(content, "(test)")
-- Multiple captures
local a, b = string.match("hello world", "(%a+) (%a+)")
assertEqual(a, "hello")
assertEqual(b, "world")
-- 7. Position captures ()
local p1, p2 = string.match("hello", "()()")
assertEqual(p1, 1)
assertEqual(p2, 1)
local pos = string.match("hello world", "()world")
assertEqual(pos, 7)
-- 8. Back-references %1-%9
-- Match repeated word
local word = string.match("hello hello", "(%a+) %1")
assertEqual(word, "hello")
-- No match if different
assertEqual(string.match("hello world", "(%a+) %1"), nil)
-- Match repeated char
local ch = string.match("aabcc", "(.)%1")
assertEqual(ch, "a")
-- 9. Balanced match %bxy
assertEqual(string.match("(hello (world))", "%b()"), "(hello (world))")
assertEqual(string.match("{a{b}c}", "%b{}"), "{a{b}c}")
assertEqual(string.match("(unbalanced", "%b()"), nil)
-- 10. Frontier pattern %f[set]
-- Transition from non-alpha to alpha
local fw = string.match("hello world", "%f[%a]%a+", 2)
assertEqual(fw, "world")
-- Transition at string start
local fs = string.match("hello", "%f[%a]%a+")
assertEqual(fs, "hello")
-- Word boundaries
local words = {}
for w in string.gmatch("one two three", "%f[%a]%a+") do
table.insert(words, w)
end
assertEqual(#words, 3)
assertEqual(words[1], "one")
assertEqual(words[2], "two")
assertEqual(words[3], "three")
-- 11. string.find
-- Basic find
local s, e = string.find("hello world", "world")
assertEqual(s, 7)
assertEqual(e, 11)
-- Plain find
s, e = string.find("hello.world", ".", 1, true)
assertEqual(s, 6)
assertEqual(e, 6)
-- Find with captures
local s2, e2, c1 = string.find("hello world", "(%a+)")
assertEqual(s2, 1)
assertEqual(e2, 5)
assertEqual(c1, "hello")
-- Not found
assertEqual(string.find("hello", "xyz"), nil)
-- Find with init
s, e = string.find("abcabc", "abc", 2)
assertEqual(s, 4)
assertEqual(e, 6)
-- Find returns nil (not false)
assert(string.find("a", "b") == nil)
assert(not (string.find("a", "b") ~= nil))
-- Find with special chars
local bf = string.find("[", "[_%w]")
assert(bf == nil)
-- 12. string.match
-- Match from init position
assertEqual(string.match("abcdef", "%a+", 4), "def")
-- Match empty pattern
assertEqual(string.match("abc", ""), "")
-- Match returns nil (not false)
assert(string.match("a", "b") == nil)
assert(not (string.match("a", "b") ~= nil))
-- Match with init
local initMatch = string.match("hello world", "(world)", 7)
assertEqual(initMatch, "world")
-- 13. string.gmatch
-- Basic iteration
local t = {}
for w in string.gmatch("hello world lua", "%a+") do
table.insert(t, w)
end
assertEqual(#t, 3)
assertEqual(t[1], "hello")
assertEqual(t[2], "world")
assertEqual(t[3], "lua")
-- With captures
local kv = {}
for k, v in string.gmatch("from=world, to=Lua", "(%w+)=(%w+)") do
kv[k] = v
end
assertEqual(kv.from, "world")
assertEqual(kv.to, "Lua")
-- gmatch with empty matches (like Lua reference: ";a;" with "a*")
local r = {}
for mm in string.gmatch(";a;", "a*") do
table.insert(r, mm)
end
assertEqual(r[1], "")
assertEqual(r[2], "a")
assertEqual(r[3], "")
-- gmatch without captures returns whole match
local t2 = {}
for mm in string.gmatch("from=world, to=Lua", "%w+=%w+") do
table.insert(t2, mm)
end
assertEqual(t2[1], "from=world")
assertEqual(t2[2], "to=Lua")
-- 14. string.gsub
-- Simple string replacement
local res, count = string.gsub("hello world", "(%w+)", "%1-%1")
assertEqual(res, "hello-hello world-world")
assertEqual(count, 2)
-- Limited replacements
res, count = string.gsub("aaa", "a", "b", 2)
assertEqual(res, "bba")
assertEqual(count, 2)
-- Replacement with %0
res = string.gsub("hello", "%w+", "[%0]")
assertEqual(res, "[hello]")
-- Function replacement
res, count = string.gsub("hello world", "%w+", function(w)
return w:upper()
end)
assertEqual(res, "HELLO WORLD")
assertEqual(count, 2)
-- Function returning nil keeps original
res = string.gsub("hello world", "%w+", function(w)
if w == "hello" then return "HI" end
end)
assertEqual(res, "HI world")
-- Table replacement
local tbl = {hello = "HI", world = "EARTH"}
res = string.gsub("hello world", "(%w+)", tbl)
assertEqual(res, "HI EARTH")
-- Table with missing key keeps original
tbl = {hello = "HI"}
res = string.gsub("hello world", "(%w+)", tbl)
assertEqual(res, "HI world")
-- gsub with magic chars in pattern
res = string.gsub("hello.world", "%.", "-")
assertEqual(res, "hello-world")
-- gsub %% in replacement
res = string.gsub("hello", "hello", "100%%")
assertEqual(res, "100%")
-- Empty pattern match (inserts between every char)
res = string.gsub("abc", "", "-")
assertEqual(res, "-a-b-c-")
-- Anchored gsub
res, count = string.gsub("abc", "^a", "x")
assertEqual(res, "xbc")
assertEqual(count, 1)
-- gsub with XML pattern
local xmlpat = '<%?xml version="1.0" encoding="UTF%-8"%?>'
local xmlstr = '<?xml version="1.0" encoding="UTF-8"?><my-xml></my-xml>'
res = string.gsub(xmlstr, xmlpat, "moo")
assertEqual(res, "moo<my-xml></my-xml>")
-- gsub with %%1
res = string.gsub("Hello %1", "%%1", "world")
assertEqual(res, "Hello world")
-- gsub counting digits
res, count = string.gsub("ab5kfd8scf4lll", "%d", "")
assertEqual(res, "abkfdscflll")
assertEqual(count, 3)
-- 15. The dash (lazy) vs literal dash
assertEqual(string.match("2024-03-14", "%d+-(%d+)-%d+"), "03")
assertEqual(string.match("2024-03-14", "(%d+)-(%d+)-(%d+)"), "2024")
local y2, m2, d2 = string.match("2024-03-14", "(%d+)-(%d+)-(%d+)")
assertEqual(y2, "2024")
assertEqual(m2, "03")
assertEqual(d2, "14")
-- 16. Edge cases
-- Empty string
assertEqual(string.match("", ".*"), "")
local fs2, fe2 = string.find("", "")
assertEqual(fs2, 1)
assertEqual(fe2, 0)
-- Pattern matching entire string
assertEqual(string.match("abc", "^(.-)$"), "abc")
-- Pattern at end of string
assertEqual(string.match("test!", "!$"), "!")
-- Multiple position captures
local p3, p4 = string.match("abcd", "()ab()cd")
assertEqual(p3, 1)
assertEqual(p4, 3)
-- string.rep with separator
assertEqual(string.rep("ab", 3, ","), "ab,ab,ab")
assertEqual(string.rep("x", 1, ","), "x")
assertEqual(string.rep("x", 0), "")
-- 17. More complex patterns
-- CSV-like parsing
local fields = {}
for f in string.gmatch("one,two,,four", "([^,]*)") do
table.insert(fields, f)
end
assertEqual(fields[1], "one")
assertEqual(fields[2], "two")
assertEqual(fields[3], "")
assertEqual(fields[4], "four")
-- Trim whitespace
local function trim(s2)
return string.match(s2, "^%s*(.-)%s*$")
end
assertEqual(trim(" hello "), "hello")
assertEqual(trim("hello"), "hello")
assertEqual(trim(" "), "")
-- Match identifier
assertEqual(string.match("my_var123", "^[%a_][%w_]*$"), "my_var123")
assertEqual(string.match("123bad", "^[%a_][%w_]*$"), nil)
-- Hex color
local hex = string.match("#FF00AA", "^#(%x%x)(%x%x)(%x%x)$")
assertEqual(hex, "FF")
local r2, g, b2 = string.match("#FF00AA", "^#(%x%x)(%x%x)(%x%x)$")
assertEqual(r2, "FF")
assertEqual(g, "00")
assertEqual(b2, "AA")
-- Email-like pattern
local user, domain = string.match("user@example.com", "([%w_.]+)@([%w_.]+)")
assertEqual(user, "user")
assertEqual(domain, "example.com")
-- 18. Error cases — malformed patterns
-- Pattern ending with lone %
assertError(function()
string.find("abc", "abc%")
end, "malformed pattern (ends with '%')")
assertError(function()
string.match("abc", "%")
end, "malformed pattern (ends with '%')")
assertError(function()
local iter = string.gmatch("abc", "a%")
iter()
end, "malformed pattern (ends with '%')")
-- %b with missing arguments
assertError(function()
string.match("abc", "%b")
end, "malformed pattern (missing arguments to '%b')")
assertError(function()
string.match("abc", "%b(")
end, "malformed pattern (missing arguments to '%b')")
-- %f not followed by [
assertError(function()
string.match("abc", "%f")
end, "missing '[' after '%f' in pattern")
assertError(function()
string.match("abc", "%fa")
end, "missing '[' after '%f' in pattern")
-- Lone % at end of replacement
assertError(function()
string.gsub("abc", "a", "x%")
end, "invalid use of '%' in replacement string")
-- Invalid escape in replacement
assertError(function()
string.gsub("abc", "a", "%z")
end, "invalid use of '%' in replacement string")
-- 19. Error cases — invalid captures
-- Unmatched close paren
assertError(function()
string.match("abc", ")")
end, "invalid pattern capture")
-- Back-reference to non-existent capture
assertError(function()
string.match("abc", "%1")
end, "invalid capture index")
-- Back-reference to capture not yet closed
assertError(function()
string.match("abab", "(%a+%1)")
end, "invalid capture index")
-- Too high capture index in replacement
assertError(function()
string.gsub("abc", "(%a+)", "%2")
end, "invalid capture index")
-- Unfinished capture used in match (tries to return it)
assertError(function()
string.match("abc", "(abc")
end, "unfinished capture")
-- Unfinished capture used in find (tries to return it)
assertError(function()
string.find("abc", "(abc")
end, "unfinished capture")
-- Nested unfinished capture
assertError(function()
string.match("abc", "(ab(c)")
end, "unfinished capture")
-- 20. Error cases — %b and %f
-- %b with missing arguments
assertError(function()
string.match("abc", "%b")
end, "malformed pattern (missing arguments to '%b')")
assertError(function()
string.match("abc", "%b(")
end, "malformed pattern (missing arguments to '%b')")
-- %f not followed by [
assertError(function()
string.match("abc", "%f")
end, "missing '[' after '%f' in pattern")
assertError(function()
string.match("abc", "%fa")
end, "missing '[' after '%f' in pattern")
-- 21. Error cases — gsub replacement string
-- Lone % at end of replacement
assertError(function()
string.gsub("abc", "a", "x%")
end, "invalid use of '%' in replacement string")
-- Invalid escape in replacement (not a digit or %)
assertError(function()
string.gsub("abc", "a", "%z")
end, "invalid use of '%' in replacement string")
-- %2 when only one capture exists
assertError(function()
string.gsub("abc", "(a)", "%2")
end, "invalid capture index")
-- %9 with no captures at all
assertError(function()
string.gsub("abc", "a", "%9")
end, "invalid capture index")
-- 22. Error cases — too many captures
assertError(function()
local pat = string.rep("(", 33) .. "a" .. string.rep(")", 33)
string.match("a", pat)
end, "too many captures")
-- 23. Error cases — unfinished capture
-- Open paren never closed
assertError(function()
string.find("abc", "(abc")
end, "unfinished capture")
assertError(function()
string.match("abc", "(ab(c)")
end, "unfinished capture")
+93 -179
View File
@@ -8,77 +8,32 @@ import {
} from "../runtime.ts";
import { untagNumber } from "../numeric.ts";
import { luaFormat } from "./format.ts";
import {
type CaptureResult,
type GsubCallbacks,
patternFind,
patternGmatch,
patternGsub,
patternMatch,
} from "./pattern.ts";
// 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);
function capturesToLua(caps: CaptureResult[]): any {
if (caps.length === 0) {return null;}
if (caps.length === 1) {
const c = caps[0];
return "s" in c ? c.s : c.position;
}
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;
return new LuaMultiRes(
caps.map((c) => ("s" in c ? c.s : c.position)),
);
}
export const stringApi = new LuaTable({
byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => {
i = i ?? 1;
j = j ?? i;
if (j > s.length) {j = s.length;}
if (i < 1) {i = 1;}
const result = [];
for (let k = i; k <= j; k++) {
result.push(s.charCodeAt(k - 1));
@@ -90,119 +45,68 @@ export const stringApi = new LuaTable({
}),
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));
const r = patternFind(s, pattern, init, plain);
if (!r) {return null;}
const result: any[] = [r.start, r.end];
for (const c of r.captures) {
result.push("s" in c ? c.s : c.position);
}
// Plain
const index = s.indexOf(pattern, init - 1);
return (index === -1)
? null
: new LuaMultiRes([index + 1, index + pattern.length]);
return new LuaMultiRes(result);
},
),
format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => {
// Unwrap tagged floats so luaFormat sees plain numbers
for (let i = 0; i < args.length; i++) {
args[i] = untagNumber(args[i]);
}
return luaFormat(format, ...args);
}),
gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
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;
};
}),
gmatch: new LuaBuiltinFunction(
(_sf, s: string, pattern: string, init = 1) => {
const iter = patternGmatch(s, pattern, init);
return () => {
const caps = iter();
if (!caps) {return;}
return capturesToLua(caps);
};
},
),
gsub: new LuaBuiltinFunction(
async (
sf,
s: string,
pattern: string,
repl: any, // string or LuaFunction
n = Infinity,
repl: any,
n?: number,
) => {
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));
const callbacks: GsubCallbacks = {};
if (typeof repl === "string") {
callbacks.replString = repl;
} else if (repl instanceof LuaTable) {
callbacks.replTable = (key: string) => {
const v = repl.get(key);
if (v === null || v === undefined || v === false) {return null;}
return String(v);
};
} else if (repl.call) {
callbacks.replFunction = async (...caps: CaptureResult[]) => {
const args = caps.map((c) => ("s" in c ? c.s : c.position));
let result = await repl.call(sf, ...args);
if (result instanceof LuaMultiRes) {
result = result.values[0];
}
if (str instanceof LuaMultiRes) {
str = str.values[0];
if (result === null || result === undefined || result === false) {
return null;
}
if (str === undefined || str === null) {
str = match[0];
}
} else if (repl instanceof LuaTable) {
str = repl.get(match[0]);
} else if (typeof repl === "string") {
str = repl.replaceAll(/%([0-9]+)/g, (_, i) => match[i]);
} else {
throw new LuaRuntimeError(
"string.gsub replacement argument should be a function, table or string",
sf,
);
}
if (match[0].length === 0) {
if (lastMatch === void 0) {
prefix = "";
} else {
prefix = s.slice(0, 1);
}
} else {
prefix = s.slice(0, match.index);
}
lastMatch = match[0];
result += `${prefix}${str}`;
s = s.slice(`${prefix}${lastMatch}`.length);
count++;
return luaToString(result);
};
} else {
throw new LuaRuntimeError(
"string.gsub replacement argument should be a function, table or string",
sf,
);
}
return new LuaMultiRes([`${result}${s}`, count]);
const [result, count] = await patternGsub(s, pattern, callbacks, n);
return new LuaMultiRes([result, count]);
},
),
len: new LuaBuiltinFunction((_sf, s: string) => {
@@ -216,43 +120,53 @@ export const stringApi = new LuaTable({
}),
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;
}
if (matches[1] === undefined) {
// No captures
return matches[0];
}
matches.shift();
return new LuaMultiRes(matches);
const caps = patternMatch(s, pattern, init);
if (!caps) {return null;}
return capturesToLua(caps);
},
),
rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => {
if (n <= 0) {return "";}
sep = sep ?? "";
return s.repeat(n) + sep;
const parts: string[] = [];
for (let i = 0; i < n; i++) {
parts.push(s);
}
return parts.join(sep);
}),
reverse: new LuaBuiltinFunction((_sf, s: string) => {
return s.split("").reverse().join("");
}),
sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => {
j = j ?? s.length;
if (i < 0) {
i = s.length + i + 1;
const len = s.length;
let start: number;
if (i > 0) {
start = i;
} else if (i < -len) {
start = 1;
} else {
start = i === 0 ? 1 : len + i + 1;
}
if (j < 0) {
j = s.length + j + 1;
let end: number;
if (j === undefined || j === null || j > len) {
end = len;
} else if (j >= 0) {
end = j;
} else if (j < -len) {
end = 0;
} else {
end = len + j + 1;
}
return s.slice(i - 1, j);
if (start <= end) {
return s.substring(start - 1, end);
}
return "";
}),
split: new LuaBuiltinFunction((_sf, s: string, sep: string) => {
return s.split(sep);
}),
// Non-standard
// Non-standard extensions
startsWith: new LuaBuiltinFunction((_sf, s: string, prefix: string) => {
return s.startsWith(prefix);
}),
+60 -149
View File
@@ -1,6 +1,6 @@
local function assertEqual(a, b)
if a ~= b then
error("Assertion failed: " .. a .. " is not equal to " .. b)
error("Assertion failed: " .. tostring(a) .. " is not equal to " .. tostring(b))
end
end
@@ -8,124 +8,80 @@ end
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.byte tests
local a, b = string.byte('Mo0')
assert(a == 77, 'string.byte() should return the code for the first char')
assert(b == nil, 'string.byte() should return only one item when no length is given')
a, b = string.byte('Mo0', 2)
assert(a == 111, 'string.byte() should return the code for the nth character')
assert(b == nil, 'string.byte() should return only one item when no length is given')
local a2, b2, c2 = string.byte('Mo0', 2, 3)
assert(a2 == 111, 'string.byte() multi-return [1]')
assert(b2 == 48, 'string.byte() multi-return [2]')
assert(c2 == nil, 'string.byte() should stop at end of string')
a2, b2 = string.byte('Mo0', 3, 20)
assert(a2 == 48, 'string.byte() should clamp to string length')
assert(b2 == nil, 'string.byte() should not return past end of string')
-- string.char tests
assertEqual(string.char(), '')
assertEqual(string.char(116, 101, 115, 116), 'test')
-- string.len tests
assertEqual(string.len(''), 0)
assertEqual(string.len('McLaren Mercedes'), 16)
-- string.lower tests
assertEqual(string.lower(''), '')
assertEqual(string.lower('McLaren Mercedes'), 'mclaren mercedes')
-- string.upper tests
assertEqual(string.upper(''), '')
assertEqual(string.upper('JavaScript'), 'JAVASCRIPT')
-- string.rep tests
assertEqual(string.rep('Ho', 0), '')
assertEqual(string.rep('Ho', 1), 'Ho')
assertEqual(string.rep('Ho', 3), 'HoHoHo')
assertEqual(string.rep("ab", 3, ","), "ab,ab,ab")
assertEqual(string.rep("x", 1, ","), "x")
-- string.reverse tests
assertEqual(string.reverse(''), '')
assertEqual(string.reverse('x'), 'x')
assertEqual(string.reverse('tpircSavaJ'), 'JavaScript')
-- string.sub tests
assertEqual(string.sub("hello", 1), "hello")
assertEqual(string.sub("hello", 1, 1), "h")
-- with negative indeses
-- with negative indices
assertEqual(string.sub("hello", -1), "o")
assertEqual(string.sub("hello", -2), "lo")
assertEqual(string.sub("hello", -2, -1), "lo")
local s = 'Pub Standards'
assertEqual(string.sub(s, 1), 'Pub Standards')
assertEqual(string.sub(s, 5), 'Standards')
assertEqual(string.sub(s, -4), 'ards')
assertEqual(string.sub(s, 1, 3), 'Pub')
assertEqual(string.sub(s, 7, 9), 'and')
assertEqual(string.sub(s, 5, -2), 'Standard')
assertEqual(string.sub(s, 0), 'Pub Standards')
-- Invoke string metatable methods
assertEqual(("hello"):len(), 5)
assertEqual(("hello"):upper(), "HELLO")
assertEqual(('Hey'):lower(), 'hey')
-- 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
-- Test string.split (non-standard)
local parts = string.split("a,b,c", ",")
assertEqual(parts[1], "a")
assertEqual(parts[2], "b")
@@ -134,49 +90,10 @@ 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
-- Test matchRegexAll (non-standard, regex-based)
local matches = {}
for match in string.matchRegexAll("hellolllbl", "(l+)") do
table.insert(matches, match)
@@ -185,9 +102,3 @@ assertEqual(#matches, 3)
assertEqual(matches[1][1], "ll")
assertEqual(matches[2][1], "lll")
assertEqual(matches[3][1], "l")
-- https://community.silverbullet.md/t/test-if-string-find-outputs-nil-not-working-as-expected/2342
assert(string.find("a", "b") == nil)
assert(not (string.find("a", "b") ~= nil))
assert(string.match("a", "b") == nil)
assert(not (string.match("a", "b") ~= nil))
-624
View File
@@ -1,624 +0,0 @@
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.')