Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/app/components/blocks/Table.borders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> {
const { nodes, toc, headingIds } = buildTree(markdown)
const { renderer, flush, renderOnce, captureCharFrame } = await createTestRenderer({
width: 100,
height: 40,
})
createRoot(renderer).render(
<App
nodes={nodes}
toc={toc}
headingIds={headingIds}
frontmatter={[]}
headingLines={{}}
fileLabel="t/tables.md"
/>,
)
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)
})
33 changes: 21 additions & 12 deletions src/app/components/blocks/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function Table({ node, id }: { node: Extract<Node, { kind: 'table' }>; 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 (
<box
Expand All @@ -35,8 +39,8 @@ export function Table({ node, id }: { node: Extract<Node, { kind: 'table' }>; id
</text>
<Row
cells={node.header}
cellLines={headerLines}
cellWidths={cellWidths}
colWidths={colWidths}
isHeader
blockId={id}
runKeyFor={ci => `h${ci}`}
Expand All @@ -45,14 +49,20 @@ export function Table({ node, id }: { node: Extract<Node, { kind: 'table' }>; id
{midRule}
</text>
{node.rows.map((row, ri) => (
<Row
key={ri}
cells={row}
cellWidths={cellWidths}
colWidths={colWidths}
blockId={id}
runKeyFor={ci => `r${ri}c${ci}`}
/>
<Fragment key={ri}>
{ri > 0 && hasWrappedRow ? (
<text fg={theme.border} height={1}>
{midRule}
</text>
) : null}
<Row
cells={row}
cellLines={rowLines[ri] ?? []}
cellWidths={cellWidths}
blockId={id}
runKeyFor={ci => `r${ri}c${ci}`}
/>
</Fragment>
))}
<text fg={theme.border} height={1}>
{botRule}
Expand All @@ -63,20 +73,19 @@ export function Table({ node, id }: { node: Extract<Node, { kind: 'table' }>; 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 (
<box flexDirection="row">
Expand Down