From b990949477f5b44126cbee5393b480d9e5f222fd Mon Sep 17 00:00:00 2001 From: PaperBoardOfficial Date: Sat, 12 Jul 2025 16:05:39 +0530 Subject: [PATCH 1/4] markdown table rendering added --- webview-ui/src/components/chat/Markdown.tsx | 59 +++- .../src/components/common/InlineParser.tsx | 320 ++++++++++++++++++ .../src/components/common/TableParser.tsx | 134 ++++++++ .../MarkdownTableIntegration.spec.tsx | 140 ++++++++ .../common/__tests__/TableParser.spec.tsx | 121 +++++++ 5 files changed, 771 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/components/common/InlineParser.tsx create mode 100644 webview-ui/src/components/common/TableParser.tsx create mode 100644 webview-ui/src/components/common/__tests__/MarkdownTableIntegration.spec.tsx create mode 100644 webview-ui/src/components/common/__tests__/TableParser.spec.tsx diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index ba838284d7d..0aad45fb6a1 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -1,12 +1,57 @@ -import { memo, useState } from "react" +import React, { memo, useState } from "react" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import { useCopyToClipboard } from "@src/utils/clipboard" import { StandardTooltip } from "@src/components/ui" import MarkdownBlock from "../common/MarkdownBlock" +import { parseTable } from "../common/TableParser" -export const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => { +const splitMarkdownAndTables = (markdownText: string, ts: number) => { + const segments: { type: 'text' | 'table'; content: string | React.ReactNode }[] = []; + const lines = markdownText.split(/\r?\n/); + let currentLineIndex = 0; + let currentTextBuffer: string[] = []; + + while (currentLineIndex < lines.length) { + const line = lines[currentLineIndex]; + if (line.trim().startsWith('|') && line.trim().endsWith('|')) { + let potentialTableLines: string[] = []; + let tempIndex = currentLineIndex; + potentialTableLines.push(lines[tempIndex]); + tempIndex++; + if (tempIndex < lines.length && lines[tempIndex].trim().match(/^\|(?:\s*[-:]+\s*\|)+\s*$/)) { + potentialTableLines.push(lines[tempIndex]); + tempIndex++; + while (tempIndex < lines.length && lines[tempIndex].trim().startsWith('|') && lines[tempIndex].trim().endsWith('|')) { + potentialTableLines.push(lines[tempIndex]); + tempIndex++; + } + const tableString = potentialTableLines.join('\n'); + const parsedTableContent = parseTable(tableString, `chat-table-${ts}-${segments.length}`); + + if (parsedTableContent) { + if (currentTextBuffer.length > 0) { + segments.push({ type: 'text', content: currentTextBuffer.join('\n') }); + currentTextBuffer = []; + } + segments.push({ type: 'table', content: parsedTableContent }); + currentLineIndex = tempIndex; + continue; + } + } + } + currentTextBuffer.push(line); + currentLineIndex++; + } + if (currentTextBuffer.length > 0) { + segments.push({ type: 'text', content: currentTextBuffer.join('\n') }); + } + + return segments; +}; + +export const Markdown = memo(({ markdown, partial, ts }: { markdown?: string; partial?: boolean; ts?: number }) => { const [isHovering, setIsHovering] = useState(false) // Shorter feedback duration for copy button flash. @@ -16,13 +61,21 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia return null } + const segments = splitMarkdownAndTables(markdown, ts || Date.now()); + return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} style={{ position: "relative" }}>
- + {segments.map((segment, index) => { + if (segment.type === 'text') { + return ; + } else { + return {segment.content}; + } + })}
{markdown && !partial && isHovering && (
{ + if (!/[\[\*_~`\|]/.test(text)) { + return text; + } + + const processedHtml = preserveHtmlTags(text); + + let processed: ReactNode | ReactNode[] = processedHtml; + + processed = processLinks(processed, keyOffset); + processed = processBold(processed, keyOffset); + processed = processItalic(processed, keyOffset); + processed = processStrikethrough(processed, keyOffset); + processed = processInlineCode(processed, keyOffset); + processed = processSpoiler(processed, keyOffset); + + return processed; +}; + +const preserveHtmlTags = (text: string): string => { + return text.replace(/<([a-z][a-z0-9]*)\b[^>]*>(.*?)<\/\1>/gi, match => { + return match; + }); +}; + +const processLinks = ( + text: string | ReactNode | ReactNode[], + keyOffset: number +): ReactNode | ReactNode[] => { + if (typeof text !== "string") { + if (Array.isArray(text)) { + return text.map((item, index) => + typeof item === "string" ? processLinks(item, keyOffset + index * 100) : item + ); + } + return text; + } + + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + let lastIndex = 0; + let result: ReactNode[] = []; + let match; + let matchIndex = 0; + + while ((match = linkRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push(text.substring(lastIndex, match.index)); + } + + const linkText = match[1]; + const linkUrl = match[2]; + + const processedLinkText = parseInlineMarkdown(linkText, keyOffset + 1000 + matchIndex); + + result.push( + + {processedLinkText} + + ); + + lastIndex = linkRegex.lastIndex; + matchIndex++; + } + + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + return result.length === 1 && typeof result[0] === "string" ? result[0] : result; +}; + +const processBold = ( + text: string | ReactNode | ReactNode[], + keyOffset: number +): ReactNode | ReactNode[] => { + if (typeof text !== "string") { + if (Array.isArray(text)) { + return text.map((item, index) => + typeof item === "string" ? processBold(item, keyOffset + index * 100) : item + ); + } + return text; + } + + const boldRegex = /\*\*(.*?)\*\*/g; + let lastIndex = 0; + let result: ReactNode[] = []; + let match; + let matchIndex = 0; + + while ((match = boldRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push(text.substring(lastIndex, match.index)); + } + + const boldContent = match[1]; + + const processedBoldContent = parseInlineMarkdown(boldContent, keyOffset + 2000 + matchIndex); + + result.push({processedBoldContent}); + + lastIndex = boldRegex.lastIndex; + matchIndex++; + } + + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + if (result.length === 1 && typeof result[0] === "string") { + const singleAsteriskRegex = /^\*(.*)\*$/; + const singleMatch = text.match(singleAsteriskRegex); + + if (singleMatch) { + const boldContent = singleMatch[1]; + const processedBoldContent = parseInlineMarkdown(boldContent, keyOffset + 3000); + return {processedBoldContent}; + } + } + + return result.length === 1 && typeof result[0] === "string" ? result[0] : result; +}; + +const processItalic = ( + text: string | ReactNode | ReactNode[], + keyOffset: number +): ReactNode | ReactNode[] => { + if (typeof text !== "string") { + if (Array.isArray(text)) { + return text.map((item, index) => + typeof item === "string" ? processItalic(item, keyOffset + index * 100) : item + ); + } + return text; + } + + const italicRegex = /_(.*?)_/g; + let lastIndex = 0; + let result: ReactNode[] = []; + let match; + let matchIndex = 0; + + while ((match = italicRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push(text.substring(lastIndex, match.index)); + } + + const italicContent = match[1]; + + const processedItalicContent = parseInlineMarkdown( + italicContent, + keyOffset + 4000 + matchIndex + ); + + result.push({processedItalicContent}); + + lastIndex = italicRegex.lastIndex; + matchIndex++; + } + + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + return result.length === 1 && typeof result[0] === "string" ? result[0] : result; +}; + +const processStrikethrough = ( + text: string | ReactNode | ReactNode[], + keyOffset: number +): ReactNode | ReactNode[] => { + if (typeof text !== "string") { + if (Array.isArray(text)) { + return text.map((item, index) => + typeof item === "string" ? processStrikethrough(item, keyOffset + index * 100) : item + ); + } + return text; + } + + const strikeRegex = /~~(.*?)~~/g; + let lastIndex = 0; + let result: ReactNode[] = []; + let match; + let matchIndex = 0; + + while ((match = strikeRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push(text.substring(lastIndex, match.index)); + } + + const strikeContent = match[1]; + + const processedStrikeContent = parseInlineMarkdown( + strikeContent, + keyOffset + 5000 + matchIndex + ); + + result.push( + + {processedStrikeContent} + + ); + + lastIndex = strikeRegex.lastIndex; + matchIndex++; + } + + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + return result.length === 1 && typeof result[0] === "string" ? result[0] : result; +}; + +const processInlineCode = ( + text: string | ReactNode | ReactNode[], + keyOffset: number +): ReactNode | ReactNode[] => { + if (typeof text !== "string") { + if (Array.isArray(text)) { + return text.map((item, index) => + typeof item === "string" ? processInlineCode(item, keyOffset + index * 100) : item + ); + } + return text; + } + + const codeRegex = /`([^`]+)`/g; + let lastIndex = 0; + let result: ReactNode[] = []; + let match; + let matchIndex = 0; + + while ((match = codeRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push(text.substring(lastIndex, match.index)); + } + + const codeContent = match[1]; + + result.push( + + {codeContent} + + ); + + lastIndex = codeRegex.lastIndex; + matchIndex++; + } + + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + return result.length === 1 && typeof result[0] === "string" ? result[0] : result; +}; + +const processSpoiler = ( + text: string | ReactNode | ReactNode[], + keyOffset: number +): ReactNode | ReactNode[] => { + if (typeof text !== "string") { + if (Array.isArray(text)) { + return text.map((item, index) => + typeof item === "string" ? processSpoiler(item, keyOffset + index * 100) : item + ); + } + return text; + } + + const spoilerRegex = /\|\|(.*?)\|\|/g; + let lastIndex = 0; + let result: ReactNode[] = []; + let match; + let matchIndex = 0; + + while ((match = spoilerRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push(text.substring(lastIndex, match.index)); + } + + const spoilerContent = match[1]; + + const processedSpoilerContent = parseInlineMarkdown( + spoilerContent, + keyOffset + 6000 + matchIndex + ); + + result.push( + + {processedSpoilerContent} + + ); + + lastIndex = spoilerRegex.lastIndex; + matchIndex++; + } + + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + return result.length === 1 && typeof result[0] === "string" ? result[0] : result; +}; \ No newline at end of file diff --git a/webview-ui/src/components/common/TableParser.tsx b/webview-ui/src/components/common/TableParser.tsx new file mode 100644 index 00000000000..f8189a7fe9e --- /dev/null +++ b/webview-ui/src/components/common/TableParser.tsx @@ -0,0 +1,134 @@ +import { parseInlineMarkdown } from "./InlineParser"; + +const containsHtmlTags = (text: string): boolean => { + return /<\/?[a-z][a-z0-9]*\b[^>]*>/i.test(text); +}; + +const parseTableHeaderCells = (headerRow: string) => { + return headerRow + .split("|") + .slice(1, -1) + .map(cell => cell.trim().replace(/^:[-]+:$|^:[-]+|[-]+:$/g, '').trim()); +}; + +const parseTableDataRows = (dataRows: string[], maxRows: number = 50) => { + const rows = []; + try { + for (let i = 0; i < dataRows.length; i++) { + const cells = dataRows[i] + .split("|") + .slice(1, -1) + .map(cell => cell.trim()); + + if (cells.length > 0) { + rows.push(cells); + } + + if (rows.length >= maxRows) { + break; + } + } + } catch (error) { + return []; + } + return rows; +}; + +export const renderTableHeader = (headerCells: string[], keyPrefix: string) => { + return ( + + + {headerCells.map((cell, idx) => ( + + {renderTableCell(cell, idx)} + + ))} + + + ); +}; + +export const renderTableBody = (rows: string[][], keyPrefix: string) => { + return ( + + {rows.map((row, rowIdx) => ( + + {row.map((cell, cellIdx) => ( + + {renderTableCell(cell, cellIdx + rowIdx * 100)} + + ))} + + ))} + + ); +}; + +const renderTableCell = (content: string, keyOffset: number) => { + if (containsHtmlTags(content)) { + return
; + } + return parseInlineMarkdown(content, keyOffset); +}; + +const isValidTable = (separatorRow: string, headerCells: string[], rows: string[][]) => { + if (!separatorRow || !separatorRow.includes("|") || !separatorRow.match(/[-:]/)) { + return false; + } + + if (headerCells.length === 0 || rows.some(row => row.length === 0)) { + return false; + } + + return true; +}; + +export const parseTable = (tableText: string, keyPrefix: string) => { + try { + const lines = tableText.trim().split("\n").filter(line => line.trim() !== ''); + + const tableStartIndex = lines.findIndex(line => line.trim().startsWith('|')); + if (tableStartIndex === -1) { + return null; + } + + const tableLines = lines.slice(tableStartIndex); + + if (tableLines.length < 2) { + return null; + } + + const headerRow = tableLines[0]; + const separatorRow = tableLines[1]; + const dataRows = tableLines.slice(2).filter(line => { + return !line.trim().match(/^[\s|:-]+$/); + }); + + const headerCells = parseTableHeaderCells(headerRow); + const rows = parseTableDataRows(dataRows); + + if (!isValidTable(separatorRow, headerCells, rows)) { + return null; + } + + return ( +
+ + {renderTableHeader(headerCells, keyPrefix)} + {renderTableBody(rows, keyPrefix)} +
+
+ ); + } catch (error) { + return null; + } +}; diff --git a/webview-ui/src/components/common/__tests__/MarkdownTableIntegration.spec.tsx b/webview-ui/src/components/common/__tests__/MarkdownTableIntegration.spec.tsx new file mode 100644 index 00000000000..3027b80c9b7 --- /dev/null +++ b/webview-ui/src/components/common/__tests__/MarkdownTableIntegration.spec.tsx @@ -0,0 +1,140 @@ +import { render } from "@testing-library/react"; +import { Markdown } from "../../chat/Markdown"; +import { vi } from "vitest"; + +vi.mock("../MarkdownBlock", () => ({ + __esModule: true, + default: vi.fn(({ markdown }) => ( +
{markdown}
+ )), +})); + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + theme: "dark", + }), +})); + +describe("Markdown Table Integration", () => { + it("renders table in markdown", () => { + const md = `Text before +| Header | +|--------| +| Cell | +Text after`; + const { container } = render(); + expect(container.querySelector("table")).toBeInTheDocument(); + expect(container.querySelector("th")?.textContent).toBe("Header"); + expect(container.querySelector("td")?.textContent).toBe("Cell"); + const markdownBlocks = container.querySelectorAll('[data-testid="mock-markdown-block"]'); + expect(markdownBlocks.length).toBe(2); + expect(markdownBlocks[0]?.textContent).toContain("Text before"); + expect(markdownBlocks[1]?.textContent).toContain("Text after"); + }); + + it("handles multiple tables", () => { + const md = `| Table 1 |\n|---------|\n| Cell |\n\n| Table 2 |\n|---------|\n| Cell |`; + const { container } = render(); + expect(container.querySelectorAll("table").length).toBe(2); + expect(container.querySelectorAll("th")[0]?.textContent).toBe("Table 1"); + expect(container.querySelectorAll("th")[1]?.textContent).toBe("Table 2"); + }); + + it("handles markdown text interspersed with tables", () => { + const md = `This is some **bold** text. + +| Header A | Header B | +|----------|----------| +| Value A1 | Value B1 | + +And here is some *italic* text after the table. + +| Header C | +|----------| +| Value C1 |`; + const { container } = render(); + expect(container.querySelectorAll("table").length).toBe(2); + const markdownBlocks = container.querySelectorAll('[data-testid="mock-markdown-block"]'); + expect(markdownBlocks.length).toBe(2); + expect(markdownBlocks[0]?.textContent).toContain("This is some **bold** text."); + expect(markdownBlocks[1]?.textContent).toContain("And here is some *italic* text after the table."); + }); + + it("handles tables at the beginning of the markdown", () => { + const md = `| Start Header | +|--------------| +| Start Cell | +Text after table.`; + const { container } = render(); + expect(container.querySelector("table")).toBeInTheDocument(); + expect(container.querySelector("th")?.textContent).toBe("Start Header"); + const markdownBlocks = container.querySelectorAll('[data-testid="mock-markdown-block"]'); + expect(markdownBlocks.length).toBe(1); + expect(markdownBlocks[0]?.textContent).toContain("Text after table."); + }); + + it("handles tables at the end of the markdown", () => { + const md = `Text before table. +| End Header | +|------------| +| End Cell |`; + const { container } = render(); + expect(container.querySelector("table")).toBeInTheDocument(); + expect(container.querySelector("th")?.textContent).toBe("End Header"); + const markdownBlocks = container.querySelectorAll('[data-testid="mock-markdown-block"]'); + expect(markdownBlocks.length).toBe(1); + expect(markdownBlocks[0]?.textContent).toContain("Text before table."); + }); + + it("handles markdown with no tables", () => { + const md = `Just some plain **markdown** text.`; + const { container } = render(); + expect(container.querySelector("table")).not.toBeInTheDocument(); + const markdownBlocks = container.querySelectorAll('[data-testid="mock-markdown-block"]'); + expect(markdownBlocks.length).toBe(1); + expect(markdownBlocks[0]?.textContent).toContain("Just some plain **markdown** text."); + }); + + it("handles empty markdown string", () => { + const { container } = render(); + expect(container.innerHTML).toBe(""); + }); + + it("handles markdown with only a table", () => { + const md = `| Only Table | +|------------| +| Cell |`; + const { container } = render(); + expect(container.querySelector("table")).toBeInTheDocument(); + expect(container.querySelector("th")?.textContent).toBe("Only Table"); + }); + + it("handles bold markdown in table cells", () => { + const md = `| Header |\n|--------|\n| Cell with **bold** text |`; + const { container } = render(); + const cellContent = container.querySelector("td"); + expect(cellContent?.innerHTML).toContain("bold"); + }); + + it("handles italic markdown in table cells", () => { + const md = `| Header |\n|--------|\n| Cell with _italic_ text |`; + const { container } = render(); + const cellContent = container.querySelector("td"); + expect(cellContent?.innerHTML).toContain("italic"); + }); + + it("handles code markdown in table cells", () => { + const md = `| Header |\n|--------|\n| Cell with \`code\` snippet |`; + const { container } = render(); + const cellContent = container.querySelector("td"); + expect(cellContent?.querySelector("code")?.textContent).toContain("code"); + }); + + it("handles link markdown in table cells", () => { + const md = `| Header |\n|--------|\n| Cell with a [link](http://example.com) |`; + const { container } = render(); + const cellContent = container.querySelector("td"); + expect(cellContent?.innerHTML).toContain(""); + }); +}); \ No newline at end of file diff --git a/webview-ui/src/components/common/__tests__/TableParser.spec.tsx b/webview-ui/src/components/common/__tests__/TableParser.spec.tsx new file mode 100644 index 00000000000..e07da1c8294 --- /dev/null +++ b/webview-ui/src/components/common/__tests__/TableParser.spec.tsx @@ -0,0 +1,121 @@ +import { render } from "@testing-library/react"; +import { parseTable } from "../TableParser"; +import { parseInlineMarkdown } from "../InlineParser"; + +describe("TableParser", () => { + it("parses simple table", () => { + const table = `| Header 1 | Header 2 | +|----------|----------| +| Cell 1 | Cell 2 |`; + const { container } = render(parseTable(table, "test-table")!); + expect(container.querySelector("table")).toBeInTheDocument(); + expect(container.querySelectorAll("th").length).toBe(2); + expect(container.querySelector("th")?.textContent).toBe("Header 1"); + expect(container.querySelectorAll("td").length).toBe(2); + expect(container.querySelector("td")?.textContent).toBe("Cell 1"); + }); + + it("handles invalid tables", () => { + expect(parseTable("invalid content", "test")).toBeNull(); + expect(parseTable("| Header |\n", "test")).toBeNull(); + expect(parseTable("| Header |\n|---|", "test")).not.toBeNull(); + }); + + it("handles HTML in cells", () => { + const table = `| Header | +|--------| +| Bold |`; + const { container } = render(parseTable(table, "html-table")!); + expect(container.querySelector("b")).toBeInTheDocument(); + expect(container.querySelector("b")?.textContent).toBe("Bold"); + }); + + it("handles markdown in cells", () => { + const table = `| Header | +|--------| +| **Bold** and _italic_ |`; + const { container } = render(parseTable(table, "markdown-table")!); + expect(container.querySelector("strong")).toBeInTheDocument(); + expect(container.querySelector("em")).toBeInTheDocument(); + expect(container.querySelector("strong")?.textContent).toBe("Bold"); + expect(container.querySelector("em")?.textContent).toBe("italic"); + }); + + it("handles empty cells", () => { + const table = `| H1 | H2 | +|----|----| +| C1 | |`; + const { container } = render(parseTable(table, "empty-cell-table")!); + expect(container.querySelectorAll("td")[1].textContent).toBe(""); + }); + + it("handles tables with extra spaces", () => { + const table = `| Header 1 | Header 2 | +|------------|------------| +| Cell 1 | Cell 2 |`; + const { container } = render(parseTable(table, "spaced-table")!); + expect(container.querySelector("th")?.textContent).toBe("Header 1"); + expect(container.querySelector("td")?.textContent).toBe("Cell 1"); + }); + + it("handles tables with alignment markers", () => { + const table = `| Left | Center | Right | +|:-----|:------:|------:| +| L | C | R |`; + const { container } = render(parseTable(table, "alignment-table")!); + expect(container.querySelectorAll("th").length).toBe(3); + expect(container.querySelectorAll("td").length).toBe(3); + }); + + it("returns null for tables with too few lines", () => { + expect(parseTable("| Header |", "short-table")).toBeNull(); + expect(parseTable("", "empty-table")).toBeNull(); + }); + + it("handles multiple data rows", () => { + const table = `| H1 | H2 | +|----|----| +| R1C1 | R1C2 | +| R2C1 | R2C2 |`; + const { container } = render(parseTable(table, "multi-row-table")!); + expect(container.querySelectorAll("tr").length).toBe(3); // 1 header + 2 data rows + expect(container.querySelectorAll("td").length).toBe(4); + }); + + it("handles tables with no data rows", () => { + const table = `| H1 | H2 | +|----|----|`; + const { container } = render(parseTable(table, "no-data-table")!); + expect(container.querySelectorAll("tr").length).toBe(1); // Only header row + expect(container.querySelectorAll("td").length).toBe(0); + }); + + it("parses bold markdown", () => { + const text = "Cell with **bold** text."; + const result = parseInlineMarkdown(text, 0); + const container = render(
{result}
).container; + expect(container.innerHTML).toContain("bold"); + }); + + it("parses italic markdown", () => { + const text = "Cell with _italic_ text."; + const result = parseInlineMarkdown(text, 0); + const container = render(
{result}
).container; + expect(container.innerHTML).toContain("italic"); + }); + + it("parses code markdown", () => { + const text = "Cell with `code` snippet."; + const result = parseInlineMarkdown(text, 0); + const container = render(
{result}
).container; + expect(container.querySelector("code")?.textContent).toContain("code"); + }); + + it("parses link markdown", () => { + const text = "Cell with a [link](http://example.com)."; + const result = parseInlineMarkdown(text, 0); + const container = render(
{result}
).container; + expect(container.innerHTML).toContain("
"); + }); +}); \ No newline at end of file From 2913beed7e4695eee91fc2338c4a36cbde098834 Mon Sep 17 00:00:00 2001 From: PaperBoardOfficial Date: Sat, 12 Jul 2025 16:43:12 +0530 Subject: [PATCH 2/4] fixed linting errors --- webview-ui/src/components/chat/Markdown.tsx | 2 +- webview-ui/src/components/common/InlineParser.tsx | 14 +++++++------- webview-ui/src/components/common/TableParser.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index 0aad45fb6a1..6691b96e60c 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -16,7 +16,7 @@ const splitMarkdownAndTables = (markdownText: string, ts: number) => { while (currentLineIndex < lines.length) { const line = lines[currentLineIndex]; if (line.trim().startsWith('|') && line.trim().endsWith('|')) { - let potentialTableLines: string[] = []; + const potentialTableLines: string[] = []; let tempIndex = currentLineIndex; potentialTableLines.push(lines[tempIndex]); tempIndex++; diff --git a/webview-ui/src/components/common/InlineParser.tsx b/webview-ui/src/components/common/InlineParser.tsx index ad8ecc76843..7b6d6018202 100644 --- a/webview-ui/src/components/common/InlineParser.tsx +++ b/webview-ui/src/components/common/InlineParser.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; export const parseInlineMarkdown = (text: string, keyOffset: number): ReactNode | ReactNode[] => { - if (!/[\[\*_~`\|]/.test(text)) { + if (!/[[*_~`|]/.test(text)) { return text; } @@ -40,7 +40,7 @@ const processLinks = ( const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; let lastIndex = 0; - let result: ReactNode[] = []; + const result: ReactNode[] = []; let match; let matchIndex = 0; @@ -92,7 +92,7 @@ const processBold = ( const boldRegex = /\*\*(.*?)\*\*/g; let lastIndex = 0; - let result: ReactNode[] = []; + const result: ReactNode[] = []; let match; let matchIndex = 0; @@ -144,7 +144,7 @@ const processItalic = ( const italicRegex = /_(.*?)_/g; let lastIndex = 0; - let result: ReactNode[] = []; + const result: ReactNode[] = []; let match; let matchIndex = 0; @@ -188,7 +188,7 @@ const processStrikethrough = ( const strikeRegex = /~~(.*?)~~/g; let lastIndex = 0; - let result: ReactNode[] = []; + const result: ReactNode[] = []; let match; let matchIndex = 0; @@ -236,7 +236,7 @@ const processInlineCode = ( const codeRegex = /`([^`]+)`/g; let lastIndex = 0; - let result: ReactNode[] = []; + const result: ReactNode[] = []; let match; let matchIndex = 0; @@ -282,7 +282,7 @@ const processSpoiler = ( const spoilerRegex = /\|\|(.*?)\|\|/g; let lastIndex = 0; - let result: ReactNode[] = []; + const result: ReactNode[] = []; let match; let matchIndex = 0; diff --git a/webview-ui/src/components/common/TableParser.tsx b/webview-ui/src/components/common/TableParser.tsx index f8189a7fe9e..37e0a68acbf 100644 --- a/webview-ui/src/components/common/TableParser.tsx +++ b/webview-ui/src/components/common/TableParser.tsx @@ -28,7 +28,7 @@ const parseTableDataRows = (dataRows: string[], maxRows: number = 50) => { break; } } - } catch (error) { + } catch (_error) { return []; } return rows; @@ -128,7 +128,7 @@ export const parseTable = (tableText: string, keyPrefix: string) => {
); - } catch (error) { + } catch (_error) { return null; } }; From cba653ababd9b90fc7605a0e29c26423ede90796 Mon Sep 17 00:00:00 2001 From: PaperBoardOfficial Date: Sat, 12 Jul 2025 16:55:50 +0530 Subject: [PATCH 3/4] fixed suggestions by ellipsis --- webview-ui/src/components/common/InlineParser.tsx | 14 ++------------ webview-ui/src/components/common/TableParser.tsx | 15 ++------------- .../common/__tests__/TableParser.spec.tsx | 15 +++++++-------- 3 files changed, 11 insertions(+), 33 deletions(-) diff --git a/webview-ui/src/components/common/InlineParser.tsx b/webview-ui/src/components/common/InlineParser.tsx index 7b6d6018202..e69d138b6b5 100644 --- a/webview-ui/src/components/common/InlineParser.tsx +++ b/webview-ui/src/components/common/InlineParser.tsx @@ -115,16 +115,6 @@ const processBold = ( result.push(text.substring(lastIndex)); } - if (result.length === 1 && typeof result[0] === "string") { - const singleAsteriskRegex = /^\*(.*)\*$/; - const singleMatch = text.match(singleAsteriskRegex); - - if (singleMatch) { - const boldContent = singleMatch[1]; - const processedBoldContent = parseInlineMarkdown(boldContent, keyOffset + 3000); - return {processedBoldContent}; - } - } return result.length === 1 && typeof result[0] === "string" ? result[0] : result; }; @@ -142,7 +132,7 @@ const processItalic = ( return text; } - const italicRegex = /_(.*?)_/g; + const italicRegex = /([*_])(.*?)\1/g; let lastIndex = 0; const result: ReactNode[] = []; let match; @@ -153,7 +143,7 @@ const processItalic = ( result.push(text.substring(lastIndex, match.index)); } - const italicContent = match[1]; + const italicContent = match[2]; const processedItalicContent = parseInlineMarkdown( italicContent, diff --git a/webview-ui/src/components/common/TableParser.tsx b/webview-ui/src/components/common/TableParser.tsx index 37e0a68acbf..c67e2f3a0f4 100644 --- a/webview-ui/src/components/common/TableParser.tsx +++ b/webview-ui/src/components/common/TableParser.tsx @@ -1,9 +1,5 @@ import { parseInlineMarkdown } from "./InlineParser"; -const containsHtmlTags = (text: string): boolean => { - return /<\/?[a-z][a-z0-9]*\b[^>]*>/i.test(text); -}; - const parseTableHeaderCells = (headerRow: string) => { return headerRow .split("|") @@ -43,7 +39,7 @@ export const renderTableHeader = (headerCells: string[], keyPrefix: string) => { key={`${keyPrefix}-header-${idx}`} className="border border-[--gray-3] px-4 py-2 text-left text-sm font-medium" > - {renderTableCell(cell, idx)} + {parseInlineMarkdown(cell, idx)} ))} @@ -64,7 +60,7 @@ export const renderTableBody = (rows: string[][], keyPrefix: string) => { key={`${keyPrefix}-cell-${rowIdx}-${cellIdx}`} className="border border-[--gray-3] px-4 py-2 text-sm" > - {renderTableCell(cell, cellIdx + rowIdx * 100)} + {parseInlineMarkdown(cell, cellIdx + rowIdx * 100)} ))} @@ -73,13 +69,6 @@ export const renderTableBody = (rows: string[][], keyPrefix: string) => { ); }; -const renderTableCell = (content: string, keyOffset: number) => { - if (containsHtmlTags(content)) { - return
; - } - return parseInlineMarkdown(content, keyOffset); -}; - const isValidTable = (separatorRow: string, headerCells: string[], rows: string[][]) => { if (!separatorRow || !separatorRow.includes("|") || !separatorRow.match(/[-:]/)) { return false; diff --git a/webview-ui/src/components/common/__tests__/TableParser.spec.tsx b/webview-ui/src/components/common/__tests__/TableParser.spec.tsx index e07da1c8294..f6e2ceab159 100644 --- a/webview-ui/src/components/common/__tests__/TableParser.spec.tsx +++ b/webview-ui/src/components/common/__tests__/TableParser.spec.tsx @@ -21,14 +21,6 @@ describe("TableParser", () => { expect(parseTable("| Header |\n|---|", "test")).not.toBeNull(); }); - it("handles HTML in cells", () => { - const table = `| Header | -|--------| -| Bold |`; - const { container } = render(parseTable(table, "html-table")!); - expect(container.querySelector("b")).toBeInTheDocument(); - expect(container.querySelector("b")?.textContent).toBe("Bold"); - }); it("handles markdown in cells", () => { const table = `| Header | @@ -104,6 +96,13 @@ describe("TableParser", () => { expect(container.innerHTML).toContain("italic"); }); + it("parses italic markdown with single asterisks", () => { + const text = "Cell with *italic* text."; + const result = parseInlineMarkdown(text, 0); + const container = render(
{result}
).container; + expect(container.innerHTML).toContain("italic"); + }); + it("parses code markdown", () => { const text = "Cell with `code` snippet."; const result = parseInlineMarkdown(text, 0); From af90617fceeb6b4f815a8edf4e7f9fae35d8e468 Mon Sep 17 00:00:00 2001 From: PaperBoardOfficial Date: Sat, 12 Jul 2025 17:03:46 +0530 Subject: [PATCH 4/4] removed ts --- webview-ui/src/components/chat/Markdown.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index 6691b96e60c..99594016838 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -7,7 +7,7 @@ import { StandardTooltip } from "@src/components/ui" import MarkdownBlock from "../common/MarkdownBlock" import { parseTable } from "../common/TableParser" -const splitMarkdownAndTables = (markdownText: string, ts: number) => { +const splitMarkdownAndTables = (markdownText: string) => { const segments: { type: 'text' | 'table'; content: string | React.ReactNode }[] = []; const lines = markdownText.split(/\r?\n/); let currentLineIndex = 0; @@ -28,7 +28,7 @@ const splitMarkdownAndTables = (markdownText: string, ts: number) => { tempIndex++; } const tableString = potentialTableLines.join('\n'); - const parsedTableContent = parseTable(tableString, `chat-table-${ts}-${segments.length}`); + const parsedTableContent = parseTable(tableString, `chat-table-${Date.now()}-${segments.length}`); if (parsedTableContent) { if (currentTextBuffer.length > 0) { @@ -51,7 +51,7 @@ const splitMarkdownAndTables = (markdownText: string, ts: number) => { return segments; }; -export const Markdown = memo(({ markdown, partial, ts }: { markdown?: string; partial?: boolean; ts?: number }) => { +export const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => { const [isHovering, setIsHovering] = useState(false) // Shorter feedback duration for copy button flash. @@ -61,7 +61,7 @@ export const Markdown = memo(({ markdown, partial, ts }: { markdown?: string; pa return null } - const segments = splitMarkdownAndTables(markdown, ts || Date.now()); + const segments = splitMarkdownAndTables(markdown); return (