@@ -10,7 +10,7 @@ function parseBlockLines(block: string): ReactNode {
1010 const lines = block . split ( "\n" ) . map ( ( l ) => l . trimEnd ( ) ) ;
1111 const nonEmptyLines = lines . filter ( Boolean ) ;
1212
13- const unorderedListLines = nonEmptyLines . filter ( ( l ) => / ^ [ - * ] \s + \S / . test ( l . trim ( ) ) ) ;
13+ const unorderedListLines = nonEmptyLines . filter ( ( l ) => / ^ [ - * + ] \s + \S / . test ( l . trim ( ) ) ) ;
1414 const isUnorderedListBlock =
1515 unorderedListLines . length > 0 && unorderedListLines . length === nonEmptyLines . length ;
1616 const orderedListLines = nonEmptyLines . filter ( ( l ) => / ^ \d + \. \s + \S / . test ( l . trim ( ) ) ) ;
@@ -23,7 +23,7 @@ function parseBlockLines(block: string): ReactNode {
2323 { lines
2424 . filter ( Boolean )
2525 . map ( ( line , i ) => (
26- < li key = { i } > { renderInlineFormatting ( line . trim ( ) . replace ( / ^ \s * [ - * ] \s + / , "" ) ) } </ li >
26+ < li key = { i } > { renderInlineFormatting ( line . trim ( ) . replace ( / ^ \s * [ - * + ] \s + / , "" ) ) } </ li >
2727 ) ) }
2828 </ ul >
2929 ) ;
@@ -41,29 +41,160 @@ function parseBlockLines(block: string): ReactNode {
4141 ) ;
4242 }
4343
44- const rendered : ReactNode [ ] = [ ] ;
45- lines . forEach ( ( line , i ) => {
46- rendered . push ( < span key = { `t-${ i } ` } > { renderInlineFormatting ( line ) } </ span > ) ;
47- if ( i < lines . length - 1 ) {
48- rendered . push ( < br key = { `br-${ i } ` } /> ) ;
44+ const segments : ReactNode [ ] = [ ] ;
45+ let i = 0 ;
46+
47+ while ( i < lines . length ) {
48+ const line = lines [ i ] . trim ( ) ;
49+
50+ if ( ! line ) {
51+ i += 1 ;
52+ continue ;
4953 }
50- } ) ;
5154
52- return (
53- < p className = "bot-message-para" >
54- { rendered }
55- </ p >
56- ) ;
55+ if ( / ^ # { 1 , 6 } \s + \S / . test ( line ) ) {
56+ const level = Math . min ( 6 , Math . max ( 1 , line . match ( / ^ # + / ) ?. [ 0 ] . length ?? 1 ) ) ;
57+ const headingText = line . replace ( / ^ # { 1 , 6 } \s + / , "" ) ;
58+ if ( level === 1 ) segments . push ( < h1 key = { `h1-${ i } ` } > { renderInlineFormatting ( headingText ) } </ h1 > ) ;
59+ if ( level === 2 ) segments . push ( < h2 key = { `h2-${ i } ` } > { renderInlineFormatting ( headingText ) } </ h2 > ) ;
60+ if ( level === 3 ) segments . push ( < h3 key = { `h3-${ i } ` } > { renderInlineFormatting ( headingText ) } </ h3 > ) ;
61+ if ( level === 4 ) segments . push ( < h4 key = { `h4-${ i } ` } > { renderInlineFormatting ( headingText ) } </ h4 > ) ;
62+ if ( level === 5 ) segments . push ( < h5 key = { `h5-${ i } ` } > { renderInlineFormatting ( headingText ) } </ h5 > ) ;
63+ if ( level === 6 ) segments . push ( < h6 key = { `h6-${ i } ` } > { renderInlineFormatting ( headingText ) } </ h6 > ) ;
64+ i += 1 ;
65+ continue ;
66+ }
67+
68+ if ( / ^ > \s * \S / . test ( line ) ) {
69+ const quoteLines : string [ ] = [ ] ;
70+ while ( i < lines . length && / ^ > \s * \S / . test ( lines [ i ] . trim ( ) ) ) {
71+ quoteLines . push ( lines [ i ] . trim ( ) . replace ( / ^ > \s ? / , "" ) ) ;
72+ i += 1 ;
73+ }
74+ segments . push (
75+ < blockquote key = { `q-${ i } ` } className = "bot-message-para" >
76+ { quoteLines . map ( ( qLine , idx ) => (
77+ < span key = { `qline-${ idx } ` } >
78+ { renderInlineFormatting ( qLine ) }
79+ { idx < quoteLines . length - 1 ? < br /> : null }
80+ </ span >
81+ ) ) }
82+ </ blockquote > ,
83+ ) ;
84+ continue ;
85+ }
86+
87+ if ( / ^ [ - * + ] \s + \S / . test ( line ) ) {
88+ const items : string [ ] = [ ] ;
89+ while ( i < lines . length && / ^ [ - * + ] \s + \S / . test ( lines [ i ] . trim ( ) ) ) {
90+ items . push ( lines [ i ] . trim ( ) . replace ( / ^ [ - * + ] \s + / , "" ) ) ;
91+ i += 1 ;
92+ }
93+ segments . push (
94+ < ul key = { `ul-${ i } ` } className = "bot-message-list" >
95+ { items . map ( ( item , idx ) => (
96+ < li key = { idx } > { renderInlineFormatting ( item ) } </ li >
97+ ) ) }
98+ </ ul > ,
99+ ) ;
100+ continue ;
101+ }
102+
103+ if ( / ^ \d + \. \s + \S / . test ( line ) ) {
104+ const items : string [ ] = [ ] ;
105+ while ( i < lines . length && / ^ \d + \. \s + \S / . test ( lines [ i ] . trim ( ) ) ) {
106+ items . push ( lines [ i ] . trim ( ) . replace ( / ^ \d + \. \s + / , "" ) ) ;
107+ i += 1 ;
108+ }
109+ segments . push (
110+ < ol key = { `ol-${ i } ` } className = "bot-message-list" >
111+ { items . map ( ( item , idx ) => (
112+ < li key = { idx } > { renderInlineFormatting ( item ) } </ li >
113+ ) ) }
114+ </ ol > ,
115+ ) ;
116+ continue ;
117+ }
118+
119+ const paragraphLines : string [ ] = [ ] ;
120+ while ( i < lines . length ) {
121+ const current = lines [ i ] . trim ( ) ;
122+ if (
123+ ! current ||
124+ / ^ [ - * + ] \s + \S / . test ( current ) ||
125+ / ^ \d + \. \s + \S / . test ( current ) ||
126+ / ^ # { 1 , 6 } \s + \S / . test ( current ) ||
127+ / ^ > \s * \S / . test ( current )
128+ ) {
129+ break ;
130+ }
131+ paragraphLines . push ( lines [ i ] ) ;
132+ i += 1 ;
133+ }
134+
135+ if ( paragraphLines . length > 0 ) {
136+ const rendered : ReactNode [ ] = [ ] ;
137+ paragraphLines . forEach ( ( pLine , idx ) => {
138+ rendered . push ( < span key = { `t-${ idx } ` } > { renderInlineFormatting ( pLine ) } </ span > ) ;
139+ if ( idx < paragraphLines . length - 1 ) {
140+ rendered . push ( < br key = { `br-${ idx } ` } /> ) ;
141+ }
142+ } ) ;
143+ segments . push (
144+ < p key = { `p-${ i } ` } className = "bot-message-para" >
145+ { rendered }
146+ </ p > ,
147+ ) ;
148+ }
149+ }
150+
151+ return < > { segments } </ > ;
57152}
58153
59154function renderInlineFormatting ( text : string ) : ReactNode {
60- const parts = text . split ( / ( \* \* [ ^ * ] + \* \* ) / g) ;
61- return parts . map ( ( part , i ) => {
62- if ( part . startsWith ( "**" ) && part . endsWith ( "**" ) && part . length > 4 ) {
63- return < strong key = { `b-${ i } ` } > { part . slice ( 2 , - 2 ) } </ strong > ;
155+ const tokens = text . split (
156+ / ( ` [ ^ ` \n ] + ` | \* \* [ ^ * \n ] + \* \* | _ _ [ ^ _ \n ] + _ _ | \* [ ^ * \n ] + \* | _ [ ^ _ \n ] + _ | (?: \[ [ ^ \] \n ] + \] \( [ ^ ) \n ] + \) ) | (?: h t t p s ? : \/ \/ [ ^ \s ] + ) ) / g,
157+ ) ;
158+
159+ return tokens . filter ( Boolean ) . map ( ( token , i ) => {
160+ const mdLink = token . match ( / ^ \[ ( [ ^ \] \n ] + ) \] \( ( [ ^ ) \s ] + ) \) $ / ) ;
161+ if ( mdLink ) {
162+ return (
163+ < a key = { `ml-${ i } ` } href = { mdLink [ 2 ] } target = "_blank" rel = "noopener noreferrer" >
164+ { mdLink [ 1 ] }
165+ </ a >
166+ ) ;
167+ }
168+
169+ if ( / ^ h t t p s ? : \/ \/ [ ^ \s ] + $ / . test ( token ) ) {
170+ return (
171+ < a key = { `l-${ i } ` } href = { token } target = "_blank" rel = "noopener noreferrer" >
172+ { token }
173+ </ a >
174+ ) ;
175+ }
176+
177+ if ( token . startsWith ( "**" ) && token . endsWith ( "**" ) && token . length > 4 ) {
178+ return < strong key = { `b-${ i } ` } > { token . slice ( 2 , - 2 ) } </ strong > ;
179+ }
180+
181+ if ( token . startsWith ( "__" ) && token . endsWith ( "__" ) && token . length > 4 ) {
182+ return < strong key = { `u-${ i } ` } > { token . slice ( 2 , - 2 ) } </ strong > ;
183+ }
184+
185+ if ( token . startsWith ( "*" ) && token . endsWith ( "*" ) && token . length > 2 ) {
186+ return < em key = { `em-${ i } ` } > { token . slice ( 1 , - 1 ) } </ em > ;
187+ }
188+
189+ if ( token . startsWith ( "_" ) && token . endsWith ( "_" ) && token . length > 2 ) {
190+ return < em key = { `emi-${ i } ` } > { token . slice ( 1 , - 1 ) } </ em > ;
191+ }
192+
193+ if ( token . startsWith ( "`" ) && token . endsWith ( "`" ) && token . length > 2 ) {
194+ return < code key = { `c-${ i } ` } > { token . slice ( 1 , - 1 ) } </ code > ;
64195 }
65196
66- return < span key = { `t-${ i } ` } > { part } </ span > ;
197+ return < span key = { `t-${ i } ` } > { token } </ span > ;
67198 } ) ;
68199}
69200
0 commit comments