UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

317 lines (273 loc) 11.7 kB
// 改进的Markdown预览同步实现 // 在 Markdown 解析阶段,记录每个内容块(段落、标题、代码块等)在源码中的起始和结束行号 // 为渲染后的元素分配更准确的行号范围信息 // 实现一个智能查找算法,根据当前光标位置或滚动位置找到最匹配的渲染元素 class MarkdownSyncManager { constructor() { this.lineMapping = []; // 存储源码行号与渲染元素的映射关系 this.initRenderer(); } // 初始化自定义渲染器,记录每个内容块的行号信息 initRenderer() { if (window.marked) { const renderer = new marked.Renderer(); // 安全处理内联文本 const safeInline = (text) => { if (typeof text === 'string') return text; if (text && typeof text.text === 'string') return marked.parseInline(text.text); return ''; }; // 段落渲染器 renderer.paragraph = (text) => { const lineInfo = this.getCurrentLineInfo(); return `<p data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="paragraph">${safeInline(text)}</p>`; }; // 代码块渲染器 renderer.code = (code, infostring, escaped) => { const lineInfo = this.getCurrentLineInfo(); let codeStr = ''; if (typeof code === 'string') codeStr = code; else if (code && typeof code.text === 'string') codeStr = code.text; else if (code && typeof code.raw === 'string') codeStr = code.raw; return `<pre data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="code"><code>${codeStr}</code></pre>`; }; // 标题渲染器 renderer.heading = (...args) => { let text = ''; let level = 2; // marked 5.x+ 只传一个对象 if (args.length === 1 && typeof args[0] === 'object') { const obj = args[0]; text = obj.text || ''; level = obj.depth || 2; } else { // 兼容老版本 text = args[0]; level = args[1]; if (typeof level === 'object' && level && typeof level.level === 'number') { level = level.level; } if (typeof text === 'object' && text && typeof text.level === 'number') { level = text.level; text = text.text || ''; } } const lineInfo = this.getCurrentLineInfo(); return `<h${level} data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="heading">${safeInline(text)}</h${level}>`; }; // 列表项渲染器 renderer.listitem = (text) => { const lineInfo = this.getCurrentLineInfo(); return `<li data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="listitem">${safeInline(text)}</li>`; }; // 使用自定义渲染器 marked.use({ renderer }); } } // 获取当前内容块的行号信息 getCurrentLineInfo() { // 这里应该根据实际解析的Markdown内容计算行号 // 由于在浏览器环境中无法直接访问解析过程,我们使用一个简化的实现 // 实际项目中可以通过修改marked的解析过程来获取精确的行号信息 return { start: 1, end: 1 }; } // 解析Markdown内容并建立行号映射关系 parseMarkdownWithLineNumbers(content) { // 清空之前的映射关系 this.lineMapping = []; // 按行分割内容 const lines = content.split('\n'); let currentLine = 0; let inCodeBlock = false; let codeBlockStart = 0; // 遍历每一行,识别内容块 while (currentLine < lines.length) { const line = lines[currentLine]; // 检查是否是代码块开始或结束 if (/^```/.test(line)) { if (!inCodeBlock) { // 代码块开始 inCodeBlock = true; codeBlockStart = currentLine + 1; } else { // 代码块结束 inCodeBlock = false; this.lineMapping.push({ startLine: codeBlockStart, endLine: currentLine + 1, type: 'code' }); } currentLine++; continue; } // 如果在代码块中,继续下一行 if (inCodeBlock) { currentLine++; continue; } // 检查是否是标题 if (/^#{1,6}\s/.test(line)) { this.lineMapping.push({ startLine: currentLine + 1, endLine: currentLine + 1, type: 'heading' }); currentLine++; continue; } // 检查是否是列表项 if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line)) { // 查找整个列表块 let listEnd = currentLine; while (listEnd < lines.length && /^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[listEnd])) { listEnd++; } this.lineMapping.push({ startLine: currentLine + 1, endLine: listEnd, type: 'list' }); currentLine = listEnd; continue; } // 检查是否是空行(段落分隔) if (line.trim() === '') { currentLine++; continue; } // 查找段落块 let paragraphEnd = currentLine; while (paragraphEnd < lines.length && lines[paragraphEnd].trim() !== '') { paragraphEnd++; } this.lineMapping.push({ startLine: currentLine + 1, endLine: paragraphEnd, type: 'paragraph' }); currentLine = paragraphEnd; } return this.lineMapping; } // 根据光标位置找到最匹配的渲染元素 findMatchingElement(cursorLine) { const previewDiv = document.getElementById('preview-html'); if (!previewDiv) return null; // 精确匹配 let bestMatch = null; let minDistance = Infinity; // 遍历所有具有行号信息的元素 const elements = previewDiv.querySelectorAll('[data-line-start][data-line-end]'); elements.forEach(el => { const startLine = parseInt(el.getAttribute('data-line-start')); const endLine = parseInt(el.getAttribute('data-line-end')); // 精确匹配 if (cursorLine >= startLine && cursorLine <= endLine) { bestMatch = el; minDistance = 0; return; } // 寻找最近的匹配 const distance = Math.min( Math.abs(cursorLine - startLine), Math.abs(cursorLine - endLine) ); if (distance < minDistance) { minDistance = distance; bestMatch = el; } }); return bestMatch; } // 滚动预览到指定行 scrollToLine(lineNumber) { const element = this.findMatchingElement(lineNumber); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); return true; } return false; } // 根据滚动位置同步预览 syncPreviewOnScroll(textarea) { const scrollTop = textarea.scrollTop; const scrollHeight = textarea.scrollHeight; const clientHeight = textarea.clientHeight; const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight) || 20; const firstVisibleLine = Math.floor(scrollTop / lineHeight) + 1; // 当滚动到顶部时,确保同步到预览的顶部 if (scrollTop === 0) { const previewDiv = document.getElementById('preview-html'); if (previewDiv) { previewDiv.scrollTop = 0; return true; } } // 当滚动到底部时,确保同步到预览的底部 if (scrollTop + clientHeight >= scrollHeight) { const previewDiv = document.getElementById('preview-html'); if (previewDiv) { previewDiv.scrollTop = previewDiv.scrollHeight; return true; } } // 对于中间位置,使用行映射来计算更准确的位置 // 计算滚动百分比 const scrollPercentage = scrollTop / (scrollHeight - clientHeight); // 使用行映射找到最接近的元素 const element = this.findMatchingElement(firstVisibleLine); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); return true; } // 如果找不到精确匹配的元素,基于滚动百分比进行估算 const previewDiv = document.getElementById('preview-html'); if (previewDiv) { const previewScrollTop = scrollPercentage * (previewDiv.scrollHeight - previewDiv.clientHeight); previewDiv.scrollTop = previewScrollTop; return true; } return false; } // 根据光标位置同步预览 syncPreviewOnCursor(textarea) { const pos = textarea.selectionStart; const textUpToPos = textarea.value.slice(0, pos); const lines = textUpToPos.split('\n'); const cursorLine = lines.length; return this.scrollToLine(cursorLine); } } // 创建全局实例 window.markdownSyncManager = new MarkdownSyncManager(); // 重写现有的同步函数 function syncPreviewToMdTextareaBlock() { const textarea = window.mdTextarea || document.getElementById('md-textarea'); if (textarea && window.markdownSyncManager) { window.markdownSyncManager.syncPreviewOnCursor(textarea); } } function syncPreviewToMdTextareaScroll() { const textarea = window.mdTextarea || document.getElementById('md-textarea'); if (textarea && window.markdownSyncManager) { window.markdownSyncManager.syncPreviewOnScroll(textarea); } } // 更新预览函数,建立行号映射 function updatePreviewFromMdTextareaWithSync() { const textarea = window.mdTextarea || document.getElementById('md-textarea'); if (!textarea) return; const content = textarea.value; // 建立行号映射 if (window.markdownSyncManager) { window.markdownSyncManager.parseMarkdownWithLineNumbers(content); } // 调用原始的更新预览函数 if (typeof updatePreviewFromMdTextarea === 'function') { updatePreviewFromMdTextarea(); } }