基于vue3.0和element-plus的组件库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
7.5 KiB

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);
}
}