oimp
Version:
A CLI tool for generating OI problem and packages
1,089 lines (975 loc) • 45.2 kB
JavaScript
//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();
}
}
});