Skip to content

Commit eb72619

Browse files
committed
Harden markdown link decoding
1 parent 2610881 commit eb72619

2 files changed

Lines changed: 38 additions & 2 deletions

File tree

apps/desktop/src/features/ai/components/MarkdownContent.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,32 @@ describe("MarkdownContent", () => {
220220
).toBeInTheDocument();
221221
});
222222

223+
it("does not crash on markdown links with malformed URI encoding", () => {
224+
renderComponent(
225+
<MarkdownContent
226+
content="Use [100% notes](/vault/100% notes.md)."
227+
pillMetrics={pillMetrics}
228+
/>,
229+
);
230+
231+
expect(
232+
screen.getByRole("button", { name: "100% notes" }),
233+
).toBeInTheDocument();
234+
});
235+
236+
it("does not crash on absolute vault file paths with literal percent signs", () => {
237+
renderComponent(
238+
<MarkdownContent
239+
content="Open `/vault/100% notes.md`."
240+
pillMetrics={pillMetrics}
241+
/>,
242+
);
243+
244+
expect(
245+
screen.getByRole("button", { name: "100% notes" }),
246+
).toBeInTheDocument();
247+
});
248+
223249
it("renders unified diff code blocks with exact line gutters", () => {
224250
renderComponent(
225251
<MarkdownContent

apps/desktop/src/features/ai/components/MarkdownContent.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ interface MarkdownContentProps {
4747
chatFontSize?: number;
4848
}
4949

50+
function safeDecodeUriComponent(value: string) {
51+
try {
52+
return decodeURIComponent(value);
53+
} catch {
54+
return value;
55+
}
56+
}
57+
5058
function parseVaultReference(value: string) {
5159
const trimmed = value.trim();
5260
const match = /^(.*?\.md)(?::(\d+)|#L(\d+))?$/i.exec(trimmed);
@@ -459,7 +467,9 @@ function renderInlineMarkdown(
459467
const linkMatch = /\[([^\]]+)\]\(([^)]+)\)/.exec(full);
460468
if (linkMatch) {
461469
const url = linkMatch[2];
462-
const decoded = decodeURIComponent(url);
470+
// Assistant output may include literal "%" characters in links.
471+
// Keep rendering resilient when the URL is not valid URI-encoded text.
472+
const decoded = safeDecodeUriComponent(url);
463473
const parsedUrlReference = resolveVaultNoteReference(decoded, {
464474
allowRelative: true,
465475
});
@@ -597,7 +607,7 @@ function renderInlineMarkdown(
597607
}
598608
} else if (match[6]) {
599609
// Absolute vault file path.
600-
const filePath = decodeURIComponent(full);
610+
const filePath = safeDecodeUriComponent(full);
601611
const excalidrawRef = parseExcalidrawReference(filePath);
602612
const pdfPathRef = parsePdfReference(filePath);
603613
const textFileRef = parseTextFileReference(filePath);

0 commit comments

Comments
 (0)