/** * Markdown渲染器 - 处理消息的Markdown格式化显示 */ class MarkdownRenderer { constructor() { this.isMarkdownMode = true; // 默认开启Markdown模式 this.codeBlockId = 0; // 用于生成唯一的代码块ID this.init(); } init() { // 加载用户偏好设置 const savedMode = localStorage.getItem('yundage_markdown_mode'); if (savedMode !== null) { this.isMarkdownMode = savedMode === 'true'; } } /** * 切换Markdown模式 */ toggleMarkdownMode() { this.isMarkdownMode = !this.isMarkdownMode; localStorage.setItem('yundage_markdown_mode', this.isMarkdownMode); return this.isMarkdownMode; } /** * 渲染Markdown内容 * @param {string} content - 原始Markdown内容 * @param {object} options - 渲染选项 * @returns {string} HTML内容 */ render(content, options = {}) { if (!this.isMarkdownMode) { // 纯文本模式,只转换换行 return this.escapeHtml(content).replace(/\n/g, '
'); } // 先强制替换所有数字序列为符号(有序列表) content = content.replace(/^\d+\.\s+/gm, '▶ '); // Markdown模式渲染 let html = content; // 1. 处理代码块(```语言\n代码\n```) html = this.renderCodeBlocks(html); // 2. 处理行内代码(`代码`) html = this.renderInlineCode(html); // 3. 处理标题(# ## ### 等) html = this.renderHeaders(html); // 4. 处理粗体(**文字** 或 __文字__) html = this.renderBold(html); // 5. 处理斜体(*文字* 或 _文字_) html = this.renderItalic(html); // 6. 处理链接([文字](链接)) html = this.renderLinks(html); // 7. 处理图片(![alt](src)) html = this.renderImages(html); // 8. 处理列表 html = this.renderLists(html); // 9. 处理引用(> 引用内容) html = this.renderBlockquotes(html); // 10. 处理表格 html = this.renderTables(html); // 11. 处理分隔线(--- 或 ***) html = this.renderHorizontalRules(html); // 12. 处理换行 html = this.renderLineBreaks(html); // 13. 最后强制处理所有数字序列,替换为符号 html = html.replace(/^\d+\.\s+/gm, '▸ '); return html; } /** * 转义HTML特殊字符 */ escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } /** * 渲染代码块 */ renderCodeBlocks(text) { const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; return text.replace(codeBlockRegex, (match, language, code) => { const id = `code-block-${this.codeBlockId++}`; const escapedCode = this.escapeHtml(code.trim()); const lang = language || 'plaintext'; return `
${lang}
${escapedCode}
`; }); } /** * 渲染行内代码 */ renderInlineCode(text) { return text.replace(/`([^`]+)`/g, '$1'); } /** * 渲染标题 */ renderHeaders(text) { // H6 到 H1(从小到大,避免误匹配) for (let i = 6; i >= 1; i--) { const regex = new RegExp(`^${'#'.repeat(i)}\\s+(.+)$`, 'gm'); text = text.replace(regex, `$1`); } return text; } getHeaderSize(level) { const sizes = ['3xl', '2xl', 'xl', 'lg', 'base', 'sm']; return sizes[level - 1] || 'base'; } /** * 渲染粗体 */ renderBold(text) { return text .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/__([^_]+)__/g, '$1'); } /** * 渲染斜体 */ renderItalic(text) { return text .replace(/\*([^*]+)\*/g, '$1') .replace(/_([^_]+)_/g, '$1'); } /** * 渲染链接 */ renderLinks(text) { return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); } /** * 渲染图片 */ renderImages(text) { return text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); } /** * 渲染列表 */ renderLists(text) { // 简化处理:直接将所有列表项包装为HTML,不添加额外符号 // 无序列表 text = text.replace(/^[\*\-\+]\s+(.+)$/gm, '
  • $1
  • '); // 有序列表(已经在前面被替换为▶符号了) text = text.replace(/^▶\s+(.+)$/gm, '
  • $1
  • '); // 统一包装所有列表项 text = text.replace(/(\n?)+/g, match => { return ``; }); return text; } /** * 渲染引用 */ renderBlockquotes(text) { return text.replace(/^>\s+(.+)$/gm, '
    $1
    '); } /** * 渲染表格 */ renderTables(text) { const tableRegex = /\|(.+)\|\n\|[\s\-\|]+\|\n((?:\|.+\|\n?)+)/g; return text.replace(tableRegex, (match, header, body) => { const headers = header.split('|').filter(h => h.trim()); const rows = body.trim().split('\n').map(row => row.split('|').filter(cell => cell.trim()) ); let tableHtml = ''; tableHtml += ''; headers.forEach(h => { tableHtml += ``; }); tableHtml += ''; rows.forEach(row => { tableHtml += ''; row.forEach(cell => { tableHtml += ``; }); tableHtml += ''; }); tableHtml += '
    ${h.trim()}
    ${cell.trim()}
    '; return tableHtml; }); } /** * 渲染分隔线 */ renderHorizontalRules(text) { return text.replace(/^[\-\*]{3,}$/gm, '
    '); } /** * 渲染换行 */ renderLineBreaks(text) { return text.replace(/\n/g, '
    '); } /** * 为代码块添加复制功能 */ initCodeCopyButtons() { document.addEventListener('click', (e) => { if (e.target.closest('.copy-code-btn')) { const btn = e.target.closest('.copy-code-btn'); const codeId = btn.dataset.codeId; const codeBlock = document.getElementById(codeId); if (codeBlock) { const code = codeBlock.textContent; navigator.clipboard.writeText(code).then(() => { const originalHtml = btn.innerHTML; btn.innerHTML = ' 已复制'; setTimeout(() => { btn.innerHTML = originalHtml; }, 2000); }); } } }); } /** * 创建Markdown工具栏 */ createToolbar() { return ` `; } /** * 处理工具栏操作 */ handleToolbarAction(action, textarea) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; const selectedText = text.substring(start, end); let replacement = ''; let cursorOffset = 0; switch (action) { case 'bold': replacement = `**${selectedText || '粗体文字'}**`; cursorOffset = selectedText ? replacement.length : 2; break; case 'italic': replacement = `*${selectedText || '斜体文字'}*`; cursorOffset = selectedText ? replacement.length : 1; break; case 'code': if (selectedText.includes('\n')) { replacement = `\`\`\`\n${selectedText}\n\`\`\``; cursorOffset = 4; } else { replacement = `\`${selectedText || '代码'}\``; cursorOffset = selectedText ? replacement.length : 1; } break; case 'link': replacement = `[${selectedText || '链接文字'}](URL)`; cursorOffset = selectedText ? replacement.length - 4 : 1; break; case 'list': replacement = `- ${selectedText || '列表项'}`; cursorOffset = 2; break; case 'quote': replacement = `> ${selectedText || '引用文字'}`; cursorOffset = 2; break; } textarea.value = text.substring(0, start) + replacement + text.substring(end); textarea.selectionStart = start + cursorOffset; textarea.selectionEnd = start + cursorOffset; textarea.focus(); } // 新增:为工具栏绑定事件 attachToolbarEvents(toolbar, textarea) { toolbar.addEventListener('click', (e) => { const button = e.target.closest('button[data-action]'); if (button) { const action = button.dataset.action; if (action === 'toggle-markdown') { this.toggleMarkdownMode(); button.textContent = this.isMarkdownMode ? 'Markdown: ON' : 'Markdown: OFF'; } else { this.insertMarkdown(textarea, action); } } }); } } // 创建全局实例 window.markdownRenderer = new MarkdownRenderer(); // 初始化工具栏函数 function initMarkdownToolbars() { console.log('初始化Markdown工具栏...'); // 为欢迎模式输入框创建工具栏 const welcomeToolbar = document.getElementById('markdownToolbar'); const welcomeInput = document.getElementById('messageInput'); if (welcomeToolbar && welcomeInput) { console.log('创建欢迎模式工具栏'); welcomeToolbar.innerHTML = window.markdownRenderer.createToolbar(); welcomeToolbar.style.display = 'flex'; window.markdownRenderer.attachToolbarEvents(welcomeToolbar, welcomeInput); } // 为聊天模式输入框创建工具栏 const chatToolbar = document.getElementById('chatModeMarkdownToolbar'); const chatInput = document.getElementById('chatModeMessageInput'); if (chatToolbar && chatInput) { console.log('创建聊天模式工具栏'); chatToolbar.innerHTML = window.markdownRenderer.createToolbar(); chatToolbar.style.display = 'flex'; window.markdownRenderer.attachToolbarEvents(chatToolbar, chatInput); } } // 初始化工具栏 document.addEventListener('DOMContentLoaded', function() { initMarkdownToolbars(); }); // 确保在所有资源加载完成后也初始化一次 window.addEventListener('load', function() { setTimeout(initMarkdownToolbars, 100); }); // 导出初始化函数供其他模块使用 window.initMarkdownToolbars = initMarkdownToolbars;