export const getIn = (obj, path) =>
path.length === 0
? obj
: getIn(obj?.[path[0]], path.slice(1));
export const merge = (lhs, rhs) => {
if (lhs === undefined) {
return rhs;
}
if (rhs === undefined) {
return lfs;
}
if (typeof lhs !== "object") {
return rhs;
}
const lkeys = Object.keys(lhs);
const rkeys = Object.keys(rhs);
const allKeys = lkeys.concat(rkeys);
return [...new Set(allKeys)].reduce(
(o, key) => ({ ...o, [key]: merge(lhs[key], rhs[key]) }),
{},
);
};
export const compareObject = (lhs, rhs) => {
const lstr = Object.entries(lhs).toSorted();
const rstr = Object.entries(rhs).toSorted();
if (lstr < rstr) {
return -1;
}
if (lstr > rstr) {
return 1;
}
throw new Error("unreachable");
};
const FROM = /[&<>'"]/g;
const ESCAPES = {
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
};
const mappingFn = c => ESCAPES[c];
export const escape = s =>
String.prototype.replace.call(s, FROM, mappingFn);
export const eq = (a, b) => {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (typeof a != "object" || typeof b != "object") {
return false;
}
if (Array.isArray(a) !== Array.isArray(b)) {
return false;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (const k in a) {
if (!b.hasOwnProperty(k)) {
return false;
}
if (!eq(a[k], b[k])) {
return false;
}
}
return true;
};
export const keys = (ks, obj) =>
ks.reduce(
(ret, k) =>
obj.hasOwnProperty(k) ? {...ret, [k]: obj[k]} : ret,
{},
);
export const difference = (a, b) => {
const diff = new Set(a);
for (const el of b) {
diff.delete(el);
}
return diff;
};
export const assocIn = (obj, path, value) =>
path.length === 0 ? obj :
path.length === 1 ? { ...obj, [path[0]]: value } :
{
...obj,
[path[0]]: assocIn(
(obj[path[0]] || {}),
path.slice(1),
value
)
};
export const dissoc = (obj, key) => {
const ret = { ...obj };
delete ret[key]
return ret;
};
export const findFirst = (arr, fn) => {
for (const x of arr) {
const ret = fn(x);
if (ret) {
return ret;
}
}
return null;
};
export const partial = (fn, ...startArgs) =>
(...endArgs) =>
fn(...startArgs, ...endArgs);
export const strSortFn = (a, b) => a.localeCompare(b, "POSIX");
export const undefinedAsNull = x => x === undefined ? null : x;
export const first = a => a[0];
export const rest = a => a.slice(1);
export const butlast = a => a.slice(a, a.length - 1);
export const last = a => a[a.length - 1];
export const take = function*(n, gen) {
let i = 0n;
for (const x of gen) {
if (i >= n) {
break;
}
i++;
yield x;
}
};
export const range = function*(x, y, step = 1n) {
if (x === undefined) {
let i = 0n;
while (true) {
yield i++;
}
}
const [from, to] = y === undefined ?
[0n, x] :
[x, y];
const fromn = BigInt(from);
const ton = BigInt(to);
const stepn = BigInt(step);
if (stepn === 0n) {
while (true) {
yield fromn;
}
}
if (step < 0n) {
for (let i = fromn; i > ton; i+= stepn) {
yield i;
}
} else {
for (let i = fromn; i < ton; i += stepn) {
yield i;
}
}
};
const t = ({ colors, err, assertEq }) => ({
assertEq,
tap: x => {
err(`tap: ${x}\n`);
return x;
},
start: msg => {
err(`${msg}:\n`);
},
testing: async (msg, fn) => {
err(`${colors.yellow("testing")}: ${msg}... `);
try {
await fn();
} catch (e) {
err(`${colors.red("ERR")}.\n`);
throw e;
}
err(`${colors.green("OK")}.\n`);
},
});
class UnimplementedError extends Error {}
export const getJSImpl = () => {
if (typeof scriptArgs !== "undefined") {
return "qjs";
}
if (typeof Deno !== "undefined") {
return "deno";
}
if (typeof process !== "undefined") {
return "node";
}
// FIXME: add browser and bun
return "unknown";
};
export const JSImpl = getJSImpl();
const mappings = {
ARGV: {
qjs: () => scriptArgs,
node: () => process.argv.slice(1),
deno: () => Deno.mainModule.substring("file://".length),
unknown: () => {
throw new UnimplementedError(`ARGV["${JSImpl}"]`);
},
},
exit: {
qjs: async () => {
const { exit } = await import("std");
return exit;
},
node: async () => process.exit,
deno: async () => Deno.exit,
unknown: () => {
throw new UnimplementedError(`exit["${JSImpl}"]`);
},
},
tconf: {
qjs: async () => await mappings.testConf.unknown(),
deno: async () => await mappings.testConf.node(),
browser: async () => {
class DOMAssertionError extends Error {}
const w = s => document.querySelector("#test-report").innerHTML += s;
const red = s => `${s}`;
const green = s => `${s}`;
const yellow = s => `${s}`;
return {
testStart: msg => w(`${msg}:\n`),
testing: msg => w(`${yellow("testing")}: ${msg}...`),
testOK: () => w(` ${green("OK")}.\n`),
assert: (x, msg = "") => {
if (!x) {
w(` ${red("ERROR")}.\n`);
w("\nSee console for details.\n");
w("If possible, do report them.");
throw new DOMAssertionError(msg);
}
},
};
},
node: async () => {
const assert = await import("node:assert/strict");
const process = await import("node:process");
const err = x => process.stderr.write(x);
const red = s => `\x1b[31m${s}\x1b[0m`;
const green = s => `\x1b[32m${s}\x1b[0m`;
const yellow = s => `\x1b[33m${s}\x1b[0m`;
return {
err,
assertEq: assert.deepEqual,
colors: {
red,
green,
yellow,
},
};
},
unknown: () => { throw new UnimplementedError(`exit["${JSImpl}"]`); },
},
};
export const exit = await mappings.exit[ JSImpl]();
export const ARGV = await mappings.ARGV[ JSImpl]();
export const tconf = await mappings.tconf[JSImpl]();
export const runTests = async tests => {
const tFinal = t(tconf);
for (const testFn of tests) {
await testFn(tFinal);
}
};