import katex from 'katex'; import { ReplyDocumentIR, ReplyNode, ReplyInline } from '../../types/replyExport'; import { escapeHtml } from './sanitize'; /** * renderLatexToMathML: 将 LaTeX 源码转为标准 MathML () * Microsoft Word (2016+/365)、WPS、Outlook 在粘贴 HTML 时原生解析 MathML 为矢量数学公式 (OMML Equation) */ export function renderLatexToMathML(latex: string, isDisplay: boolean): string { if (!latex || !latex.trim()) return ''; const trimmed = latex.trim(); try { const raw = katex.renderToString(trimmed, { output: 'mathml', displayMode: isDisplay, throwOnError: false, }); // KaTeX outputs ... const mathMatch = raw.match(//); if (mathMatch) { let mathTag = mathMatch[0]; // 确保携带标准 MathML 命名空间属性 if (!mathTag.includes('xmlns="http://www.w3.org/1998/Math/MathML"')) { mathTag = mathTag.replace('${mathTag}`; } return mathTag; } return raw; } catch (_e) { if (isDisplay) { return `
$$ ${escapeHtml(trimmed)} $$
`; } return `$${escapeHtml(trimmed)}$`; } } /** * 将 Reply Document IR 渲染为专门用于富文本剪贴板 (text/html) 的办公友好 HTML * 适用于粘贴到 Microsoft Word、Outlook、WPS、飞书文档及各类富文本编辑器 */ export function renderClipboardHtml(ir: ReplyDocumentIR): string { const bodyHtml = ir.nodes.map(renderNodeToHtml).join('\n'); return ` ${bodyHtml} `; } function renderNodeToHtml(node: ReplyNode): string { switch (node.type) { case 'heading': { const tag = `h${node.level}`; return `<${tag}>${node.children.map(renderInlineToHtml).join('')}`; } case 'paragraph': { return `

${node.children.map(renderInlineToHtml).join('')}

`; } case 'list': { const tag = node.ordered ? 'ol' : 'ul'; const itemsHtml = node.items .map((item) => `
  • ${item.map(renderInlineToHtml).join('')}
  • `) .join('\n'); return `<${tag} style="margin: 8px 0 8px 24px; padding: 0;">${itemsHtml}`; } case 'table': { const headerHtml = node.header.length > 0 ? `${node.header .map( (c) => `${c .map(renderInlineToHtml) .join('')}` ) .join('')}` : ''; const rowsHtml = node.rows .map( (row) => `${row .map( (c) => `${c .map(renderInlineToHtml) .join('')}` ) .join('')}` ) .join('\n'); return `\n${headerHtml}\n\n${rowsHtml}\n\n
    `; } case 'blockquote': { const inner = node.children.map(renderNodeToHtml).join('\n'); return `
    ${inner}
    `; } case 'code': { return `
    ${escapeHtml(
            node.value
          )}
    `; } case 'math': { return renderLatexToMathML(node.latex, node.display); } case 'mermaid': { return `
    [Mermaid 图表]\n${escapeHtml(
            node.source
          )}
    `; } case 'divider': { return '
    '; } default: return ''; } } function renderInlineToHtml(inline: ReplyInline): string { switch (inline.type) { case 'text': { let val = escapeHtml(inline.value); if (inline.bold) val = `${val}`; if (inline.italic) val = `${val}`; if (inline.strike) val = `${val}`; return val; } case 'code': { return `${escapeHtml( inline.value )}`; } case 'math': { return renderLatexToMathML(inline.latex, false); } case 'link': { const inner = inline.children.map(renderInlineToHtml).join(''); return `${inner}`; } case 'hardBreak': { return '
    '; } default: return ''; } }