/** * JSON pretty-printer for Vega-Lite specs (spec §03A; docs/architecture/08 → * "compact formatter"). * * Uses `json-stringify-pretty-compact` for Vega's house style — arrays/objects * stay on one line until they exceed a width budget, then wrap — which reads far * better for VL specs than `JSON.stringify(…, null, 2)`. Pure and portable: no * Monaco, no browser. The Monaco formatting adapter * (`infrastructure/monaco-format`) is a thin wrapper over this. */ import stringify from 'json-stringify-pretty-compact'; /** Width budget before an array/object wraps onto multiple lines. */ const DEFAULT_MAX_LINE = 80; /** Default indent (spaces) — matches the editor's default tab size (spec §07). */ const DEFAULT_INDENT = 2; /** * Pretty-print a spec *object* (not text) in the same Vega house style as * {@link formatJson}. Object-in callers — the lesson source panes, anything that * holds a spec as a parsed value — use this so their rendered JSON matches the * editor's formatting exactly. Key order is the object's own insertion order, * deliberately: where consecutive specs are small edits of one another (a lesson's * 80%→100% chain), a stable order is what keeps a line-diff between them legible. */ export function formatSpec( value: unknown, opts: { indent?: number; maxLength?: number } = {}, ): string { return stringify(value, { indent: opts.indent ?? DEFAULT_INDENT, maxLength: opts.maxLength ?? DEFAULT_MAX_LINE, }); } /** * Reformat `text` as consistently-indented JSON, or return `null` when it is not * valid JSON. Returning `null` (rather than throwing) lets callers skip a no-op * rather than mangle an in-progress edit, mirroring auto-save's "skip when * unparseable" rule (spec §03B). The result is idempotent: formatting * already-formatted text returns it unchanged. */ export function formatJson( text: string, opts: { indent?: number; maxLength?: number } = {}, ): string | null { let parsed: unknown; try { parsed = JSON.parse(text); } catch { return null; } return formatSpec(parsed, opts); }