diff --git a/src/renderer/src/components/editor/markdown-scan-ranges.test.ts b/src/renderer/src/components/editor/markdown-scan-ranges.test.ts new file mode 100644 index 00000000000..c5914498edf --- /dev/null +++ b/src/renderer/src/components/editor/markdown-scan-ranges.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' +import { findDetailsBlockStart } from './details-markdown-html' +import { markdownCodeSpanRanges, markdownFenceRanges } from './markdown-scan-ranges' + +describe('markdownCodeSpanRanges', () => { + it('reports a span whose backtick run closes on an equal run', () => { + expect(markdownCodeSpanRanges('a `
` b')).toEqual([[2, 13]]) + }) + + it('keeps a span that contains a blank line', () => { + expect(markdownCodeSpanRanges('text `a\n\nb` tail')).toEqual([[5, 11]]) + }) + + it('reports no span for backticks inside a fenced block', () => { + expect(markdownCodeSpanRanges(['```', 'a `b` c', '```'].join('\n'))).toEqual([]) + }) + + it('does not pair a fence backtick with a later prose backtick', () => { + const content = ['~~~', 'x = `abc', '~~~', '', 'prose with `code` here'].join('\n') + const spans = markdownCodeSpanRanges(content) + + expect(spans).toEqual([[29, 35]]) + expect(content.slice(29, 35)).toBe('`code`') + }) + + it('does not run a span through a four-backtick fence closer', () => { + const content = ['```', 'code', '````', '', 'tail', '', '```', 'more', '```'].join('\n') + + expect(markdownCodeSpanRanges(content)).toEqual([]) + }) +}) + +describe('findDetailsBlockStart with fenced content', () => { + it('finds a details block after a fence whose content holds an unpaired backtick', () => { + const content = [ + '~~~', + 'x = `abc', + '~~~', + '', + '
', + 'S', + '', + 'body', + '', + '
', + '', + 'prose with `code` here' + ].join('\n') + + expect(findDetailsBlockStart(content)).toBe(content.indexOf('
')) + }) + + it('finds a details block between a four-backtick closer and a later fence', () => { + const content = [ + '```', + 'code', + '````', + '', + '
', + 'S', + '', + 'x', + '', + '
', + '', + '```', + 'more', + '```' + ].join('\n') + + expect(findDetailsBlockStart(content)).toBe(content.indexOf('
')) + }) + + it('skips a details mention inside a code span that spans a blank line', () => { + expect(findDetailsBlockStart('text `a\n\n
b` tail')).toBe(-1) + }) +}) + +describe('markdownFenceRanges', () => { + it('covers the opening delimiter, content, and closing delimiter', () => { + const content = ['```', 'x', '```', ''].join('\n') + + expect(markdownFenceRanges(content)).toEqual([[0, 10]]) + }) + + it('runs an unterminated fence to the end of the content', () => { + const content = ['```', 'x', 'y'].join('\n') + + expect(markdownFenceRanges(content)).toEqual([[0, content.length]]) + }) +}) diff --git a/src/renderer/src/components/editor/markdown-scan-ranges.ts b/src/renderer/src/components/editor/markdown-scan-ranges.ts index e46a2932207..a90345e6880 100644 --- a/src/renderer/src/components/editor/markdown-scan-ranges.ts +++ b/src/renderer/src/components/editor/markdown-scan-ranges.ts @@ -46,14 +46,35 @@ export function isInsideRange(index: number, ranges: MarkdownFenceRanges): boole return ranges.some(([start, end]) => index >= start && index < end) } +function rangeEndAt(index: number, ranges: MarkdownFenceRanges): number { + for (const [start, end] of ranges) { + if (index >= start && index < end) { + return end + } + } + return -1 +} + // CommonMark code spans: a backtick run only closes on a run of the same // length, so `` `
` `` is one span even though `
` alone // isn't. Mirrors the tick-matching in raw-markdown-html.ts's inline scan. -export function markdownCodeSpanRanges(content: string): MarkdownFenceRanges { +// Fenced blocks are skipped whole — their delimiters and content are not +// inline code, and scanning them pairs a fence backtick with a later prose +// one, swallowing everything between. Blank lines are not span boundaries. +export function markdownCodeSpanRanges( + content: string, + fenceRanges: MarkdownFenceRanges = markdownFenceRanges(content) +): MarkdownFenceRanges { const ranges: [number, number][] = [] let index = 0 while (index < content.length) { + const fenceEnd = rangeEndAt(index, fenceRanges) + if (fenceEnd !== -1) { + index = fenceEnd + continue + } + if (content[index] !== '`') { index += 1 continue @@ -72,6 +93,10 @@ export function markdownCodeSpanRanges(content: string): MarkdownFenceRanges { if (candidate === -1) { break } + if (rangeEndAt(candidate, fenceRanges) !== -1) { + searchFrom = candidate + 1 + continue + } if ( (candidate === 0 || content[candidate - 1] !== '`') && content[candidate + tickCount] !== '`'