forked from mengyxu/noob-components
5 changed files with 368 additions and 75 deletions
@ -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