@@ -6,22 +6,31 @@ const singleAsteriskPattern = /(\*)([^*]*?)$/;
66const singleUnderscorePattern = / ( _ ) ( [ ^ _ ] * ?) $ / ;
77const inlineCodePattern = / ( ` ) ( [ ^ ` ] * ?) $ / ;
88const strikethroughPattern = / ( ~ ~ ) ( [ ^ ~ ] * ?) $ / ;
9- // Removed inlineKatexPattern - no longer processing single dollar signs
10- const blockKatexPattern = / ( \$ \$ ) ( [ ^ $ ] * ?) $ / ;
119
1210// Helper function to check if we have a complete code block
1311const hasCompleteCodeBlock = ( text : string ) : boolean => {
1412 const tripleBackticks = ( text . match ( / ` ` ` / g) || [ ] ) . length ;
15- return tripleBackticks > 0 && tripleBackticks % 2 === 0 && text . includes ( '\n' ) ;
13+ return (
14+ tripleBackticks > 0 && tripleBackticks % 2 === 0 && text . includes ( '\n' )
15+ ) ;
1616} ;
1717
18- // Handles incomplete links and images by removing them if not closed
18+ // Handles incomplete links and images by preserving them with a special marker
1919const handleIncompleteLinksAndImages = ( text : string ) : string => {
2020 const linkMatch = text . match ( linkImagePattern ) ;
2121
2222 if ( linkMatch ) {
23- const startIndex = text . lastIndexOf ( linkMatch [ 1 ] ) ;
24- return text . substring ( 0 , startIndex ) ;
23+ const isImage = linkMatch [ 1 ] . startsWith ( '!' ) ;
24+
25+ // For images, we still remove them as they can't show skeleton
26+ if ( isImage ) {
27+ const startIndex = text . lastIndexOf ( linkMatch [ 1 ] ) ;
28+ return text . substring ( 0 , startIndex ) ;
29+ }
30+
31+ // For links, preserve the text and close the link with a
32+ // special placeholder URL that indicates it's incomplete
33+ return `${ text } ](streamdown:incomplete-link)` ;
2534 }
2635
2736 return text ;
@@ -33,7 +42,7 @@ const handleIncompleteBold = (text: string): string => {
3342 if ( hasCompleteCodeBlock ( text ) ) {
3443 return text ;
3544 }
36-
45+
3746 const boldMatch = text . match ( boldPattern ) ;
3847
3948 if ( boldMatch ) {
@@ -85,7 +94,10 @@ const countSingleAsterisks = (text: string): number => {
8594 }
8695 // Check if this asterisk is at the beginning of a line (with optional whitespace)
8796 const beforeAsterisk = text . substring ( lineStartIndex , index ) ;
88- if ( beforeAsterisk . trim ( ) === '' && ( nextChar === ' ' || nextChar === '\t' ) ) {
97+ if (
98+ beforeAsterisk . trim ( ) === '' &&
99+ ( nextChar === ' ' || nextChar === '\t' )
100+ ) {
89101 // This is likely a list marker, don't count it
90102 return acc ;
91103 }
@@ -103,7 +115,7 @@ const handleIncompleteSingleAsteriskItalic = (text: string): string => {
103115 if ( hasCompleteCodeBlock ( text ) ) {
104116 return text ;
105117 }
106-
118+
107119 const singleAsteriskMatch = text . match ( singleAsteriskPattern ) ;
108120
109121 if ( singleAsteriskMatch ) {
@@ -121,14 +133,14 @@ const isWithinMathBlock = (text: string, position: number): boolean => {
121133 // Count dollar signs before this position
122134 let inInlineMath = false ;
123135 let inBlockMath = false ;
124-
136+
125137 for ( let i = 0 ; i < text . length && i < position ; i ++ ) {
126138 // Skip escaped dollar signs
127139 if ( text [ i ] === '\\' && text [ i + 1 ] === '$' ) {
128140 i ++ ; // Skip the next character
129141 continue ;
130142 }
131-
143+
132144 if ( text [ i ] === '$' ) {
133145 // Check for block math ($$)
134146 if ( text [ i + 1 ] === '$' ) {
@@ -141,7 +153,7 @@ const isWithinMathBlock = (text: string, position: number): boolean => {
141153 }
142154 }
143155 }
144-
156+
145157 return inInlineMath || inBlockMath ;
146158} ;
147159
@@ -173,7 +185,7 @@ const handleIncompleteSingleUnderscoreItalic = (text: string): string => {
173185 if ( hasCompleteCodeBlock ( text ) ) {
174186 return text ;
175187 }
176-
188+
177189 const singleUnderscoreMatch = text . match ( singleUnderscorePattern ) ;
178190
179191 if ( singleUnderscoreMatch ) {
@@ -221,17 +233,21 @@ const handleIncompleteInlineCode = (text: string): string => {
221233 // Already complete inline triple backticks
222234 return text ;
223235 }
224-
236+
225237 // Check if we're inside a multi-line code block (complete or incomplete)
226238 const allTripleBackticks = ( text . match ( / ` ` ` / g) || [ ] ) . length ;
227239 const insideIncompleteCodeBlock = allTripleBackticks % 2 === 1 ;
228-
240+
229241 // Don't modify text if we have complete multi-line code blocks (even pairs of ```)
230- if ( allTripleBackticks > 0 && allTripleBackticks % 2 === 0 && text . includes ( '\n' ) ) {
242+ if (
243+ allTripleBackticks > 0 &&
244+ allTripleBackticks % 2 === 0 &&
245+ text . includes ( '\n' )
246+ ) {
231247 // We have complete multi-line code blocks, don't add any backticks
232248 return text ;
233249 }
234-
250+
235251 // Special case: if text ends with ```\n (triple backticks followed by newline)
236252 // This is actually a complete code block, not incomplete
237253 if ( text . endsWith ( '```\n' ) || text . endsWith ( '```' ) ) {
@@ -289,40 +305,32 @@ const countSingleDollarSigns = (text: string): number => {
289305const handleIncompleteBlockKatex = ( text : string ) : string => {
290306 // Count all $$ pairs in the text
291307 const dollarPairs = ( text . match ( / \$ \$ / g) || [ ] ) . length ;
292-
308+
293309 // If we have an even number of $$, the block is complete
294310 if ( dollarPairs % 2 === 0 ) {
295311 return text ;
296312 }
297-
313+
298314 // If we have an odd number, add closing $$
299315 // Check if this looks like a multi-line math block (contains newlines after opening $$)
300316 const firstDollarIndex = text . indexOf ( '$$' ) ;
301- const hasNewlineAfterStart = firstDollarIndex !== - 1 && text . indexOf ( '\n' , firstDollarIndex ) !== - 1 ;
302-
317+ const hasNewlineAfterStart =
318+ firstDollarIndex !== - 1 && text . indexOf ( '\n' , firstDollarIndex ) !== - 1 ;
319+
303320 // For multi-line blocks, add newline before closing $$ if not present
304321 if ( hasNewlineAfterStart && ! text . endsWith ( '\n' ) ) {
305322 return `${ text } \n$$` ;
306323 }
307-
324+
308325 // For inline blocks or when already ending with newline, just add $$
309326 return `${ text } $$` ;
310327} ;
311328
312- // Completes incomplete inline KaTeX formatting ($)
313- // Note: Since we've disabled single dollar math delimiters in remarkMath,
314- // we should not auto-complete single dollar signs as they're likely currency symbols
315- const handleIncompleteInlineKatex = ( text : string ) : string => {
316- // Don't process single dollar signs - they're likely currency symbols, not math
317- // Only process block math ($$) which is handled separately
318- return text ;
319- } ;
320-
321329// Counts triple asterisks that are not part of quadruple or more asterisks
322330const countTripleAsterisks = ( text : string ) : number => {
323331 let count = 0 ;
324332 const matches = text . match ( / \* + / g) || [ ] ;
325-
333+
326334 for ( const match of matches ) {
327335 // Count how many complete triple asterisks are in this sequence
328336 const asteriskCount = match . length ;
@@ -331,7 +339,7 @@ const countTripleAsterisks = (text: string): number => {
331339 count += Math . floor ( asteriskCount / 3 ) ;
332340 }
333341 }
334-
342+
335343 return count ;
336344} ;
337345
@@ -341,13 +349,13 @@ const handleIncompleteBoldItalic = (text: string): string => {
341349 if ( hasCompleteCodeBlock ( text ) ) {
342350 return text ;
343351 }
344-
352+
345353 // Don't process if text is only asterisks and has 4 or more consecutive asterisks
346354 // This prevents cases like **** from being treated as incomplete ***
347355 if ( / ^ \* { 4 , } $ / . test ( text ) ) {
348356 return text ;
349357 }
350-
358+
351359 const boldItalicMatch = text . match ( boldItalicPattern ) ;
352360
353361 if ( boldItalicMatch ) {
@@ -368,8 +376,16 @@ export const parseIncompleteMarkdown = (text: string): string => {
368376
369377 let result = text ;
370378
371- // Handle incomplete links and images first (removes content)
372- result = handleIncompleteLinksAndImages ( result ) ;
379+ // Handle incomplete links and images first
380+ const processedResult = handleIncompleteLinksAndImages ( result ) ;
381+
382+ // If we added an incomplete link marker, don't process other formatting
383+ // as the content inside the link should be preserved as-is
384+ if ( processedResult . endsWith ( '](streamdown:incomplete-link)' ) ) {
385+ return processedResult ;
386+ }
387+
388+ result = processedResult ;
373389
374390 // Handle various formatting completions
375391 // Handle triple asterisks first (most specific)
@@ -380,7 +396,7 @@ export const parseIncompleteMarkdown = (text: string): string => {
380396 result = handleIncompleteSingleUnderscoreItalic ( result ) ;
381397 result = handleIncompleteInlineCode ( result ) ;
382398 result = handleIncompleteStrikethrough ( result ) ;
383-
399+
384400 // Handle KaTeX formatting (only block math with $$)
385401 result = handleIncompleteBlockKatex ( result ) ;
386402 // Note: We don't handle inline KaTeX with single $ as they're likely currency symbols
0 commit comments