From d42e0ca00c8ca1fa38bbda67582bbb385c4cf09c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 15 Oct 2026 05:51:11 +0200 Subject: [PATCH] fix(app): preserve provisional syntax at cached reveal boundaries --- .../components/markdown-prose/prepare.test.ts | 10 ++++++++++ .../src/components/markdown-prose/prepare.ts | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/markdown-prose/prepare.test.ts b/packages/app/src/components/markdown-prose/prepare.test.ts index 8883c8e1a7..bbf7f6a563 100644 --- a/packages/app/src/components/markdown-prose/prepare.test.ts +++ b/packages/app/src/components/markdown-prose/prepare.test.ts @@ -77,6 +77,16 @@ describe("source-preserving prose", () => { expect(prepare(blocks, true, length)).toBe(prepareProse(blocks, noFiles, true, length)); } }); + it.each([ + ["A ||literal|| and escaped \\* text.", 2, "A "], + ["Use `README.md` and example.com. Later.", 4, "Use "], + ["[unresolved label] later.", 12, "unresolved "], + ["[unresolved label] later.", 19, "unresolved label "], + ] as const)("keeps cached reveal boundaries literal: %s", (source, length, expected) => { + const prepare = createProsePreparer((text) => text === "README.md"); + prepare([source], true, source.length); + expect(prepare([source], true, length)).toBe(expected); + }); it("changes only prose pipes", () => { expect(prepareProse(["Keep ||literal|| and `a || b` and [a|b](https://x/a|b)."], noFiles)).toBe( "Keep \\|\\|literal\\|\\| and `a || b` and [a\\|b](https://x/a|b).", diff --git a/packages/app/src/components/markdown-prose/prepare.ts b/packages/app/src/components/markdown-prose/prepare.ts index 8dadd5de88..273622671d 100644 --- a/packages/app/src/components/markdown-prose/prepare.ts +++ b/packages/app/src/components/markdown-prose/prepare.ts @@ -368,25 +368,37 @@ function compileBlock( edits.push({ ...edit, start: positions[edit.start]!, end: positions[edit.end]! }); } const rendered = applyEdits(text, edits); + return { text: rendered, cuts, offsets: preparedOffsets(cuts, edits) }; +} +function preparedOffsets(cuts: number[], edits: Edit[]): number[] { const offsets: number[] = []; let editIndex = 0; let delta = 0; for (const cut of cuts) { - while (editIndex < edits.length && edits[editIndex]!.end <= cut) { + while ( + editIndex < edits.length && + edits[editIndex]!.end <= cut && + edits[editIndex]!.start < cut + ) { const edit = edits[editIndex++]!; delta += edit.text.length - (edit.end - edit.start); } offsets.push(cut + delta); } - return { text: rendered, cuts, offsets }; + return offsets; } function paragraphCuts(source: string, trace: InlineTrace): number[] { const cuts: number[] = []; let depth = 0; + // A bracket can remain a provisional reference/link even after its closing bracket. + // Keep that suffix together until the block settles. + const bracket = source.indexOf("["); + const stableEnd = bracket < 0 ? source.length : bracket; for (const span of trace.spans) { if (span.insideLink) continue; if (depth === 0 && span.tokens.length === 0 && source[span.start] !== "&") { - for (let i = span.start; i < span.end; i++) if (source[i] === " ") cuts.push(i + 1); + for (let i = span.start; i < span.end; i++) + if (i < stableEnd && source[i] === " ") cuts.push(i + 1); } for (const token of span.tokens) depth += token.nesting; }