From bb8f6b376d83a9b70406279c87ab8b163fb32a38 Mon Sep 17 00:00:00 2001 From: zfflxx <106017702+zfflxx@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:33:46 +0800 Subject: Fix nested markdown Rendering for table headers and rows #3331 (#3362) Co-authored-by: Ryan Fang --- packages/cli/src/ui/utils/TableRenderer.tsx | 143 ++++++++++++++++++---------- 1 file changed, 94 insertions(+), 49 deletions(-) (limited to 'packages/cli/src/ui/utils/TableRenderer.tsx') 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 = ({ 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 = ({ 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 ( - - {padded} - - ); - } - return {padded}; + return ( + + {isHeader ? ( + + + + ) : ( + + )} + {' '.repeat(paddingNeeded)} + + ); }; - const renderRow = (cells: string[], isHeader = false) => ( - - - {cells.map((cell, index) => ( - - {renderCell(cell, adjustedWidths[index] || 0, isHeader)} - - - ))} - - ); + // 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 ├─{separator}─┤; - }; + 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 ┌─{border}─┐; + return {border}; }; - const renderBottomBorder = () => { - const border = adjustedWidths - .map((width) => '─'.repeat(Math.max(0, (width || 0) - 2))) - .join('─┴─'); - return └─{border}─┘; + // 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 ( + + │{' '} + {renderedCells.map((cell, index) => ( + + {cell} + {index < renderedCells.length - 1 ? ' │ ' : ''} + + ))}{' '} + │ + + ); }; return ( - {renderTopBorder()} + {/* Top border */} + {renderBorder('top')} + + {/* Header row */} {renderRow(headers, true)} - {renderSeparator()} + + {/* Middle border */} + {renderBorder('middle')} + + {/* Data rows */} {rows.map((row, index) => ( {renderRow(row)} ))} - {renderBottomBorder()} + + {/* Bottom border */} + {renderBorder('bottom')} ); }; -- cgit v1.2.3