Files
pikasTech-HWLAB/web/hwlab-cloud-web/message-markdown.ts
T
2026-05-28 21:33:33 +08:00

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) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;"
})[char]);
}