67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
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 `<a href="${escapeHtml(safeHref)}"${safeTitle} rel="noreferrer">${text}</a>`;
|
|
};
|
|
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]);
|
|
}
|