From add5e80bd1219c67b61940d77b52386fbaf4ee81 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Thu, 3 Sep 2026 11:51:10 -0400 Subject: [PATCH] feat(table): draw row separators when any body row wraps --- .../components/blocks/Table.borders.test.tsx | 40 +++++++++++++++++++ src/app/components/blocks/Table.tsx | 33 +++++++++------ 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/app/components/blocks/Table.borders.test.tsx b/src/app/components/blocks/Table.borders.test.tsx index 31a3770..cf14389 100644 --- a/src/app/components/blocks/Table.borders.test.tsx +++ b/src/app/components/blocks/Table.borders.test.tsx @@ -67,3 +67,43 @@ test('table borders are continuous through wrapped, styled cells', async () => { } expect(holes).toEqual([]) }) + +const FLAT_FIXTURE = ['| Tool | License |', '|---|---|', '| A | MIT |', '| B | Apache-2.0 |'].join( + '\n', +) + +async function renderRows(markdown: string): Promise { + const { nodes, toc, headingIds } = buildTree(markdown) + const { renderer, flush, renderOnce, captureCharFrame } = await createTestRenderer({ + width: 100, + height: 40, + }) + createRoot(renderer).render( + , + ) + await flush({ maxPasses: 20 }) + await new Promise(r => setTimeout(r, 30)) + await renderOnce() + return captureCharFrame().split('\n') +} + +// A table with a wrapped row reads as a wall of text without separators, so +// every body row gets a rule. Dense single-line tables stay compact. +test('body rows are separated by rules when any row wraps', async () => { + const lines = await renderRows(FIXTURE) + const ruleRows = lines.filter(l => l.includes('├')) + expect(ruleRows.length).toBe(3) // header rule + one between each pair of the 3 body rows +}) + +test('single-line tables keep only the header rule', async () => { + const lines = await renderRows(FLAT_FIXTURE) + const ruleRows = lines.filter(l => l.includes('├')) + expect(ruleRows.length).toBe(1) +}) diff --git a/src/app/components/blocks/Table.tsx b/src/app/components/blocks/Table.tsx index 28cab62..bce39b1 100644 --- a/src/app/components/blocks/Table.tsx +++ b/src/app/components/blocks/Table.tsx @@ -21,6 +21,10 @@ export function Table({ node, id }: { node: Extract; id const topRule = '┌' + cellWidths.map(w => '─'.repeat(w)).join('┬') + '┐' const midRule = '├' + cellWidths.map(w => '─'.repeat(w)).join('┼') + '┤' const botRule = '└' + cellWidths.map(w => '─'.repeat(w)).join('┴') + '┘' + const headerLines = node.header.map((cell, i) => wrapInline(cell, colWidths[i] ?? 0)) + const rowLines = node.rows.map(row => row.map((cell, i) => wrapInline(cell, colWidths[i] ?? 0))) + // Wrapped rows blur together without a rule between them; dense tables stay compact. + const hasWrappedRow = rowLines.some(row => row.some(lines => lines.length > 1)) return ( ; id `h${ci}`} @@ -45,14 +49,20 @@ export function Table({ node, id }: { node: Extract; id {midRule} {node.rows.map((row, ri) => ( - `r${ri}c${ci}`} - /> + + {ri > 0 && hasWrappedRow ? ( + + {midRule} + + ) : null} + `r${ri}c${ci}`} + /> + ))} {botRule} @@ -63,20 +73,19 @@ export function Table({ node, id }: { node: Extract; id function Row({ cells, + cellLines, cellWidths, - colWidths, isHeader, blockId, runKeyFor, }: { cells: InlineNode[][] + cellLines: InlineNode[][][] cellWidths: number[] - colWidths: number[] isHeader?: boolean blockId: string runKeyFor: (ci: number) => string }) { - const cellLines = cells.map((cell, i) => wrapInline(cell, colWidths[i] ?? 0)) const lineCount = Math.max(1, ...cellLines.map(c => c.length)) return (