import { measureShrinkWrapWidth } from "../list-table-v2/measureText"; import { formatCircularReference, isCircularReferenceMarker, normalizeVueValue } from "./normalizeValue"; type PlainObject = Record; type InlineContainerKind = "object" | "array" | "map" | "set"; export const JSON_VIEW_DEFAULT_FONT = "13px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"; const FULLY_COLLAPSED_STRING = '"..."'; const FULLY_COLLAPSED_ARRAY = "[...]"; const FULLY_COLLAPSED_OBJECT = "{...}"; const MIN_KEPT_STRING_WIDTH = 150; export interface InlinePreviewOptions { maxWidth: number; showDoubleQuotes: boolean; showKeyValueSpace: boolean; } export interface InlinePreviewResult { text: string; isComplete: boolean; } function measureWidth(text: string) { return measureShrinkWrapWidth(text, JSON_VIEW_DEFAULT_FONT); } function fits(text: string, maxWidth: number) { return measureWidth(text) <= maxWidth; } function isRecordLike(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set); } function getContainerKind(value: unknown): InlineContainerKind | 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 getCollapsedContainerText(kind: InlineContainerKind) { switch (kind) { case "map": return "Map {...}"; case "set": return "Set [...]"; case "object": return FULLY_COLLAPSED_OBJECT; case "array": default: return FULLY_COLLAPSED_ARRAY; } } function formatPrimitiveText(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 formatObjectKey(key: string, showDoubleQuotes: boolean) { return showDoubleQuotes ? JSON.stringify(key) : key; } function buildTruncatedString(value: string, charCount: number) { if (charCount <= 0) { return FULLY_COLLAPSED_STRING; } const truncated = JSON.stringify(value.slice(0, charCount)); return `${truncated.slice(0, -1)}..."`; } function formatStringPreview(value: string, options: InlinePreviewOptions): InlinePreviewResult { const full = JSON.stringify(value); if (fits(full, options.maxWidth)) { return { text: full, isComplete: true }; } const preserveVisibleChars = measureWidth(full) <= MIN_KEPT_STRING_WIDTH; let low = preserveVisibleChars ? 1 : 0; let high = value.length; let bestCount = -1; while (low <= high) { const mid = Math.floor((low + high) / 2); const candidate = buildTruncatedString(value, mid); if (fits(candidate, options.maxWidth)) { bestCount = mid; low = mid + 1; } else { high = mid - 1; } } if (bestCount >= 0) { return { text: buildTruncatedString(value, bestCount), isComplete: false, }; } return { text: FULLY_COLLAPSED_STRING, isComplete: false, }; } function formatArrayPreview( value: unknown[], options: InlinePreviewOptions, ancestors: WeakMap ): InlinePreviewResult { if (value.length === 0) { return { text: "[]", isComplete: true }; } const parts: string[] = []; let isComplete = true; for (let index = 0; index < value.length; index++) { const prefix = parts.length > 0 ? `[${parts.join(", ")}, ` : "["; const tail = index < value.length - 1 ? ", ...]" : "]"; const budget = options.maxWidth - measureWidth(prefix) - measureWidth(tail); if (budget <= 0) { return { text: parts.length > 0 ? `[${parts.join(", ")}, ...]` : FULLY_COLLAPSED_ARRAY, isComplete: false, }; } const item = buildInlinePreview(value[index], { ...options, maxWidth: budget }, ancestors); const candidate = `${prefix}${item.text}${tail}`; if (!fits(candidate, options.maxWidth)) { return { text: parts.length > 0 ? `[${parts.join(", ")}, ...]` : FULLY_COLLAPSED_ARRAY, isComplete: false, }; } parts.push(item.text); if (!item.isComplete) { isComplete = false; } } return { text: `[${parts.join(", ")}]`, isComplete, }; } function formatObjectPreview( value: PlainObject, options: InlinePreviewOptions, ancestors: WeakMap ): InlinePreviewResult { const entries = Object.entries(value); if (entries.length === 0) { return { text: "{}", isComplete: true }; } const parts: string[] = []; let isComplete = true; for (let index = 0; index < entries.length; index++) { const [key, entryValue] = entries[index]; const keyText = formatObjectKey(key, options.showDoubleQuotes); const pairPrefix = `${keyText}${options.showKeyValueSpace ? ": " : ":"}`; const prefix = parts.length > 0 ? `{${parts.join(", ")}, ${pairPrefix}` : `{${pairPrefix}`; const tail = index < entries.length - 1 ? ", ...}" : "}"; const budget = options.maxWidth - measureWidth(prefix) - measureWidth(tail); if (budget <= 0) { return { text: parts.length > 0 ? `{${parts.join(", ")}, ...}` : FULLY_COLLAPSED_OBJECT, isComplete: false, }; } const item = buildInlinePreview(entryValue, { ...options, maxWidth: budget }, ancestors); const candidate = `${prefix}${item.text}${tail}`; if (!fits(candidate, options.maxWidth)) { return { text: parts.length > 0 ? `{${parts.join(", ")}, ...}` : FULLY_COLLAPSED_OBJECT, isComplete: false, }; } parts.push(`${pairPrefix}${item.text}`); if (!item.isComplete) { isComplete = false; } } return { text: `{${parts.join(", ")}}`, isComplete, }; } export function buildInlinePreview( value: unknown, options: InlinePreviewOptions, ancestors = new WeakMap() ): InlinePreviewResult { const normalizedValue = normalizeVueValue(value); if (isCircularReferenceMarker(normalizedValue)) { return { text: formatCircularReference(normalizedValue.path), isComplete: true, }; } value = normalizedValue; if (typeof value === "string") { return formatStringPreview(value, options); } const kind = getContainerKind(value); if (!kind) { return { text: formatPrimitiveText(value), isComplete: true, }; } if (kind === "map" || kind === "set") { return { text: getCollapsedContainerText(kind), isComplete: false, }; } const reference = value as object; if (ancestors.has(reference)) { return { text: formatCircularReference(), isComplete: true, }; } ancestors.set(reference, true); try { if (kind === "array") { return formatArrayPreview(value as unknown[], options, ancestors); } return formatObjectPreview(value as PlainObject, options, ancestors); } finally { ancestors.delete(reference); } }