139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
|
|
type Scalar = string | number | boolean | null | undefined;
|
|
type ScalarRow = Record<string, Scalar>;
|
|
|
|
type HeaderSpec =
|
|
| { mode: "default" }
|
|
| { mode: "auto" }
|
|
| { mode: "original" }
|
|
| { mode: "list"; labels: string[] }
|
|
| { mode: "map"; map: Record<string, string> };
|
|
|
|
function formatCell(value: unknown): string {
|
|
const text = value == null
|
|
? ""
|
|
: String(value);
|
|
return text.replaceAll("|", "\\|").replaceAll("\n", "<br>");
|
|
}
|
|
|
|
function isGuid(value: unknown): value is string {
|
|
return typeof value === "string"
|
|
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
}
|
|
|
|
function toAutoHeaderLabel(key: string): string {
|
|
const withSpaces = String(key)
|
|
.replace(/[_-]+/g, " ")
|
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
return withSpaces
|
|
.split(" ")
|
|
.filter(Boolean)
|
|
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
.join(" ");
|
|
}
|
|
|
|
function isScalar(value: unknown): value is Scalar {
|
|
return value == null || typeof value !== "object";
|
|
}
|
|
|
|
function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: ScalarRow[] } {
|
|
let rows: Array<Record<string, unknown>>;
|
|
if (Array.isArray(value)) {
|
|
rows = value.map((item) =>
|
|
item && typeof item === "object" && !Array.isArray(item)
|
|
? item as Record<string, unknown>
|
|
: { value: item },
|
|
);
|
|
} else if (value && typeof value === "object") {
|
|
rows = [value as Record<string, unknown>];
|
|
} else {
|
|
rows = [{ value }];
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
return {
|
|
headers: ["result"],
|
|
rows: [{ result: "" }],
|
|
};
|
|
}
|
|
|
|
const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))]
|
|
.filter((key) =>
|
|
rows.every((row) => isScalar(row[key])),
|
|
);
|
|
|
|
if (headers.length === 0) {
|
|
return {
|
|
headers: ["result"],
|
|
rows: [{ result: "" }],
|
|
};
|
|
}
|
|
|
|
const scalarRows: ScalarRow[] = rows.map((row) => {
|
|
const result: ScalarRow = {};
|
|
for (const [key, rowValue] of Object.entries(row)) {
|
|
if (isScalar(rowValue)) {
|
|
result[key] = rowValue;
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
|
|
return { headers, rows: scalarRows };
|
|
}
|
|
|
|
export function toMarkdownTable(
|
|
value: unknown,
|
|
pretty = false,
|
|
quoteGuids = false,
|
|
headerSpec: HeaderSpec = { mode: "default" },
|
|
): string {
|
|
const { headers, rows } = getScalarRowsAndHeaders(value);
|
|
const headerDefinitions = headers.map((key, idx) => {
|
|
let label = key;
|
|
if (headerSpec.mode === "auto") {
|
|
label = toAutoHeaderLabel(key);
|
|
} else if (headerSpec.mode === "list" && headerSpec.labels[idx]) {
|
|
label = headerSpec.labels[idx];
|
|
} else if (headerSpec.mode === "map" && headerSpec.map[key]) {
|
|
label = headerSpec.map[key];
|
|
}
|
|
return { key, label };
|
|
});
|
|
|
|
const renderCell = (raw: Scalar): string => {
|
|
const text = formatCell(raw);
|
|
return quoteGuids && isGuid(raw) ? `\`${text}\`` : text;
|
|
};
|
|
|
|
if (!pretty) {
|
|
const headerLine = `| ${headerDefinitions.map((h) => h.label).join(" | ")} |`;
|
|
const separatorLine = `| ${headerDefinitions.map(() => "---").join(" | ")} |`;
|
|
const rowLines = rows.map((row) =>
|
|
`| ${headerDefinitions.map((h) => formatCell(row[h.key])).join(" | ")} |`,
|
|
);
|
|
return [headerLine, separatorLine, ...rowLines].join("\n");
|
|
}
|
|
|
|
const widths = headerDefinitions.map((header, idx) =>
|
|
Math.max(
|
|
header.label.length,
|
|
...rows.map((row) => renderCell(row[headerDefinitions[idx].key]).length),
|
|
)
|
|
);
|
|
|
|
const renderRow = (values: string[]): string =>
|
|
`| ${values.map((v, idx) => v.padEnd(widths[idx], " ")).join(" | ")} |`;
|
|
|
|
const headerLine = renderRow(headerDefinitions.map((h) => h.label));
|
|
const separatorLine = `|-${widths.map((w) => "-".repeat(w)).join("-|-")}-|`;
|
|
const rowLines = rows.map((row) =>
|
|
renderRow(headerDefinitions.map((header) => renderCell(row[header.key]))),
|
|
);
|
|
|
|
return [headerLine, separatorLine, ...rowLines].join("\n");
|
|
}
|