| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- /**
- * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML(默认 uni_modules 包不含 markdown 插件)
- */
- function escapeHtml(s) {
- return String(s)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- }
- /** 已是 HTML 片段时不再二次转义 */
- function looksLikeHtml(s) {
- return /<\/?[a-z][\s\S]*>/i.test(s)
- }
- /**
- * @param {string} text 原始 Markdown 或纯文本
- * @returns {string} HTML
- */
- export function markdownToHtml(text) {
- if (text == null || text === '') {
- return ''
- }
- const raw = String(text)
- if (looksLikeHtml(raw)) {
- return raw
- }
- const blocks = []
- const re = /```([\s\S]*?)```/g
- let last = 0
- let m
- while ((m = re.exec(raw)) !== null) {
- if (m.index > last) {
- blocks.push({ type: 'md', text: raw.slice(last, m.index) })
- }
- blocks.push({ type: 'code', text: m[1] })
- last = m.index + m[0].length
- }
- if (last < raw.length) {
- blocks.push({ type: 'md', text: raw.slice(last) })
- }
- if (!blocks.length) {
- blocks.push({ type: 'md', text: raw })
- }
- return blocks.map((b) => (b.type === 'code' ? formatCodeBlock(b.text) : formatMdBlock(b.text))).join('')
- }
- function formatCodeBlock(code) {
- const body = escapeHtml(code.replace(/^\n|\n$/g, ''))
- 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>`
- }
- function formatMdBlock(text) {
- const lines = text.split('\n')
- const out = []
- let i = 0
- while (i < lines.length) {
- const line = lines[i]
- const trimmed = line.trim()
- if (!trimmed) {
- i += 1
- continue
- }
- if (/^#{1,3}\s+/.test(trimmed)) {
- const level = trimmed.match(/^(#+)/)[1].length
- const content = inlineFormat(escapeHtml(trimmed.replace(/^#+\s+/, '')))
- const size = level === 1 ? '32rpx' : level === 2 ? '30rpx' : '28rpx'
- out.push(
- `<div style="margin:12px 0 8px;font-size:${size};font-weight:600;line-height:1.35">${content}</div>`
- )
- i += 1
- continue
- }
- if (/^[-*]\s+/.test(trimmed)) {
- const items = []
- while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
- items.push(inlineFormat(escapeHtml(lines[i].trim().replace(/^[-*]\s+/, ''))))
- i += 1
- }
- out.push(
- `<ul style="margin:6px 0 12px;padding-left:1.25em">${items
- .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
- .join('')}</ul>`
- )
- continue
- }
- if (/^\d+\.\s+/.test(trimmed)) {
- const items = []
- while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
- items.push(inlineFormat(escapeHtml(lines[i].trim().replace(/^\d+\.\s+/, ''))))
- i += 1
- }
- out.push(
- `<ol style="margin:6px 0 12px;padding-left:1.25em">${items
- .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
- .join('')}</ol>`
- )
- continue
- }
- if (/^>\s+/.test(trimmed)) {
- const quoteLines = []
- while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
- quoteLines.push(lines[i].trim().replace(/^>\s?/, ''))
- i += 1
- }
- const body = quoteLines.map((l) => inlineFormat(escapeHtml(l))).join('<br/>')
- out.push(
- `<blockquote style="margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55)">${body}</blockquote>`
- )
- continue
- }
- const para = []
- while (i < lines.length && lines[i].trim()) {
- para.push(lines[i])
- i += 1
- }
- const body = inlineFormat(escapeHtml(para.join('\n'))).replace(/\n/g, '<br/>')
- out.push(`<p style="margin:0 0 10px;line-height:1.55;word-break:break-word">${body}</p>`)
- }
- return out.join('') || `<p style="margin:0;line-height:1.55">${inlineFormat(escapeHtml(text))}</p>`
- }
- function inlineFormat(s) {
- return s
- .replace(/`([^`]+)`/g, '<code style="padding:2px 6px;border-radius:4px;background:rgba(0,0,0,0.06);font-size:24rpx">$1</code>')
- .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
- .replace(/\*([^*]+)\*/g, '<em>$1</em>')
- }
- /** mp-html tag-style 默认样式(对象形式,见插件文档) */
- export const MP_HTML_TAG_STYLE = {
- p: 'margin:0 0 10px;line-height:1.55;word-break:break-word;',
- div: 'word-break:break-word;',
- ul: 'margin:6px 0 12px;padding-left:1.25em;',
- ol: 'margin:6px 0 12px;padding-left:1.25em;',
- li: 'margin:4px 0;line-height:1.5;',
- pre: 'margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;',
- code: 'font-family:monospace;font-size:24rpx;',
- blockquote:
- 'margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);',
- a: 'color:#16a34a;text-decoration:underline;',
- table: 'border-collapse:collapse;margin:8px 0;font-size:24rpx;max-width:100%;',
- th: 'border:1px solid #ddd4c8;padding:6px 10px;background:#f0ebe3;',
- td: 'border:1px solid #ddd4c8;padding:6px 10px;'
- }
- export const MP_HTML_CONTAINER_STYLE =
- 'display:block;width:100%;min-width:0;overflow:visible;line-height:1.55;font-size:28rpx;color:#4a4542;word-break:break-word;'
|