// 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;