import { FjsonError, type Span } from "./errors.ts"; import { type Options, resolveMaxDepth } from "./options.ts"; import { Scanner, type Token } from "./scan.ts"; /** any JSON data */ export type JsonValue = | null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; export type JsonObject = { [key: string]: JsonValue }; export interface NullNode { readonly kind: "null"; readonly span: Span; } export interface BoolNode { readonly kind: "boolean"; readonly value: boolean; readonly span: Span; } export interface NumberNode { readonly kind: "number"; readonly value: number; readonly raw: string; readonly span: Span; } export interface StringNode { readonly kind: "string"; readonly value: string; readonly span: Span; } export interface ArrayNode { readonly kind: "array"; readonly items: SyntaxNode[]; readonly span: Span; } export interface ObjectMember { readonly key: string; readonly keySpan: Span; readonly value: SyntaxNode; } export interface ObjectNode { readonly kind: "object"; readonly members: ObjectMember[]; readonly span: Span; } /** spanned AST types */ export type SyntaxNode = | NullNode | BoolNode | NumberNode | StringNode | ArrayNode | ObjectNode; function defineKey(object: JsonObject, key: string, value: JsonValue): void { // use defineProperty to avoid prototype's setter Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true, }); } /** drop span info & convert */ export function nodeToValue(node: SyntaxNode): JsonValue { switch (node.kind) { case "null": return null; case "boolean": case "number": case "string": return node.value; case "array": return node.items.map(nodeToValue); case "object": { const object: JsonObject = {}; for (const member of node.members) { defineKey(object, member.key, nodeToValue(member.value)); } return object; } } } class Parser { readonly #scanner: Scanner; readonly #maxDepth: number; constructor(source: string, maxDepth: number) { this.#scanner = new Scanner(source); this.#maxDepth = maxDepth; } parseRoot(): SyntaxNode { const token = this.#scanner.peek(); const implicit = token.kind === "eof" || ((token.kind === "string" || token.kind === "identifier") && this.#scanner.peek(1).kind === ":"); const value = implicit ? this.#object(false, 0) : this.#value(0); if (this.#scanner.peek().kind !== "eof") { throw this.#error("Unexpected content after the root value"); } return value; } parseObjectRoot(): ObjectNode { const value = this.#object(this.#scanner.peek().kind === "{", 0); if (this.#scanner.peek().kind !== "eof") { throw this.#error("Unexpected content after the root object"); } return value; } #error(message: string): FjsonError { const { start, end } = this.#scanner.peek().span; return new FjsonError("syntax", message, { start, end: Math.min(start + 1, end), }); } #expect(kind: Token["kind"]): Token { if (this.#scanner.peek().kind !== kind) { throw this.#error(`Expected \`${kind}\``); } return this.#scanner.take(); } #atEnd(closing: "}" | "]" | "eof"): boolean { const token = this.#scanner.peek(); if (token.kind === closing) return true; if (token.kind === "eof") throw this.#error(`Expected \`${closing}\``); return false; } #separator(closing: "}" | "]" | "eof"): void { if (this.#atEnd(closing)) return; const token = this.#scanner.peek(); if (token.kind === ",") { this.#scanner.take(); } else if (!token.newlineBefore) { throw this.#error("Expected `,` or newline"); } } #object(braced: boolean, depth: number): ObjectNode { const start = this.#scanner.peek().span.start; if (braced) this.#expect("{"); const closing = braced ? "}" : "eof"; const members: ObjectMember[] = []; const keys = new Map(); let end = start; while (!this.#atEnd(closing)) { const token = this.#scanner.peek(); if (token.kind !== "string" && token.kind !== "identifier") { throw this.#error("Expected an object key"); } this.#scanner.take(); const key = token.value; const keySpan = token.span; const previous = keys.get(key); if (previous !== undefined) { throw new FjsonError( "syntax", `Duplicate key ${JSON.stringify(key)}`, keySpan, previous, ); } keys.set(key, keySpan); this.#expect(":"); const value = this.#value(depth + 1); members.push({ key, keySpan, value }); end = value.span.end; this.#separator(closing); } if (braced) end = this.#expect("}").span.end; return { kind: "object", members, span: { start, end } }; } #array(depth: number): ArrayNode { const start = this.#expect("[").span.start; const items: SyntaxNode[] = []; while (!this.#atEnd("]")) { items.push(this.#value(depth + 1)); this.#separator("]"); } const end = this.#expect("]").span.end; return { kind: "array", items, span: { start, end } }; } #value(depth: number): SyntaxNode { const token = this.#scanner.peek(); const { span } = token; switch (token.kind) { case "{": case "[": if (depth > this.#maxDepth) { throw new FjsonError( "nesting", `fJSON nesting exceeds ${this.#maxDepth} levels below the root`, span, ); } return token.kind === "{" ? this.#object(true, depth) : this.#array(depth); case "string": this.#scanner.take(); return { kind: "string", value: token.value, span }; case "number": this.#scanner.take(); return { kind: "number", value: token.value, raw: token.raw, span }; case "identifier": this.#scanner.take(); if (token.value === "true" || token.value === "false") { return { kind: "boolean", value: token.value === "true", span }; } if (token.value === "null") return { kind: "null", span }; throw new FjsonError( "syntax", `Unknown value ${ JSON.stringify(token.value) }; strings must be quoted`, span, ); default: throw this.#error("Expected a value"); } } } /** parse document into AST */ export function parseTree(source: string, options?: Options): SyntaxNode { return new Parser(source, resolveMaxDepth(options)).parseRoot(); } /** parse document into object node (throws if wrong type) */ export function parseObjectTree(source: string, options?: Options): ObjectNode { return new Parser(source, resolveMaxDepth(options)).parseObjectRoot(); } /** safe unknown -> JsonValue conversion */ export function toJsonValue(value: unknown, options?: Options): JsonValue { let json: string | undefined; try { json = JSON.stringify(value); } catch { throw new TypeError("Value is not JSON serializable"); } if (json === undefined) { throw new TypeError("Value is not JSON serializable"); } return parse(json, options); } /** * parse an fJSON document. * empty or comment-only documents return `{}` */ export function parse(source: string, options?: Options): JsonValue { return nodeToValue(parseTree(source, options)); } /** parse an fJSON document which **must** be an object */ export function parseObject(source: string, options?: Options): JsonObject { const object: JsonObject = {}; for (const member of parseObjectTree(source, options).members) { defineKey(object, member.key, nodeToValue(member.value)); } return object; }