西藏巴青项目

chatMarkdown.js 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML(默认 uni_modules 包不含 markdown 插件)
  3. */
  4. function escapeHtml(s) {
  5. return String(s)
  6. .replace(/&/g, '&')
  7. .replace(/</g, '&lt;')
  8. .replace(/>/g, '&gt;')
  9. .replace(/"/g, '&quot;')
  10. }
  11. /** 已是 HTML 片段时不再二次转义 */
  12. function looksLikeHtml(s) {
  13. return /<\/?[a-z][\s\S]*>/i.test(s)
  14. }
  15. /**
  16. * @param {string} text 原始 Markdown 或纯文本
  17. * @returns {string} HTML
  18. */
  19. export function markdownToHtml(text) {
  20. if (text == null || text === '') {
  21. return ''
  22. }
  23. const raw = String(text)
  24. if (looksLikeHtml(raw)) {
  25. return raw
  26. }
  27. const blocks = []
  28. const re = /```([\s\S]*?)```/g
  29. let last = 0
  30. let m
  31. while ((m = re.exec(raw)) !== null) {
  32. if (m.index > last) {
  33. blocks.push({ type: 'md', text: raw.slice(last, m.index) })
  34. }
  35. blocks.push({ type: 'code', text: m[1] })
  36. last = m.index + m[0].length
  37. }
  38. if (last < raw.length) {
  39. blocks.push({ type: 'md', text: raw.slice(last) })
  40. }
  41. if (!blocks.length) {
  42. blocks.push({ type: 'md', text: raw })
  43. }
  44. return blocks.map((b) => (b.type === 'code' ? formatCodeBlock(b.text) : formatMdBlock(b.text))).join('')
  45. }
  46. function formatCodeBlock(code) {
  47. const body = escapeHtml(code.replace(/^\n|\n$/g, ''))
  48. return `<pre style="margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:24rpx;line-height:1.45"><code>${body}</code></pre>`
  49. }
  50. function formatMdBlock(text) {
  51. const lines = text.split('\n')
  52. const out = []
  53. let i = 0
  54. while (i < lines.length) {
  55. const line = lines[i]
  56. const trimmed = line.trim()
  57. if (!trimmed) {
  58. i += 1
  59. continue
  60. }
  61. if (/^#{1,3}\s+/.test(trimmed)) {
  62. const level = trimmed.match(/^(#+)/)[1].length
  63. const content = inlineFormat(escapeHtml(trimmed.replace(/^#+\s+/, '')))
  64. const size = level === 1 ? '32rpx' : level === 2 ? '30rpx' : '28rpx'
  65. out.push(
  66. `<div style="margin:12px 0 8px;font-size:${size};font-weight:600;line-height:1.35">${content}</div>`
  67. )
  68. i += 1
  69. continue
  70. }
  71. if (/^[-*]\s+/.test(trimmed)) {
  72. const items = []
  73. while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
  74. items.push(inlineFormat(escapeHtml(lines[i].trim().replace(/^[-*]\s+/, ''))))
  75. i += 1
  76. }
  77. out.push(
  78. `<ul style="margin:6px 0 12px;padding-left:1.25em">${items
  79. .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
  80. .join('')}</ul>`
  81. )
  82. continue
  83. }
  84. if (/^\d+\.\s+/.test(trimmed)) {
  85. const items = []
  86. while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
  87. items.push(inlineFormat(escapeHtml(lines[i].trim().replace(/^\d+\.\s+/, ''))))
  88. i += 1
  89. }
  90. out.push(
  91. `<ol style="margin:6px 0 12px;padding-left:1.25em">${items
  92. .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
  93. .join('')}</ol>`
  94. )
  95. continue
  96. }
  97. if (/^>\s+/.test(trimmed)) {
  98. const quoteLines = []
  99. while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
  100. quoteLines.push(lines[i].trim().replace(/^>\s?/, ''))
  101. i += 1
  102. }
  103. const body = quoteLines.map((l) => inlineFormat(escapeHtml(l))).join('<br/>')
  104. out.push(
  105. `<blockquote style="margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55)">${body}</blockquote>`
  106. )
  107. continue
  108. }
  109. const para = []
  110. while (i < lines.length && lines[i].trim()) {
  111. para.push(lines[i])
  112. i += 1
  113. }
  114. const body = inlineFormat(escapeHtml(para.join('\n'))).replace(/\n/g, '<br/>')
  115. out.push(`<p style="margin:0 0 10px;line-height:1.55;word-break:break-word">${body}</p>`)
  116. }
  117. return out.join('') || `<p style="margin:0;line-height:1.55">${inlineFormat(escapeHtml(text))}</p>`
  118. }
  119. function inlineFormat(s) {
  120. return s
  121. .replace(/`([^`]+)`/g, '<code style="padding:2px 6px;border-radius:4px;background:rgba(0,0,0,0.06);font-size:24rpx">$1</code>')
  122. .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
  123. .replace(/\*([^*]+)\*/g, '<em>$1</em>')
  124. }
  125. /** mp-html tag-style 默认样式(对象形式,见插件文档) */
  126. export const MP_HTML_TAG_STYLE = {
  127. p: 'margin:0 0 10px;line-height:1.55;word-break:break-word;',
  128. div: 'word-break:break-word;',
  129. ul: 'margin:6px 0 12px;padding-left:1.25em;',
  130. ol: 'margin:6px 0 12px;padding-left:1.25em;',
  131. li: 'margin:4px 0;line-height:1.5;',
  132. pre: 'margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;',
  133. code: 'font-family:monospace;font-size:24rpx;',
  134. blockquote:
  135. 'margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);',
  136. a: 'color:#16a34a;text-decoration:underline;',
  137. table: 'border-collapse:collapse;margin:8px 0;font-size:24rpx;max-width:100%;',
  138. th: 'border:1px solid #ddd4c8;padding:6px 10px;background:#f0ebe3;',
  139. td: 'border:1px solid #ddd4c8;padding:6px 10px;'
  140. }
  141. export const MP_HTML_CONTAINER_STYLE =
  142. 'display:block;width:100%;min-width:0;overflow:visible;line-height:1.55;font-size:28rpx;color:#4a4542;word-break:break-word;'