packages feed

salmon-ops-0.2.0.0: ui/vendor/tramaj-js/json.js

/**
 * JSON as Tramaj reads and writes it (`specs/node-json.md`): like any JSON
 * value, except that a number is an integer or a float, decided by its text
 * (`specs/reference.md` §3, `specs/decisions.md` §18). Plus the handful of
 * operations the rest of the port needs over it: structural equality, and
 * `specs/reference.md` §6's normative `str` rendering.
 *
 * **How a JavaScript number is classified.** A host `number` has no type of
 * its own, and `JSON.parse` reads `3` and `3.0` as the same value, so this
 * port's binding is:
 *
 * - a `number` that is a safe integer (`Number.isSafeInteger`) is an
 *   **integer**; any other `number` (`1.5`, `1e21`) is a **float**;
 * - a `Float` is a **float** whatever it holds, which is the only way to
 *   write the float `3.0`;
 * - a `bigint` is an **integer**. It is how an integer-form JSON number
 *   beyond the range is carried, digits intact, to the decoder that refuses
 *   it.
 *
 * What the evaluator hands back is in **canonical form**: an integer is a
 * plain `number`, and a float is a plain `number` unless its value is a safe
 * integer, in which case it is a `Float`. A host that never meets a
 * whole-valued float therefore never meets a `Float`.
 *
 * **Integer range.** This port has the guaranteed range only,
 * `-(2^53 - 1)` to `2^53 - 1` (`specs/reference.md` §13, the corpus's
 * `int53`): an integer is held in a double, which is exact there and nowhere
 * beyond.
 *
 * `JSON.parse` and `JSON.stringify` lose the type (`3.0` becomes `3`, in
 * both directions); `parseJson` and `stringify` keep it.
 */
/**
 * A float, stated as one. `new Float(3)` is the float `3.0`, where the plain
 * number `3` is the integer. Prefer `float(n)`, which wraps only when the
 * plain number would be read as an integer.
 */
export class Float {
    value;
    constructor(value) {
        this.value = value;
    }
    /** So that host arithmetic and comparisons see the number. */
    valueOf() {
        return this.value;
    }
    /** The text `stringify` writes: `3.0`, never `3`. */
    toString() {
        return formatFloat(this.value);
    }
    /**
     * `JSON.stringify` cannot write `3.0`, so through it a `Float` degrades to
     * its plain number and the type is lost. `stringify` keeps it.
     */
    toJSON() {
        return this.value;
    }
}
/** The float of that value, in canonical form: wrapped only when a plain number would be an integer. */
export function float(n) {
    return Number.isSafeInteger(n) ? new Float(n) : n;
}
/** Whether a double is an integer this port holds exactly: a whole number in the guaranteed range. */
export function inIntegerRange(n) {
    return Number.isSafeInteger(n);
}
/**
 * A JSON number as its type and its value, or `null` for anything else. A
 * `bigint` outside the integer range has no exact double: its `value` is the
 * nearest one, which `inIntegerRange` refuses.
 */
export function classifyNumber(v) {
    if (typeof v === "number")
        return { type: Number.isSafeInteger(v) ? "integer" : "float", value: v };
    if (typeof v === "bigint")
        return { type: "integer", value: Number(v) };
    if (v instanceof Float)
        return { type: "float", value: v.value };
    return null;
}
export function isInteger(v) {
    return classifyNumber(v)?.type === "integer";
}
export function isFloat(v) {
    return classifyNumber(v)?.type === "float";
}
/** The value of a number of either type, or `undefined` for anything else. */
export function numberValue(v) {
    return classifyNumber(v)?.value;
}
/** A number outside the value domain (`specs/reference.md` §3). */
export class JsonNumberError extends Error {
    name = "JsonNumberError";
}
/**
 * The numbers of a JSON value as Tramaj values, at any depth and in canonical
 * form, or a `JsonNumberError` for one that cannot be a value. This is the
 * one place that decides what a number outside the value domain becomes
 * (`specs/reference.md` §3, `specs/node-json.md` *Decoding*):
 *
 * - an integer outside the integer range is refused, not rounded;
 * - a float that is not finite (`1e400` read from text, a host `NaN`) is
 *   refused;
 * - a negative zero is zero: `-0.0` is `0.0` and `-0` is `0`.
 */
export function normalizeNumbers(v) {
    if (v === null || typeof v === "boolean" || typeof v === "string")
        return v;
    const n = classifyNumber(v);
    if (n !== null)
        return normalizeNumber(n.type, n.value);
    if (Array.isArray(v))
        return v.map(normalizeNumbers);
    const o = v;
    return jsonObjectFrom(ownKeys(o).map((k) => [k, normalizeNumbers(o[k])]));
}
/** One number, as `normalizeNumbers` treats it. */
export function normalizeNumber(type, value) {
    if (type === "integer") {
        if (!inIntegerRange(value)) {
            throw new JsonNumberError("an integer is outside the integer range, -(2^53 - 1) to 2^53 - 1");
        }
        return value === 0 ? 0 : value;
    }
    if (!Number.isFinite(value)) {
        throw new JsonNumberError("a float is not finite: it is too large for a double");
    }
    return float(value === 0 ? 0 : value);
}
/** A JSON object, which a `Float` is not. */
export function isJsonObject(v) {
    return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Float);
}
/** Own-property read, so a JSON object's inherited `constructor`/`toString` never answers. */
export function getField(o, key) {
    return Object.prototype.hasOwnProperty.call(o, key) ? o[key] : undefined;
}
export function hasField(o, key) {
    return Object.prototype.hasOwnProperty.call(o, key);
}
export function ownKeys(o) {
    return Object.keys(o);
}
/**
 * Builds an object from dynamic keys without `__proto__` reaching the
 * prototype setter, which a plain assignment would.
 */
export function jsonObjectFrom(entries) {
    const out = {};
    for (const [k, v] of entries) {
        Object.defineProperty(out, k, { value: v, enumerable: true, writable: true, configurable: true });
    }
    return out;
}
/**
 * Structural, with object keys unordered. An integer and a float are never
 * equal, whatever they hold: `3` and `3.0` are two values.
 */
export function jsonEqual(a, b) {
    const na = classifyNumber(a);
    const nb = classifyNumber(b);
    if (na !== null || nb !== null) {
        if (na === null || nb === null || na.type !== nb.type)
            return false;
        // Two integers beyond the range are compared by their digits.
        if (typeof a === "bigint" && typeof b === "bigint")
            return a === b;
        return na.value === nb.value;
    }
    if (a === b)
        return true;
    if (Array.isArray(a) && Array.isArray(b)) {
        if (a.length !== b.length)
            return false;
        return a.every((x, i) => jsonEqual(x, b[i]));
    }
    if (isJsonObject(a) && isJsonObject(b)) {
        const ka = ownKeys(a);
        const kb = ownKeys(b);
        if (ka.length !== kb.length)
            return false;
        return ka.every((k) => hasField(b, k) && jsonEqual(a[k], b[k]));
    }
    return false;
}
/**
 * An integer as its decimal digits. ECMAScript's `Number::toString` writes
 * every whole number below `1e21` that way, which covers the integer range.
 */
export function formatInteger(n) {
    return String(n);
}
/**
 * A float as the shortest round-trip text of ECMAScript's `Number::toString`,
 * with `.0` appended when that text has neither a fraction nor an exponent:
 * `1.0`, `0.1`, `100000000000.0`, `1e+21` (`specs/node-json.md` *Numbers*).
 *
 * A value that is not finite has no JSON text and is never a Tramaj value. It
 * is written as a float no double holds, which `parseJson` reads back as one
 * the decoder refuses.
 */
export function formatFloat(n) {
    if (!Number.isFinite(n))
        return n < 0 ? "-1e400" : "1e400";
    const text = String(n);
    return /^-?[0-9]+$/.test(text) ? `${text}.0` : text;
}
function isJsonNumber(v) {
    return typeof v === "number" || typeof v === "bigint" || v instanceof Float;
}
/** A number of either type as JSON text that keeps the type. */
function formatJsonNumber(v) {
    if (typeof v === "bigint")
        return formatInteger(v);
    if (v instanceof Float)
        return formatFloat(v.value);
    return Number.isSafeInteger(v) ? formatInteger(v) : formatFloat(v);
}
export function quoteString(s) {
    return JSON.stringify(s);
}
/** Compact JSON with object keys sorted and numbers by their type (`specs/reference.md` §6). */
export function compactJson(v) {
    if (v === null)
        return "null";
    if (typeof v === "boolean")
        return v ? "true" : "false";
    if (isJsonNumber(v))
        return formatJsonNumber(v);
    if (typeof v === "string")
        return quoteString(v);
    if (Array.isArray(v))
        return `[${v.map(compactJson).join(",")}]`;
    const keys = ownKeys(v).sort(compareStrings);
    return `{${keys.map((k) => `${quoteString(k)}:${compactJson(v[k])}`).join(",")}}`;
}
/** `str`'s rendering (`specs/reference.md` §6): raw at the top level, compact below it. */
export function displayString(v) {
    if (v === null)
        return "";
    if (typeof v === "boolean")
        return v ? "true" : "false";
    if (isJsonNumber(v))
        return formatJsonNumber(v);
    if (typeof v === "string")
        return v;
    return compactJson(v);
}
export function compareStrings(a, b) {
    return a < b ? -1 : a > b ? 1 : 0;
}
// Writing ---------------------------------------------------------------------
/**
 * JSON text that keeps the type of each number, object keys in the order the
 * object holds them. Compact, or with `indent` spaces per level when given;
 * an empty array or object stays on one line either way.
 */
export function stringify(v, indent = 0) {
    const step = " ".repeat(Math.max(0, indent));
    const block = (open, close, pad, items) => {
        if (items.length === 0)
            return open + close;
        if (step === "")
            return open + items.join(",") + close;
        return `${open}\n${items.map((i) => pad + step + i).join(",\n")}\n${pad}${close}`;
    };
    const go = (x, pad) => {
        if (x === null)
            return "null";
        if (typeof x === "boolean")
            return x ? "true" : "false";
        if (isJsonNumber(x))
            return formatJsonNumber(x);
        if (typeof x === "string")
            return quoteString(x);
        const inner = pad + step;
        if (Array.isArray(x))
            return block("[", "]", pad, x.map((i) => go(i, inner)));
        const sep = step === "" ? ":" : ": ";
        return block("{", "}", pad, ownKeys(x).map((k) => quoteString(k) + sep + go(x[k], inner)));
    };
    return go(v, "");
}
// Reading ---------------------------------------------------------------------
export class JsonParseError extends Error {
    name = "JsonParseError";
}
const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/y;
const STRING = /"(?:[^"\\]|\\.)*"/y;
const WORDS = [
    ["null", null],
    ["true", true],
    ["false", false],
];
/**
 * Parses JSON text (RFC 8259), typing each number by its text: one with
 * neither a fraction nor an exponent is an integer, one with either is a
 * float, so `3` is an integer and `3.0` and `3e0` are floats.
 *
 * No number is refused here; refusing is left to whoever decodes the value
 * (`normalizeNumbers`, which the evaluator applies to a context and
 * `nodeFromJson` to a node). An integer-form number beyond the integer range
 * is read as a `bigint`, digits intact, and a float too large for a double as
 * a `Float` holding an infinity. Of two equal keys in one object, the last
 * wins.
 */
export function parseJson(text) {
    let pos = 0;
    const fail = (what) => new JsonParseError(`${what} at offset ${pos}`);
    const ws = () => {
        for (;;) {
            const c = text[pos];
            if (c === " " || c === "\n" || c === "\r" || c === "\t")
                pos += 1;
            else
                return;
        }
    };
    const string = () => {
        STRING.lastIndex = pos;
        const m = STRING.exec(text);
        if (m === null)
            throw fail("expected a string");
        let s;
        try {
            // The host's decoder owns the escapes and refuses a raw control character.
            s = JSON.parse(m[0]);
        }
        catch {
            throw fail("invalid string");
        }
        pos += m[0].length;
        return s;
    };
    const number = () => {
        NUMBER.lastIndex = pos;
        const m = NUMBER.exec(text);
        if (m === null)
            throw fail("unexpected character");
        pos += m[0].length;
        const n = Number(m[0]);
        if (m[1] !== undefined || m[2] !== undefined)
            return Number.isFinite(n) ? float(n) : new Float(n);
        return Number.isSafeInteger(n) ? n : BigInt(m[0]);
    };
    const value = () => {
        ws();
        const c = text[pos];
        if (c === undefined)
            throw fail("unexpected end of input");
        if (c === '"')
            return string();
        if (c === "[") {
            pos += 1;
            const items = [];
            ws();
            if (text[pos] === "]") {
                pos += 1;
                return items;
            }
            for (;;) {
                items.push(value());
                ws();
                if (text[pos] === ",")
                    pos += 1;
                else if (text[pos] === "]")
                    break;
                else
                    throw fail("expected , or ]");
            }
            pos += 1;
            return items;
        }
        if (c === "{") {
            pos += 1;
            const entries = [];
            ws();
            if (text[pos] === "}") {
                pos += 1;
                return jsonObjectFrom(entries);
            }
            for (;;) {
                ws();
                const k = string();
                ws();
                if (text[pos] !== ":")
                    throw fail("expected :");
                pos += 1;
                entries.push([k, value()]);
                ws();
                if (text[pos] === ",")
                    pos += 1;
                else if (text[pos] === "}")
                    break;
                else
                    throw fail("expected , or }");
            }
            pos += 1;
            return jsonObjectFrom(entries);
        }
        for (const [word, v] of WORDS) {
            if (text.startsWith(word, pos)) {
                pos += word.length;
                return v;
            }
        }
        return number();
    };
    const v = value();
    ws();
    if (pos !== text.length)
        throw fail("unexpected trailing text");
    return v;
}