/** * 轻量级 Markdown 渲染,用于聊天气泡中的文本。 * 支持:链接、粗体、斜体、行内代码、代码块、换行。 */ function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderInline(text: string): string { // 行内代码 `code` — 优先处理,避免干扰其他标记 let html = text.replace(/`([^`]+)`/g, '$1'); // 图片 ![alt](url) html = html.replace( /!\[([^\]]*)]\(([^)]+)\)/g, '$1', ); // 链接 [text](url) html = html.replace( /\[([^\]]+)]\(([^)]+)\)/g, (_, label, url) => { const safeUrl = url.startsWith('http://') || url.startsWith('https://') || url.startsWith('mailto:') ? url : `http://${url}`; return `${label}`; }, ); // 粗体 **text** 或 __text__ html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); // 斜体 *text* 或 _text_ html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/_(.+?)_/g, '$1'); // 删除线 ~~text~~ html = html.replace(/~~(.+?)~~/g, '$1'); return html; } export function renderMarkdown(text: string): string { const lines = text.split('\n'); const result: string[] = []; let inCodeBlock = false; let codeBuf: string[] = []; let codeLang = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('```')) { if (inCodeBlock) { // 关闭代码块 const langClass = codeLang ? ` class="lang-${escapeHtml(codeLang)}"` : ''; result.push(`${escapeHtml(codeBuf.join('\n'))}`); codeBuf = []; codeLang = ''; inCodeBlock = false; } else { inCodeBlock = true; codeLang = line.slice(3).trim(); } continue; } if (inCodeBlock) { codeBuf.push(line); continue; } // 空行 if (line.trim() === '') { result.push('
'); continue; } // 标题 # ~ ###### const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { const level = headingMatch[1].length; const content = renderInline(escapeHtml(headingMatch[2])); result.push(`${content}`); continue; } // 无序列表 - 或 * if (/^[\s]*[-*+]\s+(.+)$/.test(line)) { const content = renderInline(escapeHtml(line.replace(/^[\s]*[-*+]\s+/, ''))); result.push(`
  • ${content}
  • `); continue; } // 有序列表 1. 2. const orderedMatch = line.match(/^(\s*)\d+\.\s+(.+)$/); if (orderedMatch) { const content = renderInline(escapeHtml(orderedMatch[2])); result.push(`
  • ${content}
  • `); continue; } // 普通段落 const content = renderInline(escapeHtml(line)); result.push(`

    ${content}

    `); } // 未闭合的代码块 if (inCodeBlock) { result.push(`
    ${escapeHtml(codeBuf.join('\n'))}
    `); } return result.join('\n'); }