import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types"; type PlainObject = Record; type JsonContainerKind = "object" | "array" | "map" | "set"; type JsonContainerValue = PlainObject | unknown[] | Map | Set; interface RowContext { path: string; level: number; key?: string; displayKey?: string; keyValue?: unknown; index?: number; showComma: boolean; posInSet: number; setSize: number; } interface LineCounter { nextLineNumber: number; } function isRecordLike(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set); } function buildNodeId(path: string, type: JsonViewNodeType) { return `${path}:${type}`; } function buildObjectPath(parentPath: string, key: string) { if (/^[A-Za-z_$][\w$]*$/.test(key)) { return `${parentPath}.${key}`; } return `${parentPath}[${JSON.stringify(key)}]`; } function buildArrayPath(parentPath: string, index: number) { return `${parentPath}[${index}]`; } function buildMapPath(parentPath: string, key: unknown, index: number) { if (typeof key === "string") { return `${parentPath}.get(${JSON.stringify(key)})`; } if (typeof key === "number" || typeof key === "boolean" || typeof key === "bigint") { return `${parentPath}.get(${String(key)})`; } if (key === null) { return `${parentPath}.get(null)`; } if (key === undefined) { return `${parentPath}.get(undefined)`; } if (typeof key === "symbol") { return `${parentPath}.get(${String(key)})`; } return `${parentPath}.entries[${index}]`; } function isCollapsedByDefault(level: number, length: number, options: BuildVisibleJsonRowsOptions) { const deepCollapsed = typeof options.deep === "number" ? level + 1 > options.deep : false; const lengthCollapsed = typeof options.collapsedNodeLength === "number" ? length > options.collapsedNodeLength : false; return deepCollapsed || lengthCollapsed; } function isExpanded(path: string, level: number, length: number, options: BuildVisibleJsonRowsOptions) { const explicit = options.expandedState.get(path); if (explicit !== undefined) { return explicit; } return !isCollapsedByDefault(level, length, options); } function formatKeyDisplay(value: unknown) { if (typeof value === "string") { return JSON.stringify(value); } if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (typeof value === "bigint") { return `${String(value)}n`; } if (value === null) { return "null"; } if (value === undefined) { return "undefined"; } if (typeof value === "symbol") { return value.toString(); } if (typeof value === "function") { return value.name ? `[Function ${value.name}]` : "[Function]"; } if (Array.isArray(value)) { return `Array(${value.length})`; } if (value instanceof Map) { return `Map(${value.size})`; } if (value instanceof Set) { return `Set(${value.size})`; } return "Object"; } function formatPrimitiveDisplay(value: unknown) { if (typeof value === "string") { return JSON.stringify(value); } if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (typeof value === "bigint") { return `${String(value)}n`; } if (value === null) { return "null"; } if (value === undefined) { return "undefined"; } if (typeof value === "symbol") { return value.toString(); } if (typeof value === "function") { return value.name ? `[Function ${value.name}]` : "[Function]"; } return String(value); } function getContainerKind(value: unknown): JsonContainerKind | null { if (Array.isArray(value)) { return "array"; } if (value instanceof Map) { return "map"; } if (value instanceof Set) { return "set"; } if (isRecordLike(value)) { return "object"; } return null; } function getContainerLength(value: JsonContainerValue, kind: JsonContainerKind) { switch (kind) { case "array": return (value as unknown[]).length; case "map": return (value as Map).size; case "set": return (value as Set).size; case "object": default: return Object.keys(value as PlainObject).length; } } function getContainerContent(kind: JsonContainerKind, state: "start" | "end" | "collapsed") { switch (kind) { case "map": if (state === "start") return "Map {"; if (state === "end") return "}"; return "Map {...}"; case "set": if (state === "start") return "Set ["; if (state === "end") return "]"; return "Set [...]"; case "object": if (state === "start") return "{"; if (state === "end") return "}"; return "{...}"; case "array": default: if (state === "start") return "["; if (state === "end") return "]"; return "[...]"; } } function getContainerNodeType(kind: JsonContainerKind, state: "start" | "end" | "collapsed"): JsonViewNodeType { if (kind === "object" || kind === "map") { if (state === "start") return "objectStart"; if (state === "end") return "objectEnd"; return "objectCollapsed"; } if (state === "start") return "arrayStart"; if (state === "end") return "arrayEnd"; return "arrayCollapsed"; } function countValueLines(value: unknown, ancestors = new WeakSet()): number { const kind = getContainerKind(value); if (!kind) { return 1; } const reference = value as object; if (ancestors.has(reference)) { return 1; } ancestors.add(reference); const total = kind === "array" ? 2 + (value as unknown[]).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0) : kind === "set" ? 2 + Array.from(value as Set).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0) : kind === "map" ? 2 + Array.from((value as Map).values()).reduce( (sum, entry) => sum + countValueLines(entry, ancestors), 0 ) : 2 + Object.values(value as PlainObject).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0); ancestors.delete(reference); return total; } function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakSet) { if (kind === "array") { return (value as unknown[]).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0); } if (kind === "set") { return Array.from(value as Set).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0); } if (kind === "map") { return Array.from((value as Map).values()).reduce( (sum, entry) => sum + countValueLines(entry, ancestors), 0 ); } return Object.values(value as PlainObject).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0); } function createNode( counter: LineCounter, context: RowContext, type: JsonViewNodeType, content: string, value: unknown, extras?: Partial ): JsonViewNode { return { id: buildNodeId(context.path, type), path: context.path, lineNumber: counter.nextLineNumber++, level: context.level, key: context.key, displayKey: context.displayKey, keyValue: context.keyValue, index: context.index, content, showComma: context.showComma, type, value, isLeaf: type === "content", hasToggle: false, isExpanded: false, posInSet: context.posInSet, setSize: context.setSize, ...extras, }; } function appendValueRows( value: unknown, context: RowContext, rows: JsonViewNode[], options: BuildVisibleJsonRowsOptions, counter: LineCounter, ancestors: WeakSet ) { const kind = getContainerKind(value); if (!kind) { rows.push(createNode(counter, context, "content", formatPrimitiveDisplay(value), value, { isLeaf: true })); return; } const reference = value as object; if (ancestors.has(reference)) { rows.push(createNode(counter, context, "content", "[Circular]", value, { isLeaf: true })); return; } ancestors.add(reference); appendContainerRows(kind, value as JsonContainerValue, context, rows, options, counter, ancestors); ancestors.delete(reference); } function appendContainerRows( kind: JsonContainerKind, value: JsonContainerValue, context: RowContext, rows: JsonViewNode[], options: BuildVisibleJsonRowsOptions, counter: LineCounter, ancestors: WeakSet ) { const length = getContainerLength(value, kind); const expanded = isExpanded(context.path, context.level, length, options); const totalLineCount = 2 + countContainerChildLines(kind, value, ancestors); if (!expanded) { rows.push( createNode(counter, context, getContainerNodeType(kind, "collapsed"), getContainerContent(kind, "collapsed"), value, { length, hasToggle: true, isExpanded: false, isLeaf: false, }) ); counter.nextLineNumber += totalLineCount - 1; return; } rows.push( createNode(counter, context, getContainerNodeType(kind, "start"), getContainerContent(kind, "start"), value, { length, hasToggle: true, isExpanded: true, isLeaf: false, showComma: false, }) ); if (kind === "object") { Object.entries(value as PlainObject).forEach(([key, childValue], index) => { appendValueRows( childValue, { path: buildObjectPath(context.path, key), level: context.level + 1, key, showComma: index < length - 1, posInSet: index + 1, setSize: length, }, rows, options, counter, ancestors ); }); } else if (kind === "array") { (value as unknown[]).forEach((childValue, index) => { appendValueRows( childValue, { path: buildArrayPath(context.path, index), level: context.level + 1, index, showComma: index < length - 1, posInSet: index + 1, setSize: length, }, rows, options, counter, ancestors ); }); } else if (kind === "set") { Array.from(value as Set).forEach((childValue, index) => { appendValueRows( childValue, { path: buildArrayPath(context.path, index), level: context.level + 1, index, showComma: index < length - 1, posInSet: index + 1, setSize: length, }, rows, options, counter, ancestors ); }); } else { Array.from(value as Map).forEach(([keyValue, childValue], index) => { appendValueRows( childValue, { path: buildMapPath(context.path, keyValue, index), level: context.level + 1, displayKey: formatKeyDisplay(keyValue), keyValue, showComma: index < length - 1, posInSet: index + 1, setSize: length, }, rows, options, counter, ancestors ); }); } rows.push( createNode(counter, context, getContainerNodeType(kind, "end"), getContainerContent(kind, "end"), value, { length, hasToggle: false, isExpanded: expanded, isLeaf: false, key: undefined, displayKey: undefined, keyValue: undefined, index: undefined, }) ); } export function buildVisibleJsonRows(value: unknown, options: BuildVisibleJsonRowsOptions) { const rows: JsonViewNode[] = []; const counter: LineCounter = { nextLineNumber: 1 }; appendValueRows( value, { path: options.rootPath, level: 0, showComma: false, posInSet: 1, setSize: 1, }, rows, options, counter, new WeakSet() ); return rows; }