feat: Add markdown table rendering support (#1955)
Co-authored-by: heartyguy <heartyguy@users.noreply.github.com> Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
parent
1a30b9656f
commit
a4062cb44a
|
@ -0,0 +1,280 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from 'ink-testing-library';
|
||||||
|
import { MarkdownDisplay } from './MarkdownDisplay.js';
|
||||||
|
|
||||||
|
describe('MarkdownDisplay', () => {
|
||||||
|
describe('Table Rendering', () => {
|
||||||
|
it('should render a simple table', () => {
|
||||||
|
const tableMarkdown = `
|
||||||
|
| Name | Age | City |
|
||||||
|
|------|-----|------|
|
||||||
|
| John | 25 | NYC |
|
||||||
|
| Jane | 30 | LA |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={tableMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Name');
|
||||||
|
expect(lastFrame()).toContain('Age');
|
||||||
|
expect(lastFrame()).toContain('City');
|
||||||
|
expect(lastFrame()).toContain('John');
|
||||||
|
expect(lastFrame()).toContain('25');
|
||||||
|
expect(lastFrame()).toContain('NYC');
|
||||||
|
expect(lastFrame()).toContain('Jane');
|
||||||
|
expect(lastFrame()).toContain('30');
|
||||||
|
expect(lastFrame()).toContain('LA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle tables with varying column widths', () => {
|
||||||
|
const tableMarkdown = `
|
||||||
|
| Short | Medium Column | Very Long Column Name |
|
||||||
|
|-------|---------------|----------------------|
|
||||||
|
| A | Some text | This is a longer text content |
|
||||||
|
| B | More content | Another piece of content here |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={tableMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Short');
|
||||||
|
expect(lastFrame()).toContain('Medium Column');
|
||||||
|
expect(lastFrame()).toContain('Very Long Column Name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty cells in tables', () => {
|
||||||
|
const tableMarkdown = `
|
||||||
|
| Col1 | Col2 | Col3 |
|
||||||
|
|------|------|------|
|
||||||
|
| A | | C |
|
||||||
|
| | B | |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={tableMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Col1');
|
||||||
|
expect(lastFrame()).toContain('Col2');
|
||||||
|
expect(lastFrame()).toContain('Col3');
|
||||||
|
expect(lastFrame()).toContain('A');
|
||||||
|
expect(lastFrame()).toContain('B');
|
||||||
|
expect(lastFrame()).toContain('C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed content with tables', () => {
|
||||||
|
const mixedMarkdown = `
|
||||||
|
# Header
|
||||||
|
|
||||||
|
Some paragraph text before the table.
|
||||||
|
|
||||||
|
| Feature | Status | Notes |
|
||||||
|
|---------|--------|-------|
|
||||||
|
| Auth | Done | OAuth |
|
||||||
|
| API | WIP | REST |
|
||||||
|
|
||||||
|
Some text after the table.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={mixedMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Header');
|
||||||
|
expect(lastFrame()).toContain('Some paragraph text before the table.');
|
||||||
|
expect(lastFrame()).toContain('Feature');
|
||||||
|
expect(lastFrame()).toContain('Status');
|
||||||
|
expect(lastFrame()).toContain('Auth');
|
||||||
|
expect(lastFrame()).toContain('Done');
|
||||||
|
expect(lastFrame()).toContain('Some text after the table.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle tables with empty cells at edges', () => {
|
||||||
|
const tableMarkdown = `
|
||||||
|
| | Middle | |
|
||||||
|
|-|--------|-|
|
||||||
|
| | Value | |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={tableMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Middle');
|
||||||
|
expect(lastFrame()).toContain('Value');
|
||||||
|
// Should maintain column structure even with empty edge cells
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PR reviewer test case 1', () => {
|
||||||
|
const tableMarkdown = `
|
||||||
|
| Package | Lines of Code |
|
||||||
|
|---------|---------------|
|
||||||
|
| CLI | 18407 |
|
||||||
|
| Core | 14445 |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={tableMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('Package');
|
||||||
|
expect(output).toContain('Lines of Code');
|
||||||
|
expect(output).toContain('CLI');
|
||||||
|
expect(output).toContain('18407');
|
||||||
|
expect(output).toContain('Core');
|
||||||
|
expect(output).toContain('14445');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PR reviewer test case 2 - long table', () => {
|
||||||
|
const tableMarkdown = `
|
||||||
|
| Letter | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| a | 15 |
|
||||||
|
| b | 2 |
|
||||||
|
| c | 26 |
|
||||||
|
| Total | 283 |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={tableMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('Letter');
|
||||||
|
expect(output).toContain('Count');
|
||||||
|
expect(output).toContain('a');
|
||||||
|
expect(output).toContain('15');
|
||||||
|
expect(output).toContain('Total');
|
||||||
|
expect(output).toContain('283');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render malformed tables', () => {
|
||||||
|
const malformedMarkdown = `
|
||||||
|
| This looks like a table |
|
||||||
|
But there's no separator line
|
||||||
|
| So it shouldn't render as table |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={malformedMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should render as regular text, not a table
|
||||||
|
expect(lastFrame()).toContain('| This looks like a table |');
|
||||||
|
expect(lastFrame()).toContain("But there's no separator line");
|
||||||
|
expect(lastFrame()).toContain("| So it shouldn't render as table |");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not crash when rendering a very wide table in a narrow terminal', () => {
|
||||||
|
const wideTable = `
|
||||||
|
| Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | Col 6 | Col 7 | Col 8 | Col 9 | Col 10 |
|
||||||
|
|-------|-------|-------|-------|-------|-------|-------|-------|-------|--------|
|
||||||
|
| ${'a'.repeat(20)} | ${'b'.repeat(20)} | ${'c'.repeat(20)} | ${'d'.repeat(
|
||||||
|
20,
|
||||||
|
)} | ${'e'.repeat(20)} | ${'f'.repeat(20)} | ${'g'.repeat(20)} | ${'h'.repeat(
|
||||||
|
20,
|
||||||
|
)} | ${'i'.repeat(20)} | ${'j'.repeat(20)} |
|
||||||
|
`;
|
||||||
|
|
||||||
|
const renderNarrow = () =>
|
||||||
|
render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={wideTable}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={40}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The important part is that this does not throw an error.
|
||||||
|
expect(renderNarrow).not.toThrow();
|
||||||
|
|
||||||
|
// We can also check that it rendered *something*.
|
||||||
|
const { lastFrame } = renderNarrow();
|
||||||
|
expect(lastFrame()).not.toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Existing Functionality', () => {
|
||||||
|
it('should render headers correctly', () => {
|
||||||
|
const headerMarkdown = `
|
||||||
|
# H1 Header
|
||||||
|
## H2 Header
|
||||||
|
### H3 Header
|
||||||
|
#### H4 Header
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={headerMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('H1 Header');
|
||||||
|
expect(lastFrame()).toContain('H2 Header');
|
||||||
|
expect(lastFrame()).toContain('H3 Header');
|
||||||
|
expect(lastFrame()).toContain('H4 Header');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render code blocks correctly', () => {
|
||||||
|
const codeMarkdown = `
|
||||||
|
\`\`\`javascript
|
||||||
|
const x = 42;
|
||||||
|
console.log(x);
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<MarkdownDisplay
|
||||||
|
text={codeMarkdown}
|
||||||
|
isPending={false}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('const x = 42;');
|
||||||
|
expect(lastFrame()).toContain('console.log(x);');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
||||||
import { Text, Box } from 'ink';
|
import { Text, Box } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { colorizeCode } from './CodeColorizer.js';
|
import { colorizeCode } from './CodeColorizer.js';
|
||||||
|
import { TableRenderer } from './TableRenderer.js';
|
||||||
|
|
||||||
interface MarkdownDisplayProps {
|
interface MarkdownDisplayProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -43,12 +44,17 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
||||||
const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/;
|
const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/;
|
||||||
const olItemRegex = /^([ \t]*)(\d+)\. +(.*)/;
|
const olItemRegex = /^([ \t]*)(\d+)\. +(.*)/;
|
||||||
const hrRegex = /^ *([-*_] *){3,} *$/;
|
const hrRegex = /^ *([-*_] *){3,} *$/;
|
||||||
|
const tableRowRegex = /^\s*\|(.+)\|\s*$/;
|
||||||
|
const tableSeparatorRegex = /^\s*\|?\s*(:?-+:?)\s*(\|\s*(:?-+:?)\s*)+\|?\s*$/;
|
||||||
|
|
||||||
const contentBlocks: React.ReactNode[] = [];
|
const contentBlocks: React.ReactNode[] = [];
|
||||||
let inCodeBlock = false;
|
let inCodeBlock = false;
|
||||||
let codeBlockContent: string[] = [];
|
let codeBlockContent: string[] = [];
|
||||||
let codeBlockLang: string | null = null;
|
let codeBlockLang: string | null = null;
|
||||||
let codeBlockFence = '';
|
let codeBlockFence = '';
|
||||||
|
let inTable = false;
|
||||||
|
let tableRows: string[][] = [];
|
||||||
|
let tableHeaders: string[] = [];
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
const key = `line-${index}`;
|
const key = `line-${index}`;
|
||||||
|
@ -85,11 +91,71 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
||||||
const ulMatch = line.match(ulItemRegex);
|
const ulMatch = line.match(ulItemRegex);
|
||||||
const olMatch = line.match(olItemRegex);
|
const olMatch = line.match(olItemRegex);
|
||||||
const hrMatch = line.match(hrRegex);
|
const hrMatch = line.match(hrRegex);
|
||||||
|
const tableRowMatch = line.match(tableRowRegex);
|
||||||
|
const tableSeparatorMatch = line.match(tableSeparatorRegex);
|
||||||
|
|
||||||
if (codeFenceMatch) {
|
if (codeFenceMatch) {
|
||||||
inCodeBlock = true;
|
inCodeBlock = true;
|
||||||
codeBlockFence = codeFenceMatch[1];
|
codeBlockFence = codeFenceMatch[1];
|
||||||
codeBlockLang = codeFenceMatch[2] || null;
|
codeBlockLang = codeFenceMatch[2] || null;
|
||||||
|
} else if (tableRowMatch && !inTable) {
|
||||||
|
// Potential table start - check if next line is separator
|
||||||
|
if (
|
||||||
|
index + 1 < lines.length &&
|
||||||
|
lines[index + 1].match(tableSeparatorRegex)
|
||||||
|
) {
|
||||||
|
inTable = true;
|
||||||
|
tableHeaders = tableRowMatch[1].split('|').map((cell) => cell.trim());
|
||||||
|
tableRows = [];
|
||||||
|
} else {
|
||||||
|
// Not a table, treat as regular text
|
||||||
|
contentBlocks.push(
|
||||||
|
<Box key={key}>
|
||||||
|
<Text wrap="wrap">
|
||||||
|
<RenderInline text={line} />
|
||||||
|
</Text>
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (inTable && tableSeparatorMatch) {
|
||||||
|
// Skip separator line - already handled
|
||||||
|
} else if (inTable && tableRowMatch) {
|
||||||
|
// Add table row
|
||||||
|
const cells = tableRowMatch[1].split('|').map((cell) => cell.trim());
|
||||||
|
// Ensure row has same column count as headers
|
||||||
|
while (cells.length < tableHeaders.length) {
|
||||||
|
cells.push('');
|
||||||
|
}
|
||||||
|
if (cells.length > tableHeaders.length) {
|
||||||
|
cells.length = tableHeaders.length;
|
||||||
|
}
|
||||||
|
tableRows.push(cells);
|
||||||
|
} else if (inTable && !tableRowMatch) {
|
||||||
|
// End of table
|
||||||
|
if (tableHeaders.length > 0 && tableRows.length > 0) {
|
||||||
|
contentBlocks.push(
|
||||||
|
<RenderTable
|
||||||
|
key={`table-${contentBlocks.length}`}
|
||||||
|
headers={tableHeaders}
|
||||||
|
rows={tableRows}
|
||||||
|
terminalWidth={terminalWidth}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
inTable = false;
|
||||||
|
tableRows = [];
|
||||||
|
tableHeaders = [];
|
||||||
|
|
||||||
|
// Process current line as normal
|
||||||
|
if (line.trim().length > 0) {
|
||||||
|
contentBlocks.push(
|
||||||
|
<Box key={key}>
|
||||||
|
<Text wrap="wrap">
|
||||||
|
<RenderInline text={line} />
|
||||||
|
</Text>
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (hrMatch) {
|
} else if (hrMatch) {
|
||||||
contentBlocks.push(
|
contentBlocks.push(
|
||||||
<Box key={key}>
|
<Box key={key}>
|
||||||
|
@ -194,6 +260,18 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle table at end of content
|
||||||
|
if (inTable && tableHeaders.length > 0 && tableRows.length > 0) {
|
||||||
|
contentBlocks.push(
|
||||||
|
<RenderTable
|
||||||
|
key={`table-${contentBlocks.length}`}
|
||||||
|
headers={tableHeaders}
|
||||||
|
rows={tableRows}
|
||||||
|
terminalWidth={terminalWidth}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <>{contentBlocks}</>;
|
return <>{contentBlocks}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -443,4 +521,20 @@ const RenderListItemInternal: React.FC<RenderListItemProps> = ({
|
||||||
|
|
||||||
const RenderListItem = React.memo(RenderListItemInternal);
|
const RenderListItem = React.memo(RenderListItemInternal);
|
||||||
|
|
||||||
|
interface RenderTableProps {
|
||||||
|
headers: string[];
|
||||||
|
rows: string[][];
|
||||||
|
terminalWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderTableInternal: React.FC<RenderTableProps> = ({
|
||||||
|
headers,
|
||||||
|
rows,
|
||||||
|
terminalWidth,
|
||||||
|
}) => (
|
||||||
|
<TableRenderer headers={headers} rows={rows} terminalWidth={terminalWidth} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const RenderTable = React.memo(RenderTableInternal);
|
||||||
|
|
||||||
export const MarkdownDisplay = React.memo(MarkdownDisplayInternal);
|
export const MarkdownDisplay = React.memo(MarkdownDisplayInternal);
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, Box } from 'ink';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
|
|
||||||
|
interface TableRendererProps {
|
||||||
|
headers: string[];
|
||||||
|
rows: string[][];
|
||||||
|
terminalWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom table renderer for markdown tables
|
||||||
|
* We implement our own instead of using ink-table due to module compatibility issues
|
||||||
|
*/
|
||||||
|
export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||||
|
headers,
|
||||||
|
rows,
|
||||||
|
terminalWidth,
|
||||||
|
}) => {
|
||||||
|
// Calculate column widths
|
||||||
|
const columnWidths = headers.map((header, index) => {
|
||||||
|
const headerWidth = header.length;
|
||||||
|
const maxRowWidth = Math.max(
|
||||||
|
...rows.map((row) => (row[index] || '').length),
|
||||||
|
);
|
||||||
|
return Math.max(headerWidth, maxRowWidth) + 2; // Add padding
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure table fits within terminal width
|
||||||
|
const totalWidth = columnWidths.reduce((sum, width) => sum + width + 1, 1);
|
||||||
|
const scaleFactor =
|
||||||
|
totalWidth > terminalWidth ? terminalWidth / totalWidth : 1;
|
||||||
|
const adjustedWidths = columnWidths.map((width) =>
|
||||||
|
Math.floor(width * scaleFactor),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCell = (content: string, width: number, isHeader = false) => {
|
||||||
|
// The actual space for content inside the padding
|
||||||
|
const contentWidth = Math.max(0, width - 2);
|
||||||
|
|
||||||
|
let cellContent = content;
|
||||||
|
if (content.length > contentWidth) {
|
||||||
|
if (contentWidth <= 3) {
|
||||||
|
// Not enough space for '...'
|
||||||
|
cellContent = content.substring(0, contentWidth);
|
||||||
|
} else {
|
||||||
|
cellContent = content.substring(0, contentWidth - 3) + '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad the content to fill the cell
|
||||||
|
const padded = cellContent.padEnd(contentWidth, ' ');
|
||||||
|
|
||||||
|
if (isHeader) {
|
||||||
|
return (
|
||||||
|
<Text bold color={Colors.AccentCyan}>
|
||||||
|
{padded}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Text>{padded}</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRow = (cells: string[], isHeader = false) => (
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Text>│ </Text>
|
||||||
|
{cells.map((cell, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{renderCell(cell, adjustedWidths[index] || 0, isHeader)}
|
||||||
|
<Text> │ </Text>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSeparator = () => {
|
||||||
|
const separator = adjustedWidths
|
||||||
|
.map((width) => '─'.repeat(Math.max(0, (width || 0) - 2)))
|
||||||
|
.join('─┼─');
|
||||||
|
return <Text>├─{separator}─┤</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTopBorder = () => {
|
||||||
|
const border = adjustedWidths
|
||||||
|
.map((width) => '─'.repeat(Math.max(0, (width || 0) - 2)))
|
||||||
|
.join('─┬─');
|
||||||
|
return <Text>┌─{border}─┐</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBottomBorder = () => {
|
||||||
|
const border = adjustedWidths
|
||||||
|
.map((width) => '─'.repeat(Math.max(0, (width || 0) - 2)))
|
||||||
|
.join('─┴─');
|
||||||
|
return <Text>└─{border}─┘</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginY={1}>
|
||||||
|
{renderTopBorder()}
|
||||||
|
{renderRow(headers, true)}
|
||||||
|
{renderSeparator()}
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<React.Fragment key={index}>{renderRow(row)}</React.Fragment>
|
||||||
|
))}
|
||||||
|
{renderBottomBorder()}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue