57 lines
2.0 KiB
JavaScript
57 lines
2.0 KiB
JavaScript
// @ts-check
|
|
import { Marked } from "../vendor/marked/marked.esm.js";
|
|
import DOMPurify from "../vendor/dompurify/purify.es.mjs";
|
|
import { escHtml } from "./dom.js";
|
|
|
|
const markdown = new Marked({
|
|
gfm: true,
|
|
breaks: true,
|
|
renderer: {
|
|
// Treat embedded HTML as text. Replies can format Markdown, not UI controls.
|
|
html: ({ text }) => escHtml(text),
|
|
checkbox: ({ checked }) => checked ? "☑ " : "☐ ",
|
|
},
|
|
});
|
|
|
|
/** Parse assistant text into passive, sanitized message content. @param {string} text */
|
|
export function renderMarkdown(text) {
|
|
if (!DOMPurify.isSupported) return document.createTextNode(text);
|
|
try {
|
|
const html = markdown.parse(text, { async: false });
|
|
const fragment = DOMPurify.sanitize(html, {
|
|
ALLOWED_TAGS: [
|
|
"p", "br", "strong", "em", "del", "a", "img", "ul", "ol", "li",
|
|
"h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "code", "hr",
|
|
"table", "thead", "tbody", "tr", "th", "td",
|
|
],
|
|
ALLOWED_ATTR: ["href", "title", "src", "alt", "start", "align"],
|
|
ALLOW_DATA_ATTR: false,
|
|
ALLOW_ARIA_ATTR: false,
|
|
RETURN_DOM_FRAGMENT: true,
|
|
});
|
|
for (const link of fragment.querySelectorAll("a")) {
|
|
if (!link.hasAttribute("href")) {
|
|
link.replaceWith(...link.childNodes);
|
|
} else {
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
}
|
|
}
|
|
for (const image of fragment.querySelectorAll("img")) image.loading = "lazy";
|
|
for (const table of fragment.querySelectorAll("table")) {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "markdown-table";
|
|
wrapper.tabIndex = 0;
|
|
wrapper.setAttribute("role", "region");
|
|
wrapper.setAttribute("aria-label", "Tabla de la respuesta");
|
|
table.replaceWith(wrapper);
|
|
wrapper.appendChild(table);
|
|
}
|
|
for (const code of fragment.querySelectorAll("pre")) code.tabIndex = 0;
|
|
return fragment;
|
|
} catch (err) {
|
|
console.warn("[ui] Markdown rendering failed", err);
|
|
return document.createTextNode(text);
|
|
}
|
|
}
|