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 <ryan.fang@gllue.com>
---
 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<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>
   );
 };
-- 
cgit v1.2.3

