11import {
22 boldItalicPattern ,
33 boldPattern ,
4- doubleUnderscoreGlobalPattern ,
54 fourOrMoreAsterisksPattern ,
65 halfCompleteUnderscorePattern ,
76 italicPattern ,
@@ -108,12 +107,31 @@ const shouldSkipAsterisk = (
108107} ;
109108
110109// OPTIMIZATION: Counts single asterisks without split("").reduce()
111- // Counts single asterisks that are not part of double asterisks, not escaped, not list markers, and not word-internal
110+ // Counts single asterisks that are not part of double asterisks, not escaped, not list markers, not word-internal,
111+ // and not inside fenced code blocks
112112export const countSingleAsterisks = ( text : string ) : number => {
113113 let count = 0 ;
114+ let inCodeBlock = false ;
114115 const len = text . length ;
115116
116117 for ( let index = 0 ; index < len ; index += 1 ) {
118+ // Track fenced code blocks (```)
119+ if (
120+ text [ index ] === "`" &&
121+ index + 2 < len &&
122+ text [ index + 1 ] === "`" &&
123+ text [ index + 2 ] === "`"
124+ ) {
125+ inCodeBlock = ! inCodeBlock ;
126+ index += 2 ;
127+ continue ;
128+ }
129+
130+ // Skip content inside fenced code blocks
131+ if ( inCodeBlock ) {
132+ continue ;
133+ }
134+
117135 if ( text [ index ] !== "*" ) {
118136 continue ;
119137 }
@@ -171,12 +189,31 @@ const shouldSkipUnderscore = (
171189} ;
172190
173191// OPTIMIZATION: Counts single underscores without split("").reduce()
174- // Counts single underscores that are not part of double underscores, not escaped, and not in math blocks
192+ // Counts single underscores that are not part of double underscores, not escaped, not in math blocks,
193+ // and not inside fenced code blocks
175194export const countSingleUnderscores = ( text : string ) : number => {
176195 let count = 0 ;
196+ let inCodeBlock = false ;
177197 const len = text . length ;
178198
179199 for ( let index = 0 ; index < len ; index += 1 ) {
200+ // Track fenced code blocks (```)
201+ if (
202+ text [ index ] === "`" &&
203+ index + 2 < len &&
204+ text [ index + 1 ] === "`" &&
205+ text [ index + 2 ] === "`"
206+ ) {
207+ inCodeBlock = ! inCodeBlock ;
208+ index += 2 ;
209+ continue ;
210+ }
211+
212+ // Skip content inside fenced code blocks
213+ if ( inCodeBlock ) {
214+ continue ;
215+ }
216+
180217 if ( text [ index ] !== "_" ) {
181218 continue ;
182219 }
@@ -193,13 +230,37 @@ export const countSingleUnderscores = (text: string): number => {
193230} ;
194231
195232// Counts triple asterisks that are not part of quadruple or more asterisks
233+ // and not inside fenced code blocks
196234// OPTIMIZATION: Count *** without regex to avoid allocation
197235export const countTripleAsterisks = ( text : string ) : number => {
198236 let count = 0 ;
199237 let consecutiveAsterisks = 0 ;
238+ let inCodeBlock = false ;
200239
201240 // biome-ignore lint/style/useForOf: "Need index access to check character codes for performance"
202241 for ( let i = 0 ; i < text . length ; i += 1 ) {
242+ // Track fenced code blocks (```)
243+ if (
244+ text [ i ] === "`" &&
245+ i + 2 < text . length &&
246+ text [ i + 1 ] === "`" &&
247+ text [ i + 2 ] === "`"
248+ ) {
249+ // Flush any pending asterisks before toggling
250+ if ( consecutiveAsterisks >= 3 ) {
251+ count += Math . floor ( consecutiveAsterisks / 3 ) ;
252+ }
253+ consecutiveAsterisks = 0 ;
254+ inCodeBlock = ! inCodeBlock ;
255+ i += 2 ;
256+ continue ;
257+ }
258+
259+ // Skip content inside fenced code blocks
260+ if ( inCodeBlock ) {
261+ continue ;
262+ }
263+
203264 if ( text [ i ] === "*" ) {
204265 consecutiveAsterisks += 1 ;
205266 } else {
@@ -219,6 +280,60 @@ export const countTripleAsterisks = (text: string): number => {
219280 return count ;
220281} ;
221282
283+ // Counts ** pairs outside fenced code blocks
284+ const countDoubleAsterisksOutsideCodeBlocks = ( text : string ) : number => {
285+ let count = 0 ;
286+ let inCodeBlock = false ;
287+
288+ for ( let i = 0 ; i < text . length ; i += 1 ) {
289+ if (
290+ text [ i ] === "`" &&
291+ i + 2 < text . length &&
292+ text [ i + 1 ] === "`" &&
293+ text [ i + 2 ] === "`"
294+ ) {
295+ inCodeBlock = ! inCodeBlock ;
296+ i += 2 ;
297+ continue ;
298+ }
299+ if ( inCodeBlock ) {
300+ continue ;
301+ }
302+ if ( text [ i ] === "*" && i + 1 < text . length && text [ i + 1 ] === "*" ) {
303+ count += 1 ;
304+ i += 1 ;
305+ }
306+ }
307+ return count ;
308+ } ;
309+
310+ // Counts __ pairs outside fenced code blocks
311+ const countDoubleUnderscoresOutsideCodeBlocks = ( text : string ) : number => {
312+ let count = 0 ;
313+ let inCodeBlock = false ;
314+
315+ for ( let i = 0 ; i < text . length ; i += 1 ) {
316+ if (
317+ text [ i ] === "`" &&
318+ i + 2 < text . length &&
319+ text [ i + 1 ] === "`" &&
320+ text [ i + 2 ] === "`"
321+ ) {
322+ inCodeBlock = ! inCodeBlock ;
323+ i += 2 ;
324+ continue ;
325+ }
326+ if ( inCodeBlock ) {
327+ continue ;
328+ }
329+ if ( text [ i ] === "_" && i + 1 < text . length && text [ i + 1 ] === "_" ) {
330+ count += 1 ;
331+ i += 1 ;
332+ }
333+ }
334+ return count ;
335+ } ;
336+
222337// Helper to check if bold marker should not be completed
223338const shouldSkipBoldCompletion = (
224339 text : string ,
@@ -268,7 +383,7 @@ export const handleIncompleteBold = (text: string): string => {
268383 return text ;
269384 }
270385
271- const asteriskPairs = ( text . match ( / \* \* / g ) || [ ] ) . length ;
386+ const asteriskPairs = countDoubleAsterisksOutsideCodeBlocks ( text ) ;
272387 if ( asteriskPairs % 2 === 1 ) {
273388 // Check for half-complete closing marker: **content* should become **content**
274389 // The trailing * is the first char of the closing ** being streamed
@@ -324,9 +439,8 @@ export const handleIncompleteDoubleUnderscoreItalic = (
324439 if ( halfCompleteMatch ) {
325440 const markerIndex = text . lastIndexOf ( halfCompleteMatch [ 1 ] ) ;
326441 if ( ! isWithinCodeBlock ( text , markerIndex ) ) {
327- const underscorePairs = (
328- text . match ( doubleUnderscoreGlobalPattern ) || [ ]
329- ) . length ;
442+ const underscorePairs =
443+ countDoubleUnderscoresOutsideCodeBlocks ( text ) ;
330444 if ( underscorePairs % 2 === 1 ) {
331445 return `${ text } _` ;
332446 }
@@ -347,17 +461,36 @@ export const handleIncompleteDoubleUnderscoreItalic = (
347461 return text ;
348462 }
349463
350- const underscorePairs = ( text . match ( / _ _ / g ) || [ ] ) . length ;
464+ const underscorePairs = countDoubleUnderscoresOutsideCodeBlocks ( text ) ;
351465 if ( underscorePairs % 2 === 1 ) {
352466 return `${ text } __` ;
353467 }
354468
355469 return text ;
356470} ;
357471
358- // Helper function to find the first single asterisk index
472+ // Helper function to find the first single asterisk index (skips fenced code blocks)
359473const findFirstSingleAsteriskIndex = ( text : string ) : number => {
474+ let inCodeBlock = false ;
475+
360476 for ( let i = 0 ; i < text . length ; i += 1 ) {
477+ // Track fenced code blocks (```)
478+ if (
479+ text [ i ] === "`" &&
480+ i + 2 < text . length &&
481+ text [ i + 1 ] === "`" &&
482+ text [ i + 2 ] === "`"
483+ ) {
484+ inCodeBlock = ! inCodeBlock ;
485+ i += 2 ;
486+ continue ;
487+ }
488+
489+ // Skip content inside fenced code blocks
490+ if ( inCodeBlock ) {
491+ continue ;
492+ }
493+
361494 if (
362495 text [ i ] === "*" &&
363496 text [ i - 1 ] !== "*" &&
@@ -434,9 +567,28 @@ export const handleIncompleteSingleAsteriskItalic = (text: string): string => {
434567 return text ;
435568} ;
436569
437- // Helper function to find the first single underscore index
570+ // Helper function to find the first single underscore index (skips fenced code blocks)
438571const findFirstSingleUnderscoreIndex = ( text : string ) : number => {
572+ let inCodeBlock = false ;
573+
439574 for ( let i = 0 ; i < text . length ; i += 1 ) {
575+ // Track fenced code blocks (```)
576+ if (
577+ text [ i ] === "`" &&
578+ i + 2 < text . length &&
579+ text [ i + 1 ] === "`" &&
580+ text [ i + 2 ] === "`"
581+ ) {
582+ inCodeBlock = ! inCodeBlock ;
583+ i += 2 ;
584+ continue ;
585+ }
586+
587+ // Skip content inside fenced code blocks
588+ if ( inCodeBlock ) {
589+ continue ;
590+ }
591+
440592 if (
441593 text [ i ] === "_" &&
442594 text [ i - 1 ] !== "_" &&
@@ -486,9 +638,8 @@ const handleTrailingAsterisksForUnderscore = (text: string): string | null => {
486638 }
487639
488640 const textWithoutTrailingAsterisks = text . slice ( 0 , - 2 ) ;
489- const asteriskPairsAfterRemoval = (
490- textWithoutTrailingAsterisks . match ( / \* \* / g) || [ ]
491- ) . length ;
641+ const asteriskPairsAfterRemoval =
642+ countDoubleAsterisksOutsideCodeBlocks ( textWithoutTrailingAsterisks ) ;
492643
493644 // If removing trailing ** makes the count odd, it was added to close an unclosed **
494645 if ( asteriskPairsAfterRemoval % 2 !== 1 ) {
@@ -562,7 +713,7 @@ export const handleIncompleteSingleUnderscoreItalic = (
562713
563714// Helper to check if bold-italic markers are already balanced
564715const areBoldItalicMarkersBalanced = ( text : string ) : boolean => {
565- const asteriskPairs = ( text . match ( / \* \* / g ) || [ ] ) . length ;
716+ const asteriskPairs = countDoubleAsterisksOutsideCodeBlocks ( text ) ;
566717 const singleAsterisks = countSingleAsterisks ( text ) ;
567718 return asteriskPairs % 2 === 0 && singleAsterisks % 2 === 0 ;
568719} ;
0 commit comments