From aab478359bdedf8a0a66926d218bd124ce184fea Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 25 Mar 2026 22:40:25 -0400 Subject: [PATCH] fix: decode HTML entities in wikilinks before re-escaping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit marked encodes " to " in text nodes, causing linkifyWikilinks to double-escape it (& → &) so the visible link text showed " instead of the actual character. Decode marked's entities on the matched title/label before running escapeHtmlAttr so the output is correct in both the href attribute and the visible link text. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/utils/tags.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/tags.ts b/frontend/src/utils/tags.ts index 2474319..05949b5 100644 --- a/frontend/src/utils/tags.ts +++ b/frontend/src/utils/tags.ts @@ -5,6 +5,17 @@ function escapeHtmlAttr(s: string): string { return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } +// Decode HTML entities that marked introduces before we re-escape for our own output. +// Order matters: & must be last to avoid double-decoding. +function decodeHtmlEntities(s: string): string { + return s + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&"); +} + export function extractTags(body: string): string[] { const cleaned = body.replace(CODE_FENCE_RE, ""); const tags = new Set(); @@ -39,8 +50,8 @@ export function linkifyWikilinks(html: string): string { .map((part, i) => { if (i % 2 === 1) return part; return part.replace(WIKILINK_RE, (_full, title: string, display?: string) => { - const trimmed = title.trim(); - const label = display || trimmed; + const trimmed = decodeHtmlEntities(title.trim()); + const label = display ? decodeHtmlEntities(display) : trimmed; const encoded = encodeURIComponent(trimmed); return `${escapeHtmlAttr(label)}`; });