Skip to content

Commit bde872f

Browse files
authored
fix(auto-close): ignore syntax surrounded by spaces (#282)
1 parent baff140 commit bde872f

3 files changed

Lines changed: 61 additions & 23 deletions

File tree

packages/comark/src/internal/parse/auto-close/index.ts

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

packages/comark/test/auto-close.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ $$formula → $$formula$$
2525
~Hello → ~Hello~
2626
~~Hello → ~~Hello~~
2727
~Hello~ → ~Hello~
28-
~~Hello~~ → ~~Hello~~`
28+
~~Hello~~ → ~~Hello~~
29+
* not valid → * not valid
30+
** not valid → ** not valid
31+
*** not valid → *** not valid
32+
_ not valid → _ not valid
33+
__ not valid → __ not valid
34+
~ not valid → ~ not valid
35+
~~ not valid → ~~ not valid`
2936

3037
const multilines = `
3138
| Month | Savings
@@ -215,6 +222,11 @@ describe('autoCloseMarkdown - Comark Components', () => {
215222
expect(autoCloseMarkdown(input)).toBe(expected)
216223
})
217224

225+
it('should not close a bullet-list asterisk with nested bold', () => {
226+
const input = '* **Preheat:** Set '
227+
expect(autoCloseMarkdown(input)).toBe(input)
228+
})
229+
218230
it('valid italic syntax', () => {
219231
const input = '*italic'
220232
const expected = '*italic*'

test/bundle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => {
6767
"@comark/react": "39.9k (58 files)",
6868
"@comark/svelte": "40.1k (66 files)",
6969
"@comark/vue": "56.0k (62 files)",
70-
"comark": "364k (134 files)",
70+
"comark": "366k (134 files)",
7171
}
7272
`)
7373
})

0 commit comments

Comments
 (0)