┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ // Data-driven ASCII table shared by the markdown transform and the │
│ // Table component. │
│ import { G, checkGrid, len, pad, wrap, wrapHard, type PadSide } from "./ascii"; │
│ │
│ export type Cell = { text: string; align: PadSide }; │
│ │
│ export function buildAsciiTable(rows: Cell[][], headerRows: number, width: number) │
│ : string { │
│ const nCols = Math.max(...rows.map((r) => r.length)); │
│ const norm = rows.map((r) => { │
│ const out = [...r]; │
│ while (out.length < nCols) out.push({ text: "", align: "end" }); │
│ return out; │
│ }); │
│ │
│ // Natural column widths, then shave the widest until the table fits. │
│ // Cells wrap to their final column width. │
│ const colW = Array.from({ length: nCols }, (_, i) => │
│ Math.max(1, ...norm.map((r) => len(r[i].text))), │
│ ); │
│ const total = () => 1 + colW.reduce((s, w) => s + w + 3, 0); │
│ const MIN_COL = 5; │
│ while (total() > width && colW.some((w) => w > MIN_COL)) { │
│ colW[colW.indexOf(Math.max(...colW))]--; │
│ } │
│ │
│ const edge = (l: string, m: string, r: string, h = G.H) => │
│ l + colW.map((w) => h.repeat(w + 2)).join(m) + r; │
│ │
│ // When any row wraps, dashed separators between body rows keep the │
│ // logical rows readable; all-single-line tables stay compact. │
│ const cellLines = norm.map((row) => row.map((c, i) => wrap(c.text, colW[i]).spli │
│ t("\n"))); │
│ const anyWrapped = cellLines.some((row) => row.some((l) => l.length > 1)); │
│ │
│ const out: string[] = [edge(G.TL, G.TJ, G.TR)]; │
│ norm.forEach((row, ri) => { │
│ if (anyWrapped && ri > headerRows) out.push(edge(G.LJ, G.X, G.RJ, G.HD)); │
│ const h = Math.max(...cellLines[ri].map((l) => l.length)); │
│ for (let k = 0; k < h; k++) { │
│ out.push( │
│ G.V + │
│ row.map((c, i) => ` ${pad(cellLines[ri][i][k] ?? "", colW[i], c.align)} │
│ `).join(G.V) + │
│ G.V, │
│ ); │
│ } │
│ if (ri === headerRows - 1) out.push(edge(G.LJ, G.X, G.RJ)); │
│ }); │
│ out.push(edge(G.BL, G.BJ, G.BR)); │
│ return checkGrid(out.join("\n"), width, "table"); │
│ } │
│ │
│ // A table too wide for its frame transposed: each body row becomes a │
│ // key/value block keyed by the header row, so any column count fits. │
│ export function buildTransposedTable(rows: Cell[][], headerRows: number, width: nu │
│ mber): string { │
│ const keys = rows[headerRows - 1].map((c) => c.text); │
│ const body = rows.slice(headerRows); │
│ const inner = width - 4; │
│ const keyW = Math.min(Math.max(1, ...keys.map(len)), Math.max(1, Math.floor((inn │
│ er - 2) / 2))); │
│ const valW = inner - keyW - 2; │
│ │
│ const out: string[] = [`${G.TL}${G.H.repeat(width - 2)}${G.TR}`]; │
│ body.forEach((row, ri) => { │
│ if (ri) out.push(`${G.LJ}${G.HD.repeat(width - 2)}${G.RJ}`); │
│ keys.forEach((key, i) => { │
│ const kLines = wrapHard(key, keyW).split("\n"); │
│ const vLines = wrapHard(row[i]?.text ?? "", valW).split("\n"); │
│ for (let j = 0; j < Math.max(kLines.length, vLines.length); j++) { │
│ out.push(`${G.V} ${pad(kLines[j] ?? "", keyW)} ${pad(vLines[j] ?? "", val │
│ W)} ${G.V}`); │
│ } │
│ }); │
│ }); │
│ out.push(`${G.BL}${G.H.repeat(width - 2)}${G.BR}`); │
│ return checkGrid(out.join("\n"), width, "transposed table"); │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ // Data-driven ASCII table shared by the mar │
│ kdown transform and the │
│ // Table component. │
│ import { G, checkGrid, len, pad, wrap, wrapH │
│ ard, type PadSide } from "./ascii"; │
│ │
│ export type Cell = { text: string; align: Pa │
│ dSide }; │
│ │
│ export function buildAsciiTable(rows: Cell[] │
│ [], headerRows: number, width: number): stri │
│ ng { │
│ const nCols = Math.max(...rows.map((r) => │
│ r.length)); │
│ const norm = rows.map((r) => { │
│ const out = [...r]; │
│ while (out.length < nCols) out.push({ te │
│ xt: "", align: "end" }); │
│ return out; │
│ }); │
│ │
│ // Natural column widths, then shave the w │
│ idest until the table fits. │
│ // Cells wrap to their final column width. │
│ const colW = Array.from({ length: nCols }, │
│ (_, i) => │
│ Math.max(1, ...norm.map((r) => len(r[i]. │
│ text))), │
│ ); │
│ const total = () => 1 + colW.reduce((s, w) │
│ => s + w + 3, 0); │
│ const MIN_COL = 5; │
│ while (total() > width && colW.some((w) => │
│ w > MIN_COL)) { │
│ colW[colW.indexOf(Math.max(...colW))]--; │
│ } │
│ │
│ const edge = (l: string, m: string, r: str │
│ ing, h = G.H) => │
│ l + colW.map((w) => h.repeat(w + 2)).joi │
│ n(m) + r; │
│ │
│ // When any row wraps, dashed separators b │
│ etween body rows keep the │
│ // logical rows readable; all-single-line │
│ tables stay compact. │
│ const cellLines = norm.map((row) => row.ma │
│ p((c, i) => wrap(c.text, colW[i]).split("\n" │
│ ))); │
│ const anyWrapped = cellLines.some((row) => │
│ row.some((l) => l.length > 1)); │
│ │
│ const out: string[] = [edge(G.TL, G.TJ, G. │
│ TR)]; │
│ norm.forEach((row, ri) => { │
│ if (anyWrapped && ri > headerRows) out.p │
│ ush(edge(G.LJ, G.X, G.RJ, G.HD)); │
│ const h = Math.max(...cellLines[ri].map( │
│ (l) => l.length)); │
│ for (let k = 0; k < h; k++) { │
│ out.push( │
│ G.V + │
│ row.map((c, i) => ` ${pad(cellLine │
│ s[ri][i][k] ?? "", colW[i], c.align)} `).joi │
│ n(G.V) + │
│ G.V, │
│ ); │
│ } │
│ if (ri === headerRows - 1) out.push(edge │
│ (G.LJ, G.X, G.RJ)); │
│ }); │
│ out.push(edge(G.BL, G.BJ, G.BR)); │
│ return checkGrid(out.join("\n"), width, "t │
│ able"); │
│ } │
│ │
│ // A table too wide for its frame transposed │
│ : each body row becomes a │
│ // key/value block keyed by the header row, │
│ so any column count fits. │
│ export function buildTransposedTable(rows: C │
│ ell[][], headerRows: number, width: number): │
│ string { │
│ const keys = rows[headerRows - 1].map((c) │
│ => c.text); │
│ const body = rows.slice(headerRows); │
│ const inner = width - 4; │
│ const keyW = Math.min(Math.max(1, ...keys. │
│ map(len)), Math.max(1, Math.floor((inner - 2 │
│ ) / 2))); │
│ const valW = inner - keyW - 2; │
│ │
│ const out: string[] = [`${G.TL}${G.H.repea │
│ t(width - 2)}${G.TR}`]; │
│ body.forEach((row, ri) => { │
│ if (ri) out.push(`${G.LJ}${G.HD.repeat(w │
│ idth - 2)}${G.RJ}`); │
│ keys.forEach((key, i) => { │
│ const kLines = wrapHard(key, keyW).spl │
│ it("\n"); │
│ const vLines = wrapHard(row[i]?.text ? │
│ ? "", valW).split("\n"); │
│ for (let j = 0; j < Math.max(kLines.le │
│ ngth, vLines.length); j++) { │
│ out.push(`${G.V} ${pad(kLines[j] ?? │
│ "", keyW)} ${pad(vLines[j] ?? "", valW)} ${ │
│ G.V}`); │
│ } │
│ }); │
│ }); │
│ out.push(`${G.BL}${G.H.repeat(width - 2)}$ │
│ {G.BR}`); │
│ return checkGrid(out.join("\n"), width, "t │
│ ransposed table"); │
│ } │
└──────────────────────────────────────────────┘