import type { Options } from "./options.ts"; import { type JsonObject, type JsonValue, toJsonValue } from "./parse.ts"; const WIDTH = 80; function isObject(value: JsonValue): value is JsonObject { return typeof value === "object" && value !== null && !Array.isArray(value); } /** render an object key (leaving simple identifiers unquoted) */ export function keyText(key: string): string { if (/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(key)) return key; return JSON.stringify(key); } function scalar(value: null | boolean | number | string): string { if (typeof value === "number") { return Number.isFinite(value) ? String(value) : "null"; } return JSON.stringify(value); } function flat(value: JsonValue): string | undefined { if (Array.isArray(value)) return value.length === 0 ? "[]" : undefined; if (isObject(value)) { return Object.keys(value).length === 0 ? "{}" : undefined; } return scalar(value); } interface Output { text: string; } function write( value: JsonValue, implicit: boolean, indent: number, out: Output, ): void { const single = flat(value); if (single !== undefined) { out.text += single; return; } if (isObject(value)) { const child = implicit ? indent : indent + 2; if (!implicit) out.text += "{\n"; Object.keys(value).forEach((key, index) => { if (index > 0) out.text += "\n"; out.text += `${" ".repeat(child)}${keyText(key)}: `; write(value[key], false, child, out); }); if (!implicit) out.text += `\n${" ".repeat(indent)}}`; return; } const items = value as JsonValue[]; const inline = items.map(flat); if (inline.every((text): text is string => text !== undefined)) { const text = `[${inline.join(", ")}]`; const line = out.text.slice(out.text.lastIndexOf("\n") + 1); if ([...line].length + [...text].length <= WIDTH) { out.text += text; return; } } out.text += "[\n"; items.forEach((item, index) => { if (index > 0) out.text += "\n"; out.text += " ".repeat(indent + 2); write(item, false, indent + 2, out); }); out.text += `\n${" ".repeat(indent)}]`; } /** * Convert a JavaScript value into a pretty-printed fJSON document: two-space * indentation, unquoted identifier keys, implicit nonempty root objects, and * newline-separated entries. Short scalar arrays stay inline; longer arrays * and nested containers expand toward an 80-column width. No final newline. * * Values pass through JSON semantics first, so `undefined`, functions, * symbols, bigints, and circular structures fail with `TypeError`, and * non-finite numbers print as `null`. Values beyond the nesting limit fail * with an `FjsonError` of kind `"nesting"` rather than producing text that * cannot be parsed back. */ export function stringify(value: unknown, options?: Options): string { const out: Output = { text: "" }; write(toJsonValue(value, options), true, 0, out); return out.text; } /** pretty-print validated JSON */ export function printJson(value: JsonValue, implicit: boolean): string { const out: Output = { text: "" }; write(value, implicit, 0, out); return out.text; }