39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
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: `&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;
|
|||
|
|
});
|
|||
|
|
};
|