/** UTF-16 code-unit offsets into the parsed source. */ export interface Span { readonly start: number; readonly end: number; } /** malformed input | bad Unicode escape | nesting exceeds limit */ export type FjsonErrorKind = "syntax" | "unicode" | "nesting"; function location(name: string, source: string, offset: number): string { const prefix = source.slice(0, Math.max(0, Math.min(offset, source.length))); let line = 1; let column = 1; let previousCr = false; for (const ch of prefix) { if (ch === "\r") { line += 1; column = 1; } else if (ch === "\n") { if (!previousCr) line += 1; column = 1; } else { column += 1; } previousCr = ch === "\r"; } return `${name}:${line}:${column}`; } /** Represents a failure to parse an fJSON document */ export class FjsonError extends SyntaxError { readonly kind: FjsonErrorKind; readonly span: Span; readonly previous?: Span; constructor( kind: FjsonErrorKind, message: string, span: Span, previous?: Span, ) { super(message); this.kind = kind; this.span = span; if (previous !== undefined) this.previous = previous; } /** Render against the same source that was parsed. */ render(name: string, source: string): string { let message = `${location(name, source, this.span.start)}: ${this.message}`; if (this.previous !== undefined) { message += `\n${ location(name, source, this.previous.start) }: First defined here`; } return message; } }