const NAMED: Record = { 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: `&quot;`), so after * Synapse decodes once the card showed a literal `"`. 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; }); };