Spaces:
Running
Running
| import { marked } from 'marked'; | |
| import DOMPurify from 'dompurify'; | |
| marked.setOptions({ | |
| gfm: true, | |
| breaks: true, | |
| }); | |
| function escapeHtml(s) { | |
| return String(s) | |
| .replace(/&/g, '&') | |
| .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 `<div class="code-block"><button type="button" class="copy-btn" aria-label="Copy code">Copy</button><pre><code${langClass}>${escapeHtml(text)}</code></pre></div>`; | |
| }, | |
| }, | |
| }); | |
| // 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'] }); | |
| } | |