diff --git a/src/renderer/src/components/editor/MarkdownPreview.test.ts b/src/renderer/src/components/editor/MarkdownPreview.test.ts index 9a6c57de889..216eaad5d05 100644 --- a/src/renderer/src/components/editor/MarkdownPreview.test.ts +++ b/src/renderer/src/components/editor/MarkdownPreview.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import type { Worktree } from '../../../../shared/types' import { + decodeMarkdownPreviewAnchor, deriveMarkdownPreviewSourceRoot, findMarkdownPreviewOpenedEditFileId, findMarkdownPreviewSourceOpenFile, @@ -31,6 +32,10 @@ function makeWorktree(id: string, path: string): Worktree { } describe('MarkdownPreview source link routing', () => { + it('falls back to the raw anchor when percent-decoding fails', () => { + expect(decodeMarkdownPreviewAnchor('%E0%A4%A')).toBe('%E0%A4%A') + }) + it('keeps the explicit source worktree when it exists', () => { const source = makeWorktree('wt-source', '/repo') const nested = makeWorktree('wt-nested', '/repo/packages/app') diff --git a/src/renderer/src/components/editor/MarkdownPreview.tsx b/src/renderer/src/components/editor/MarkdownPreview.tsx index 0ef3f394ae1..dea87e6704e 100644 --- a/src/renderer/src/components/editor/MarkdownPreview.tsx +++ b/src/renderer/src/components/editor/MarkdownPreview.tsx @@ -315,6 +315,14 @@ function parseLineTarget(hash: string): { line: number; column?: number } | null return { line: Number(match[1]), column: match[2] ? Number(match[2]) : undefined } } +export function decodeMarkdownPreviewAnchor(rawAnchor: string): string { + try { + return decodeURIComponent(rawAnchor) + } catch { + return rawAnchor + } +} + function normalizeMarkdownPreviewAbsolutePath(absolutePath: string): string { return absolutePath.replaceAll('\\', '/') } @@ -684,7 +692,7 @@ export default function MarkdownPreview({ return false } - const decodedAnchor = decodeURIComponent(rawAnchor) + const decodedAnchor = decodeMarkdownPreviewAnchor(rawAnchor) let target: HTMLElement | null = null for (const candidate of body.querySelectorAll('[id]')) { if (candidate.id === decodedAnchor) {