Fix nested markdown Rendering for table headers and rows #3331 (#3362)

Co-authored-by: Ryan Fang <ryan.fang@gllue.com>
This commit is contained in:
zfflxx
2025-07-07 13:33:46 +08:00
committed by GitHub
parent b70fba5b09
commit bb8f6b376d
4 changed files with 304 additions and 192 deletions

View File

@@ -7,6 +7,7 @@
import React from 'react';
import { Text, Box } from 'ink';
import { Colors } from '../colors.js';
import { RenderInline, getPlainTextLength } from './InlineMarkdownRenderer.js';
interface TableRendererProps {
headers: string[];
@@ -23,11 +24,11 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
rows,
terminalWidth,
}) => {
// Calculate column widths
// Calculate column widths using actual display width after markdown processing
const columnWidths = headers.map((header, index) => {
const headerWidth = header.length;
const headerWidth = getPlainTextLength(header);
const maxRowWidth = Math.max(
...rows.map((row) => (row[index] || '').length),
...rows.map((row) => getPlainTextLength(row[index] || '')),
);
return Math.max(headerWidth, maxRowWidth) + 2; // Add padding
});
@@ -40,75 +41,119 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
Math.floor(width * scaleFactor),
);
const renderCell = (content: string, width: number, isHeader = false) => {
// The actual space for content inside the padding
// Helper function to render a cell with proper width
const renderCell = (
content: string,
width: number,
isHeader = false,
): React.ReactNode => {
const contentWidth = Math.max(0, width - 2);
const displayWidth = getPlainTextLength(content);
let cellContent = content;
if (content.length > contentWidth) {
if (displayWidth > contentWidth) {
if (contentWidth <= 3) {
// Not enough space for '...'
cellContent = content.substring(0, contentWidth);
// Just truncate by character count
cellContent = content.substring(
0,
Math.min(content.length, contentWidth),
);
} else {
cellContent = content.substring(0, contentWidth - 3) + '...';
// Truncate preserving markdown formatting using binary search
let left = 0;
let right = content.length;
let bestTruncated = content;
// Binary search to find the optimal truncation point
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const candidate = content.substring(0, mid);
const candidateWidth = getPlainTextLength(candidate);
if (candidateWidth <= contentWidth - 3) {
bestTruncated = candidate;
left = mid + 1;
} else {
right = mid - 1;
}
}
cellContent = bestTruncated + '...';
}
}
// Pad the content to fill the cell
const padded = cellContent.padEnd(contentWidth, ' ');
// Calculate exact padding needed
const actualDisplayWidth = getPlainTextLength(cellContent);
const paddingNeeded = Math.max(0, contentWidth - actualDisplayWidth);
if (isHeader) {
return (
<Text bold color={Colors.AccentCyan}>
{padded}
</Text>
);
}
return <Text>{padded}</Text>;
return (
<Text>
{isHeader ? (
<Text bold color={Colors.AccentCyan}>
<RenderInline text={cellContent} />
</Text>
) : (
<RenderInline text={cellContent} />
)}
{' '.repeat(paddingNeeded)}
</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>
);
// Helper function to render border
const renderBorder = (type: 'top' | 'middle' | 'bottom'): React.ReactNode => {
const chars = {
top: { left: '┌', middle: '┬', right: '┐', horizontal: '─' },
middle: { left: '├', middle: '┼', right: '┤', horizontal: '─' },
bottom: { left: '└', middle: '┴', right: '┘', horizontal: '─' },
};
const renderSeparator = () => {
const separator = adjustedWidths
.map((width) => '─'.repeat(Math.max(0, (width || 0) - 2)))
.join('─┼─');
return <Text>{separator}</Text>;
const char = chars[type];
const borderParts = adjustedWidths.map((w) => char.horizontal.repeat(w));
const border = char.left + borderParts.join(char.middle) + char.right;
return <Text>{border}</Text>;
};
const renderTopBorder = () => {
const border = adjustedWidths
.map((width) => '─'.repeat(Math.max(0, (width || 0) - 2)))
.join('─┬─');
return <Text>{border}</Text>;
};
// Helper function to render a table row
const renderRow = (cells: string[], isHeader = false): React.ReactNode => {
const renderedCells = cells.map((cell, index) => {
const width = adjustedWidths[index] || 0;
return renderCell(cell || '', width, isHeader);
});
const renderBottomBorder = () => {
const border = adjustedWidths
.map((width) => '─'.repeat(Math.max(0, (width || 0) - 2)))
.join('─┴─');
return <Text>{border}</Text>;
return (
<Text>
{' '}
{renderedCells.map((cell, index) => (
<React.Fragment key={index}>
{cell}
{index < renderedCells.length - 1 ? ' │ ' : ''}
</React.Fragment>
))}{' '}
</Text>
);
};
return (
<Box flexDirection="column" marginY={1}>
{renderTopBorder()}
{/* Top border */}
{renderBorder('top')}
{/* Header row */}
{renderRow(headers, true)}
{renderSeparator()}
{/* Middle border */}
{renderBorder('middle')}
{/* Data rows */}
{rows.map((row, index) => (
<React.Fragment key={index}>{renderRow(row)}</React.Fragment>
))}
{renderBottomBorder()}
{/* Bottom border */}
{renderBorder('bottom')}
</Box>
);
};