Files
cinny/src/app/utils/htmlEntities.ts
T
Lotus CIandClaude Opus 5.5 cccd78fd43
CI / Build & Quality Checks (push) Successful in 4m16s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
fix(embeds): decode leftover HTML entities in link-preview text (#187 DP17)
Steam double-encodes its meta tags (`"`), so after Synapse's
single decode the card showed "The "Perpetual Testing
Initiative" …". The preview's og:title / og:description /
og:site_name are now decoded once where the preview is fetched, so every
card (about 20 read those fields directly) gets clean text. Rendered as
React text only, so decoding can't inject markup; exactly one level is
decoded.

Verified on the Portal 2 store link: "The \"Perpetual Testing
Initiative\" has been expanded…", no literal " left on the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-24 11:24:28 -04:00

39 lines
1.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const NAMED: Record<string, string> = {
amp: '&',
lt: '<',
gt: '>',
quot: '"',
apos: "'",
nbsp: ' ',
hellip: '…',
mdash: '—',
ndash: '–',
lsquo: '‘',
rsquo: '’',
ldquo: '“',
rdquo: '”',
trade: '™',
copy: '©',
reg: '®',
};
/**
* Decode HTML character references left in plain text. Link previews need it:
* some sites double-encode their meta tags (Steam: `&amp;quot;`), so after
* Synapse decodes once the card showed a literal `&quot;`. The result is only
* ever rendered as React text, so decoding can't inject markup.
*/
export const decodeHtmlEntities = (text: string): string => {
if (!text.includes('&')) return text;
return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (match, ref: string) => {
if (ref[0] === '#') {
const code =
ref[1] === 'x' || ref[1] === 'X' ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
return Number.isFinite(code) && code > 0 && code <= 0x10ffff
? String.fromCodePoint(code)
: match;
}
return NAMED[ref.toLowerCase()] ?? match;
});
};