markdownRenderer.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // markdownRenderer.js
  2. export function renderMarkdown(raw) {
  3. // 过滤结束标记
  4. let text = raw.replace(/\[DONE\]/g, '')
  5. // ── 预处理:在块级标识前插入换行(处理 AI 不换行直接切换块的情况)──
  6. // 在 ## 标题前插入换行(前面不是换行时)
  7. text = text.replace(/([^\n])(#{1,6} )/g, '$1\n$2')
  8. // 在无序列表项前插入换行
  9. text = text.replace(/([^\n])([ \t]*[-*+] )/g, '$1\n$2')
  10. // 在有序列表项前插入换行
  11. text = text.replace(/([^\n])([ \t]*\d+\. )/g, '$1\n$2')
  12. // 在引用块前插入换行
  13. text = text.replace(/([^\n])(> )/g, '$1\n$2')
  14. // ── 按段落分割处理(双换行为段落分隔)──
  15. // 先把单换行统一,再按块处理
  16. const lines = text.split('\n')
  17. const blocks = []
  18. let i = 0
  19. while (i < lines.length) {
  20. const line = lines[i]
  21. // 空行跳过
  22. if (line.trim() === '') {
  23. i++
  24. continue
  25. }
  26. // 代码块
  27. if (line.startsWith('```')) {
  28. const lang = line.slice(3).trim()
  29. const codeLines = []
  30. i++
  31. while (i < lines.length && !lines[i].startsWith('```')) {
  32. codeLines.push(lines[i])
  33. i++
  34. }
  35. i++ // 跳过结束 ```
  36. const escaped = codeLines.join('\n')
  37. .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  38. blocks.push(`<pre><code${lang ? ` class="language-${lang}"` : ''}>${escaped}</code></pre>`)
  39. continue
  40. }
  41. // 标题
  42. const headingMatch = line.match(/^(#{1,6}) (.+)/)
  43. if (headingMatch) {
  44. const level = headingMatch[1].length
  45. blocks.push(`<h${level}>${applyInline(headingMatch[2])}</h${level}>`)
  46. i++
  47. continue
  48. }
  49. // 分割线
  50. if (/^[-*_]{3,}\s*$/.test(line)) {
  51. blocks.push('<hr>')
  52. i++
  53. continue
  54. }
  55. // 引用块
  56. if (line.startsWith('> ') || line === '>') {
  57. const content = line.slice(line.startsWith('> ') ? 2 : 1)
  58. blocks.push(`<blockquote>${applyInline(content)}</blockquote>`)
  59. i++
  60. continue
  61. }
  62. // 无序列表:收集连续的列表项
  63. if (/^[ \t]*[-*+] /.test(line)) {
  64. const items = []
  65. while (i < lines.length && /^[ \t]*[-*+] /.test(lines[i])) {
  66. const m = lines[i].match(/^[ \t]*[-*+] (.+)/)
  67. if (m) items.push(`<li>${applyInline(m[1])}</li>`)
  68. i++
  69. }
  70. blocks.push(`<ul>${items.join('')}</ul>`)
  71. continue
  72. }
  73. // 有序列表:收集连续的列表项
  74. if (/^[ \t]*\d+\. /.test(line)) {
  75. const items = []
  76. while (i < lines.length && /^[ \t]*\d+\. /.test(lines[i])) {
  77. const m = lines[i].match(/^[ \t]*\d+\. (.+)/)
  78. if (m) items.push(`<li>${applyInline(m[1])}</li>`)
  79. i++
  80. }
  81. blocks.push(`<ol>${items.join('')}</ol>`)
  82. continue
  83. }
  84. // 普通段落
  85. blocks.push(`<p>${applyInline(line)}</p>`)
  86. i++
  87. }
  88. return blocks.join('\n')
  89. }
  90. // 行内样式处理
  91. function applyInline(text) {
  92. return text
  93. .replace(/\*\*\*([^*\n]+?)\*\*\*/g, '<strong><em>$1</em></strong>')
  94. .replace(/\*\*([^*\n]+?)\*\*/g, '<strong>$1</strong>')
  95. .replace(/\*([^*\n]+?)\*/g, '<em>$1</em>')
  96. .replace(/~~([^~\n]+?)~~/g, '<del>$1</del>')
  97. .replace(/`([^`\n]+)`/g, '<code>$1</code>')
  98. .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
  99. }