|
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" |
3 | 3 | import styled from "styled-components" |
4 | 4 | import { visit } from "unist-util-visit" |
5 | 5 | import rehypeKatex from "rehype-katex" |
6 | 6 | import remarkMath from "remark-math" |
| 7 | +import remarkGfm from "remark-gfm" |
7 | 8 |
|
8 | 9 | import { vscode } from "@src/utils/vscode" |
9 | 10 | import { useExtensionState } from "@src/context/ExtensionStateContext" |
@@ -191,117 +192,154 @@ const StyledMarkdown = styled.div` |
191 | 192 | text-decoration-color: var(--vscode-textLink-activeForeground); |
192 | 193 | } |
193 | 194 | } |
| 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 | + } |
194 | 225 | ` |
195 | 226 |
|
196 | 227 | 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 |
211 | 238 | } |
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 | | - } |
225 | 239 |
|
226 | | - e.preventDefault() |
| 240 | + e.preventDefault() |
227 | 241 |
|
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://", "") |
230 | 244 |
|
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 | + } |
238 | 252 |
|
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 | + } |
243 | 257 |
|
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 | + } |
250 | 264 |
|
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 |
266 | 274 |
|
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 | + } |
269 | 278 |
|
270 | | - if (!codeNode?.props?.children) { |
271 | | - return null |
272 | | - } |
| 279 | + const { className = "", children: codeChildren } = codeEl.props |
273 | 280 |
|
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 | + } |
290 | 288 |
|
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 | + } |
296 | 297 |
|
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 | + } |
300 | 318 |
|
301 | 319 | 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> |
305 | 343 | ) |
306 | 344 | }) |
307 | 345 |
|
|
0 commit comments