@@ -5,8 +5,14 @@ interface MarkdownProps {
55 className ?: string
66}
77
8+ // Enable soft line breaks (single newline becomes <br>)
9+ // Models often expect newlines to be presented as line breaks.
10+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
11+ const softLineBreaks = true
12+
813type Token =
914 | { type : 'text' , content : string }
15+ | { type : 'break' }
1016 | { type : 'bold' , children : Token [ ] }
1117 | { type : 'italic' , children : Token [ ] }
1218 | { type : 'code' , content : string }
@@ -152,7 +158,7 @@ function parseMarkdown(text: string): Token[] {
152158 }
153159 tokens . push ( {
154160 type : 'paragraph' ,
155- children : parseInline ( paraLines . join ( ' ' ) ) ,
161+ children : parseInline ( paraLines . join ( '\n ' ) ) ,
156162 } )
157163 }
158164
@@ -249,13 +255,18 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
249255 return [ items , i ]
250256}
251257
258+ /**
259+ * Convert inline tokens back to plain text (for alt text, link text, etc.)
260+ */
252261function tokensToString ( tokens : Token [ ] ) : string {
253262 return tokens
254263 . map ( token => {
255264 switch ( token . type ) {
256265 case 'text' :
257266 case 'code' :
258267 return token . content
268+ case 'break' :
269+ return ' '
259270 case 'bold' :
260271 case 'italic' :
261272 case 'link' :
@@ -272,6 +283,10 @@ function parseInline(text: string): Token[] {
272283 return tokens
273284}
274285
286+ /**
287+ * Recursively parse inline markdown elements.
288+ * Returns a tuple of [tokens, charsConsumed].
289+ */
275290function parseInlineRecursive ( text : string , stop ?: string ) : [ Token [ ] , number ] {
276291 const tokens : Token [ ] = [ ]
277292 let i = 0
@@ -417,6 +432,7 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
417432 let j = i
418433 while (
419434 j < text . length &&
435+ ( text [ j ] !== '\n' || ! softLineBreaks ) &&
420436 text [ j ] !== '`' &&
421437 ! ( text . startsWith ( '**' , j ) || text . startsWith ( '__' , j ) ) &&
422438 text [ j ] !== '*' &&
@@ -431,6 +447,13 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
431447 j ++
432448 }
433449
450+ // soft line break
451+ if ( softLineBreaks && text [ i ] === '\n' ) {
452+ tokens . push ( { type : 'break' } )
453+ i ++
454+ continue
455+ }
456+
434457 if ( j === i ) {
435458 // didn't consume anything – treat the single char literally
436459 tokens . push ( { type : 'text' , content : text [ i ] ?? '' } )
@@ -444,7 +467,9 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
444467 return [ tokens , i ]
445468}
446469
447- /** Split a table row, trimming outer pipes and whitespace */
470+ /**
471+ * Split a table row, trimming outer pipes and whitespace
472+ */
448473function splitTableRow ( row : string ) : string [ ] {
449474 const cells : string [ ] = [ ]
450475 let current = ''
@@ -504,12 +529,17 @@ function isClosingAsterisk(text: string, pos: number): boolean {
504529 return ! / \s / . test ( prev ) // prev char is not whitespace
505530}
506531
532+ /**
533+ * Render tokens to React elements.
534+ */
507535function renderTokens ( tokens : Token [ ] , keyPrefix = '' ) : ReactNode [ ] {
508536 return tokens . map ( ( token , index ) => {
509537 const key = `${ keyPrefix } ${ index } `
510538 switch ( token . type ) {
511539 case 'text' :
512540 return token . content
541+ case 'break' :
542+ return createElement ( 'br' , { key } )
513543 case 'code' :
514544 return createElement ( 'code' , { key } , token . content )
515545 case 'bold' :
0 commit comments