UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

1,089 lines (975 loc) 45.2 kB
//md-editor的具体实现 document.addEventListener('DOMContentLoaded', function () { // // 光标变动时预览区跟随 // window.editor.onDidChangeCursorPosition(function (e) { // scrollPreviewToEditorLine(e.position.lineNumber); // }); const mdTextarea = document.getElementById('md-textarea'); window.mdTextarea = mdTextarea; // 根据光标位置找到最匹配的渲染元素 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 高亮 if (window.marked && window.hljs) { 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: 'text' }).value; }, langPrefix: 'language-' }); } // 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 }; }; 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 = 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 = getCurrentLineInfo(lineNoMap); 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 = getCurrentLineInfo(lineNoMap); return `<li data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="listitem">${safeInline(text)}</li>`; }; // 表格渲染器 renderer.table = (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 = (quote) => { const lineInfo = getCurrentLineInfo(lineNoMap); return `<blockquote data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="blockquote">${quote}</blockquote>`; }; // 图片渲染器 renderer.image = (href, title, text) => { const lineInfo = getCurrentLineInfo(lineNoMap); const titleAttr = title ? ` title="${title}"` : ''; const altAttr = text ? ` alt="${text}"` : ''; return `<img src="${href}"${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}"` : ''; return `<a href="${href}"${titleAttr} data-line-start="${lineInfo.start}" data-line-end="${lineInfo.end}" data-block-type="link">${text}</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' }); } // const lines = fixedContent.split('\n'); // for (let i = 0; i < lines.length; i++) { // window._markedLineNoMap.push(i + 1); // } } 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) { previewDiv.querySelectorAll('pre code').forEach(function (block) { window.hljs.highlightElement(block); if (!block.classList.contains('hljs')) block.classList.add('hljs'); }); } // 代码块复制按钮和样例导入按钮 // 新逻辑:找到"样例/示例"标题后,依次为其后所有代码块添加按钮,编号顺延,取消后后续编号顺延 // 只处理第一个"样例/示例"标题后的所有代码块 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; } } 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(); } // 移除旧的复制按钮 const oldCopyBtn = pre.querySelector('.copy-btn'); if (oldCopyBtn) { oldCopyBtn.remove(); } }); // 递推编号和类型 let seq = 0; codeBlocks.forEach((pre, i) => { if (cancelState[i]) return; // 复制按钮(保留在pre右上角) 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); // 外部按钮容器 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; // textarea 光标变动时预览区段落/代码块级同步 window.mdTextarea.addEventListener('keyup', syncPreviewToMdTextareaBlock); window.mdTextarea.addEventListener('click', syncPreviewToMdTextareaBlock); // 滚动同步函数 function syncPreviewToMdTextareaScroll() { const textarea = window.mdTextarea || document.getElementById('md-textarea'); if (textarea) { syncPreviewOnScroll(textarea); } } window.syncPreviewToMdTextareaScroll = syncPreviewToMdTextareaScroll; // textarea 滚动时预览区同步到首个可见段落 window.mdTextarea.addEventListener('scroll', function () { syncPreviewToMdTextareaScroll(); // const scrollTop = window.mdTextarea.scrollTop; // const lineHeight = parseInt(window.getComputedStyle(mdTextarea).lineHeight) || 20; // const allLines = window.mdTextarea.value.split('\n'); // const firstVisibleLine = Math.floor(scrollTop / lineHeight); // // 找到首个可见段落的起始行 // let blockStart = 0, inCode = false; // for (let i = 0; i <= firstVisibleLine; i++) { // const line = allLines[i]; // if (/^``/.test(line)) inCode = !inCode; // if (!inCode && line.trim() === '' && i < firstVisibleLine) blockStart = i + 1; // } // // 取该段落内容 // let blockEnd = allLines.length; // inCode = false; // for (let i = blockStart; i < allLines.length; i++) { // const line = allLines[i]; // if (/^``/.test(line)) inCode = !inCode; // if (!inCode && line.trim() === '' && i > blockStart) { blockEnd = i; break; } // } // const blockText = allLines.slice(blockStart, blockEnd).join('\n').trim(); // const previewDiv = document.getElementById('preview-html'); // if (previewDiv && blockText) { // let found = null; // previewDiv.childNodes.forEach(el => { // if (!found && el.textContent && el.textContent.trim().replace(/\s+/g, '') === blockText.replace(/\s+/g, '')) { // found = el; // } // }); // if (found) { // found.scrollIntoView({ behavior: 'smooth', block: 'start' }); // } // } }); }); // 下面是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 setStatus(message) { panelstatus.textContent = message; // 清除之前的计时器,确保消息显示时间准确 if (panelstatus.timeoutId) { clearTimeout(panelstatus.timeoutId); } // 3秒后清除状态消息 panelstatus.timeoutId = setTimeout(() => { if (panelstatus.textContent === message) { panelstatus.textContent = ''; } }, 3000); } // 滚动到选中内容,确保可见 function scrollToSelection() { const textAreaRect = textArea.getBoundingClientRect(); const lineHeight = parseInt(getComputedStyle(textArea).lineHeight); const startPos = textArea.selectionStart; // 估算选中内容的行号和位置 const lines = textArea.value.substring(0, startPos).split('\n'); const lineNumber = lines.length - 1; const scrollTop = lineNumber * lineHeight - textAreaRect.height / 2; // 平滑滚动到选中内容 textArea.scrollTo({ top: scrollTop, behavior: 'smooth' }); } // 事件监听 - 修复了事件绑定和冲突问题 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; } // 在查找面板显示时处理其他快捷键 // if (searchPanel.style.display === 'block') { // // Enter键查找下一个 // if (e.key === 'Enter' && // document.activeElement !== findInput && // document.activeElement !== replaceInput) { // e.preventDefault(); // findNext(); // } // // F3键查找下一个,Shift+F3查找前一个 // if (e.key === 'F3') { // e.preventDefault(); // e.stopPropagation(); // if (e.shiftKey) { // findPrev(); // } else { // findNext(); // } // } // // Alt+R 替换 // if (e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'r') { // e.preventDefault(); // e.stopPropagation(); // replace(); // } // // Alt+A 全部替换 // if (e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'a') { // e.preventDefault(); // e.stopPropagation(); // replaceAll(); // } // // Esc键关闭面板 // if (e.key === 'Escape') { // e.preventDefault(); // hideSearchPanel(); // } // } }, 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(); } } });