@@ -235,8 +235,12 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
235235
236236 while ( i < text . length ) {
237237 if ( stop && text . startsWith ( stop , i ) ) {
238- // if we're closing "_", only do so when it's a real closing‐underscore
239- if ( stop !== '_' || isClosingUnderscore ( text , i ) ) {
238+ // validate closing delimiter
239+ const validClosing =
240+ ( stop !== '_' || isClosingUnderscore ( text , i ) ) &&
241+ ( stop !== '*' || isClosingAsterisk ( text , i ) )
242+
243+ if ( validClosing ) {
240244 return [ tokens , i ]
241245 }
242246 }
@@ -339,31 +343,12 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
339343 continue
340344 }
341345
342- // Italic opener: "*" always, "_" only when isOpeningUnderscore
343- if ( text [ i ] === '*' || text [ i ] === '_' && isOpeningUnderscore ( text , i ) ) {
346+ // Italic opener: "*" when left-flanking, "_" only when isOpeningUnderscore
347+ if (
348+ text [ i ] === '*' && isOpeningAsterisk ( text , i ) ||
349+ text [ i ] === '_' && isOpeningUnderscore ( text , i )
350+ ) {
344351 const delimiter = text [ i ]
345- // For '*' only: if surrounding non-space chars are digits, treat as literal
346- if ( delimiter === '*' ) {
347- let j = i - 1
348- while ( j >= 0 && text [ j ] === ' ' ) j --
349- const characterAtJ = text [ j ]
350- if ( characterAtJ === undefined ) {
351- throw new Error ( `Character at index ${ j } is undefined` )
352- }
353- const prevIsDigit = j >= 0 && / \d / . test ( characterAtJ )
354- let k = i + 1
355- while ( k < text . length && text [ k ] === ' ' ) k ++
356- const characterAtK = text [ k ]
357- if ( characterAtK === undefined ) {
358- throw new Error ( `Character at index ${ j } is undefined` )
359- }
360- const nextIsDigit = k < text . length && / \d / . test ( characterAtK )
361- if ( prevIsDigit && nextIsDigit ) {
362- tokens . push ( { type : 'text' , content : delimiter } )
363- i ++
364- continue
365- }
366- }
367352
368353 // look ahead for the rest of the text
369354 const rest = text . slice ( i + 1 )
@@ -385,23 +370,29 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
385370 // Otherwise, consume plain text until next special character or end
386371 let j = i
387372 while (
388- j < text . length
389- && text [ j ] !== '`'
390- && ! ( text . startsWith ( '**' , j ) || text . startsWith ( '__' , j ) )
391- && text [ j ] !== '*'
392- && ! ( text [ j ] === '_' && isOpeningUnderscore ( text , j ) )
393- && text [ j ] !== '['
394- // only break on stop when it's a real delimiter
395- && ! ( stop
396- && text . startsWith ( stop , j )
397- && ( stop !== '_' || isClosingUnderscore ( text , j ) ) )
398- // handle  for images but not for `text!`
399- && ! ( text [ j ] === '!' && j + 1 < text . length && text [ j + 1 ] === '[' )
373+ j < text . length &&
374+ text [ j ] !== '`' &&
375+ ! ( text . startsWith ( '**' , j ) || text . startsWith ( '__' , j ) ) &&
376+ text [ j ] !== '*' &&
377+ ! ( text [ j ] === '_' && isOpeningUnderscore ( text , j ) ) &&
378+ text [ j ] !== '[' &&
379+ ! ( stop &&
380+ text . startsWith ( stop , j ) &&
381+ ( stop !== '_' || isClosingUnderscore ( text , j ) ) &&
382+ ( stop !== '*' || isClosingAsterisk ( text , j ) ) ) &&
383+ ! ( text [ j ] === '!' && j + 1 < text . length && text [ j + 1 ] === '[' )
400384 ) {
401385 j ++
402386 }
403- tokens . push ( { type : 'text' , content : text . slice ( i , j ) } )
404- i = j
387+
388+ if ( j === i ) {
389+ // didn't consume anything – treat the single char literally
390+ tokens . push ( { type : 'text' , content : text [ i ] ?? '' } )
391+ i ++
392+ } else {
393+ tokens . push ( { type : 'text' , content : text . slice ( i , j ) } )
394+ i = j
395+ }
405396 }
406397
407398 return [ tokens , i ]
@@ -420,6 +411,15 @@ function isClosingUnderscore(text: string, pos: number): boolean {
420411 return ! / \s / . test ( prev ) && ! / \w / . test ( next )
421412}
422413
414+ function isOpeningAsterisk ( text : string , pos : number ) : boolean {
415+ const next = text [ pos + 1 ] ?? '\n'
416+ return ! / \s / . test ( next ) // next char is not whitespace
417+ }
418+ function isClosingAsterisk ( text : string , pos : number ) : boolean {
419+ const prev = text [ pos - 1 ] ?? '\n'
420+ return ! / \s / . test ( prev ) // prev char is not whitespace
421+ }
422+
423423function renderTokens ( tokens : Token [ ] , keyPrefix = '' ) : ReactNode [ ] {
424424 return tokens . map ( ( token , index ) => {
425425 const key = `${ keyPrefix } ${ index } `
0 commit comments