char/fjson

friendly JSON implementation in TypeScript

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

Charlotte Somsplit scanner from parser17b13b5

main
7.7 KiB279 linesraw
1import { FjsonError, type Span } from "./errors.ts";
2import { type Options, resolveMaxDepth } from "./options.ts";
3import { Scanner, type Token } from "./scan.ts";
4
5/** any JSON data */
6export type JsonValue =
7  | null
8  | boolean
9  | number
10  | string
11  | JsonValue[]
12  | { [key: string]: JsonValue };
13
14export type JsonObject = { [key: string]: JsonValue };
15
16export interface NullNode {
17  readonly kind: "null";
18  readonly span: Span;
19}
20export interface BoolNode {
21  readonly kind: "boolean";
22  readonly value: boolean;
23  readonly span: Span;
24}
25export interface NumberNode {
26  readonly kind: "number";
27  readonly value: number;
28  readonly raw: string;
29  readonly span: Span;
30}
31export interface StringNode {
32  readonly kind: "string";
33  readonly value: string;
34  readonly span: Span;
35}
36export interface ArrayNode {
37  readonly kind: "array";
38  readonly items: SyntaxNode[];
39  readonly span: Span;
40}
41export interface ObjectMember {
42  readonly key: string;
43  readonly keySpan: Span;
44  readonly value: SyntaxNode;
45}
46export interface ObjectNode {
47  readonly kind: "object";
48  readonly members: ObjectMember[];
49  readonly span: Span;
50}
51
52/** spanned AST types */
53export type SyntaxNode =
54  | NullNode
55  | BoolNode
56  | NumberNode
57  | StringNode
58  | ArrayNode
59  | ObjectNode;
60
61function defineKey(object: JsonObject, key: string, value: JsonValue): void {
62  // use defineProperty to avoid prototype's setter
63  Object.defineProperty(object, key, {
64    value,
65    enumerable: true,
66    writable: true,
67    configurable: true,
68  });
69}
70
71/** drop span info & convert */
72export function nodeToValue(node: SyntaxNode): JsonValue {
73  switch (node.kind) {
74    case "null":
75      return null;
76    case "boolean":
77    case "number":
78    case "string":
79      return node.value;
80    case "array":
81      return node.items.map(nodeToValue);
82    case "object": {
83      const object: JsonObject = {};
84      for (const member of node.members) {
85        defineKey(object, member.key, nodeToValue(member.value));
86      }
87      return object;
88    }
89  }
90}
91
92class Parser {
93  readonly #scanner: Scanner;
94  readonly #maxDepth: number;
95
96  constructor(source: string, maxDepth: number) {
97    this.#scanner = new Scanner(source);
98    this.#maxDepth = maxDepth;
99  }
100
101  parseRoot(): SyntaxNode {
102    const token = this.#scanner.peek();
103    const implicit = token.kind === "eof" ||
104      ((token.kind === "string" || token.kind === "identifier") &&
105        this.#scanner.peek(1).kind === ":");
106    const value = implicit ? this.#object(false, 0) : this.#value(0);
107    if (this.#scanner.peek().kind !== "eof") {
108      throw this.#error("Unexpected content after the root value");
109    }
110    return value;
111  }
112
113  parseObjectRoot(): ObjectNode {
114    const value = this.#object(this.#scanner.peek().kind === "{", 0);
115    if (this.#scanner.peek().kind !== "eof") {
116      throw this.#error("Unexpected content after the root object");
117    }
118    return value;
119  }
120
121  #error(message: string): FjsonError {
122    const { start, end } = this.#scanner.peek().span;
123    return new FjsonError("syntax", message, {
124      start,
125      end: Math.min(start + 1, end),
126    });
127  }
128
129  #expect(kind: Token["kind"]): Token {
130    if (this.#scanner.peek().kind !== kind) {
131      throw this.#error(`Expected \`${kind}\``);
132    }
133    return this.#scanner.take();
134  }
135
136  #atEnd(closing: "}" | "]" | "eof"): boolean {
137    const token = this.#scanner.peek();
138    if (token.kind === closing) return true;
139    if (token.kind === "eof") throw this.#error(`Expected \`${closing}\``);
140    return false;
141  }
142
143  #separator(closing: "}" | "]" | "eof"): void {
144    if (this.#atEnd(closing)) return;
145    const token = this.#scanner.peek();
146    if (token.kind === ",") {
147      this.#scanner.take();
148    } else if (!token.newlineBefore) {
149      throw this.#error("Expected `,` or newline");
150    }
151  }
152
153  #object(braced: boolean, depth: number): ObjectNode {
154    const start = this.#scanner.peek().span.start;
155    if (braced) this.#expect("{");
156    const closing = braced ? "}" : "eof";
157    const members: ObjectMember[] = [];
158    const keys = new Map<string, Span>();
159    let end = start;
160    while (!this.#atEnd(closing)) {
161      const token = this.#scanner.peek();
162      if (token.kind !== "string" && token.kind !== "identifier") {
163        throw this.#error("Expected an object key");
164      }
165      this.#scanner.take();
166      const key = token.value;
167      const keySpan = token.span;
168      const previous = keys.get(key);
169      if (previous !== undefined) {
170        throw new FjsonError(
171          "syntax",
172          `Duplicate key ${JSON.stringify(key)}`,
173          keySpan,
174          previous,
175        );
176      }
177      keys.set(key, keySpan);
178      this.#expect(":");
179      const value = this.#value(depth + 1);
180      members.push({ key, keySpan, value });
181      end = value.span.end;
182      this.#separator(closing);
183    }
184    if (braced) end = this.#expect("}").span.end;
185    return { kind: "object", members, span: { start, end } };
186  }
187
188  #array(depth: number): ArrayNode {
189    const start = this.#expect("[").span.start;
190    const items: SyntaxNode[] = [];
191    while (!this.#atEnd("]")) {
192      items.push(this.#value(depth + 1));
193      this.#separator("]");
194    }
195    const end = this.#expect("]").span.end;
196    return { kind: "array", items, span: { start, end } };
197  }
198
199  #value(depth: number): SyntaxNode {
200    const token = this.#scanner.peek();
201    const { span } = token;
202    switch (token.kind) {
203      case "{":
204      case "[":
205        if (depth > this.#maxDepth) {
206          throw new FjsonError(
207            "nesting",
208            `fJSON nesting exceeds ${this.#maxDepth} levels below the root`,
209            span,
210          );
211        }
212        return token.kind === "{"
213          ? this.#object(true, depth)
214          : this.#array(depth);
215      case "string":
216        this.#scanner.take();
217        return { kind: "string", value: token.value, span };
218      case "number":
219        this.#scanner.take();
220        return { kind: "number", value: token.value, raw: token.raw, span };
221      case "identifier":
222        this.#scanner.take();
223        if (token.value === "true" || token.value === "false") {
224          return { kind: "boolean", value: token.value === "true", span };
225        }
226        if (token.value === "null") return { kind: "null", span };
227        throw new FjsonError(
228          "syntax",
229          `Unknown value ${
230            JSON.stringify(token.value)
231          }; strings must be quoted`,
232          span,
233        );
234      default:
235        throw this.#error("Expected a value");
236    }
237  }
238}
239
240/** parse document into AST */
241export function parseTree(source: string, options?: Options): SyntaxNode {
242  return new Parser(source, resolveMaxDepth(options)).parseRoot();
243}
244
245/** parse document into object node (throws if wrong type) */
246export function parseObjectTree(source: string, options?: Options): ObjectNode {
247  return new Parser(source, resolveMaxDepth(options)).parseObjectRoot();
248}
249
250/** safe unknown -> JsonValue conversion */
251export function toJsonValue(value: unknown, options?: Options): JsonValue {
252  let json: string | undefined;
253  try {
254    json = JSON.stringify(value);
255  } catch {
256    throw new TypeError("Value is not JSON serializable");
257  }
258  if (json === undefined) {
259    throw new TypeError("Value is not JSON serializable");
260  }
261  return parse(json, options);
262}
263
264/**
265 * parse an fJSON document.
266 * empty or comment-only documents return `{}`
267 */
268export function parse(source: string, options?: Options): JsonValue {
269  return nodeToValue(parseTree(source, options));
270}
271
272/** parse an fJSON document which **must** be an object */
273export function parseObject(source: string, options?: Options): JsonObject {
274  const object: JsonObject = {};
275  for (const member of parseObjectTree(source, options).members) {
276    defineKey(object, member.key, nodeToValue(member.value));
277  }
278  return object;
279}