import katex from 'katex'; export interface RenderMarkdownOptions { preserveNewlines?: boolean; } /** * 健壮的行内 Markdown、LaTeX 公式与有序/无序列表渲染引擎 * 支持: * - 块级数学公式 ($$...$$) 与行内数学公式 ($...$) * - 粗体 (**text** / __text__) * - 斜体 (*text* / _text_) * - 粗斜体 (***text***) * - 删除线 (~~text~~) * - 下划线 (text) * - 行内代码 (`code`) * - 超链接 ([text](url)) * - 有序列表 (1. item, 2. item ...) * - 无序列表 (- item, * item ...) * - 荧光高亮 ( / 预置语法) * - 安全内联样式标签 () */ export function renderInlineMarkdown(content: string, options: RenderMarkdownOptions = {}): string { if (!content) return ''; let text = content; // 1. 占位保护预处理:优先解析并提取数学公式,避免公式内部的 * 或 _ 被 Markdown 解析器误伤 // 注意:占位符严禁使用包含 _ 或 * 等 Markdown 特殊字符,采用纯大写字母标识符 const mathPlaceholders: string[] = []; const placeholderPrefix = 'XMATHTOKENX'; // 1.1 块级独立公式 $$ ... $$ text = text.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => { try { const rendered = katex.renderToString(math.trim(), { displayMode: true, throwOnError: false, }); const idx = mathPlaceholders.length; mathPlaceholders.push(rendered); return `${placeholderPrefix}${idx}XENDX`; } catch { const idx = mathPlaceholders.length; mathPlaceholders.push(`$$${math}$$`); return `${placeholderPrefix}${idx}XENDX`; } }); // 1.2 行内公式 $ ... $ text = text.replace(/\$([^\$\n]+?)\$/g, (_, math) => { try { const rendered = katex.renderToString(math.trim(), { displayMode: false, throwOnError: false, }); const idx = mathPlaceholders.length; mathPlaceholders.push(rendered); return `${placeholderPrefix}${idx}XENDX`; } catch { const idx = mathPlaceholders.length; mathPlaceholders.push(`$${math}$`); return `${placeholderPrefix}${idx}XENDX`; } }); // 2. 行内代码保护 `code` const codePlaceholders: string[] = []; const codePlaceholderPrefix = 'XCODETOKENX'; text = text.replace(/`([^`\n]+)`/g, (_, code) => { const rendered = `${escapeHtml(code)}`; const idx = codePlaceholders.length; codePlaceholders.push(rendered); return `${codePlaceholderPrefix}${idx}XENDX`; }); // 3. 粗斜体 ***text*** text = text.replace(/\*\*\*([^\*\n]+?)\*\*\*/g, '$1'); // 4. 粗体 **text** 或 __text__ text = text.replace(/\*\*([^\*\n]+?)\*\*/g, '$1'); text = text.replace(/__([^_\n]+?)__/g, '$1'); // 5. 斜体 *text* 或 _text_ text = text.replace(/\*([^\*\n]+?)\*/g, '$1'); text = text.replace(/_([^_\n]+?)_/g, '$1'); // 6. 删除线 ~~text~~ text = text.replace(/~~([^~\n]+?)~~/g, '$1'); // 7. 链接 [text](url) text = text.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, '$1'); // 8. 还原行内代码占位符 codePlaceholders.forEach((codeHtml, idx) => { text = text.replace(`${codePlaceholderPrefix}${idx}XENDX`, () => codeHtml); }); // 9. 还原数学公式占位符 mathPlaceholders.forEach((mathHtml, idx) => { text = text.replace(`${placeholderPrefix}${idx}XENDX`, () => mathHtml); }); // 10. 处理有序列表、无序列表与换行 if (options.preserveNewlines !== false) { const rawLines = text.split('\n'); const resultParts: string[] = []; let i = 0; while (i < rawLines.length) { const line = rawLines[i]; const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/); const unorderedMatch = line.match(/^(\s*)[-*+]\s+(.*)$/); if (orderedMatch) { const listItems: string[] = []; const startNum = parseInt(orderedMatch[2], 10) || 1; while (i < rawLines.length) { const curMatch = rawLines[i].match(/^\s*\d+\.\s+(.*)$/); if (!curMatch) break; listItems.push(curMatch[1]); i++; } resultParts.push( `
    ` + listItems.map((li) => `
  1. ${li}
  2. `).join('') + `
` ); } else if (unorderedMatch) { const listItems: string[] = []; while (i < rawLines.length) { const curMatch = rawLines[i].match(/^\s*[-*+]\s+(.*)$/); if (!curMatch) break; listItems.push(curMatch[1]); i++; } resultParts.push( `` ); } else { resultParts.push(line); i++; } } let outHtml = ''; for (let p = 0; p < resultParts.length; p++) { const cur = resultParts[p]; if (p > 0) { const prev = resultParts[p - 1]; const isPrevBlock = prev.startsWith('/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }