Migrated to TypeScript.
This commit is contained in:
138
src/markdown.ts
Normal file
138
src/markdown.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// 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");
|
||||
}
|
||||
Reference in New Issue
Block a user