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 "
33import styled from "styled-components"
44import { visit } from "unist-util-visit"
55import rehypeKatex from "rehype-katex"
66import remarkMath from "remark-math"
7+ import remarkGfm from "remark-gfm"
78
89import { vscode } from "@src/utils/vscode"
910import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -15,68 +16,6 @@ interface MarkdownBlockProps {
1516 markdown ?: string
1617}
1718
18- /**
19- * Custom remark plugin that converts plain URLs in text into clickable links
20- *
21- * The original bug: We were converting text nodes into paragraph nodes,
22- * which broke the markdown structure because text nodes should remain as text nodes
23- * within their parent elements (like paragraphs, list items, etc.).
24- * This caused the entire content to disappear because the structure became invalid.
25- */
26- const remarkUrlToLink = ( ) => {
27- return ( tree : any ) => {
28- // Visit all "text" nodes in the markdown AST (Abstract Syntax Tree)
29- visit ( tree , "text" , ( node : any , index , parent ) => {
30- const urlRegex = / h t t p s ? : \/ \/ [ ^ \s < > ) " ] + / g
31- const matches = node . value . match ( urlRegex )
32-
33- if ( ! matches || ! parent ) {
34- return
35- }
36-
37- const parts = node . value . split ( urlRegex )
38- const children : any [ ] = [ ]
39- const cleanedMatches = matches . map ( ( url : string ) => url . replace ( / [ . , ; : ! ? ' " ] + $ / , "" ) )
40-
41- parts . forEach ( ( part : string , i : number ) => {
42- if ( part ) {
43- children . push ( { type : "text" , value : part } )
44- }
45-
46- if ( cleanedMatches [ i ] ) {
47- const originalUrl = matches [ i ]
48- const cleanedUrl = cleanedMatches [ i ]
49- const removedPunctuation = originalUrl . substring ( cleanedUrl . length )
50-
51- // Create a proper link node with all required properties
52- children . push ( {
53- type : "link" ,
54- url : cleanedUrl ,
55- title : null ,
56- children : [ { type : "text" , value : cleanedUrl } ] ,
57- data : {
58- hProperties : {
59- href : cleanedUrl ,
60- } ,
61- } ,
62- } )
63-
64- if ( removedPunctuation ) {
65- children . push ( { type : "text" , value : removedPunctuation } )
66- }
67- }
68- } )
69-
70- // Replace the original text node with our new nodes in the parent's children array.
71- // This preserves the document structure while adding our links.
72- parent . children . splice ( index ! , 1 , ...children )
73-
74- // Return SKIP to prevent visiting the newly created nodes
75- return [ "skip" , index ! + children . length ]
76- } )
77- }
78- }
79-
8019const StyledMarkdown = styled . div `
8120 code:not(pre > code) {
8221 font-family: var(--vscode-editor-font-family, monospace);
@@ -191,117 +130,153 @@ const StyledMarkdown = styled.div`
191130 text-decoration-color: var(--vscode-textLink-activeForeground);
192131 }
193132 }
133+
134+ /* Table styles for remark-gfm */
135+ table {
136+ width: 100%;
137+ border-collapse: collapse;
138+ margin: 1em 0;
139+ overflow-x: auto;
140+ display: block;
141+ }
142+
143+ th,
144+ td {
145+ border: 1px solid var(--vscode-panel-border);
146+ padding: 8px 12px;
147+ text-align: left;
148+ }
149+
150+ th {
151+ background-color: var(--vscode-editor-background);
152+ font-weight: 600;
153+ color: var(--vscode-foreground);
154+ }
155+
156+ tr:nth-child(even) {
157+ background-color: var(--vscode-editor-inactiveSelectionBackground);
158+ }
159+
160+ tr:hover {
161+ background-color: var(--vscode-list-hoverBackground);
162+ }
194163`
195164
196165const 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- } )
166+ const { theme : _theme } = useExtensionState ( )
167+
168+ const components = {
169+ a : ( { href, children, ...props } : any ) => {
170+ const handleClick = ( e : React . MouseEvent < HTMLAnchorElement > ) => {
171+ // Only process file:// protocol or local file paths
172+ const isLocalPath = href ?. startsWith ( "file://" ) || href ?. startsWith ( "/" ) || ! href ?. includes ( "://" )
173+
174+ if ( ! isLocalPath ) {
175+ return
211176 }
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- }
225177
226- e . preventDefault ( )
178+ e . preventDefault ( )
227179
228- // Handle absolute vs project-relative paths
229- let filePath = href . replace ( "file://" , "" )
180+ // Handle absolute vs project-relative paths
181+ let filePath = href . replace ( "file://" , "" )
230182
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- }
183+ // Extract line number if present
184+ const match = filePath . match ( / ( .* ) : ( \d + ) ( - \d + ) ? $ / )
185+ let values = undefined
186+ if ( match ) {
187+ filePath = match [ 1 ]
188+ values = { line : parseInt ( match [ 2 ] ) }
189+ }
238190
239- // Add ./ prefix if needed
240- if ( ! filePath . startsWith ( "/" ) && ! filePath . startsWith ( "./" ) ) {
241- filePath = "./" + filePath
242- }
191+ // Add ./ prefix if needed
192+ if ( ! filePath . startsWith ( "/" ) && ! filePath . startsWith ( "./" ) ) {
193+ filePath = "./" + filePath
194+ }
243195
244- vscode . postMessage ( {
245- type : "openFile" ,
246- text : filePath ,
247- values,
248- } )
249- }
250-
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- }
266-
267- // For all other code blocks, use CodeBlock with copy button
268- const codeNode = children ?. [ 0 ]
269-
270- if ( ! codeNode ?. props ?. children ) {
271- return null
272- }
273-
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- }
290-
291- return < code { ...props } />
292- } ,
293- } ,
196+ vscode . postMessage ( {
197+ type : "openFile" ,
198+ text : filePath ,
199+ values,
200+ } )
201+ }
202+
203+ return (
204+ < a { ...props } href = { href } onClick = { handleClick } >
205+ { children }
206+ </ a >
207+ )
294208 } ,
295- } )
209+ pre : ( { children, ..._props } : any ) => {
210+ // The structure from react-markdown v9 is: pre > code > text
211+ const codeEl = children as React . ReactElement
296212
297- useEffect ( ( ) => {
298- setMarkdown ( markdown || "" )
299- } , [ markdown , setMarkdown , theme ] )
213+ if ( ! codeEl || ! codeEl . props ) {
214+ return < pre > { children } </ pre >
215+ }
216+
217+ const { className = "" , children : codeChildren } = codeEl . props
218+
219+ // Get the actual code text
220+ let codeString = ""
221+ if ( typeof codeChildren === "string" ) {
222+ codeString = codeChildren
223+ } else if ( Array . isArray ( codeChildren ) ) {
224+ codeString = codeChildren . filter ( ( child ) => typeof child === "string" ) . join ( "" )
225+ }
226+
227+ // Handle mermaid diagrams
228+ if ( className . includes ( "language-mermaid" ) ) {
229+ return (
230+ < div style = { { margin : "1em 0" } } >
231+ < MermaidBlock code = { codeString } />
232+ </ div >
233+ )
234+ }
235+
236+ // Extract language from className
237+ const match = / l a n g u a g e - ( \w + ) / . exec ( className )
238+ const language = match ? match [ 1 ] : "text"
239+
240+ // Wrap CodeBlock in a div to ensure proper separation
241+ return (
242+ < div style = { { margin : "1em 0" } } >
243+ < CodeBlock source = { codeString } language = { language } />
244+ </ div >
245+ )
246+ } ,
247+ code : ( { children, className, ...props } : any ) => {
248+ // This handles inline code
249+ return (
250+ < code className = { className } { ...props } >
251+ { children }
252+ </ code >
253+ )
254+ } ,
255+ }
300256
301257 return (
302- < div style = { { } } >
303- < StyledMarkdown > { reactContent } </ StyledMarkdown >
304- </ div >
258+ < StyledMarkdown >
259+ < ReactMarkdown
260+ remarkPlugins = { [
261+ remarkGfm ,
262+ remarkMath ,
263+ ( ) => {
264+ return ( tree : any ) => {
265+ visit ( tree , "code" , ( node : any ) => {
266+ if ( ! node . lang ) {
267+ node . lang = "text"
268+ } else if ( node . lang . includes ( "." ) ) {
269+ node . lang = node . lang . split ( "." ) . slice ( - 1 ) [ 0 ]
270+ }
271+ } )
272+ }
273+ } ,
274+ ] }
275+ rehypePlugins = { [ rehypeKatex as any ] }
276+ components = { components } >
277+ { markdown || "" }
278+ </ ReactMarkdown >
279+ </ StyledMarkdown >
305280 )
306281} )
307282
0 commit comments