| const ALLOWED_RICH_TAGS = new Set([ |
| 'A', 'B', 'BLOCKQUOTE', 'BR', 'CODE', 'DEL', 'DIV', 'EM', 'H1', 'H2', 'H3', |
| 'H4', 'H5', 'H6', 'HR', 'I', 'LI', 'OL', 'P', 'PRE', 'S', 'SMALL', 'SPAN', |
| 'STRONG', 'SUB', 'SUP', 'U', 'UL' |
| ]); |
|
|
| export function escapeHtml(value) { |
| return String(value ?? '') |
| .replaceAll('&', '&') |
| .replaceAll('<', '<') |
| .replaceAll('>', '>') |
| .replaceAll('"', '"') |
| .replaceAll("'", '''); |
| } |
|
|
| export function setSafeRichHtml(element, value) { |
| const template = document.createElement('template'); |
| template.innerHTML = String(value ?? ''); |
|
|
| for (const node of Array.from(template.content.querySelectorAll('*'))) { |
| if (!ALLOWED_RICH_TAGS.has(node.tagName)) { |
| node.replaceWith(document.createTextNode(node.textContent || '')); |
| continue; |
| } |
|
|
| for (const attribute of Array.from(node.attributes)) { |
| const name = attribute.name.toLowerCase(); |
| const allowed = node.tagName === 'A' && (name === 'href' || name === 'title'); |
| if (!allowed) node.removeAttribute(attribute.name); |
| } |
|
|
| if (node.tagName === 'A') { |
| const href = node.getAttribute('href'); |
| if (href) { |
| try { |
| const url = new URL(href, window.location.href); |
| if (url.protocol !== 'http:' && url.protocol !== 'https:') node.removeAttribute('href'); |
| } catch (_) { |
| node.removeAttribute('href'); |
| } |
| } |
| node.setAttribute('target', '_blank'); |
| node.setAttribute('rel', 'noopener noreferrer'); |
| } |
| } |
|
|
| element.replaceChildren(template.content.cloneNode(true)); |
| } |
|
|