From 23aab087f367156e5e68be49a7132c3ee6836f22 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 18 Sep 2026 19:42:37 -0700 Subject: [PATCH 1/2] fix(editor): preserve line endings and fenced code boundaries --- .../editor/markdown-code-span-scanner.ts | 3 +++ .../editor/markdown-code-stripping.ts | 4 ++-- .../editor/markdown-fence-scanner.ts | 8 +++---- .../markdown-scanner-boundaries.test.ts | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/editor/markdown-code-span-scanner.ts b/src/renderer/src/components/editor/markdown-code-span-scanner.ts index c1323c9ce0e..26081412c50 100644 --- a/src/renderer/src/components/editor/markdown-code-span-scanner.ts +++ b/src/renderer/src/components/editor/markdown-code-span-scanner.ts @@ -123,6 +123,9 @@ export function createMarkdownCodeSpanScanner(content: string): MarkdownCodeSpan } previousWasBoundary = isBoundary || endsWithLine previousQuoteDepth = quoteDepth + if (isBoundary) { + return + } let index = lineStart while (index < lineEnd) { diff --git a/src/renderer/src/components/editor/markdown-code-stripping.ts b/src/renderer/src/components/editor/markdown-code-stripping.ts index 47015eb5ee8..8aa8ebc4766 100644 --- a/src/renderer/src/components/editor/markdown-code-stripping.ts +++ b/src/renderer/src/components/editor/markdown-code-stripping.ts @@ -9,7 +9,7 @@ export function stripMarkdownCode(content: string): string { let rangeIndex = 0 let sanitized = '' - forEachMarkdownLine(content, (lineStart, lineEnd) => { + forEachMarkdownLine(content, (lineStart, lineEnd, nextLineStart) => { while (rangeIndex < ranges.length && ranges[rangeIndex][1] <= lineStart) { rangeIndex += 1 } @@ -21,7 +21,7 @@ export function stripMarkdownCode(content: string): string { } sanitized += content.slice(cursor, lineEnd) if (lineEnd < content.length) { - sanitized += '\n' + sanitized += content.slice(lineEnd, nextLineStart) } }) diff --git a/src/renderer/src/components/editor/markdown-fence-scanner.ts b/src/renderer/src/components/editor/markdown-fence-scanner.ts index 4afed0185a4..ed350b556e4 100644 --- a/src/renderer/src/components/editor/markdown-fence-scanner.ts +++ b/src/renderer/src/components/editor/markdown-fence-scanner.ts @@ -6,14 +6,12 @@ export type MarkdownFenceTracker = { consume: (line: string) => boolean } -// A top-level opener may be indented by at most three spaces. Closers are matched -// separately below because marked allows them to be indented within the fence. +// Top-level fence delimiters may be indented by at most three spaces. const FENCE_LINE = /^[ ]{0,3}(`{3,}|~{3,})/ -const INDENTED_FENCE_LINE = /^[ \t]*(`{3,}|~{3,})/ // marked lets a closer trail a run of fence characters, e.g. ```~~~ closes a ``` block. const CLOSING_FENCE_SUFFIX = /^[~`]*[ \t\r]*$/ -/** Tracks CommonMark fenced code blocks across the lines of one document. */ +/** Tracks fenced blocks using the editor parser's closing rules. */ export function createMarkdownFenceTracker(): MarkdownFenceTracker { let marker = '' let length = 0 @@ -23,7 +21,7 @@ export function createMarkdownFenceTracker(): MarkdownFenceTracker { return length > 0 }, consume(line: string): boolean { - const match = length > 0 ? INDENTED_FENCE_LINE.exec(line) : FENCE_LINE.exec(line) + const match = FENCE_LINE.exec(line) if (!match) { return false } diff --git a/src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts b/src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts index 305af2dfc12..a79fb834a3d 100644 --- a/src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts +++ b/src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts @@ -1,3 +1,6 @@ +import { marked } from 'marked' +import { stripMarkdownCode } from './markdown-code-stripping' +import { getMarkdownFenceRanges } from './markdown-fence-scanner' import { describe, expect, it } from 'vitest' import { markdownCodeSpanRanges, markdownFenceRanges } from './markdown-scan-ranges' import { createMarkdownCodeSpanScanner } from './markdown-code-span-scanner' @@ -19,3 +22,22 @@ describe('standalone Markdown boundaries', () => { expect(markdownFenceRanges(source)).toEqual([[0, 13]]) }) }) + +it('does not expose inline spans inside fenced content', () => { + const source = '```\n`x`\n```' + expect(createMarkdownCodeSpanScanner(source).findSpanEnd(4)).toBeNull() +}) + +it.each(['\n', '\r\n', '\r'])('retains %j terminators while stripping code', (eol) => { + expect(stripMarkdownCode(['before', '`code`', 'after'].join(eol))).toBe( + ['before', '', 'after'].join(eol) + ) +}) + +it.each(['```~~~', ' ```'])('matches the parser on the closer %j', (closer) => { + const source = `\`\`\`\ncode\n${closer}\n
after
\n` + const code = marked.lexer(source)[0] + expect(code.type).toBe('code') + expect(getMarkdownFenceRanges(source)).toEqual([[0, code.raw.length]]) + expect(stripMarkdownCode(source).includes('
after
')).toBe(closer === '```~~~') +}) From 5bd59968e41647b7bd9404b9cf5096d00945ba0f Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 18 Sep 2026 19:43:12 -0700 Subject: [PATCH 2/2] test(editor): use parser-compatible fence indentation --- .../src/components/editor/markdown-fence-scanner.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/editor/markdown-fence-scanner.test.ts b/src/renderer/src/components/editor/markdown-fence-scanner.test.ts index 879ee1b09a8..34b45df1a71 100644 --- a/src/renderer/src/components/editor/markdown-fence-scanner.test.ts +++ b/src/renderer/src/components/editor/markdown-fence-scanner.test.ts @@ -32,7 +32,7 @@ describe('createMarkdownFenceTracker', () => { ['exact-length closer', '```\nbody\n```'], ['longer closer', '```\nbody\n`````'], ['closer with trailing whitespace', '```\nbody\n``` \t'], - ['indented closer', '```\nbody\n ```'], + ['indented closer', '```\nbody\n ```'], ['tilde closer', '~~~\nbody\n~~~'] ])('closes on a %s', (_name, content) => { expect(insideFenceLines(content)).toEqual(['body'])