diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/sjs.js | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/src/sjs.js b/src/sjs.js new file mode 100644 index 0000000..799f964 --- /dev/null +++ b/src/sjs.js @@ -0,0 +1,304 @@ +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 => `<span style="color: red; ">${s}</span>`; + const green = s => `<span style="color: green; ">${s}</span>`; + const yellow = s => `<span style="color: darkgoldenrod;">${s}</span>`; + 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); + } +}; |
