@@ -218,6 +218,30 @@ export function autoCloseMarkdown(markdown: string): string {
218218 return result
219219}
220220
221+ /** Whitespace or a line boundary (empty string) — treated the same for flanking checks. */
222+ function isSpaceOrBoundary ( ch : string ) : boolean {
223+ return ch === '' || ch === ' ' || ch === '\t'
224+ }
225+
226+ /**
227+ * Scans a run of the same delimiter char starting at `start`.
228+ * Returns the run length and whether it's surrounded by whitespace/boundaries
229+ * (in which case it's neither left- nor right-flanking and cannot delimit emphasis).
230+ */
231+ function scanDelimiterRun ( line : string , start : number , marker : string ) {
232+ const len = line . length
233+ let end = start
234+ while ( end + 1 < len && line [ end + 1 ] === marker ) end ++
235+ const prevCh = start > 0 ? line [ start - 1 ] : ''
236+ const afterCh = end + 1 < len ? line [ end + 1 ] : ''
237+ return {
238+ end,
239+ length : end - start + 1 ,
240+ afterCh,
241+ surroundedBySpace : isSpaceOrBoundary ( prevCh ) && isSpaceOrBoundary ( afterCh ) ,
242+ }
243+ }
244+
221245/**
222246 * Closes inline markers (*, **, ***, ~~, `, $, $$, [, () on the last line
223247 * without using regex - pure character scanning in O(n) time
@@ -342,36 +366,38 @@ function closeInlineMarkersLinear(line: string): string {
342366 continue
343367 }
344368
369+ // A delimiter run surrounded by whitespace (e.g. `* item`, `** not valid`) is
370+ // neither left- nor right-flanking per CommonMark, so it cannot delimit emphasis.
345371 if ( ch === '*' ) {
346- asteriskCount ++
347- // Track ** positions (not part of ***)
348- if ( i + 1 < len && line [ i + 1 ] === '*' ) {
349- const isPartOfTriple = ( i > 0 && line [ i - 1 ] === '*' ) || ( i + 2 < len && line [ i + 2 ] === '*' )
350- if ( ! isPartOfTriple ) {
351- doubleAsteriskPositions . push ( i )
352- }
372+ const run = scanDelimiterRun ( line , i , '*' )
373+ if ( ! run . surroundedBySpace ) {
374+ asteriskCount += run . length
375+ // Track a lone `**` run (exactly two asterisks) for complete-pair detection.
376+ if ( run . length === 2 ) doubleAsteriskPositions . push ( i )
353377 }
378+ i = run . end // Skip the rest of the run (loop increments past it)
354379 } else if ( ch === '_' ) {
355- // Skip intra-word underscores (not emphasis delimiters per CommonMark)
356- const nextCh = i + 1 < len ? line [ i + 1 ] : ''
380+ const run = scanDelimiterRun ( line , i , '_' )
357381 const prevIsWord =
358382 ( prevCh >= 'a' && prevCh <= 'z' ) || ( prevCh >= 'A' && prevCh <= 'Z' ) || ( prevCh >= '0' && prevCh <= '9' )
359383 const nextIsWord =
360- ( nextCh >= 'a' && nextCh <= 'z' ) || ( nextCh >= 'A' && nextCh <= 'Z' ) || ( nextCh >= '0' && nextCh <= '9' )
361- if ( ! ( prevIsWord && nextIsWord ) ) {
362- underscoreCount ++
363- // Track __ positions (for bold)
364- if ( nextCh === '_' ) {
365- doubleUnderscorePositions . push ( i )
366- }
384+ ( run . afterCh >= 'a' && run . afterCh <= 'z' ) ||
385+ ( run . afterCh >= 'A' && run . afterCh <= 'Z' ) ||
386+ ( run . afterCh >= '0' && run . afterCh <= '9' )
387+ // Also skip intra-word underscores (not emphasis delimiters per CommonMark).
388+ if ( ! ( prevIsWord && nextIsWord ) && ! run . surroundedBySpace ) {
389+ underscoreCount += run . length
390+ // Track a lone `__` run (exactly two underscores) for bold.
391+ if ( run . length === 2 ) doubleUnderscorePositions . push ( i )
367392 }
393+ i = run . end
368394 } else if ( ch === '~' ) {
369- if ( i + 1 < len && line [ i + 1 ] === '~' ) {
370- doubleTildeCount ++
371- i ++ // Skip second tilde since we counted the pair
372- } else {
373- singleTildeCount ++
395+ const run = scanDelimiterRun ( line , i , '~' )
396+ if ( ! run . surroundedBySpace ) {
397+ doubleTildeCount += run . length >> 1 // number of `~~` pairs in the run
398+ if ( run . length & 1 ) singleTildeCount ++ // leftover single `~`
374399 }
400+ i = run . end
375401 }
376402 }
377403
0 commit comments