UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

1,176 lines (1,039 loc) 47.3 kB
"use strict"; import { showSaveMsg } from "./ide-message.js"; import { setStatus } from "./ide-message.js"; import { isTableSeparator, updateCurrentFileDisplay, saveCurrentFile, getCurrentProblemId } from "./ide-utility.js"; export async function initMdEditor(editorDivId) { console.log("start initMdEditor"); // 初始化 textarea 到 #editor 内部 let mdTextarea = document.getElementById(editorDivId); if (!mdTextarea) { mdTextarea = document.createElement('textarea'); mdTextarea.id = editorDivId; mdTextarea.style.cssText = 'display:none; position:absolute; left:0; top:0; width:100%; height:100%; resize:none; font-size:15px; font-family:inherit; padding:12px 18px 32px 18px; box-sizing:border-box; outline:none; background:#1e1e1e; color:#e5e7eb; border:none; z-index:2;'; document.getElementById('editor').appendChild(mdTextarea); } window.mdTextarea = mdTextarea; //查找替换实现 findSearchInMdeditorHandlAddEvent(); // md 渲染相关 initRender(); window.syncPreviewToMdTextareaScroll = syncPreviewToMdTextareaScroll; window.updatePreviewFromMdTextarea = updatePreviewFromMdTextarea; // textarea 滚动时预览区同步到首个可见段落 window.mdTextarea.addEventListener('scroll', function () { syncPreviewToMdTextareaScroll(); }); // textarea 光标变动时预览区段落/代码块级同步 mdTextarea.addEventListener('keyup', syncPreviewToMdTextareaBlock); mdTextarea.addEventListener('click', syncPreviewToMdTextareaBlock); // 监听textarea内容变化 mdTextarea.addEventListener('input', function () { window.isMdDirty = mdTextarea.value !== window.lastSavedContent; updateCurrentFileDisplay(); window.updatePreviewFromMdTextarea(mdTextarea.value); }); // Ctrl+S保存 mdTextarea.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 's') { //showSaveMsg('保存中...'); e.preventDefault(); saveCurrentFile(); updateCurrentFileDisplay(); } }); // 粘贴图片 mdTextarea.addEventListener('paste', async function (event) { const items = event.clipboardData && event.clipboardData.items; if (!items) return; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.kind === 'file' && item.type.startsWith('image/')) { event.preventDefault(); const file = item.getAsFile(); const ext = file.type.split('/')[1] || 'png'; const rand = Array.from({ length: 16 }, () => Math.random().toString(36)[2]).join(''); const filename = rand + '.' + ext; let problemId = getCurrentProblemId && getCurrentProblemId(); if (!problemId) { alert('无法识别题目ID,图片粘贴失败'); return; } const formData = new FormData(); formData.append('file', file, filename); formData.append('filename', filename); formData.append('problemId', problemId); //console.log(formData); try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); if (data && data.relPath) { // 在光标处插入图片语法 const start = mdTextarea.selectionStart; const end = mdTextarea.selectionEnd; const insertText = `![粘贴图片](file://additional_file/${filename})`; mdTextarea.value = mdTextarea.value.slice(0, start) + insertText + mdTextarea.value.slice(end); mdTextarea.selectionStart = mdTextarea.selectionEnd = start + insertText.length; isMdDirty = true; updateCurrentFileDisplay(); updatePreviewFromMdTextarea(mdTextarea.value); } else { alert('图片上传失败'); } } catch (e) { alert('图片上传异常: ' + e.message); } } } }); // Ctrl+S 保存 window.addEventListener('keydown', function (e) { // 如果当前是Markdown编辑模式,不在此处处理保存,因为mdTextarea上已经有监听器 if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); // 避免在Markdown编辑模式下重复触发保存 if (!window.currentIsMarkdown) { saveCurrentFile(); } } }); return mdTextarea } //md-editor的具体实现 const mdTextarea = document.getElementById('md-textarea'); // 根据光标位置找到最匹配的渲染元素 function 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; } // 根据滚动位置同步预览 function 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 previewDiv = document.getElementById('preview-html'); if (previewDiv) { const previewScrollTop = scrollPercentage * (previewDiv.scrollHeight - previewDiv.clientHeight); previewDiv.scrollTop = previewScrollTop; return true; } // 使用行映射找到最接近的元素 const element = findMatchingElement(firstVisibleLine); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); return true; } return false; } // 滚动预览到指定行 function scrollToLine(lineNumber) { const element = findMatchingElement(lineNumber); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); return true; } return false; } // 预览区滚动到对应行 function scrollPreviewToEditorLine(line) { const element = findMatchingElement(line); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); return true; } return false; } // 根据光标位置同步预览 function syncPreviewOnCursor(textarea) { const pos = textarea.selectionStart; const textUpToPos = textarea.value.slice(0, pos); const lines = textUpToPos.split('\n'); const cursorLine = lines.length; return scrollToLine(cursorLine); } // marked renderer with data-line for sync const getCurrentLineInfo = (lineNoMap) => { const rtn = lineNoMap.shift() || { startLine: 1, endLine: 1 }; return { start: rtn.startLine, end: rtn.endLine }; }; // 安全文本处理函数 const safeText = (text) => { if (typeof text === 'string') return text; if (text && typeof text.text === 'string') return text.text; return ''; }; function initRender() { // 配置 marked 高亮 if (window.marked && window.hljs) { // 配置highlight.js忽略未转义HTML警告,因为我们完全控制输入源 window.hljs.configure({ignoreUnescapedHTML: true}); marked.setOptions({ highlight: function (code, lang) { if (window.hljs && lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; } // 未定义语言自动降级为 text return hljs.highlight(code, { language: 'plaintext' }).value; }, langPrefix: 'language-' }); } // HTML特殊字符转义函数 function escapeHtml(text) { const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } if (window.marked) { const renderer = new marked.Renderer(); //配置renderer规则 let lineNoMap = []; // 安全处理内联文本 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 = getCurrentLineInfo(lineNoMap); 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 = getCurrentLineInfo(lineNoMap); let codeStr = ''; if (typeof code === 'string') codeStr = escapeHtml(code); else if (code && typeof code.text === 'string') codeStr = escapeHtml(code.text); else if (code && typeof code.raw === 'string') codeStr = escapeHtml(code.raw); return `<pre data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="code"><code>${codeStr}</code></pre>`; }; // 标题渲染器 renderer.heading = function (token) { const content = this.parser.parseInline(token.tokens); const lineInfo = getCurrentLineInfo(lineNoMap); return `<h${token.depth} data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="heading">${content}</h${token.depth}>`; }; // 列表项渲染器 renderer.listitem = function (text) { const lineInfo = getCurrentLineInfo(lineNoMap); const content = this.parser.parse(text.tokens); return `<li data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="listitem">${content}</li>`; }; // 自定义表格渲染器 renderer.table = function (token) { const { header, rows, align } = token; // 渲染表头单元格 const renderHeaderCell = (cell, index) => { const alignClass = align[index] ? `text-${align[index]}` : ''; // 解析表头内的行内元素(如链接、粗体等) const content = this.parser.parseInline(cell.tokens); return `<th class="${alignClass}">${content}</th>`; }; // 渲染内容单元格 const renderBodyCell = (cell, index) => { const alignClass = align[index] ? `text-${align[index]}` : ''; // 解析内容单元格内的行内元素 const content = this.parser.parseInline(cell.tokens); return `<td class="${alignClass}">${content}</td>`; }; // 渲染表头行 const lineInfo1 = getCurrentLineInfo(lineNoMap); const headerRow = `<tr data-line-start="${lineInfo1.start}" data-line-end="${lineInfo1.end}">${header.map(renderHeaderCell).join('')}</tr>`; const thead = `<thead>${headerRow}</thead>`; // 渲染内容行 const bodyRows = rows.map((row) =>{ const lineInfo1 = getCurrentLineInfo(lineNoMap); return `<tr data-line-start="${lineInfo1.start}" data-line-end="${lineInfo1.end}">${row.map(renderBodyCell).join('')}</tr>`; } ).join(''); const tbody = `<tbody>${bodyRows}</tbody>`; // 生成完整表格 HTML(可添加自定义属性) const lineInfo = getCurrentLineInfo(lineNoMap); return `<table data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="table">${thead}${tbody}</table>`; }; // // 表格渲染器 // renderer.table = (header, body) => { // console.log(header,body) // const lineInfo = getCurrentLineInfo(lineNoMap); // return `<table data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="table"><thead>${header}</thead><tbody>${body}</tbody></table>`; // }; // // 表格单元格渲染器 // renderer.tablerow = (content) => { // const lineInfo = getCurrentLineInfo(lineNoMap); // return `<tr data-line-start='${lineInfo.start}' data-line-end='${lineInfo.end}' data-block-type='tablerow'>${content}</tr>`; // }; // // 表格头部单元格渲染器 // renderer.tablecell = (content, flags) => { // const lineInfo = getCurrentLineInfo(lineNoMap); // const tag = flags.header ? 'th' : 'td'; // const align = flags.align ? ` align="${flags.align}"` : ''; // return `<${tag}${align} data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="tablecell">${content}</${tag}>`; // }; // 引用块渲染器 renderer.blockquote = function (token) { // console.log(token); const content = this.parser.parse(token.tokens); const lineInfo = getCurrentLineInfo(lineNoMap); return `<blockquote data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="blockquote">${content}</blockquote>`; }; // 图片渲染器 // renderer.image = (href, title, text) => { // const lineInfo = getCurrentLineInfo(lineNoMap); // const titleAttr = title ? ` title="${title}"` : ''; // const altAttr = text ? ` alt="${text}"` : ''; // return `<img src="${href.toString()}" ${altAttr} ${titleAttr} data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="image" />`; // }; // // 链接渲染器 // renderer.link = (href, title, text) => { // const lineInfo = getCurrentLineInfo(lineNoMap); // const titleAttr = title ? ` title="${title}"` : ''; // const safeHref = href || '#'; // const safeTextContent = safeInline(text); // console.log(href,title,text); // return `<a href="${safeHref}"${titleAttr} data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="link">${safeTextContent}</a>`; // }; // 数学公式渲染器(行内公式) // renderer.codespan = (text) => { // const lineInfo = getCurrentLineInfo(lineNoMap); // // 检查是否是数学公式(以$开头和结尾) // if (text.startsWith('$') && text.endsWith('$') && text.length > 2) { // return `<span data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="math-inline">${text}</span>`; // } // return `<code data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="codespan">${text}</code>`; // }; marked.use({ renderer }); window._markedLineNoMap = lineNoMap; } } // 预览区整体渲染markdown function updatePreviewFromMdTextarea(content) { // 替换 file://additional_file/xxx 为 /files/additional_file/xxx const fixedContent = content.replace(/file:\/\/additional_file\//g, '/files/additional_file/'); // 统计每一行的起始行号 if (window._markedLineNoMap) { window._markedLineNoMap.length = 0; // 清空之前的映射关系 const lineMapping = window._markedLineNoMap; // 按行分割内容 const lines = fixedContent.split('\n'); let currentLine = 0; let inCodeBlock = false; let codeBlockStart = 0; let inTableBlock = false; let tableBlockStart = 0; // 遍历每一行,识别内容块 while (currentLine < lines.length) { const line = lines[currentLine]; // 检查是否是代码块开始或结束 if (/^```/.test(line)) { if (!inCodeBlock) { // 代码块开始 inCodeBlock = true; codeBlockStart = currentLine + 1; } else { // 代码块结束 inCodeBlock = false; lineMapping.push({ startLine: codeBlockStart, endLine: currentLine + 1, type: 'code' }); } currentLine++; continue; } // 如果在代码块中,继续下一行 if (inCodeBlock) { currentLine++; continue; } // 检查是否是表格开始 // 表格通常由 |---|---| 这样的分隔行开始 if (!inTableBlock && isTableSeparator(line)) { // 查找表格的开始行(通常是表头) let tableStart = currentLine; while (tableStart > 0 && lines[tableStart - 1].includes('|')) { tableStart--; } inTableBlock = true; tableBlockStart = tableStart + 1; currentLine++; continue; } // 检查表格是否结束 if (inTableBlock && !line.includes('|') && line.trim() !== '') { // 表格结束 inTableBlock = false; lineMapping.push({ startLine: tableBlockStart, endLine: currentLine, type: 'table' }); } // 如果在表格中,继续下一行 if (inTableBlock) { // 检查是否是表格的最后一行 if (currentLine + 1 >= lines.length || (!lines[currentLine + 1].includes('|') && lines[currentLine + 1].trim() !== '')) { // 表格结束 inTableBlock = false; lineMapping.push({ startLine: tableBlockStart, endLine: currentLine + 1, type: 'table' }); } currentLine++; continue; } // 检查是否是标题 if (/^#{1,6}\s/.test(line)) { 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++; } lineMapping.push({ startLine: currentLine + 1, endLine: listEnd, type: 'list' }); currentLine = listEnd; continue; } // 检查是否是引用块 if (/^>/.test(line)) { let blockquoteEnd = currentLine; while (blockquoteEnd < lines.length && /^>/.test(lines[blockquoteEnd])) { blockquoteEnd++; } lineMapping.push({ startLine: currentLine + 1, endLine: blockquoteEnd, type: 'blockquote' }); currentLine = blockquoteEnd; continue; } // 检查是否是分隔线 if (/^[-*]{3,}$/.test(line.trim())) { lineMapping.push({ startLine: currentLine + 1, endLine: currentLine + 1, type: 'hr' }); currentLine++; continue; } // 检查是否是空行(段落分隔) if (line.trim() === '') { currentLine++; continue; } // 查找段落块 let paragraphEnd = currentLine; while (paragraphEnd < lines.length && lines[paragraphEnd].trim() !== '') { paragraphEnd++; } lineMapping.push({ startLine: currentLine + 1, endLine: paragraphEnd, type: 'paragraph' }); currentLine = paragraphEnd; } // 如果文件以表格结束,确保表格被正确处理 if (inTableBlock) { lineMapping.push({ startLine: tableBlockStart, endLine: currentLine, type: 'table' }); } } let contents = fixedContent; contents = contents.replace(/\\\{/g, '\\\\{').replace(/\\\}/g, '\\\\}'); // console.log('【调试】updatePreviewFromEditor 渲染 html:', contents); const html = marked.parse(contents); // console.log('【调试】updatePreviewFromMdTextarea 渲染 html:', html); const previewDiv = document.getElementById('preview-html'); if (previewDiv) { previewDiv.innerHTML = html; // 代码高亮 if (window.hljs) { // 配置highlight.js忽略未转义HTML警告 window.hljs.configure({ignoreUnescapedHTML: true}); previewDiv.querySelectorAll('pre code').forEach(function (block) { // 先清空可能已有的高亮标记 block.className = ''; // 应用代码高亮 window.hljs.highlightElement(block); // 确保添加hljs类名以应用样式 if (!block.classList.contains('hljs')) { block.classList.add('hljs'); } // 为代码块添加暗色主题样式 block.style.cssText += 'background:#1e1e1e;color:#d4d4d4;'; }); } // 代码块复制按钮和样例导入按钮 // 修改逻辑:为所有代码块添加复制按钮,为"样例/示例"标题后的代码块添加额外功能按钮 const preList = Array.from(previewDiv.querySelectorAll('pre')); const headingList = Array.from(previewDiv.querySelectorAll('h1, h2, h3, h4, h5, h6')); // 找到第一个"样例/示例"标题 let firstSampleHeading = null; for (let h of headingList) { if (/样例|示例|Sample|Example|sample|example/i.test(h.textContent)) { firstSampleHeading = h; break; } } // 为所有代码块添加复制按钮 preList.forEach((pre, index) => { // 移除旧的复制按钮 const oldCopyBtn = pre.querySelector('.copy-btn'); if (oldCopyBtn) { oldCopyBtn.remove(); } // 添加复制按钮 const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.className = 'copy-btn'; copyBtn.style.cssText = 'position:absolute;top:6px;right:12px;font-size:12px;padding:2px 8px;border-radius:5px;background:#f3f4f6;color:#222;border:1px solid #d1d5db;cursor:pointer;z-index:10;transition:background 0.2s;'; copyBtn.onmouseenter = function () { this.style.background = '#f3f4f6'; }; copyBtn.onmouseleave = function () { this.style.background = 'transparent'; }; copyBtn.onclick = function (e) { e.stopPropagation(); const code = pre.querySelector('code'); if (code) { navigator.clipboard.writeText(code.innerText); copyBtn.textContent = '已复制'; setTimeout(() => { copyBtn.textContent = '复制'; }, 1200); } }; pre.style.position = 'relative'; pre.appendChild(copyBtn); }); // 仅为"样例/示例"标题后的代码块添加额外功能按钮 if (firstSampleHeading) { // 找到 heading 后的所有 pre(包括所有后续pre,不管中间有没有新heading) let cur = firstSampleHeading.nextElementSibling; let codeBlocks = []; while (cur) { if (cur.tagName === 'PRE') codeBlocks.push(cur); cur = cur.nextElementSibling; } // 用于保存每个代码块的"是否被取消"状态 let cancelState = codeBlocks.map(() => false); // 渲染所有按钮的函数 function renderSampleBtns() { // 先移除所有已存在的按钮容器 codeBlocks.forEach(pre => { // 移除外部按钮容器 if (pre.previousElementSibling && pre.previousElementSibling.classList && pre.previousElementSibling.classList.contains('sample-btn-outer-bar')) { pre.previousElementSibling.remove(); } }); // 递推编号和类型 let seq = 0; codeBlocks.forEach((pre, i) => { if (cancelState[i]) return; // 外部按钮容器 const outerBar = document.createElement('div'); outerBar.className = 'sample-btn-outer-bar'; outerBar.style.cssText = 'display:flex;justify-content:flex-end;align-items:center;margin-bottom:2px;'; const buttonGroup = document.createElement('div'); buttonGroup.style.cssText = 'display:inline-flex;align-items:center;border-radius:5px;overflow:hidden;'; const bar = document.createElement('div'); bar.className = 'sample-btn-bar'; bar.style.cssText = 'display:flex;align-items:center;'; let idx = seq + 1; let type = (idx % 2 === 1) ? 'in' : 'ans'; let fileIdx = Math.floor((idx + 1) / 2).toString().padStart(2, '0'); const btn = document.createElement('button'); btn.textContent = `写入 sample${fileIdx}.${type}`; btn.className = 'import-sample-btn'; btn.title = `将此代码块内容导入 sample/${'sample' + fileIdx + '.' + type}`; btn.style.cssText = 'font-size:12px;padding:2px 12px;background:transparent;color:#222;border:none;cursor:pointer;z-index:10;transition:background 0.2s;'; btn.onmouseenter = function () { this.style.background = '#f3f4f6'; }; btn.onmouseleave = function () { this.style.background = 'transparent'; }; btn.onclick = async function (e) { e.stopPropagation(); const code = pre.querySelector('code'); if (code) { const content = code.innerText.replace(/\r?\n$/, '') + '\n'; const msg = `即将写入文件: sample/sample${fileIdx}.${type}\n\n内容如下(UTF-8编码):\n\n${content.length > 200 ? content.slice(0, 200) + '...(内容过长已截断)' : content}`; if (!window.confirm(msg)) return; const res = await fetch('/api/file', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ path: `sample/sample${fileIdx}.${type}`, content }) }); if (res.ok) { showSaveMsg(`已导入 sample${fileIdx}.${type}`); } else { showSaveMsg(`导入 sample${fileIdx}.${type} 失败`, true); } } }; // 导入按钮 const importBtn = document.createElement('button'); importBtn.textContent = `读入 sample${fileIdx}.${type}`; importBtn.className = 'import-from-sample-btn'; importBtn.title = `将 sample/${'sample' + fileIdx + '.' + type} 文件内容导入此代码块,并同步到 Markdown`; importBtn.style.cssText = btn.style.cssText; importBtn.onmouseenter = btn.onmouseenter; importBtn.onmouseleave = btn.onmouseleave; importBtn.onclick = async function (e) { e.stopPropagation(); // 拉取 sample 文件内容 const res = await fetch(`/api/file?path=sample/sample${fileIdx}.${type}`); if (!res.ok) { showSaveMsg(`读取 sample${fileIdx}.${type} 失败`, true); return; } const content = await res.text(); // 找到 pre > code const code = pre.querySelector('code'); if (!code) return; // 替换 code 内容 code.innerText = content; // 动画高亮 pre.classList.add('import-flash'); setTimeout(() => pre.classList.remove('import-flash'), 1200); // 同步修改 markdown 源码 // 1. 找到该代码块在 markdown 源码中的起止行 const md = mdTextarea.value; const lines = md.split('\n'); let codeBlockIdx = 0, startLine = -1, endLine = -1, inCode = false; for (let i = 0; i < lines.length; i++) { if (/^```/.test(lines[i])) { inCode = !inCode; if (inCode) { codeBlockIdx++; if (codeBlockIdx === idx) startLine = i + 1; } else { if (codeBlockIdx === idx && startLine !== -1) { endLine = i; break; } } } } if (startLine !== -1 && endLine !== -1) { lines.splice(startLine, endLine - startLine, ...content.replace(/\r/g, '').split('\n')); mdTextarea.value = lines.join('\n'); isMdDirty = true; updateCurrentFileDisplay(); updatePreviewFromMdTextarea(mdTextarea.value); } showSaveMsg(`已导入 sample${fileIdx}.${type} 到代码块`); }; bar.appendChild(importBtn); // 竖线分隔符 const separator = document.createElement('span'); separator.style.cssText = 'width:1px;height:16px;background:#d1d5db;'; // bar.appendChild(separator); bar.appendChild(btn); // 竖线分隔符 const separator2 = document.createElement('span'); separator2.style.cssText = 'width:1px;height:16px;background:#d1d5db;'; // bar.appendChild(separator2); const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.className = 'cancel-sample-btn'; cancelBtn.title = '取消本代码块的导入,后续编号顺延'; cancelBtn.style.cssText = 'font-size:12px;padding:2px 12px;background:transparent;color:#222;border:none;cursor:pointer;z-index:10;transition:background 0.2s;'; cancelBtn.onmouseenter = function () { this.style.background = '#f3f4f6'; }; cancelBtn.onmouseleave = function () { this.style.background = 'transparent'; }; cancelBtn.onclick = function (e) { e.stopPropagation(); cancelState[i] = true; renderSampleBtns(); }; bar.appendChild(cancelBtn); buttonGroup.appendChild(bar); outerBar.appendChild(buttonGroup); pre.parentNode.insertBefore(outerBar, pre); seq++; }); } renderSampleBtns(); } if (window.renderMathInElement) { try { window.renderMathInElement(previewDiv, { delimiters: [ { left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false } ], throwOnError: false }); } catch (e) { console.error('KaTeX渲染失败', e); } } } } window.updatePreviewFromMdTextarea = updatePreviewFromMdTextarea; function syncPreviewToMdTextareaBlock() { const textarea = window.mdTextarea || document.getElementById('md-textarea'); if (textarea) { syncPreviewOnCursor(textarea); } } window.syncPreviewToMdTextareaBlock = syncPreviewToMdTextareaBlock; // 滚动同步函数 function syncPreviewToMdTextareaScroll() { const textarea = window.mdTextarea || document.getElementById('md-textarea'); if (textarea) { syncPreviewOnScroll(textarea); } } // 下面是md-editor的查找替换实现 // 获取DOM元素 - 所有id都添加了md-editor-前缀 const textArea = document.getElementById('md-textarea'); const searchPanel = document.getElementById('md-editor-searchPanel'); const findInput = document.getElementById('md-editor-findInput'); const replaceInput = document.getElementById('md-editor-replaceInput'); const findPrevBtn = document.getElementById('md-editor-findPrevBtn'); const findNextBtn = document.getElementById('md-editor-findNextBtn'); const replaceBtn = document.getElementById('md-editor-replaceBtn'); const replaceAllBtn = document.getElementById('md-editor-replaceAllBtn'); const closeBtn = document.getElementById('md-editor-closeBtn'); const caseSensitive = document.getElementById('md-editor-caseSensitive'); const panelstatus = document.getElementById('md-editor-status'); // 当前查找位置 let currentPos = 0; // 显示查找替换面板 function showSearchPanel() { searchPanel.style.display = 'block'; findInput.focus(); // 如果文本区域有选中内容,自动填入查找框 if (textArea.selectionEnd > textArea.selectionStart) { findInput.value = textArea.value.substring( textArea.selectionStart, textArea.selectionEnd ); } } // 隐藏查找替换面板 function hideSearchPanel() { searchPanel.style.display = 'none'; textArea.focus(); } // 查找下一个匹配项 - 修复了循环查找和索引计算问题 function findNext() { const findText = findInput.value.trim(); if (!findText) { setStatus('请输入要查找的内容'); return; } const text = textArea.value; const caseSens = caseSensitive.checked; const textLength = text.length; const findLength = findText.length; // 处理空文本情况 if (textLength === 0) { setStatus('文本区域为空'); return; } // 处理查找内容长于文本的情况 if (findLength > textLength) { setStatus(`找不到 "${findText}"`); return; } // 确定搜索的起始位置 let startPos = currentPos; if (startPos >= textLength) startPos = 0; // 使用正则表达式优化查找逻辑,解决大小写问题 const flags = caseSens ? 'g' : 'gi'; const regex = new RegExp(findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags); // 从起始位置开始搜索 let match; let foundIndex = -1; // 重置正则表达式的lastIndex以确保正确搜索 regex.lastIndex = startPos; while ((match = regex.exec(text)) !== null) { // 确保匹配结果正确(处理全局匹配时的lastIndex问题) if (match.index >= startPos) { foundIndex = match.index; break; } // 防止无限循环 if (regex.lastIndex >= textLength) break; } // 如果没有找到,从头开始搜索 if (foundIndex === -1 && startPos > 0) { regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { foundIndex = match.index; break; } } if (foundIndex !== -1) { // 选中找到的文本 textArea.focus(); textArea.setSelectionRange(foundIndex, foundIndex + findLength); currentPos = foundIndex + findLength; // 确保选中内容可见 scrollToSelection(); setStatus(`找到 "${findText}"`); } else { setStatus(`找不到 "${findText}"`); currentPos = 0; } } // 查找前一个匹配项 - 修复了反向查找的逻辑错误 function findPrev() { const findText = findInput.value.trim(); if (!findText) { setStatus('请输入要查找的内容'); return; } const text = textArea.value; const caseSens = caseSensitive.checked; const textLength = text.length; const findLength = findText.length; // 处理空文本情况 if (textLength === 0) { setStatus('文本区域为空'); return; } // 处理查找内容长于文本的情况 if (findLength > textLength) { setStatus(`找不到 "${findText}"`); return; } // 确定搜索的起始位置(从当前位置往前搜索) let startPos = currentPos > 0 ? currentPos - 1 : textLength; if (startPos < 0) startPos = 0; // 使用正则表达式优化查找逻辑 const flags = caseSens ? '' : 'i'; const regex = new RegExp(findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags); let foundIndex = -1; // 从起始位置向前搜索 for (let i = Math.min(startPos, textLength - findLength); i >= 0; i--) { const substring = text.substring(i, i + findLength); if (regex.test(substring)) { foundIndex = i; break; } } // 如果没找到,从文本末尾继续搜索 if (foundIndex === -1 && startPos < textLength) { for (let i = textLength - findLength; i >= 0; i--) { const substring = text.substring(i, i + findLength); if (regex.test(substring)) { foundIndex = i; break; } } } if (foundIndex !== -1) { // 选中找到的文本 textArea.focus(); textArea.setSelectionRange(foundIndex, foundIndex + findLength); currentPos = foundIndex; // 确保选中内容可见 scrollToSelection(); setStatus(`找到 "${findText}"`); } else { setStatus(`找不到 "${findText}"`); currentPos = textLength; } } // 替换当前匹配项 - 修复了替换后光标位置错误 function replace() { const findText = findInput.value.trim(); const replaceText = replaceInput.value; if (!findText) { setStatus('请输入要查找的内容'); return; } // 检查是否有选中的文本与查找文本匹配 const start = textArea.selectionStart; const end = textArea.selectionEnd; const selectedText = textArea.value.substring(start, end); const matches = caseSensitive.checked ? (selectedText === findText) : (selectedText.toLowerCase() === findText.toLowerCase()); if (matches) { // 替换选中的文本 const textBefore = textArea.value.substring(0, start); const textAfter = textArea.value.substring(end); textArea.value = textBefore + replaceText + textAfter; // 计算新的光标位置 const newPos = start + replaceText.length; currentPos = newPos; // 设置新的选中范围 textArea.setSelectionRange(newPos, newPos); setStatus(`替换了一处 "${findText}"`); } // 查找下一个 findNext(); } // 替换所有匹配项 - 优化了替换效率和计数准确性 function replaceAll() { const findText = findInput.value.trim(); const replaceText = replaceInput.value; if (!findText) { setStatus('请输入要查找的内容'); return; } const text = textArea.value; const caseSens = caseSensitive.checked; // 处理空文本情况 if (text.length === 0) { setStatus('文本区域为空'); return; } // 创建正则表达式,g表示全局匹配,i表示不区分大小写 const flags = 'g' + (caseSens ? '' : 'i'); const regex = new RegExp(findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags); // 计算替换数量(使用匹配数组长度) const matches = text.match(regex); const count = matches ? matches.length : 0; if (count > 0) { // 执行替换 textArea.value = text.replace(regex, replaceText); setStatus(`已替换 ${count} 处 "${findText}"`); currentPos = 0; // 取消选中状态 textArea.setSelectionRange(0, 0); } else { setStatus(`找不到 "${findText}"`); } } // 滚动到选中内容,确保可见 function scrollToSelection() { const textArea = window.mdTextarea || document.getElementById('md-textarea'); const textAreaRect = textArea.getBoundingClientRect(); const lineHeight = parseInt(getComputedStyle(textArea).lineHeight) || 20; const startPos = textArea.selectionStart; // 估算选中内容的行号和位置 const textBeforeCursor = textArea.value.substring(0, startPos); const lines = textBeforeCursor.split('\n'); const lineNumber = lines.length; const charInLine = lines[lines.length - 1].length; // 计算滚动位置,使选中行位于textarea中间 const scrollTop = (lineNumber - 1) * lineHeight - textAreaRect.height / 2 + lineHeight; // 直接滚动到选中内容 textArea.scrollTo({ top: Math.max(0, scrollTop) }); // 强制触发滚动事件以同步预览 if (window.syncPreviewToMdTextareaScroll) { setTimeout(() => { window.syncPreviewToMdTextareaScroll(); }, 50); } } export async function findSearchInMdeditorHandlAddEvent() { // 事件监听 - 修复了事件绑定和冲突问题 findPrevBtn.addEventListener('click', findPrev); findNextBtn.addEventListener('click', findNext); replaceBtn.addEventListener('click', replace); replaceAllBtn.addEventListener('click', replaceAll); closeBtn.addEventListener('click', hideSearchPanel); // 文本区域内容变化时重置当前位置 textArea.addEventListener('input', () => { currentPos = 0; }); // 当查找输入框内容变化时重置当前位置 findInput.addEventListener('input', () => { currentPos = 0; }); // 切换区分大小写时重置当前位置 caseSensitive.addEventListener('change', () => { currentPos = 0; }); // 处理快捷键 - 修复了快捷键冲突和触发问题 textArea.addEventListener('keydown', (e) => { // 精确检测Ctrl+F或Cmd+F const isOnlyCtrlOrCmd = (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey; // Ctrl+F (Windows/Linux) 或 Cmd+F (Mac) 显示查找面板 if (isOnlyCtrlOrCmd && e.key.toLowerCase() === 'f') { e.preventDefault(); e.stopPropagation(); if (searchPanel.style.display === 'block') { hideSearchPanel(); } else { showSearchPanel(); } return; } }, true); // 使用捕获阶段处理,避免事件被其他处理程序阻止 // 点击文本区域外部关闭面板 - 修复了关闭逻辑 document.addEventListener('click', (e) => { if (searchPanel.style.display === 'block' && !searchPanel.contains(e.target) && e.target !== textArea) { // 检查是否点击了textarea容器内的其他元素 if (!e.target.closest('.md-editor-textarea-container')) { hideSearchPanel(); } } }); }