Skip to content

Commit 8472a35

Browse files
committed
feat: add markdown table rendering support
- Migrate from react-remark to react-markdown for improved compatibility - Add remark-gfm plugin to enable GitHub Flavored Markdown tables - Add comprehensive table styling for VSCode theme compatibility - Ensure proper code block separation with wrapper divs - Maintain all existing markdown features (links, code blocks, math, etc.)
1 parent 6216075 commit 8472a35

File tree

1 file changed

+134
-96
lines changed

1 file changed

+134
-96
lines changed

webview-ui/src/components/common/MarkdownBlock.tsx

Lines changed: 134 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { memo, useEffect } from "react"
2-
import { useRemark } from "react-remark"
1+
import React, { memo } from "react"
2+
import ReactMarkdown from "react-markdown"
33
import styled from "styled-components"
44
import { visit } from "unist-util-visit"
55
import rehypeKatex from "rehype-katex"
66
import remarkMath from "remark-math"
7+
import remarkGfm from "remark-gfm"
78

89
import { vscode } from "@src/utils/vscode"
910
import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -191,117 +192,154 @@ const StyledMarkdown = styled.div`
191192
text-decoration-color: var(--vscode-textLink-activeForeground);
192193
}
193194
}
195+
196+
/* Table styles for remark-gfm */
197+
table {
198+
width: 100%;
199+
border-collapse: collapse;
200+
margin: 1em 0;
201+
overflow-x: auto;
202+
display: block;
203+
}
204+
205+
th,
206+
td {
207+
border: 1px solid var(--vscode-panel-border);
208+
padding: 8px 12px;
209+
text-align: left;
210+
}
211+
212+
th {
213+
background-color: var(--vscode-editor-background);
214+
font-weight: 600;
215+
color: var(--vscode-foreground);
216+
}
217+
218+
tr:nth-child(even) {
219+
background-color: var(--vscode-editor-inactiveSelectionBackground);
220+
}
221+
222+
tr:hover {
223+
background-color: var(--vscode-list-hoverBackground);
224+
}
194225
`
195226

196227
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
197-
const { theme } = useExtensionState()
198-
const [reactContent, setMarkdown] = useRemark({
199-
remarkPlugins: [
200-
remarkUrlToLink,
201-
remarkMath,
202-
() => {
203-
return (tree) => {
204-
visit(tree, "code", (node: any) => {
205-
if (!node.lang) {
206-
node.lang = "text"
207-
} else if (node.lang.includes(".")) {
208-
node.lang = node.lang.split(".").slice(-1)[0]
209-
}
210-
})
228+
const { theme: _theme } = useExtensionState()
229+
230+
const components = {
231+
a: ({ href, children, ...props }: any) => {
232+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
233+
// Only process file:// protocol or local file paths
234+
const isLocalPath = href?.startsWith("file://") || href?.startsWith("/") || !href?.includes("://")
235+
236+
if (!isLocalPath) {
237+
return
211238
}
212-
},
213-
],
214-
rehypePlugins: [rehypeKatex as any],
215-
rehypeReactOptions: {
216-
components: {
217-
a: ({ href, children, ...props }: any) => {
218-
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
219-
// Only process file:// protocol or local file paths
220-
const isLocalPath = href.startsWith("file://") || href.startsWith("/") || !href.includes("://")
221-
222-
if (!isLocalPath) {
223-
return
224-
}
225239

226-
e.preventDefault()
240+
e.preventDefault()
227241

228-
// Handle absolute vs project-relative paths
229-
let filePath = href.replace("file://", "")
242+
// Handle absolute vs project-relative paths
243+
let filePath = href.replace("file://", "")
230244

231-
// Extract line number if present
232-
const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
233-
let values = undefined
234-
if (match) {
235-
filePath = match[1]
236-
values = { line: parseInt(match[2]) }
237-
}
245+
// Extract line number if present
246+
const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
247+
let values = undefined
248+
if (match) {
249+
filePath = match[1]
250+
values = { line: parseInt(match[2]) }
251+
}
238252

239-
// Add ./ prefix if needed
240-
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
241-
filePath = "./" + filePath
242-
}
253+
// Add ./ prefix if needed
254+
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
255+
filePath = "./" + filePath
256+
}
243257

244-
vscode.postMessage({
245-
type: "openFile",
246-
text: filePath,
247-
values,
248-
})
249-
}
258+
vscode.postMessage({
259+
type: "openFile",
260+
text: filePath,
261+
values,
262+
})
263+
}
250264

251-
return (
252-
<a {...props} href={href} onClick={handleClick}>
253-
{children}
254-
</a>
255-
)
256-
},
257-
pre: ({ node: _, children }: any) => {
258-
// Check for Mermaid diagrams first
259-
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
260-
const child = children[0] as React.ReactElement<{ className?: string }>
261-
262-
if (child.props?.className?.includes("language-mermaid")) {
263-
return child
264-
}
265-
}
265+
return (
266+
<a {...props} href={href} onClick={handleClick}>
267+
{children}
268+
</a>
269+
)
270+
},
271+
pre: ({ children, ..._props }: any) => {
272+
// The structure from react-markdown v9 is: pre > code > text
273+
const codeEl = children as React.ReactElement
266274

267-
// For all other code blocks, use CodeBlock with copy button
268-
const codeNode = children?.[0]
275+
if (!codeEl || !codeEl.props) {
276+
return <pre>{children}</pre>
277+
}
269278

270-
if (!codeNode?.props?.children) {
271-
return null
272-
}
279+
const { className = "", children: codeChildren } = codeEl.props
273280

274-
const language =
275-
(Array.isArray(codeNode.props?.className)
276-
? codeNode.props.className
277-
: [codeNode.props?.className]
278-
).map((c: string) => c?.replace("language-", ""))[0] || "javascript"
279-
280-
const rawText = codeNode.props.children[0] || ""
281-
return <CodeBlock source={rawText} language={language} />
282-
},
283-
code: (props: any) => {
284-
const className = props.className || ""
285-
286-
if (className.includes("language-mermaid")) {
287-
const codeText = String(props.children || "")
288-
return <MermaidBlock code={codeText} />
289-
}
281+
// Get the actual code text
282+
let codeString = ""
283+
if (typeof codeChildren === "string") {
284+
codeString = codeChildren
285+
} else if (Array.isArray(codeChildren)) {
286+
codeString = codeChildren.filter((child) => typeof child === "string").join("")
287+
}
290288

291-
return <code {...props} />
292-
},
293-
},
294-
},
295-
})
289+
// Handle mermaid diagrams
290+
if (className.includes("language-mermaid")) {
291+
return (
292+
<div style={{ margin: "1em 0" }}>
293+
<MermaidBlock code={codeString} />
294+
</div>
295+
)
296+
}
296297

297-
useEffect(() => {
298-
setMarkdown(markdown || "")
299-
}, [markdown, setMarkdown, theme])
298+
// Extract language from className
299+
const match = /language-(\w+)/.exec(className)
300+
const language = match ? match[1] : "text"
301+
302+
// Wrap CodeBlock in a div to ensure proper separation
303+
return (
304+
<div style={{ margin: "1em 0" }}>
305+
<CodeBlock source={codeString} language={language} />
306+
</div>
307+
)
308+
},
309+
code: ({ children, className, ...props }: any) => {
310+
// This handles inline code
311+
return (
312+
<code className={className} {...props}>
313+
{children}
314+
</code>
315+
)
316+
},
317+
}
300318

301319
return (
302-
<div style={{}}>
303-
<StyledMarkdown>{reactContent}</StyledMarkdown>
304-
</div>
320+
<StyledMarkdown>
321+
<ReactMarkdown
322+
remarkPlugins={[
323+
remarkGfm,
324+
remarkUrlToLink,
325+
remarkMath,
326+
() => {
327+
return (tree: any) => {
328+
visit(tree, "code", (node: any) => {
329+
if (!node.lang) {
330+
node.lang = "text"
331+
} else if (node.lang.includes(".")) {
332+
node.lang = node.lang.split(".").slice(-1)[0]
333+
}
334+
})
335+
}
336+
},
337+
]}
338+
rehypePlugins={[rehypeKatex as any]}
339+
components={components}>
340+
{markdown || ""}
341+
</ReactMarkdown>
342+
</StyledMarkdown>
305343
)
306344
})
307345

0 commit comments

Comments
 (0)