Files
memind/src/utils/markdown.ts
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

121 lines
3.3 KiB
TypeScript

/**
* 轻量级 Markdown 渲染,用于聊天气泡中的文本。
* 支持:链接、粗体、斜体、行内代码、代码块、换行。
*/
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function renderInline(text: string): string {
// 行内代码 `code` — 优先处理,避免干扰其他标记
let html = text.replace(/`([^`]+)`/g, '<code>$1</code>');
// 图片 ![alt](url)
html = html.replace(
/!\[([^\]]*)]\(([^)]+)\)/g,
'<img src="$2" alt="$1" loading="lazy" class="md-img" />',
);
// 链接 [text](url)
html = html.replace(
/\[([^\]]+)]\(([^)]+)\)/g,
(_, label, url) => {
const safeUrl = url.startsWith('http://') || url.startsWith('https://') || url.startsWith('mailto:')
? url
: `http://${url}`;
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${label}</a>`;
},
);
// 粗体 **text** 或 __text__
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// 斜体 *text* 或 _text_
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/_(.+?)_/g, '<em>$1</em>');
// 删除线 ~~text~~
html = html.replace(/~~(.+?)~~/g, '<s>$1</s>');
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(`<pre${langClass}><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
codeBuf = [];
codeLang = '';
inCodeBlock = false;
} else {
inCodeBlock = true;
codeLang = line.slice(3).trim();
}
continue;
}
if (inCodeBlock) {
codeBuf.push(line);
continue;
}
// 空行
if (line.trim() === '') {
result.push('<br>');
continue;
}
// 标题 # ~ ######
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const content = renderInline(escapeHtml(headingMatch[2]));
result.push(`<h${level} class="md-h${level}">${content}</h${level}>`);
continue;
}
// 无序列表 - 或 *
if (/^[\s]*[-*+]\s+(.+)$/.test(line)) {
const content = renderInline(escapeHtml(line.replace(/^[\s]*[-*+]\s+/, '')));
result.push(`<li class="md-li">${content}</li>`);
continue;
}
// 有序列表 1. 2.
const orderedMatch = line.match(/^(\s*)\d+\.\s+(.+)$/);
if (orderedMatch) {
const content = renderInline(escapeHtml(orderedMatch[2]));
result.push(`<li class="md-li">${content}</li>`);
continue;
}
// 普通段落
const content = renderInline(escapeHtml(line));
result.push(`<p class="md-p">${content}</p>`);
}
// 未闭合的代码块
if (inCodeBlock) {
result.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
}
return result.join('\n');
}