Files
sk-az-tools/src/markdown.ts

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