import { marked } from "./third_party/marked/marked.esm.js"; export const messageMarkdownOptions = Object.freeze({ gfm: true, breaks: false, renderer: messageMarkdownRenderer() }); export function renderMessageMarkdown(markdown) { return marked.parse(String(markdown ?? ""), messageMarkdownOptions); } export function hardenRenderedMarkdown(root) { for (const element of root.querySelectorAll("script, style, iframe, object, embed, form, input, button, textarea, select")) { element.replaceWith(document.createTextNode(element.textContent ?? "")); } for (const element of root.querySelectorAll("*")) { for (const attribute of [...element.attributes]) { const name = attribute.name.toLowerCase(); if (name.startsWith("on") || name === "style") { element.removeAttribute(attribute.name); } } } for (const anchor of root.querySelectorAll("a[href]")) { const href = safeMarkdownHref(anchor.getAttribute("href")); if (!href) { anchor.removeAttribute("href"); } else { anchor.setAttribute("href", href); anchor.setAttribute("rel", "noreferrer"); } } } function messageMarkdownRenderer() { const renderer = new marked.Renderer(); renderer.html = ({ text }) => escapeHtml(text); renderer.link = ({ href, title, tokens }) => { const text = renderer.parser.parseInline(tokens); const safeHref = safeMarkdownHref(href); if (!safeHref) return text; const safeTitle = title ? ` title="${escapeHtml(title)}"` : ""; return `${text}`; }; renderer.image = ({ text }) => escapeHtml(text ?? ""); return renderer; } function safeMarkdownHref(value) { const href = String(value ?? "").trim(); if (!href) return null; if (/^(?:https?:|mailto:)/iu.test(href)) return href; if (/^[#/?]/u.test(href)) return href; return null; } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" })[char]); }