/** * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML(默认 uni_modules 包不含 markdown 插件) */ function escapeHtml(s) { return String(s) .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 `
${body}`
}
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(
`${body}` ) 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, '
${body}
`) } return out.join('') || `${inlineFormat(escapeHtml(text))}
` } function inlineFormat(s) { return s .replace(/`([^`]+)`/g, '$1')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
}
/** 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;'