// markdownRenderer.js export function renderMarkdown(raw) { // 过滤结束标记 let text = raw.replace(/\[DONE\]/g, '') // ── 预处理:在块级标识前插入换行(处理 AI 不换行直接切换块的情况)── // 在 ## 标题前插入换行(前面不是换行时) text = text.replace(/([^\n])(#{1,6} )/g, '$1\n$2') // 在无序列表项前插入换行 text = text.replace(/([^\n])([ \t]*[-*+] )/g, '$1\n$2') // 在有序列表项前插入换行 text = text.replace(/([^\n])([ \t]*\d+\. )/g, '$1\n$2') // 在引用块前插入换行 text = text.replace(/([^\n])(> )/g, '$1\n$2') // ── 按段落分割处理(双换行为段落分隔)── // 先把单换行统一,再按块处理 const lines = text.split('\n') const blocks = [] let i = 0 while (i < lines.length) { const line = lines[i] // 空行跳过 if (line.trim() === '') { i++ continue } // 代码块 if (line.startsWith('```')) { const lang = line.slice(3).trim() const codeLines = [] i++ while (i < lines.length && !lines[i].startsWith('```')) { codeLines.push(lines[i]) i++ } i++ // 跳过结束 ``` const escaped = codeLines.join('\n') .replace(/&/g, '&').replace(//g, '>') blocks.push(`
${escaped}
`) continue } // 标题 const headingMatch = line.match(/^(#{1,6}) (.+)/) if (headingMatch) { const level = headingMatch[1].length blocks.push(`${applyInline(headingMatch[2])}`) i++ continue } // 分割线 if (/^[-*_]{3,}\s*$/.test(line)) { blocks.push('
') i++ continue } // 引用块 if (line.startsWith('> ') || line === '>') { const content = line.slice(line.startsWith('> ') ? 2 : 1) blocks.push(`
${applyInline(content)}
`) i++ continue } // 无序列表:收集连续的列表项 if (/^[ \t]*[-*+] /.test(line)) { const items = [] while (i < lines.length && /^[ \t]*[-*+] /.test(lines[i])) { const m = lines[i].match(/^[ \t]*[-*+] (.+)/) if (m) items.push(`
  • ${applyInline(m[1])}
  • `) i++ } blocks.push(``) continue } // 有序列表:收集连续的列表项 if (/^[ \t]*\d+\. /.test(line)) { const items = [] while (i < lines.length && /^[ \t]*\d+\. /.test(lines[i])) { const m = lines[i].match(/^[ \t]*\d+\. (.+)/) if (m) items.push(`
  • ${applyInline(m[1])}
  • `) i++ } blocks.push(`
      ${items.join('')}
    `) continue } // 普通段落 blocks.push(`

    ${applyInline(line)}

    `) i++ } return blocks.join('\n') } // 行内样式处理 function applyInline(text) { // 先转义 HTML 特殊字符,避免 AI 返回的 < > & 破坏 rich-text 渲染; // 转义后再叠加 markdown 标记(标记字符 * _ ` ~ [ ] ( ) 不属于 HTML 特殊字符,不受影响) return escapeHtml(text) .replace(/\*\*\*([^*\n]+?)\*\*\*/g, '$1') .replace(/\*\*([^*\n]+?)\*\*/g, '$1') .replace(/\*([^*\n]+?)\*/g, '$1') .replace(/~~([^~\n]+?)~~/g, '$1') .replace(/`([^`\n]+)`/g, '$1') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') } // HTML 转义 function escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') }