import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types"; import { formatCircularReference, isCircularReferenceMarker, normalizeVueValue, } from "./normalizeValue"; 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 toDisplayPath(path: string, rootPath: string) { if (path === rootPath) { return "$"; } if (path.startsWith(`${rootPath}.`)) { return `$${path.slice(rootPath.length)}`; } if (path.startsWith(`${rootPath}[`)) { return `$${path.slice(rootPath.length)}`; } return path; } 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) { const normalizedValue = normalizeVueValue(value); if (isCircularReferenceMarker(normalizedValue)) { return formatCircularReference(normalizedValue.path); } value = normalizedValue; 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) { const normalizedValue = normalizeVueValue(value); if (isCircularReferenceMarker(normalizedValue)) { return formatCircularReference(normalizedValue.path); } value = normalizedValue; 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 { value = normalizeVueValue(value); if (isCircularReferenceMarker(value)) { return 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 WeakMap()): number { value = normalizeVueValue(value); if (isCircularReferenceMarker(value)) { return 1; } const kind = getContainerKind(value); if (!kind) { return 1; } const reference = value as object; if (ancestors.has(reference)) { return 1; } ancestors.set(reference, true); 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: WeakMap) { 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: WeakMap ) { const normalizedValue = normalizeVueValue(value, toDisplayPath(context.path, options.rootPath)); if (isCircularReferenceMarker(normalizedValue)) { rows.push( createNode( counter, context, "content", formatCircularReference(normalizedValue.path ?? toDisplayPath(context.path, options.rootPath)), normalizedValue, { isLeaf: true } ) ); return; } value = normalizedValue; const kind = getContainerKind(value); if (!kind) { rows.push(createNode(counter, context, "content", formatPrimitiveDisplay(value), value, { isLeaf: true })); return; } const reference = value as object; const existingPath = ancestors.get(reference); if (existingPath) { rows.push( createNode(counter, context, "content", formatCircularReference(toDisplayPath(existingPath, options.rootPath)), value, { isLeaf: true, }) ); return; } ancestors.set(reference, context.path); 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: WeakMap ) { const length = getContainerLength(value, kind); const expanded = isExpanded(context.path, context.level, length, options); const totalLineCount = 2 + countContainerChildLines(kind, value, new WeakMap()); 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) => { const normalizedKeyValue = normalizeVueValue(keyValue, toDisplayPath(context.path, options.rootPath)); appendValueRows( childValue, { path: buildMapPath(context.path, normalizedKeyValue, index), level: context.level + 1, displayKey: formatKeyDisplay(normalizedKeyValue), keyValue: normalizedKeyValue, 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 WeakMap() ); return rows; }