基于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.
 
 
 
 

542 lines
15 KiB

import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types";
import { buildInlinePreview } from "./inlinePreview";
import {
formatCircularReference,
isCircularReferenceMarker,
normalizeVueValue,
} from "./normalizeValue";
type PlainObject = Record<string, unknown>;
type JsonContainerKind = "object" | "array" | "map" | "set";
type JsonContainerValue = PlainObject | unknown[] | Map<unknown, unknown> | Set<unknown>;
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<string, unknown> {
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 supportsInlinePreview(kind: JsonContainerKind) {
return kind === "object" || kind === "array";
}
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<unknown, unknown>).size;
case "set":
return (value as Set<unknown>).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<object, true>()): 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<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0)
: kind === "set"
? 2 + Array.from(value as Set<unknown>).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0)
: kind === "map"
? 2 +
Array.from((value as Map<unknown, unknown>).values()).reduce<number>(
(sum, entry) => sum + countValueLines(entry, ancestors),
0
)
: 2 +
Object.values(value as PlainObject).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0);
ancestors.delete(reference);
return total;
}
function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakMap<object, true>) {
if (kind === "array") {
return (value as unknown[]).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0);
}
if (kind === "set") {
return Array.from(value as Set<unknown>).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0);
}
if (kind === "map") {
return Array.from((value as Map<unknown, unknown>).values()).reduce<number>(
(sum, entry) => sum + countValueLines(entry, ancestors),
0
);
}
return Object.values(value as PlainObject).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0);
}
function createNode(
counter: LineCounter,
context: RowContext,
type: JsonViewNodeType,
content: string,
value: unknown,
extras?: Partial<JsonViewNode>
): 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<object, string>
) {
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<object, string>
) {
const length = getContainerLength(value, kind);
let collapsedByDefault = isCollapsedByDefault(context.level, length, options);
let inlinePreviewResult: ReturnType<typeof buildInlinePreview> | null = null;
const showDoubleQuotes = options.showDoubleQuotes ?? true;
const showKeyValueSpace = options.showKeyValueSpace ?? true;
if (options.inline && supportsInlinePreview(kind) && (collapsedByDefault || options.collapseFullyInlineNode)) {
inlinePreviewResult = buildInlinePreview(value, {
maxWidth: options.maxInlineDiplayWidth ?? 600,
showDoubleQuotes,
showKeyValueSpace,
});
if (!collapsedByDefault && options.collapseFullyInlineNode && inlinePreviewResult.isComplete) {
collapsedByDefault = true;
}
}
const explicitExpanded = options.expandedState.get(context.path);
const expanded = explicitExpanded !== undefined ? explicitExpanded : !collapsedByDefault;
const totalLineCount = 2 + countContainerChildLines(kind, value, new WeakMap<object, true>());
if (!expanded) {
const collapsedContent =
inlinePreviewResult?.text ?? (options.inline && supportsInlinePreview(kind)
? buildInlinePreview(value, {
maxWidth: options.maxInlineDiplayWidth ?? 600,
showDoubleQuotes,
showKeyValueSpace,
}).text
: getContainerContent(kind, "collapsed"));
rows.push(
createNode(counter, context, getContainerNodeType(kind, "collapsed"), collapsedContent, 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<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 {
Array.from(value as Map<unknown, unknown>).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<object, string>()
);
return rows;
}