49 lines
1.7 KiB
JavaScript
49 lines
1.7 KiB
JavaScript
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));
|
|
}
|