oimp
Version:
A CLI tool for generating OI problem and packages
317 lines (273 loc) • 11.7 kB
JavaScript
// 改进的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();
}
}