import { marked } from 'marked'; import DOMPurify from 'dompurify'; marked.setOptions({ gfm: true, breaks: true, }); function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>'); } // Wrap each fenced code block in a container with a copy button. // The click handler lives in TraceViewer (event delegation on .copy-btn). marked.use({ renderer: { code({ text, lang }) { const langClass = lang ? ` class="language-${String(lang).replace(/[^\w-]/g, '')}"` : ''; return `
${escapeHtml(text)}
`; }, }, }); // All rendered links open in a new tab and are sandboxed against // window.opener / referrer leaks. DOMPurify.addHook('afterSanitizeAttributes', (node) => { if (node.tagName === 'A') { node.setAttribute('target', '_blank'); node.setAttribute('rel', 'noopener noreferrer'); } }); export function renderMarkdown(text) { if (!text) return ''; const html = marked.parse(String(text)); return DOMPurify.sanitize(html, { ADD_ATTR: ['target', 'rel'] }); }