Render Aligned Tables in the Terminal
Render terminal tables with column alignment, ignoring ANSI color codes in width calculations and adding a header separator.
Code
export type Alignment = "left" | "right" | "center";
export interface TableOptions {
readonly headers?: readonly string[];
readonly align?: readonly Alignment[];
}
const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
export function stripAnsi(value: string): string {
return value.replace(ANSI_PATTERN, "");
}
export function renderTable(
rows: readonly (readonly string[])[],
options: TableOptions = {},
): string {
if (!Array.isArray(rows)) throw new TypeError("rows must be an array");
const headers = options.headers ?? [];
if (!Array.isArray(headers)) throw new TypeError("headers must be an array");
const allRows: readonly (readonly string[])[] = headers.length > 0 ? [headers, ...rows] : rows;
if (allRows.length === 0) return "";
const columnCount = Math.max(...allRows.map((row) => row.length));
if (columnCount === 0) return "";
for (const row of allRows) {
if (!Array.isArray(row)) throw new TypeError("every row must be an array");
for (const cell of row) {
if (typeof cell !== "string") throw new TypeError("cells must be strings");
}
}
const widths = Array.from({ length: columnCount }, (_, column) =>
Math.max(...allRows.map((row) => stripAnsi(row[column] ?? "").length)),
);
const align = options.align ?? [];
const renderRow = (row: readonly string[]): string =>
Array.from({ length: columnCount }, (_, column) =>
pad(row[column] ?? "", widths[column], align[column] ?? "left"),
).join(" | ");
const separator = widths.map((width) => "-".repeat(width)).join("-+-");
const lines = allRows.map(renderRow);
if (headers.length > 0) lines.splice(1, 0, separator);
return lines.join("\n");
}
function pad(value: string, width: number, alignment: Alignment): string {
const missing = Math.max(0, width - stripAnsi(value).length);
if (alignment === "right") return " ".repeat(missing) + value;
if (alignment === "center") {
const left = Math.floor(missing / 2);
return " ".repeat(left) + value + " ".repeat(missing - left);
}
return value + " ".repeat(missing);
}
const table = renderTable(
[
["\u001B[32mready\u001B[0m", "42 ms", "0.4%"],
["failed", "1204 ms", "12.5%"],
],
{ headers: ["status", "latency", "errors"], align: ["left", "right", "right"] },
);
console.log(table);
console.log(stripAnsi("\u001B[31mred\u001B[0m").length);Report a problem with this exampleOpens a pre-filled issue on GitHub.