1- import  React ,  {  type  ReactNode  }  from  'react' 
21import  remarkParse  from  'remark-parse' 
32import  {  unified  }  from  'unified' 
43
54import  {  logger  }  from  './logger' 
65
76import  type  { 
8-   Root , 
9-   Content , 
10-   Text , 
11-   Emphasis , 
12-   Strong , 
13-   InlineCode , 
7+   Blockquote , 
148  Code , 
9+   Content , 
1510  Heading , 
11+   InlineCode , 
1612  List , 
1713  ListItem , 
18-   Blockquote , 
14+   Paragraph , 
15+   Root , 
16+   Text , 
1917}  from  'mdast' 
2018
2119export  interface  MarkdownPalette  { 
@@ -82,170 +80,80 @@ const resolvePalette = (
8280
8381const  processor  =  unified ( ) . use ( remarkParse ) 
8482
85- // Render inline content - this is what gets placed INSIDE the <text> wrapper 
86- function  renderInlineContent ( 
87-   node : Content , 
88-   key : string  |  number  |  undefined , 
89-   palette : MarkdownPalette , 
90- ) : ReactNode  { 
83+ function  nodeToPlainText ( node : Content  |  Root ) : string  { 
9184  switch  ( node . type )  { 
92-     case  'text ' :
93-       return  ( node  as  Text ) . value 
85+     case  'root ' :
86+       return  ( node  as  Root ) . children . map ( nodeToPlainText ) . join ( '' ) 
9487
95-     case  'emphasis ' :
88+     case  'paragraph ' :
9689      return  ( 
97-         < em  key = { key } > 
98-           { ( node  as  Emphasis ) . children . map ( ( child ,  index )  => 
99-             renderInlineContent ( child ,  index ,  palette ) , 
100-           ) } 
101-         </ em > 
90+         ( node  as  Paragraph ) . children . map ( nodeToPlainText ) . join ( '' )  +  '\n\n' 
10291      ) 
10392
104-     case  'strong' :
105-       return  ( 
106-         < strong  key = { key } > 
107-           { ( node  as  Strong ) . children . map ( ( child ,  index )  => 
108-             renderInlineContent ( child ,  index ,  palette ) , 
109-           ) } 
110-         </ strong > 
111-       ) 
93+     case  'text' :
94+       return  ( node  as  Text ) . value 
11295
11396    case  'inlineCode' :
114-       return  ( 
115-         < span  key = { key }  fg = { palette . inlineCodeFg } > 
116-           { ( node  as  InlineCode ) . value } 
117-         </ span > 
118-       ) 
97+       return  `\`${ ( node  as  InlineCode ) . value }  
11998
120-     case  'break' :
121-       return  '\n' 
99+     case  'heading' : { 
100+       const  heading  =  node  as  Heading 
101+       const  prefix  =  '#' . repeat ( Math . max ( 1 ,  Math . min ( heading . depth ,  6 ) ) ) 
102+       const  content  =  heading . children . map ( nodeToPlainText ) . join ( '' ) 
103+       return  `${ prefix } ${ content }  
104+     } 
122105
123-     default :
124-       return  null 
125-   } 
126- } 
106+     case  'list' : { 
107+       const  list  =  node  as  List 
108+       return  ( 
109+         list . children 
110+           . map ( ( item ,  idx )  =>  { 
111+             const  marker  =  list . ordered  ? `${ idx  +  1 }   : '- ' 
112+             const  text  =  ( item  as  ListItem ) . children 
113+               . map ( nodeToPlainText ) 
114+               . join ( '' ) 
115+               . trimEnd ( ) 
116+             return  marker  +  text 
117+           } ) 
118+           . join ( '\n' )  +  '\n\n' 
119+       ) 
120+     } 
127121
128- // Convert markdown AST to inline JSX elements 
129- function  markdownToInline ( 
130-   node : Content  |  Root , 
131-   palette : MarkdownPalette , 
132- ) : ReactNode [ ]  { 
133-   const  result : ReactNode [ ]  =  [ ] 
122+     case  'listItem' : { 
123+       const  listItem  =  node  as  ListItem 
124+       return  listItem . children . map ( nodeToPlainText ) . join ( '' ) 
125+     } 
134126
135-   switch  ( node . type )  { 
136-     case  'root' :
137-       node . children . forEach ( ( child ,  index )  =>  { 
138-         result . push ( ...markdownToInline ( child ,  palette ) ) 
139-         // Add spacing between blocks 
140-         if  ( index  <  node . children . length  -  1 )  { 
141-           result . push ( '\n' ) 
142-         } 
143-       } ) 
144-       break 
127+     case  'blockquote' : { 
128+       const  blockquote  =  node  as  Blockquote 
129+       const  content  =  blockquote . children 
130+         . map ( ( child )  =>  nodeToPlainText ( child ) . replace ( / ^ / gm,  '> ' ) ) 
131+         . join ( '' ) 
132+       return  `${ content }  
133+     } 
145134
146-     case  'paragraph' :
147-       node . children . forEach ( ( child )  =>  { 
148-         result . push ( renderInlineContent ( child ,  undefined ,  palette ) ) 
149-       } ) 
150-       result . push ( '\n' ) 
151-       break 
152- 
153-     case  'heading' :
154-       const  headingNode  =  node  as  Heading 
155-       const  depth  =  headingNode . depth 
156-       const  headingPrefix  =  '#' . repeat ( depth )  +  ' ' 
157-       const  headingColor  = 
158-         palette . headingFg [ depth ]  ??  palette . headingFg [ 2 ]  ??  'white' 
159- 
160-       result . push ( 
161-         < strong  fg = { headingColor } > 
162-           { headingPrefix } 
163-           { headingNode . children . map ( ( child )  => 
164-             renderInlineContent ( child ,  undefined ,  palette ) , 
165-           ) } 
166-         </ strong > , 
167-       ) 
168-       result . push ( '\n' ) 
169-       break 
170- 
171-     case  'list' :
172-       const  listNode  =  node  as  List 
173-       listNode . children . forEach ( ( item ,  index )  =>  { 
174-         const  bullet  =  listNode . ordered  ? `${ index  +  1 }   : '• ' 
175-         result . push ( < span  fg = { palette . listBulletFg } > { bullet } </ span > ) 
176- 
177-         // Extract inline content from list item paragraphs 
178-         const  listItem  =  item  as  ListItem 
179-         listItem . children . forEach ( ( child )  =>  { 
180-           if  ( child . type  ===  'paragraph' )  { 
181-             child . children . forEach ( ( inlineChild )  =>  { 
182-               result . push ( renderInlineContent ( inlineChild ,  undefined ,  palette ) ) 
183-             } ) 
184-           } 
185-         } ) 
186-         result . push ( '\n' ) 
187-       } ) 
188-       break 
189- 
190-     case  'code' :
191-       const  codeNode  =  node  as  Code 
192-       const  codeBg  =  palette . codeBackground 
193-       const  headerLabel  =  codeNode . lang  ? `[${ codeNode . lang }   : '[code]' 
194- 
195-       result . push ( '\n' ) 
196-       result . push ( 
197-         < span  fg = { palette . codeHeaderFg }  bg = { codeBg } > 
198-           { headerLabel } 
199-         </ span > , 
200-       ) 
201-       result . push ( '\n' ) 
202-       result . push ( 
203-         < span  fg = { palette . codeTextFg }  bg = { codeBg } > 
204-           { codeNode . value } 
205-         </ span > , 
206-       ) 
207-       result . push ( '\n' ) 
208-       break 
209- 
210-     case  'blockquote' :
211-       const  blockquoteNode  =  node  as  Blockquote 
212-       result . push ( < span  fg = { palette . blockquoteBorderFg } > │ </ span > ) 
213-       result . push ( 
214-         < em  fg = { palette . blockquoteTextFg } > 
215-           { blockquoteNode . children . map ( ( child )  =>  { 
216-             if  ( child . type  ===  'paragraph' )  { 
217-               return  child . children . map ( ( inlineChild )  => 
218-                 renderInlineContent ( inlineChild ,  undefined ,  palette ) , 
219-               ) 
220-             } 
221-             return  null 
222-           } ) } 
223-         </ em > , 
224-       ) 
225-       result . push ( '\n' ) 
226-       break 
135+     case  'code' : { 
136+       const  code  =  node  as  Code 
137+       const  header  =  code . lang  ? `\`\`\`${ code . lang }   : '```\n' 
138+       return  `${ header } ${ code . value }  
139+     } 
227140
228-     case  'thematicBreak' :
229-       result . push ( < span  fg = { palette . dividerFg } > { '─' . repeat ( 40 ) } </ span > ) 
230-       result . push ( '\n' ) 
231-       break 
141+     default :
142+       return  '' 
232143  } 
233- 
234-   return  result 
235144} 
236145
237- // Main function - returns inline JSX elements (no <text> wrapper) 
238146export  function  renderMarkdown ( 
239147  markdown : string , 
240148  options : MarkdownRenderOptions  =  { } , 
241- ) : ReactNode  { 
149+ ) : string  { 
242150  try  { 
243-     const  ast  =  processor . parse ( markdown ) 
244151    const  palette  =  resolvePalette ( options . palette ) 
245-     const   inlineElements   =   markdownToInline ( ast ,   palette ) 
152+     void   palette   // Keep signature compatibility for future color styling 
246153
247-     // Return a fragment containing all inline elements 
248-     return  < > { inlineElements } </ > 
154+     const  ast  =  processor . parse ( markdown ) 
155+     const  text  =  nodeToPlainText ( ast ) . replace ( / \s + $ / g,  '' ) 
156+     return  text 
249157  }  catch  ( error )  { 
250158    logger . error ( error ,  'Failed to parse markdown' ) 
251159    return  markdown 
@@ -268,7 +176,7 @@ export function hasIncompleteCodeFence(content: string): boolean {
268176export  function  renderStreamingMarkdown ( 
269177  content : string , 
270178  options : MarkdownRenderOptions  =  { } , 
271- ) : ReactNode  { 
179+ ) : string  { 
272180  if  ( ! hasMarkdown ( content ) )  { 
273181    return  content 
274182  } 
@@ -285,19 +193,15 @@ export function renderStreamingMarkdown(
285193  const  completeSection  =  content . slice ( 0 ,  lastFenceIndex ) 
286194  const  pendingSection  =  content . slice ( lastFenceIndex ) 
287195
288-   const  nodes :  ReactNode [ ]  =  [ ] 
196+   const  parts :  string [ ]  =  [ ] 
289197
290198  if  ( completeSection . length  >  0 )  { 
291-     nodes . push ( renderMarkdown ( completeSection ,  options ) ) 
199+     parts . push ( renderMarkdown ( completeSection ,  options ) ) 
292200  } 
293201
294202  if  ( pendingSection . length  >  0 )  { 
295-     nodes . push ( pendingSection ) 
296-   } 
297- 
298-   if  ( nodes . length  ===  1 )  { 
299-     return  nodes [ 0 ] 
203+     parts . push ( pendingSection ) 
300204  } 
301205
302-   return  React . createElement ( React . Fragment ,   null ,  ... nodes ) 
206+   return  parts . join ( '' ) 
303207} 
0 commit comments