From ecfa4e0437dc1049fc6460b688f88d3af4c1c08f Mon Sep 17 00:00:00 2001 From: Dev Randalpura Date: Thu, 26 Feb 2026 17:31:21 -0800 Subject: [PATCH] fix(ui): correct styled table width calculations (#20042) --- .../src/ui/utils/InlineMarkdownRenderer.tsx | 194 ++++-- .../cli/src/ui/utils/TableRenderer.test.tsx | 131 +++- packages/cli/src/ui/utils/TableRenderer.tsx | 64 +- ...lates-column-widths-based-on-ren-.snap.svg | 39 ++ ...lates-width-correctly-for-conten-.snap.svg | 45 ++ ...not-parse-markdown-inside-code-s-.snap.svg | 40 ++ ...es-nested-markdown-styles-recurs-.snap.svg | 39 ++ ...dles-non-ASCII-characters-emojis-.snap.svg | 24 +- ...d-headers-without-showing-markers.snap.svg | 32 +- ...rer-renders-a-3x3-table-correctly.snap.svg | 34 +- ...h-mixed-content-lengths-correctly.snap.svg | 610 +++++++++--------- ...g-headers-and-4-columns-correctly.snap.svg | 60 +- ...ers-a-table-with-mixed-emojis-As-.snap.svg | 24 +- ...rs-a-table-with-only-Asian-chara-.snap.svg | 24 +- ...ers-a-table-with-only-emojis-and-.snap.svg | 24 +- ...ers-complex-markdown-in-rows-and-.snap.svg | 53 ++ ...rs-correctly-when-headers-are-em-.snap.svg | 8 +- ...rs-correctly-when-there-are-more-.snap.svg | 12 +- ...eaders-and-renders-them-correctly.snap.svg | 14 +- ...-wraps-all-long-columns-correctly.snap.svg | 52 +- ...olumns-with-punctuation-correctly.snap.svg | 50 +- ...wraps-long-cell-content-correctly.snap.svg | 26 +- ...-long-and-short-columns-correctly.snap.svg | 28 +- .../__snapshots__/TableRenderer.test.tsx.snap | 64 ++ .../src/ui/utils/markdownParsingUtils.test.ts | 223 +++++++ 25 files changed, 1312 insertions(+), 602 deletions(-) create mode 100644 packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-column-widths-based-on-ren-.snap.svg create mode 100644 packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-width-correctly-for-conten-.snap.svg create mode 100644 packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-does-not-parse-markdown-inside-code-s-.snap.svg create mode 100644 packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-nested-markdown-styles-recurs-.snap.svg create mode 100644 packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-complex-markdown-in-rows-and-.snap.svg create mode 100644 packages/cli/src/ui/utils/markdownParsingUtils.test.ts diff --git a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx index 430b27eeb3..02a34842f4 100644 --- a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx +++ b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx @@ -6,6 +6,12 @@ import React from 'react'; import { Text } from 'ink'; +import chalk from 'chalk'; +import { + resolveColor, + INK_SUPPORTED_NAMES, + INK_NAME_TO_HEX_MAP, +} from '../themes/color-utils.js'; import { theme } from '../semantic-colors.js'; import { debugLogger } from '@google/gemini-cli-core'; import { stripUnsafeCharacters } from './textUtils.js'; @@ -23,46 +29,108 @@ interface RenderInlineProps { defaultColor?: string; } -const RenderInlineInternal: React.FC = ({ - text: rawText, - defaultColor, -}) => { - const text = stripUnsafeCharacters(rawText); +/** + * Helper to apply color to a string using ANSI escape codes, + * consistent with how Ink's colorize works. + */ +const ansiColorize = (str: string, color: string | undefined): string => { + if (!color) return str; + const resolved = resolveColor(color); + if (!resolved) return str; + + if (resolved.startsWith('#')) { + return chalk.hex(resolved)(str); + } + + const mappedHex = INK_NAME_TO_HEX_MAP[resolved]; + if (mappedHex) { + return chalk.hex(mappedHex)(str); + } + + if (INK_SUPPORTED_NAMES.has(resolved)) { + switch (resolved) { + case 'black': + return chalk.black(str); + case 'red': + return chalk.red(str); + case 'green': + return chalk.green(str); + case 'yellow': + return chalk.yellow(str); + case 'blue': + return chalk.blue(str); + case 'magenta': + return chalk.magenta(str); + case 'cyan': + return chalk.cyan(str); + case 'white': + return chalk.white(str); + case 'gray': + case 'grey': + return chalk.gray(str); + default: + return str; + } + } + + return str; +}; + +/** + * Converts markdown text into a string with ANSI escape codes. + * This mirrors the parsing logic in InlineMarkdownRenderer.tsx + */ +export const parseMarkdownToANSI = ( + text: string, + defaultColor?: string, +): string => { const baseColor = defaultColor ?? theme.text.primary; // Early return for plain text without markdown or URLs if (!/[*_~`<[https?:]/.test(text)) { - return {text}; + return ansiColorize(text, baseColor); } - const nodes: React.ReactNode[] = []; - let lastIndex = 0; + let result = ''; const inlineRegex = - /(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|.*?<\/u>|https?:\/\/\S+)/g; + /(\*\*\*.*?\*\*\*|\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|.*?<\/u>|https?:\/\/\S+)/g; + let lastIndex = 0; let match; while ((match = inlineRegex.exec(text)) !== null) { if (match.index > lastIndex) { - nodes.push( - - {text.slice(lastIndex, match.index)} - , - ); + result += ansiColorize(text.slice(lastIndex, match.index), baseColor); } const fullMatch = match[0]; - let renderedNode: React.ReactNode = null; - const key = `m-${match.index}`; + let styledPart = ''; try { if ( - fullMatch.startsWith('**') && + fullMatch.endsWith('***') && + fullMatch.startsWith('***') && + fullMatch.length > (BOLD_MARKER_LENGTH + ITALIC_MARKER_LENGTH) * 2 + ) { + styledPart = chalk.bold( + chalk.italic( + parseMarkdownToANSI( + fullMatch.slice( + BOLD_MARKER_LENGTH + ITALIC_MARKER_LENGTH, + -BOLD_MARKER_LENGTH - ITALIC_MARKER_LENGTH, + ), + baseColor, + ), + ), + ); + } else if ( fullMatch.endsWith('**') && + fullMatch.startsWith('**') && fullMatch.length > BOLD_MARKER_LENGTH * 2 ) { - renderedNode = ( - - {fullMatch.slice(BOLD_MARKER_LENGTH, -BOLD_MARKER_LENGTH)} - + styledPart = chalk.bold( + parseMarkdownToANSI( + fullMatch.slice(BOLD_MARKER_LENGTH, -BOLD_MARKER_LENGTH), + baseColor, + ), ); } else if ( fullMatch.length > ITALIC_MARKER_LENGTH * 2 && @@ -77,23 +145,25 @@ const RenderInlineInternal: React.FC = ({ text.substring(inlineRegex.lastIndex, inlineRegex.lastIndex + 2), ) ) { - renderedNode = ( - - {fullMatch.slice(ITALIC_MARKER_LENGTH, -ITALIC_MARKER_LENGTH)} - + styledPart = chalk.italic( + parseMarkdownToANSI( + fullMatch.slice(ITALIC_MARKER_LENGTH, -ITALIC_MARKER_LENGTH), + baseColor, + ), ); } else if ( fullMatch.startsWith('~~') && fullMatch.endsWith('~~') && fullMatch.length > STRIKETHROUGH_MARKER_LENGTH * 2 ) { - renderedNode = ( - - {fullMatch.slice( + styledPart = chalk.strikethrough( + parseMarkdownToANSI( + fullMatch.slice( STRIKETHROUGH_MARKER_LENGTH, -STRIKETHROUGH_MARKER_LENGTH, - )} - + ), + baseColor, + ), ); } else if ( fullMatch.startsWith('`') && @@ -102,11 +172,7 @@ const RenderInlineInternal: React.FC = ({ ) { const codeMatch = fullMatch.match(/^(`+)(.+?)\1$/s); if (codeMatch && codeMatch[2]) { - renderedNode = ( - - {codeMatch[2]} - - ); + styledPart = ansiColorize(codeMatch[2], theme.text.accent); } } else if ( fullMatch.startsWith('[') && @@ -117,58 +183,54 @@ const RenderInlineInternal: React.FC = ({ if (linkMatch) { const linkText = linkMatch[1]; const url = linkMatch[2]; - renderedNode = ( - - {linkText} - ({url}) - - ); + styledPart = + parseMarkdownToANSI(linkText, baseColor) + + ansiColorize(' (', baseColor) + + ansiColorize(url, theme.text.link) + + ansiColorize(')', baseColor); } } else if ( fullMatch.startsWith('') && fullMatch.endsWith('') && fullMatch.length > - UNDERLINE_TAG_START_LENGTH + UNDERLINE_TAG_END_LENGTH - 1 // -1 because length is compared to combined length of start and end tags + UNDERLINE_TAG_START_LENGTH + UNDERLINE_TAG_END_LENGTH - 1 ) { - renderedNode = ( - - {fullMatch.slice( + styledPart = chalk.underline( + parseMarkdownToANSI( + fullMatch.slice( UNDERLINE_TAG_START_LENGTH, -UNDERLINE_TAG_END_LENGTH, - )} - + ), + baseColor, + ), ); } else if (fullMatch.match(/^https?:\/\//)) { - renderedNode = ( - - {fullMatch} - - ); + styledPart = ansiColorize(fullMatch, theme.text.link); } } catch (e) { debugLogger.warn('Error parsing inline markdown part:', fullMatch, e); - renderedNode = null; + styledPart = ''; } - nodes.push( - renderedNode ?? ( - - {fullMatch} - - ), - ); + result += styledPart || ansiColorize(fullMatch, baseColor); lastIndex = inlineRegex.lastIndex; } if (lastIndex < text.length) { - nodes.push( - - {text.slice(lastIndex)} - , - ); + result += ansiColorize(text.slice(lastIndex), baseColor); } - return <>{nodes.filter((node) => node !== null)}; + return result; +}; + +const RenderInlineInternal: React.FC = ({ + text: rawText, + defaultColor, +}) => { + const text = stripUnsafeCharacters(rawText); + const ansiText = parseMarkdownToANSI(text, defaultColor); + + return {ansiText}; }; export const RenderInline = React.memo(RenderInlineInternal); diff --git a/packages/cli/src/ui/utils/TableRenderer.test.tsx b/packages/cli/src/ui/utils/TableRenderer.test.tsx index e9d84e6649..3960e8befe 100644 --- a/packages/cli/src/ui/utils/TableRenderer.test.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.test.tsx @@ -267,7 +267,6 @@ describe('TableRenderer', () => { await waitUntilReady(); const output = lastFrame(); - expect(output).toContain('Comprehensive Architectural'); expect(output).toContain('protocol buffers'); expect(output).toContain('exponential backoff'); @@ -378,4 +377,134 @@ describe('TableRenderer', () => { await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); + + it.each([ + { + name: 'renders complex markdown in rows and calculates widths correctly', + headers: ['Feature', 'Markdown'], + rows: [ + ['Bold', '**Bold Text**'], + ['Italic', '_Italic Text_'], + ['Combined', '***Bold and Italic***'], + ['Link', '[Google](https://google.com)'], + ['Code', '`const x = 1`'], + ['Strikethrough', '~~Strike~~'], + ['Underline', 'Underline'], + ], + terminalWidth: 80, + waitForText: 'Bold Text', + assertions: (output: string) => { + expect(output).not.toContain('**Bold Text**'); + expect(output).toContain('Bold Text'); + expect(output).not.toContain('_Italic Text_'); + expect(output).toContain('Italic Text'); + expect(output).toContain('Bold and Italic'); + expect(output).toContain('Google'); + expect(output).toContain('https://google.com'); + expect(output).toContain('(https://google.com)'); + expect(output).toContain('const x = 1'); + expect(output).not.toContain('`const x = 1`'); + expect(output).toContain('Strike'); + expect(output).toContain('Underline'); + }, + }, + { + name: 'calculates column widths based on rendered text, not raw markdown', + headers: ['Col 1', 'Col 2', 'Col 3'], + rows: [ + ['**123456**', 'Normal', 'Short'], + ['Short', '**123456**', 'Normal'], + ['Normal', 'Short', '**123456**'], + ], + terminalWidth: 40, + waitForText: '123456', + assertions: (output: string) => { + expect(output).toContain('123456'); + const dataLines = output.split('\n').filter((l) => /123456/.test(l)); + expect(dataLines.length).toBe(3); + }, + }, + { + name: 'handles nested markdown styles recursively', + headers: ['Header 1', 'Header 2', 'Header 3'], + rows: [ + ['**Bold with _Italic_ and ~~Strike~~**', 'Normal', 'Short'], + ['Short', '**Bold with _Italic_ and ~~Strike~~**', 'Normal'], + ['Normal', 'Short', '**Bold with _Italic_ and ~~Strike~~**'], + ], + terminalWidth: 100, + waitForText: 'Bold with Italic and Strike', + assertions: (output: string) => { + expect(output).not.toContain('**'); + expect(output).not.toContain('_'); + expect(output).not.toContain('~~'); + expect(output).toContain('Bold with Italic and Strike'); + }, + }, + { + name: 'calculates width correctly for content with URLs and styles', + headers: ['Col 1', 'Col 2', 'Col 3'], + rows: [ + ['Visit [Google](https://google.com)', 'Plain Text', 'More Info'], + ['Info Here', 'Visit [Bing](https://bing.com)', 'Links'], + ['Check This', 'Search', 'Visit [Yahoo](https://yahoo.com)'], + ], + terminalWidth: 120, + waitForText: 'Visit Google', + assertions: (output: string) => { + expect(output).toContain('Visit Google'); + expect(output).toContain('Visit Bing'); + expect(output).toContain('Visit Yahoo'); + expect(output).toContain('https://google.com'); + expect(output).toContain('https://bing.com'); + expect(output).toContain('https://yahoo.com'); + expect(output).toContain('(https://google.com)'); + const dataLine = output + .split('\n') + .find((l) => l.includes('Visit Google')); + expect(dataLine).toContain('Visit Google'); + }, + }, + { + name: 'does not parse markdown inside code snippets', + headers: ['Col 1', 'Col 2', 'Col 3'], + rows: [ + ['`**not bold**`', '`_not italic_`', '`~~not strike~~`'], + ['`[not link](url)`', '`not underline`', '`https://not.link`'], + ['Normal Text', 'More Code: `*test*`', '`***nested***`'], + ], + terminalWidth: 100, + waitForText: '**not bold**', + assertions: (output: string) => { + expect(output).toContain('**not bold**'); + expect(output).toContain('_not italic_'); + expect(output).toContain('~~not strike~~'); + expect(output).toContain('[not link](url)'); + expect(output).toContain('not underline'); + expect(output).toContain('https://not.link'); + expect(output).toContain('***nested***'); + }, + }, + ])( + '$name', + async ({ headers, rows, terminalWidth, waitForText, assertions }) => { + const renderResult = renderWithProviders( + , + { width: terminalWidth }, + ); + const { lastFrame, waitUntilReady, unmount } = renderResult; + await waitUntilReady(); + + const output = lastFrame(); + expect(output).toBeDefined(); + expect(output).toContain(waitForText); + assertions(output); + await expect(renderResult).toMatchSvgSnapshot(); + unmount(); + }, + ); }); diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx index ab1981762c..143b1fe015 100644 --- a/packages/cli/src/ui/utils/TableRenderer.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.tsx @@ -5,18 +5,19 @@ */ import React, { useMemo } from 'react'; -import { Text, Box } from 'ink'; +import { styledCharsToString } from '@alcalzone/ansi-tokenize'; import { + Text, + Box, type StyledChar, toStyledCharacters, - styledCharsToString, styledCharsWidth, wordBreakStyledChars, wrapStyledChars, widestLineFromStyledChars, } from 'ink'; import { theme } from '../semantic-colors.js'; -import { RenderInline } from './InlineMarkdownRenderer.js'; +import { parseMarkdownToANSI } from './InlineMarkdownRenderer.js'; import { stripUnsafeCharacters } from './textUtils.js'; interface TableRendererProps { @@ -29,6 +30,19 @@ const MIN_COLUMN_WIDTH = 5; const COLUMN_PADDING = 2; const TABLE_MARGIN = 2; +/** + * Parses markdown to StyledChar array by first converting to ANSI. + * This ensures character counts are accurate (markdown markers are removed + * and styles are applied to the character's internal style object). + */ +const parseMarkdownToStyledChars = ( + text: string, + defaultColor?: string, +): StyledChar[] => { + const ansi = parseMarkdownToANSI(text, defaultColor); + return toStyledCharacters(ansi); +}; + const calculateWidths = (styledChars: StyledChar[]) => { const contentWidth = styledCharsWidth(styledChars); @@ -53,25 +67,26 @@ export const TableRenderer: React.FC = ({ rows, terminalWidth, }) => { - // Clean headers: remove bold markers since we already render headers as bold - // and having them can break wrapping when the markers are split across lines. - const cleanedHeaders = useMemo( - () => headers.map((header) => header.replace(/\*\*(.*?)\*\*/g, '$1')), - [headers], - ); - const styledHeaders = useMemo( () => - cleanedHeaders.map((header) => - toStyledCharacters(stripUnsafeCharacters(header)), + headers.map((header) => + parseMarkdownToStyledChars( + stripUnsafeCharacters(header), + theme.text.link, + ), ), - [cleanedHeaders], + [headers], ); const styledRows = useMemo( () => rows.map((row) => - row.map((cell) => toStyledCharacters(stripUnsafeCharacters(cell))), + row.map((cell) => + parseMarkdownToStyledChars( + stripUnsafeCharacters(cell), + theme.text.primary, + ), + ), ), [rows], ); @@ -132,7 +147,7 @@ export const TableRenderer: React.FC = ({ const scale = (availableWidth - finalTotalShortColumnWidth) / - (totalMinWidth - finalTotalShortColumnWidth); + (totalMinWidth - finalTotalShortColumnWidth) || 0; finalContentWidths = constraints.map((c) => { if (c.maxWidth <= MIN_COLUMN_WIDTH && finalTotalShortColumnWidth > 0) { return c.minWidth; @@ -201,6 +216,7 @@ export const TableRenderer: React.FC = ({ return { wrappedHeaders, wrappedRows, adjustedWidths }; }, [styledHeaders, styledRows, terminalWidth]); + // Helper function to render a cell with proper width const renderCell = ( content: ProcessedLine, @@ -216,10 +232,10 @@ export const TableRenderer: React.FC = ({ {isHeader ? ( - + {content.text} ) : ( - + {content.text} )} {' '.repeat(paddingNeeded)} @@ -253,18 +269,18 @@ export const TableRenderer: React.FC = ({ }); return ( - - {' '} + + {renderedCells.map((cell, index) => ( - {cell} + {cell} {index < renderedCells.length - 1 && ( - {' │ '} + )} - ))}{' '} + ))} - + ); }; @@ -274,7 +290,7 @@ export const TableRenderer: React.FC = ({ rowIndex?: number, isHeader = false, ): React.ReactNode => { - const key = isHeader ? 'header' : `${rowIndex}`; + const key = rowIndex === -1 ? 'header' : `${rowIndex}`; const maxHeight = Math.max(...wrappedCells.map((lines) => lines.length), 1); const visualRows: React.ReactNode[] = []; diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-column-widths-based-on-ren-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-column-widths-based-on-ren-.snap.svg new file mode 100644 index 0000000000..e01d29e15d --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-column-widths-based-on-ren-.snap.svg @@ -0,0 +1,39 @@ + + + + + ┌────────┬────────┬────────┐ + + Col 1 + + Col 2 + + Col 3 + + ├────────┼────────┼────────┤ + + 123456 + + Normal + + Short + + + Short + + 123456 + + Normal + + + Normal + + Short + + 123456 + + └────────┴────────┴────────┘ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-width-correctly-for-conten-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-width-correctly-for-conten-.snap.svg new file mode 100644 index 0000000000..f6f83c0cb0 --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-calculates-width-correctly-for-conten-.snap.svg @@ -0,0 +1,45 @@ + + + + + ┌───────────────────────────────────┬───────────────────────────────┬─────────────────────────────────┐ + + Col 1 + + Col 2 + + Col 3 + + ├───────────────────────────────────┼───────────────────────────────┼─────────────────────────────────┤ + + Visit Google ( + https://google.com + ) + + Plain Text + + More Info + + + Info Here + + Visit Bing ( + https://bing.com + ) + + Links + + + Check This + + Search + + Visit Yahoo ( + https://yahoo.com + ) + + └───────────────────────────────────┴───────────────────────────────┴─────────────────────────────────┘ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-does-not-parse-markdown-inside-code-s-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-does-not-parse-markdown-inside-code-s-.snap.svg new file mode 100644 index 0000000000..68069bd0ab --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-does-not-parse-markdown-inside-code-s-.snap.svg @@ -0,0 +1,40 @@ + + + + + ┌─────────────────┬──────────────────────┬──────────────────┐ + + Col 1 + + Col 2 + + Col 3 + + ├─────────────────┼──────────────────────┼──────────────────┤ + + **not bold** + + _not italic_ + + ~~not strike~~ + + + [not link](url) + + <u>not underline</u> + + https://not.link + + + Normal Text + + More Code: + *test* + + ***nested*** + + └─────────────────┴──────────────────────┴──────────────────┘ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-nested-markdown-styles-recurs-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-nested-markdown-styles-recurs-.snap.svg new file mode 100644 index 0000000000..3269e29f19 --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-nested-markdown-styles-recurs-.snap.svg @@ -0,0 +1,39 @@ + + + + + ┌─────────────────────────────┬─────────────────────────────┬─────────────────────────────┐ + + Header 1 + + Header 2 + + Header 3 + + ├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┤ + + Bold with Italic and Strike + + Normal + + Short + + + Short + + Bold with Italic and Strike + + Normal + + + Normal + + Short + + Bold with Italic and Strike + + └─────────────────────────────┴─────────────────────────────┴─────────────────────────────┘ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-non-ASCII-characters-emojis-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-non-ASCII-characters-emojis-.snap.svg index d9612cce33..13898e8641 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-non-ASCII-characters-emojis-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-non-ASCII-characters-emojis-.snap.svg @@ -7,25 +7,25 @@ ┌──────────────┬────────────┬───────────────┐ Emoji 😃 - + Asian 汉字 - + Mixed 🚀 Text ├──────────────┼────────────┼───────────────┤ - Start 🌟 End - - 你好世界 - - Rocket 🚀 Man + Start 🌟 End + + 你好世界 + + Rocket 🚀 Man - Thumbs 👍 Up - - こんにちは - - Fire 🔥 + Thumbs 👍 Up + + こんにちは + + Fire 🔥 └──────────────┴────────────┴───────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-wrapped-bold-headers-without-showing-markers.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-wrapped-bold-headers-without-showing-markers.snap.svg index 0118d133cf..30d847e86c 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-wrapped-bold-headers-without-showing-markers.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-handles-wrapped-bold-headers-without-showing-markers.snap.svg @@ -7,40 +7,40 @@ ┌─────────────┬───────┬─────────┐ Very Long - + Short - + Another Bold Header - - + + Long That Will - - + + Header Wrap - - + + ├─────────────┼───────┼─────────┤ - Data 1 - - Data - - Data 3 + Data 1 + + Data + + Data 3 - - 2 - + + 2 + └─────────────┴───────┴─────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-3x3-table-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-3x3-table-correctly.snap.svg index 84e4d856f6..dea907221c 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-3x3-table-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-3x3-table-correctly.snap.svg @@ -7,32 +7,32 @@ ┌──────────────┬──────────────┬──────────────┐ Header 1 - + Header 2 - + Header 3 ├──────────────┼──────────────┼──────────────┤ - Row 1, Col 1 - - Row 1, Col 2 - - Row 1, Col 3 + Row 1, Col 1 + + Row 1, Col 2 + + Row 1, Col 3 - Row 2, Col 1 - - Row 2, Col 2 - - Row 2, Col 3 + Row 2, Col 1 + + Row 2, Col 2 + + Row 2, Col 3 - Row 3, Col 1 - - Row 3, Col 2 - - Row 3, Col 3 + Row 3, Col 1 + + Row 3, Col 2 + + Row 3, Col 3 └──────────────┴──────────────┴──────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly.snap.svg index 95654cb4d8..f5a00dbe7c 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly.snap.svg @@ -7,394 +7,394 @@ ┌─────────────────────────────┬──────────────────────────────┬─────────────────────────────┬──────────────────────────────┬─────┬────────┬─────────┬───────┐ Comprehensive Architectural - + Implementation Details for - + Longitudinal Performance - + Strategic Security Framework - + Key - + Status - + Version - + Owner Specification for the - + the High-Throughput - + Analysis Across - + for Mitigating Sophisticated - - - - + + + + Distributed Infrastructure - + Asynchronous Message - + Multi-Regional Cloud - + Cross-Site Scripting - - - - + + + + Layer - + Processing Pipeline with - + Deployment Clusters - + Vulnerabilities - - - - + + + + - + Extended Scalability - - - - - - + + + + + + - + Features and Redundancy - - - - - - + + + + + + - + Protocols - - - - - - + + + + + + ├─────────────────────────────┼──────────────────────────────┼─────────────────────────────┼──────────────────────────────┼─────┼────────┼─────────┼───────┤ - The primary architecture - - Each message is processed - - Historical data indicates a - - A multi-layered defense - - INF - - Active - - v2.4 - - J. + The primary architecture + + Each message is processed + + Historical data indicates a + + A multi-layered defense + + INF + + Active + + v2.4 + + J. - utilizes a decoupled - - through a series of - - significant reduction in - - strategy incorporates - - - - - Doe + utilizes a decoupled + + through a series of + + significant reduction in + + strategy incorporates + + + + + Doe - microservices approach, - - specialized workers that - - tail latency when utilizing - - content security policies, - - - - + microservices approach, + + specialized workers that + + tail latency when utilizing + + content security policies, + + + + - leveraging container - - handle data transformation, - - edge computing nodes closer - - input sanitization - - - - + leveraging container + + handle data transformation, + + edge computing nodes closer + + input sanitization + + + + - orchestration for - - validation, and persistent - - to the geographic location - - libraries, and regular - - - - + orchestration for + + validation, and persistent + + to the geographic location + + libraries, and regular + + + + - scalability and fault - - storage using a persistent - - of the end-user base. - - automated penetration - - - - + scalability and fault + + storage using a persistent + + of the end-user base. + + automated penetration + + + + - tolerance in high-load - - queue. - - - testing routines. - - - - + tolerance in high-load + + queue. + + + testing routines. + + + + - scenarios. - - - Monitoring tools have - - - - - + scenarios. + + + Monitoring tools have + + + + + - - The pipeline features - - captured a steady increase - - Developers are required to - - - - + + The pipeline features + + captured a steady increase + + Developers are required to + + + + - This layer provides the - - built-in retry mechanisms - - in throughput efficiency - - undergo mandatory security - - - - + This layer provides the + + built-in retry mechanisms + + in throughput efficiency + + undergo mandatory security + + + + - fundamental building blocks - - with exponential backoff to - - since the introduction of - - training focusing on the - - - - + fundamental building blocks + + with exponential backoff to + + since the introduction of + + training focusing on the + + + + - for service discovery, load - - ensure message delivery - - the vectorized query engine - - OWASP Top Ten to ensure that - - - - + for service discovery, load + + ensure message delivery + + the vectorized query engine + + OWASP Top Ten to ensure that + + + + - balancing, and - - integrity even during - - in the primary data - - security is integrated into - - - - + balancing, and + + integrity even during + + in the primary data + + security is integrated into + + + + - inter-service communication - - transient network or service - - warehouse. - - the initial design phase. - - - - + inter-service communication + + transient network or service + + warehouse. + + the initial design phase. + + + + - via highly efficient - - failures. - - - - - - + via highly efficient + + failures. + + + + + + - protocol buffers. - - - Resource utilization - - The implementation of a - - - - + protocol buffers. + + + Resource utilization + + The implementation of a + + + + - - Horizontal autoscaling is - - metrics demonstrate that - - robust Identity and Access - - - - + + Horizontal autoscaling is + + metrics demonstrate that + + robust Identity and Access + + + + - Advanced telemetry and - - triggered automatically - - the transition to - - Management system ensures - - - - + Advanced telemetry and + + triggered automatically + + the transition to + + Management system ensures + + + + - logging integrations allow - - based on the depth of the - - serverless compute for - - that the principle of least - - - - + logging integrations allow + + based on the depth of the + + serverless compute for + + that the principle of least + + + + - for real-time monitoring of - - processing queue, ensuring - - intermittent tasks has - - privilege is strictly - - - - + for real-time monitoring of + + processing queue, ensuring + + intermittent tasks has + + privilege is strictly + + + + - system health and rapid - - consistent performance - - resulted in a thirty - - enforced across all - - - - + system health and rapid + + consistent performance + + resulted in a thirty + + enforced across all + + + + - identification of - - during unexpected traffic - - percent cost optimization. - - environments. - - - - + identification of + + during unexpected traffic + + percent cost optimization. + + environments. + + + + - bottlenecks within the - - spikes. - - - - - - + bottlenecks within the + + spikes. + + + + + + - service mesh. - - - - - - - + service mesh. + + + + + + + └─────────────────────────────┴──────────────────────────────┴─────────────────────────────┴──────────────────────────────┴─────┴────────┴─────────┴───────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-long-headers-and-4-columns-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-long-headers-and-4-columns-correctly.snap.svg index b4d6353c3c..8da55efa8b 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-long-headers-and-4-columns-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-long-headers-and-4-columns-correctly.snap.svg @@ -7,56 +7,56 @@ ┌───────────────┬───────────────┬──────────────────┬──────────────────┐ Very Long - + Very Long - + Very Long Column - + Very Long Column Column Header - + Column Header - + Header Three - + Header Four One - + Two - - + + ├───────────────┼───────────────┼──────────────────┼──────────────────┤ - Data 1.1 - - Data 1.2 - - Data 1.3 - - Data 1.4 + Data 1.1 + + Data 1.2 + + Data 1.3 + + Data 1.4 - Data 2.1 - - Data 2.2 - - Data 2.3 - - Data 2.4 + Data 2.1 + + Data 2.2 + + Data 2.3 + + Data 2.4 - Data 3.1 - - Data 3.2 - - Data 3.3 - - Data 3.4 + Data 3.1 + + Data 3.2 + + Data 3.3 + + Data 3.4 └───────────────┴───────────────┴──────────────────┴──────────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-mixed-emojis-As-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-mixed-emojis-As-.snap.svg index 707bf53f43..0db46485e0 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-mixed-emojis-As-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-mixed-emojis-As-.snap.svg @@ -7,25 +7,25 @@ ┌───────────────┬───────────────────┬────────────────┐ Mixed 😃 中文 - + Complex 🚀 日本語 - + Text 📝 한국어 ├───────────────┼───────────────────┼────────────────┤ - 你好 😃 - - こんにちは 🚀 - - 안녕하세요 📝 + 你好 😃 + + こんにちは 🚀 + + 안녕하세요 📝 - World 🌍 - - Code 💻 - - Pizza 🍕 + World 🌍 + + Code 💻 + + Pizza 🍕 └───────────────┴───────────────────┴────────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-Asian-chara-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-Asian-chara-.snap.svg index 0f51eba244..b808d1e335 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-Asian-chara-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-Asian-chara-.snap.svg @@ -7,25 +7,25 @@ ┌──────────────┬─────────────────┬───────────────┐ Chinese 中文 - + Japanese 日本語 - + Korean 한국어 ├──────────────┼─────────────────┼───────────────┤ - 你好 - - こんにちは - - 안녕하세요 + 你好 + + こんにちは + + 안녕하세요 - 世界 - - 世界 - - 세계 + 世界 + + 世界 + + 세계 └──────────────┴─────────────────┴───────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-emojis-and-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-emojis-and-.snap.svg index 1a849696dd..9277078253 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-emojis-and-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-table-with-only-emojis-and-.snap.svg @@ -7,25 +7,25 @@ ┌──────────┬───────────┬──────────┐ Happy 😀 - + Rocket 🚀 - + Heart ❤️ ├──────────┼───────────┼──────────┤ - Smile 😃 - - Fire 🔥 - - Love 💖 + Smile 😃 + + Fire 🔥 + + Love 💖 - Cool 😎 - - Star ⭐ - - Blue 💙 + Cool 😎 + + Star ⭐ + + Blue 💙 └──────────┴───────────┴──────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-complex-markdown-in-rows-and-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-complex-markdown-in-rows-and-.snap.svg new file mode 100644 index 0000000000..8b251c3ab2 --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-complex-markdown-in-rows-and-.snap.svg @@ -0,0 +1,53 @@ + + + + + ┌───────────────┬─────────────────────────────┐ + + Feature + + Markdown + + ├───────────────┼─────────────────────────────┤ + + Bold + + Bold Text + + + Italic + + Italic Text + + + Combined + + Bold and Italic + + + Link + + Google ( + https://google.com + ) + + + Code + + const x = 1 + + + Strikethrough + + Strike + + + Underline + + Underline + + └───────────────┴─────────────────────────────┘ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-headers-are-em-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-headers-are-em-.snap.svg index 2cc7b1cadd..b2523badcd 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-headers-are-em-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-headers-are-em-.snap.svg @@ -6,13 +6,13 @@ ┌────────┬────────┐ - + ├────────┼────────┤ - Data 1 - - Data 2 + Data 1 + + Data 2 └────────┴────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-there-are-more-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-there-are-more-.snap.svg index 452bb1fb12..89ad1cfb4c 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-there-are-more-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-correctly-when-there-are-more-.snap.svg @@ -7,17 +7,17 @@ ┌──────────┬──────────┬──────────┐ Header 1 - + Header 2 - + Header 3 ├──────────┼──────────┼──────────┤ - Data 1 - - Data 2 - + Data 1 + + Data 2 + └──────────┴──────────┴──────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-strips-bold-markers-from-headers-and-renders-them-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-strips-bold-markers-from-headers-and-renders-them-correctly.snap.svg index 6de776060b..717a8803f8 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-strips-bold-markers-from-headers-and-renders-them-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-strips-bold-markers-from-headers-and-renders-them-correctly.snap.svg @@ -7,18 +7,18 @@ ┌─────────────┬───────────────┬──────────────┐ Bold Header - + Normal Header - + Another Bold ├─────────────┼───────────────┼──────────────┤ - Data 1 - - Data 2 - - Data 3 + Data 1 + + Data 2 + + Data 3 └─────────────┴───────────────┴──────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-all-long-columns-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-all-long-columns-correctly.snap.svg index 4b459cfea0..e59cefbc72 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-all-long-columns-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-all-long-columns-correctly.snap.svg @@ -7,45 +7,45 @@ ┌────────────────┬────────────────┬─────────────────┐ Col 1 - + Col 2 - + Col 3 ├────────────────┼────────────────┼─────────────────┤ - This is a very - - This is also a - - And this is the + This is a very + + This is also a + + And this is the - long text that - - very long text - - third long text + long text that + + very long text + + third long text - needs wrapping - - that needs - - that needs + needs wrapping + + that needs + + that needs - in column 1 - - wrapping in - - wrapping in + in column 1 + + wrapping in + + wrapping in - - column 2 - - column 3 + + column 2 + + column 3 └────────────────┴────────────────┴─────────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-columns-with-punctuation-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-columns-with-punctuation-correctly.snap.svg index 7173ce475f..42f7b188f8 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-columns-with-punctuation-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-columns-with-punctuation-correctly.snap.svg @@ -7,44 +7,44 @@ ┌───────────────────┬───────────────┬─────────────────┐ Punctuation 1 - + Punctuation 2 - + Punctuation 3 ├───────────────────┼───────────────┼─────────────────┤ - Start. Stop. - - Semi; colon: - - At@ Hash# + Start. Stop. + + Semi; colon: + + At@ Hash# - Comma, separated. - - Pipe| Slash/ - - Dollar$ + Comma, separated. + + Pipe| Slash/ + + Dollar$ - Exclamation! - - Backslash\ - - Percent% Caret^ + Exclamation! + + Backslash\ + + Percent% Caret^ - Question? - - - Ampersand& + Question? + + + Ampersand& - hyphen-ated - - - Asterisk* + hyphen-ated + + + Asterisk* └───────────────────┴───────────────┴─────────────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-long-cell-content-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-long-cell-content-correctly.snap.svg index 7f7b67a7dd..2cfd46bc54 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-long-cell-content-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-long-cell-content-correctly.snap.svg @@ -7,28 +7,28 @@ ┌───────┬─────────────────────────────┬───────┐ Col 1 - + Col 2 - + Col 3 ├───────┼─────────────────────────────┼───────┤ - Short - - This is a very long cell - - Short + Short + + This is a very long cell + + Short - - content that should wrap to - + + content that should wrap to + - - multiple lines - + + multiple lines + └───────┴─────────────────────────────┴───────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-mixed-long-and-short-columns-correctly.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-mixed-long-and-short-columns-correctly.snap.svg index 3ff0542a26..0e5dbcbb30 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-mixed-long-and-short-columns-correctly.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-wraps-mixed-long-and-short-columns-correctly.snap.svg @@ -7,29 +7,29 @@ ┌───────┬──────────────────────────┬────────┐ Short - + Long - + Medium ├───────┼──────────────────────────┼────────┤ - Tiny - - This is a very long text - - Not so + Tiny + + This is a very long text + + Not so - - that definitely needs to - - long + + that definitely needs to + + long - - wrap to the next line - + + wrap to the next line + └───────┴──────────────────────────┴────────┘ diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap index 48bc00993a..9b5c1e875a 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap @@ -1,5 +1,53 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`TableRenderer > 'calculates column widths based on ren…' 1`] = ` +" +┌────────┬────────┬────────┐ +│ Col 1 │ Col 2 │ Col 3 │ +├────────┼────────┼────────┤ +│ 123456 │ Normal │ Short │ +│ Short │ 123456 │ Normal │ +│ Normal │ Short │ 123456 │ +└────────┴────────┴────────┘ +" +`; + +exports[`TableRenderer > 'calculates width correctly for conten…' 1`] = ` +" +┌───────────────────────────────────┬───────────────────────────────┬─────────────────────────────────┐ +│ Col 1 │ Col 2 │ Col 3 │ +├───────────────────────────────────┼───────────────────────────────┼─────────────────────────────────┤ +│ Visit Google (https://google.com) │ Plain Text │ More Info │ +│ Info Here │ Visit Bing (https://bing.com) │ Links │ +│ Check This │ Search │ Visit Yahoo (https://yahoo.com) │ +└───────────────────────────────────┴───────────────────────────────┴─────────────────────────────────┘ +" +`; + +exports[`TableRenderer > 'does not parse markdown inside code s…' 1`] = ` +" +┌─────────────────┬──────────────────────┬──────────────────┐ +│ Col 1 │ Col 2 │ Col 3 │ +├─────────────────┼──────────────────────┼──────────────────┤ +│ **not bold** │ _not italic_ │ ~~not strike~~ │ +│ [not link](url) │ not underline │ https://not.link │ +│ Normal Text │ More Code: *test* │ ***nested*** │ +└─────────────────┴──────────────────────┴──────────────────┘ +" +`; + +exports[`TableRenderer > 'handles nested markdown styles recurs…' 1`] = ` +" +┌─────────────────────────────┬─────────────────────────────┬─────────────────────────────┐ +│ Header 1 │ Header 2 │ Header 3 │ +├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┤ +│ Bold with Italic and Strike │ Normal │ Short │ +│ Short │ Bold with Italic and Strike │ Normal │ +│ Normal │ Short │ Bold with Italic and Strike │ +└─────────────────────────────┴─────────────────────────────┴─────────────────────────────┘ +" +`; + exports[`TableRenderer > 'handles non-ASCII characters (emojis …' 1`] = ` " ┌──────────────┬────────────┬───────────────┐ @@ -44,6 +92,22 @@ exports[`TableRenderer > 'renders a table with only emojis and …' 1`] = ` " `; +exports[`TableRenderer > 'renders complex markdown in rows and …' 1`] = ` +" +┌───────────────┬─────────────────────────────┐ +│ Feature │ Markdown │ +├───────────────┼─────────────────────────────┤ +│ Bold │ Bold Text │ +│ Italic │ Italic Text │ +│ Combined │ Bold and Italic │ +│ Link │ Google (https://google.com) │ +│ Code │ const x = 1 │ +│ Strikethrough │ Strike │ +│ Underline │ Underline │ +└───────────────┴─────────────────────────────┘ +" +`; + exports[`TableRenderer > 'renders correctly when headers are em…' 1`] = ` " ┌────────┬────────┐ diff --git a/packages/cli/src/ui/utils/markdownParsingUtils.test.ts b/packages/cli/src/ui/utils/markdownParsingUtils.test.ts new file mode 100644 index 0000000000..05f19f09f7 --- /dev/null +++ b/packages/cli/src/ui/utils/markdownParsingUtils.test.ts @@ -0,0 +1,223 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeAll, vi } from 'vitest'; +import chalk from 'chalk'; +import { parseMarkdownToANSI } from './InlineMarkdownRenderer.js'; + +// Mock the theme to use explicit colors instead of empty strings from the default theme. +// This ensures that ansiColorize actually applies ANSI codes that we can verify. +vi.mock('../semantic-colors.js', () => ({ + theme: { + text: { + primary: 'white', + accent: 'cyan', + link: 'blue', + }, + }, +})); + +import { theme } from '../semantic-colors.js'; +import { resolveColor, INK_NAME_TO_HEX_MAP } from '../themes/color-utils.js'; +import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js'; + +describe('parsingUtils', () => { + beforeAll(() => { + themeManager.setActiveTheme(DEFAULT_THEME.name); + themeManager.setTerminalBackground(undefined); + }); + + /** + * Helper to replicate the colorization logic for expected values. + */ + const expectedColorize = (str: string, color: string) => { + const resolved = resolveColor(color); + if (!resolved) return str; + if (resolved.startsWith('#')) return chalk.hex(resolved)(str); + const mappedHex = INK_NAME_TO_HEX_MAP[resolved]; + if (mappedHex) return chalk.hex(mappedHex)(str); + + // Simple mapping for standard colors if they aren't in the hex map + switch (resolved) { + case 'black': + return chalk.black(str); + case 'red': + return chalk.red(str); + case 'green': + return chalk.green(str); + case 'yellow': + return chalk.yellow(str); + case 'blue': + return chalk.blue(str); + case 'magenta': + return chalk.magenta(str); + case 'cyan': + return chalk.cyan(str); + case 'white': + return chalk.white(str); + case 'gray': + case 'grey': + return chalk.gray(str); + default: + return str; + } + }; + + const primary = (str: string) => expectedColorize(str, theme.text.primary); + const accent = (str: string) => expectedColorize(str, theme.text.accent); + const link = (str: string) => expectedColorize(str, theme.text.link); + + describe('parseMarkdownToANSI', () => { + it('should return plain text with default color', () => { + const input = 'Hello world'; + const output = parseMarkdownToANSI(input); + expect(output).toBe(primary(input)); + }); + + it('should handle bold text', () => { + const input = 'This is **bold** text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${chalk.bold(primary('bold'))}${primary(' text')}`, + ); + }); + + it('should handle italic text with *', () => { + const input = 'This is *italic* text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${chalk.italic(primary('italic'))}${primary(' text')}`, + ); + }); + + it('should handle italic text with _', () => { + const input = 'This is _italic_ text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${chalk.italic(primary('italic'))}${primary(' text')}`, + ); + }); + + it('should handle bold italic text with ***', () => { + const input = 'This is ***bold italic*** text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${chalk.bold(chalk.italic(primary('bold italic')))}${primary(' text')}`, + ); + }); + + it('should handle strikethrough text', () => { + const input = 'This is ~~strikethrough~~ text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${chalk.strikethrough(primary('strikethrough'))}${primary(' text')}`, + ); + }); + + it('should handle inline code', () => { + const input = 'This is `code` text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${accent('code')}${primary(' text')}`, + ); + }); + + it('should handle links', () => { + const input = 'Check [this link](https://example.com)'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('Check ')}${primary('this link')}${primary(' (')}${link( + 'https://example.com', + )}${primary(')')}`, + ); + }); + + it('should handle bare URLs', () => { + const input = 'Visit https://google.com now'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('Visit ')}${link('https://google.com')}${primary(' now')}`, + ); + }); + + it('should handle underline tags', () => { + const input = 'This is underlined text'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${primary('This is ')}${chalk.underline(primary('underlined'))}${primary(' text')}`, + ); + }); + + it('should handle complex mixed markdown', () => { + const input = '**Bold** and *italic* and `code` and [link](url)'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + `${chalk.bold(primary('Bold'))}${primary(' and ')}${chalk.italic( + primary('italic'), + )}${primary(' and ')}${accent('code')}${primary(' and ')}${primary( + 'link', + )}${primary(' (')}${link('url')}${primary(')')}`, + ); + }); + + it('should respect custom default color', () => { + const customColor = 'cyan'; + const input = 'Hello **world**'; + const output = parseMarkdownToANSI(input, customColor); + const cyan = (str: string) => expectedColorize(str, 'cyan'); + expect(output).toBe(`${cyan('Hello ')}${chalk.bold(cyan('world'))}`); + }); + + it('should handle nested formatting in bold/italic', () => { + const input = '**Bold with *italic* inside**'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + chalk.bold( + `${primary('Bold with ')}${chalk.italic(primary('italic'))}${primary( + ' inside', + )}`, + ), + ); + }); + + it('should handle hex colors as default', () => { + const hexColor = '#ff00ff'; + const input = 'Hello **world**'; + const output = parseMarkdownToANSI(input, hexColor); + const magenta = (str: string) => chalk.hex('#ff00ff')(str); + expect(output).toBe( + `${magenta('Hello ')}${chalk.bold(magenta('world'))}`, + ); + }); + + it('should override default color with link color', () => { + const input = 'Check [link](url)'; + const output = parseMarkdownToANSI(input, 'red'); + const red = (str: string) => chalk.red(str); + expect(output).toBe( + `${red('Check ')}${red('link')}${red(' (')}${link('url')}${red(')')}`, + ); + }); + + it('should override default color with accent color for code', () => { + const input = 'Code: `const x = 1`'; + const output = parseMarkdownToANSI(input, 'green'); + const green = (str: string) => chalk.green(str); + const cyan = (str: string) => chalk.cyan(str); + expect(output).toBe(`${green('Code: ')}${cyan('const x = 1')}`); + }); + + it('should handle nested formatting with color overrides', () => { + const input = '**Bold with `code` inside**'; + const output = parseMarkdownToANSI(input); + expect(output).toBe( + chalk.bold( + `${primary('Bold with ')}${accent('code')}${primary(' inside')}`, + ), + ); + }); + }); +});