// Markdown — render LLM output as proper rich text (bold, lists, headers, // inline code, code fences, tables, blockquotes) the way ChatGPT / Claude do. // // Pipeline per render: // 1. strip ... chain-of-thought (defence-in-depth — server // already strips, but old cached responses may still have it) // 2. parse with marked (GitHub-flavoured markdown) // 3. sanitize with DOMPurify (so we never inject untrusted HTML) // 4. set via dangerouslySetInnerHTML inside a styled wrapper // // Props: // text — markdown source string // translate? — wrap each text node through TranslatedText? default false // (currently we translate the WHOLE string upstream and pass // the translated text in, so this is off by default) // className?, style? — passed to the wrapper const _stripThink = (s) => (window.stripThinking ? window.stripThinking(s) : (s || '')); const _renderMarkdown = (raw) => { if (!raw) return ''; const cleaned = _stripThink(String(raw)); if (!window.marked) { // Library not loaded yet — render as preformatted text so it's at least readable. const esc = cleaned.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]); return `

${esc.replace(/\n/g, '
')}

`; } try { window.marked.setOptions({ gfm: true, breaks: true }); const html = window.marked.parse(cleaned); return window.DOMPurify ? window.DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }) : html; } catch (e) { console.warn('markdown render failed:', e); return cleaned; } }; const Markdown = ({ text, className, style }) => { // useTranslated runs the LLM-translate roundtrip when language ≠ English. // If TranslatedText isn't loaded (very early render), fall back to raw text. const translated = window.useTranslated ? window.useTranslated(text) : text; const html = React.useMemo(() => _renderMarkdown(translated), [translated]); return (
); }; window.Markdown = Markdown;