import { domainOf } from "./domain.js"; import { serializePrimitive } from "./primitive.js"; import { stringAndSymbolicEntriesOf } from "./records.js"; import { isDotAccessible, register } from "./registry.js"; export const snapshot = (data, opts = {}) => _serialize(data, { onUndefined: `$ark.undefined`, onBigInt: n => `$ark.bigint-${n}`, ...opts }, []); export const print = (data, opts) => console.log(printable(data, opts)); export const printable = (data, opts) => { switch (domainOf(data)) { case "object": const o = data; const ctorName = o.constructor?.name ?? "Object"; return (ctorName === "Object" || ctorName === "Array" ? opts?.quoteKeys === false ? stringifyUnquoted(o, opts?.indent ?? 0, "") : JSON.stringify(_serialize(o, printableOpts, []), null, opts?.indent) : stringifyUnquoted(o, opts?.indent ?? 0, "")); case "symbol": return printableOpts.onSymbol(data); default: return serializePrimitive(data); } }; const stringifyUnquoted = (value, indent, currentIndent) => { if (typeof value === "function") return printableOpts.onFunction(value); if (typeof value !== "object" || value === null) return serializePrimitive(value); const nextIndent = currentIndent + " ".repeat(indent); if (Array.isArray(value)) { if (value.length === 0) return "[]"; const items = value .map(item => stringifyUnquoted(item, indent, nextIndent)) .join(",\n" + nextIndent); return indent ? `[\n${nextIndent}${items}\n${currentIndent}]` : `[${items}]`; } const ctorName = value.constructor?.name ?? "Object"; if (ctorName === "Object") { const keyValues = stringAndSymbolicEntriesOf(value).map(([key, val]) => { const stringifiedKey = typeof key === "symbol" ? printableOpts.onSymbol(key) : isDotAccessible(key) ? key : JSON.stringify(key); const stringifiedValue = stringifyUnquoted(val, indent, nextIndent); return `${nextIndent}${stringifiedKey}: ${stringifiedValue}`; }); if (keyValues.length === 0) return "{}"; return indent ? `{\n${keyValues.join(",\n")}\n${currentIndent}}` : `{${keyValues.join(", ")}}`; } if (value instanceof Date) return describeCollapsibleDate(value); if ("expression" in value && typeof value.expression === "string") return value.expression; return ctorName; }; const printableOpts = { onCycle: () => "(cycle)", onSymbol: v => `Symbol(${register(v)})`, onFunction: v => `Function(${register(v)})` }; const _serialize = (data, opts, seen) => { switch (domainOf(data)) { case "object": { const o = data; if ("toJSON" in o && typeof o.toJSON === "function") return o.toJSON(); if (typeof o === "function") return printableOpts.onFunction(o); if (seen.includes(o)) return "(cycle)"; const nextSeen = [...seen, o]; if (Array.isArray(o)) return o.map(item => _serialize(item, opts, nextSeen)); if (o instanceof Date) return o.toDateString(); const result = {}; for (const k in o) result[k] = _serialize(o[k], opts, nextSeen); for (const s of Object.getOwnPropertySymbols(o)) { result[opts.onSymbol?.(s) ?? s.toString()] = _serialize(o[s], opts, nextSeen); } return result; } case "symbol": return printableOpts.onSymbol(data); case "bigint": return opts.onBigInt?.(data) ?? `${data}n`; case "undefined": return opts.onUndefined ?? "undefined"; case "string": return data.replace(/\\/g, "\\\\"); default: return data; } }; /** * Converts a Date instance to a human-readable description relative to its precision */ export const describeCollapsibleDate = (date) => { const year = date.getFullYear(); const month = date.getMonth(); const dayOfMonth = date.getDate(); const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); const milliseconds = date.getMilliseconds(); if (month === 0 && dayOfMonth === 1 && hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) return `${year}`; const datePortion = `${months[month]} ${dayOfMonth}, ${year}`; if (hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) return datePortion; let timePortion = date.toLocaleTimeString(); const suffix = timePortion.endsWith(" AM") || timePortion.endsWith(" PM") ? timePortion.slice(-3) : ""; if (suffix) timePortion = timePortion.slice(0, -suffix.length); if (milliseconds) timePortion += `.${pad(milliseconds, 3)}`; else if (timeWithUnnecessarySeconds.test(timePortion)) timePortion = timePortion.slice(0, -3); return `${timePortion + suffix}, ${datePortion}`; }; const months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; const timeWithUnnecessarySeconds = /:\d\d:00$/; const pad = (value, length) => String(value).padStart(length, "0");