import { Marked } from "marked"; import sanitizeHtml from "sanitize-html"; import { escapeHtml, safeRelativeHref } from "./safe-html.js"; function safeMarkdownHref(raw: string | undefined): string | undefined { if (!raw) return undefined; const href = raw.trim(); if (!href) return undefined; if (href.startsWith("#")) { return /^#[A-Za-z0-9_-]+$/.test(href) ? href : undefined; } if (href.startsWith("//")) return undefined; return safeRelativeHref(href); } const markdown = new Marked({ gfm: true, breaks: false, renderer: { html(token) { return escapeHtml(token.text); }, image(token) { return `[image: ${escapeHtml(token.text)}]`; }, }, }); function sanitizeRenderedMarkdown(html: string): string { return sanitizeHtml(html, { allowedTags: [ "p", "br", "strong", "em", "del", "code", "pre", "blockquote", "ul", "ol", "li", "table", "thead", "tbody", "tr", "th", "td", "h1", "h2", "h3", "h4", "h5", "h6", "a", "span", ], allowedAttributes: { a: ["href", "title"], code: ["class"], span: ["class"], }, allowedClasses: { code: [/^language-[A-Za-z0-9_-]+$/], span: ["md-image-alt", "md-link-text"], }, allowedSchemes: [], allowProtocolRelative: false, disallowedTagsMode: "discard", transformTags: { a: (tagName, attribs) => { const href = safeMarkdownHref(attribs.href); if (!href) return { tagName: "span", attribs: { class: "md-link-text" } }; const safeAttribs: Record = { href }; if (attribs.title) safeAttribs.title = attribs.title; return { tagName, attribs: safeAttribs }; }, }, }); } export function renderRunReportMarkdown(source: string): string { const rendered = markdown.parse(source, { async: false }); return sanitizeRenderedMarkdown(typeof rendered === "string" ? rendered : String(rendered)); }