diff options
| author | zfflxx <[email protected]> | 2025-07-07 13:33:46 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-07 05:33:46 +0000 |
| commit | bb8f6b376d83a9b70406279c87ab8b163fb32a38 (patch) | |
| tree | 543d6484b2a35eece50957e22212cae3dbd24c6e /packages/cli/src/ui/utils/TableRenderer.tsx | |
| parent | b70fba5b09b274c8a751d3c579fc31fe847f497f (diff) | |
Fix nested markdown Rendering for table headers and rows #3331 (#3362)
Co-authored-by: Ryan Fang <[email protected]>
Diffstat (limited to 'packages/cli/src/ui/utils/TableRenderer.tsx')
| -rw-r--r-- | packages/cli/src/ui/utils/TableRenderer.tsx | 143 |
1 files changed, 94 insertions, 49 deletions
diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx index 745e5135..2ec19549 100644 --- a/packages/cli/src/ui/utils/TableRenderer.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.tsx @@ -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; - const renderTopBorder = () => { - const border = adjustedWidths - .map((width) => '─'.repeat(Math.max(0, (width || 0) - 2))) - .join('─┬─'); - return <Text>┌─{border}─┐</Text>; + return <Text>{border}</Text>; }; - const renderBottomBorder = () => { - 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); + }); + + 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> ); }; |
