99 whitespaceOrMarkersPattern ,
1010} from "./patterns" ;
1111import {
12- hasCompleteCodeBlock ,
1312 isHorizontalRule ,
13+ isWithinCodeBlock ,
1414 isWithinLinkOrImageUrl ,
1515 isWithinMathBlock ,
1616 isWordChar ,
@@ -234,10 +234,6 @@ const shouldSkipBoldCompletion = (
234234
235235// Completes incomplete bold formatting (**)
236236export const handleIncompleteBold = ( text : string ) : string => {
237- if ( hasCompleteCodeBlock ( text ) ) {
238- return text ;
239- }
240-
241237 const boldMatch = text . match ( boldPattern ) ;
242238 if ( ! boldMatch ) {
243239 return text ;
@@ -246,6 +242,11 @@ export const handleIncompleteBold = (text: string): string => {
246242 const contentAfterMarker = boldMatch [ 2 ] ;
247243 const markerIndex = text . lastIndexOf ( boldMatch [ 1 ] ) ;
248244
245+ // Check if the bold marker is within a code block
246+ if ( isWithinCodeBlock ( text , markerIndex ) ) {
247+ return text ;
248+ }
249+
249250 if ( shouldSkipBoldCompletion ( text , contentAfterMarker , markerIndex ) ) {
250251 return text ;
251252 }
@@ -292,10 +293,6 @@ const shouldSkipItalicCompletion = (
292293export const handleIncompleteDoubleUnderscoreItalic = (
293294 text : string
294295) : string => {
295- if ( hasCompleteCodeBlock ( text ) ) {
296- return text ;
297- }
298-
299296 const italicMatch = text . match ( italicPattern ) ;
300297 if ( ! italicMatch ) {
301298 return text ;
@@ -304,6 +301,11 @@ export const handleIncompleteDoubleUnderscoreItalic = (
304301 const contentAfterMarker = italicMatch [ 2 ] ;
305302 const markerIndex = text . lastIndexOf ( italicMatch [ 1 ] ) ;
306303
304+ // Check if the italic marker is within a code block
305+ if ( isWithinCodeBlock ( text , markerIndex ) ) {
306+ return text ;
307+ }
308+
307309 if ( shouldSkipItalicCompletion ( text , contentAfterMarker , markerIndex ) ) {
308310 return text ;
309311 }
@@ -346,11 +348,6 @@ const findFirstSingleAsteriskIndex = (text: string): number => {
346348
347349// Completes incomplete italic formatting with single asterisks (*)
348350export const handleIncompleteSingleAsteriskItalic = ( text : string ) : string => {
349- // Don't process if inside a complete code block
350- if ( hasCompleteCodeBlock ( text ) ) {
351- return text ;
352- }
353-
354351 const singleAsteriskMatch = text . match ( singleAsteriskPattern ) ;
355352
356353 if ( ! singleAsteriskMatch ) {
@@ -363,6 +360,11 @@ export const handleIncompleteSingleAsteriskItalic = (text: string): string => {
363360 return text ;
364361 }
365362
363+ // Check if the asterisk is within a code block
364+ if ( isWithinCodeBlock ( text , firstSingleAsteriskIndex ) ) {
365+ return text ;
366+ }
367+
366368 // Get content after the first single asterisk
367369 const contentAfterFirstAsterisk = text . substring (
368370 firstSingleAsteriskIndex + 1
@@ -430,15 +432,43 @@ const insertClosingUnderscore = (text: string): string => {
430432 return `${ text } _` ;
431433} ;
432434
435+ // Helper to handle trailing ** for proper nesting of _ and ** markers
436+ const handleTrailingAsterisksForUnderscore = ( text : string ) : string | null => {
437+ if ( ! text . endsWith ( "**" ) ) {
438+ return null ;
439+ }
440+
441+ const textWithoutTrailingAsterisks = text . slice ( 0 , - 2 ) ;
442+ const asteriskPairsAfterRemoval = (
443+ textWithoutTrailingAsterisks . match ( / \* \* / g) || [ ]
444+ ) . length ;
445+
446+ // If removing trailing ** makes the count odd, it was added to close an unclosed **
447+ if ( asteriskPairsAfterRemoval % 2 !== 1 ) {
448+ return null ;
449+ }
450+
451+ const firstDoubleAsteriskIndex = textWithoutTrailingAsterisks . indexOf ( "**" ) ;
452+ const underscoreIndex = findFirstSingleUnderscoreIndex (
453+ textWithoutTrailingAsterisks
454+ ) ;
455+
456+ // If ** opened before _, then _ should close before **
457+ if (
458+ firstDoubleAsteriskIndex !== - 1 &&
459+ underscoreIndex !== - 1 &&
460+ firstDoubleAsteriskIndex < underscoreIndex
461+ ) {
462+ return `${ textWithoutTrailingAsterisks } _**` ;
463+ }
464+
465+ return null ;
466+ } ;
467+
433468// Completes incomplete italic formatting with single underscores (_)
434469export const handleIncompleteSingleUnderscoreItalic = (
435470 text : string
436471) : string => {
437- // Don't process if inside a complete code block
438- if ( hasCompleteCodeBlock ( text ) ) {
439- return text ;
440- }
441-
442472 const singleUnderscoreMatch = text . match ( singleUnderscorePattern ) ;
443473
444474 if ( ! singleUnderscoreMatch ) {
@@ -451,6 +481,11 @@ export const handleIncompleteSingleUnderscoreItalic = (
451481 return text ;
452482 }
453483
484+ // Check if the underscore is within a code block
485+ if ( isWithinCodeBlock ( text , firstSingleUnderscoreIndex ) ) {
486+ return text ;
487+ }
488+
454489 // Get content after the first single underscore
455490 const contentAfterFirstUnderscore = text . substring (
456491 firstSingleUnderscoreIndex + 1
@@ -467,49 +502,72 @@ export const handleIncompleteSingleUnderscoreItalic = (
467502
468503 const singleUnderscores = countSingleUnderscores ( text ) ;
469504 if ( singleUnderscores % 2 === 1 ) {
505+ // Check if we need to insert _ before trailing ** for proper nesting
506+ const trailingResult = handleTrailingAsterisksForUnderscore ( text ) ;
507+ if ( trailingResult !== null ) {
508+ return trailingResult ;
509+ }
470510 return insertClosingUnderscore ( text ) ;
471511 }
472512
473513 return text ;
474514} ;
475515
476- // Completes incomplete bold-italic formatting (***)
477- export const handleIncompleteBoldItalic = ( text : string ) : string => {
478- // Don't process if inside a complete code block
479- if ( hasCompleteCodeBlock ( text ) ) {
480- return text ;
516+ // Helper to check if bold-italic markers are already balanced
517+ const areBoldItalicMarkersBalanced = ( text : string ) : boolean => {
518+ const asteriskPairs = ( text . match ( / \* \* / g) || [ ] ) . length ;
519+ const singleAsterisks = countSingleAsterisks ( text ) ;
520+ return asteriskPairs % 2 === 0 && singleAsterisks % 2 === 0 ;
521+ } ;
522+
523+ // Helper to check if bold-italic should be skipped
524+ const shouldSkipBoldItalicCompletion = (
525+ text : string ,
526+ contentAfterMarker : string ,
527+ markerIndex : number
528+ ) : boolean => {
529+ if (
530+ ! contentAfterMarker ||
531+ whitespaceOrMarkersPattern . test ( contentAfterMarker )
532+ ) {
533+ return true ;
481534 }
482535
536+ if ( isWithinCodeBlock ( text , markerIndex ) ) {
537+ return true ;
538+ }
539+
540+ return isHorizontalRule ( text , markerIndex , "*" ) ;
541+ } ;
542+
543+ // Completes incomplete bold-italic formatting (***)
544+ export const handleIncompleteBoldItalic = ( text : string ) : string => {
483545 // Don't process if text is only asterisks and has 4 or more consecutive asterisks
484- // This prevents cases like **** from being treated as incomplete ***
485546 if ( fourOrMoreAsterisksPattern . test ( text ) ) {
486547 return text ;
487548 }
488549
489550 const boldItalicMatch = text . match ( boldItalicPattern ) ;
490551
491- if ( boldItalicMatch ) {
492- // Don't close if there's no meaningful content after the opening markers
493- // boldItalicMatch[2] contains the content after ***
494- // Check if content is only whitespace or other emphasis markers
495- const contentAfterMarker = boldItalicMatch [ 2 ] ;
496- if (
497- ! contentAfterMarker ||
498- whitespaceOrMarkersPattern . test ( contentAfterMarker )
499- ) {
500- return text ;
501- }
552+ if ( ! boldItalicMatch ) {
553+ return text ;
554+ }
502555
503- // Check if the *** is a horizontal rule
504- const markerIndex = text . lastIndexOf ( boldItalicMatch [ 1 ] ) ;
505- if ( isHorizontalRule ( text , markerIndex , "*" ) ) {
506- return text ;
507- }
556+ const contentAfterMarker = boldItalicMatch [ 2 ] ;
557+ const markerIndex = text . lastIndexOf ( boldItalicMatch [ 1 ] ) ;
508558
509- const tripleAsteriskCount = countTripleAsterisks ( text ) ;
510- if ( tripleAsteriskCount % 2 === 1 ) {
511- return `${ text } ***` ;
559+ if ( shouldSkipBoldItalicCompletion ( text , contentAfterMarker , markerIndex ) ) {
560+ return text ;
561+ }
562+
563+ const tripleAsteriskCount = countTripleAsterisks ( text ) ;
564+ if ( tripleAsteriskCount % 2 === 1 ) {
565+ // If both ** and * are balanced, don't add closing ***
566+ // The *** is likely overlapping markers (e.g., **bold and *italic***)
567+ if ( areBoldItalicMarkersBalanced ( text ) ) {
568+ return text ;
512569 }
570+ return `${ text } ***` ;
513571 }
514572
515573 return text ;
0 commit comments