From a4062cb44aab771822fcc4d0fb772bbcde256c1d Mon Sep 17 00:00:00 2001 From: Tian Jian Wang Date: Mon, 30 Jun 2025 20:25:19 -0700 Subject: feat: Add markdown table rendering support (#1955) Co-authored-by: heartyguy Co-authored-by: Allen Hutchison --- packages/cli/src/ui/utils/MarkdownDisplay.tsx | 94 +++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) (limited to 'packages/cli/src/ui/utils/MarkdownDisplay.tsx') diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index d78360b5..55f1ce57 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { Text, Box } from 'ink'; import { Colors } from '../colors.js'; import { colorizeCode } from './CodeColorizer.js'; +import { TableRenderer } from './TableRenderer.js'; interface MarkdownDisplayProps { text: string; @@ -43,12 +44,17 @@ const MarkdownDisplayInternal: React.FC = ({ const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/; const olItemRegex = /^([ \t]*)(\d+)\. +(.*)/; const hrRegex = /^ *([-*_] *){3,} *$/; + const tableRowRegex = /^\s*\|(.+)\|\s*$/; + const tableSeparatorRegex = /^\s*\|?\s*(:?-+:?)\s*(\|\s*(:?-+:?)\s*)+\|?\s*$/; const contentBlocks: React.ReactNode[] = []; let inCodeBlock = false; let codeBlockContent: string[] = []; let codeBlockLang: string | null = null; let codeBlockFence = ''; + let inTable = false; + let tableRows: string[][] = []; + let tableHeaders: string[] = []; lines.forEach((line, index) => { const key = `line-${index}`; @@ -85,11 +91,71 @@ const MarkdownDisplayInternal: React.FC = ({ const ulMatch = line.match(ulItemRegex); const olMatch = line.match(olItemRegex); const hrMatch = line.match(hrRegex); + const tableRowMatch = line.match(tableRowRegex); + const tableSeparatorMatch = line.match(tableSeparatorRegex); if (codeFenceMatch) { inCodeBlock = true; codeBlockFence = codeFenceMatch[1]; 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( + + + + + , + ); + } + } 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( + , + ); + } + inTable = false; + tableRows = []; + tableHeaders = []; + + // Process current line as normal + if (line.trim().length > 0) { + contentBlocks.push( + + + + + , + ); + } } else if (hrMatch) { contentBlocks.push( @@ -194,6 +260,18 @@ const MarkdownDisplayInternal: React.FC = ({ ); } + // Handle table at end of content + if (inTable && tableHeaders.length > 0 && tableRows.length > 0) { + contentBlocks.push( + , + ); + } + return <>{contentBlocks}; }; @@ -443,4 +521,20 @@ const RenderListItemInternal: React.FC = ({ const RenderListItem = React.memo(RenderListItemInternal); +interface RenderTableProps { + headers: string[]; + rows: string[][]; + terminalWidth: number; +} + +const RenderTableInternal: React.FC = ({ + headers, + rows, + terminalWidth, +}) => ( + +); + +const RenderTable = React.memo(RenderTableInternal); + export const MarkdownDisplay = React.memo(MarkdownDisplayInternal); -- cgit v1.2.3