forked from mengyxu/noob-components
5 changed files with 368 additions and 75 deletions
@ -0,0 +1,299 @@
@@ -0,0 +1,299 @@
|
||||
import { measureShrinkWrapWidth } from "../list-table-v2/measureText"; |
||||
import { formatCircularReference, isCircularReferenceMarker, normalizeVueValue } from "./normalizeValue"; |
||||
|
||||
type PlainObject = Record<string, unknown>; |
||||
|
||||
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<string, unknown> { |
||||
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<object, true> |
||||
): 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<object, true> |
||||
): 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<object, true>() |
||||
): 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); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue