char/fjson

friendly JSON implementation in TypeScript

git clone https://git.t4t.associates/char/fjson

Charlotte Somrestructurebb03820

main
3.1 KiB99 linesraw
1import type { Options } from "./options.ts";
2import { type JsonObject, type JsonValue, toJsonValue } from "./parse.ts";
3
4const WIDTH = 80;
5
6function isObject(value: JsonValue): value is JsonObject {
7  return typeof value === "object" && value !== null && !Array.isArray(value);
8}
9
10/** render an object key (leaving simple identifiers unquoted) */
11export function keyText(key: string): string {
12  if (/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(key)) return key;
13  return JSON.stringify(key);
14}
15
16function scalar(value: null | boolean | number | string): string {
17  if (typeof value === "number") {
18    return Number.isFinite(value) ? String(value) : "null";
19  }
20  return JSON.stringify(value);
21}
22
23function flat(value: JsonValue): string | undefined {
24  if (Array.isArray(value)) return value.length === 0 ? "[]" : undefined;
25  if (isObject(value)) {
26    return Object.keys(value).length === 0 ? "{}" : undefined;
27  }
28  return scalar(value);
29}
30
31interface Output {
32  text: string;
33}
34
35function write(
36  value: JsonValue,
37  implicit: boolean,
38  indent: number,
39  out: Output,
40): void {
41  const single = flat(value);
42  if (single !== undefined) {
43    out.text += single;
44    return;
45  }
46  if (isObject(value)) {
47    const child = implicit ? indent : indent + 2;
48    if (!implicit) out.text += "{\n";
49    Object.keys(value).forEach((key, index) => {
50      if (index > 0) out.text += "\n";
51      out.text += `${" ".repeat(child)}${keyText(key)}: `;
52      write(value[key], false, child, out);
53    });
54    if (!implicit) out.text += `\n${" ".repeat(indent)}}`;
55    return;
56  }
57  const items = value as JsonValue[];
58  const inline = items.map(flat);
59  if (inline.every((text): text is string => text !== undefined)) {
60    const text = `[${inline.join(", ")}]`;
61    const line = out.text.slice(out.text.lastIndexOf("\n") + 1);
62    if ([...line].length + [...text].length <= WIDTH) {
63      out.text += text;
64      return;
65    }
66  }
67  out.text += "[\n";
68  items.forEach((item, index) => {
69    if (index > 0) out.text += "\n";
70    out.text += " ".repeat(indent + 2);
71    write(item, false, indent + 2, out);
72  });
73  out.text += `\n${" ".repeat(indent)}]`;
74}
75
76/**
77 * Convert a JavaScript value into a pretty-printed fJSON document: two-space
78 * indentation, unquoted identifier keys, implicit nonempty root objects, and
79 * newline-separated entries. Short scalar arrays stay inline; longer arrays
80 * and nested containers expand toward an 80-column width. No final newline.
81 *
82 * Values pass through JSON semantics first, so `undefined`, functions,
83 * symbols, bigints, and circular structures fail with `TypeError`, and
84 * non-finite numbers print as `null`. Values beyond the nesting limit fail
85 * with an `FjsonError` of kind `"nesting"` rather than producing text that
86 * cannot be parsed back.
87 */
88export function stringify(value: unknown, options?: Options): string {
89  const out: Output = { text: "" };
90  write(toJsonValue(value, options), true, 0, out);
91  return out.text;
92}
93
94/** pretty-print validated JSON */
95export function printJson(value: JsonValue, implicit: boolean): string {
96  const out: Output = { text: "" };
97  write(value, implicit, 0, out);
98  return out.text;
99}