fix: BOM处理、异步读取、10MB文件限制、滚动同步精度优化

This commit is contained in:
thzxx
2026-05-18 17:33:09 +08:00
parent 1a5ec08728
commit 21d7b2da9f
2 changed files with 153 additions and 14 deletions
+20 -9
View File
@@ -1,6 +1,9 @@
const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { readFile, stat } = require('fs/promises');
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
let mainWindow = null; let mainWindow = null;
let activeFilePath = null; // Currently active tab's file path let activeFilePath = null; // Currently active tab's file path
@@ -149,15 +152,23 @@ async function showOpenDialog() {
return null; return null;
} }
function readFileContent(filePath) { async function readFileContent(filePath) {
return fs.readFileSync(filePath, 'utf-8'); // 文件大小检查
const stats = await stat(filePath);
if (stats.size > MAX_FILE_SIZE) {
throw new Error(`文件过大(${(stats.size / 1024 / 1024).toFixed(1)} MB),暂不支持超过 10MB 的文件`);
}
let content = await readFile(filePath, 'utf-8');
// 剥离 UTF-8 BOMWindows 某些编辑器会添加)
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
return content;
} }
// Open file in a new tab (renderer manages tabs) // Open file in a new tab (renderer manages tabs)
function openFileInTab(filePath) { async function openFileInTab(filePath) {
if (!mainWindow || mainWindow.isDestroyed()) return; if (!mainWindow || mainWindow.isDestroyed()) return;
try { try {
const content = readFileContent(filePath); const content = await readFileContent(filePath);
activeFilePath = filePath; activeFilePath = filePath;
startWatching(filePath); startWatching(filePath);
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
@@ -213,7 +224,7 @@ ipcMain.handle('dialog:openFile', async () => {
try { try {
const files = await showOpenDialog(); const files = await showOpenDialog();
if (files && files.length > 0) { if (files && files.length > 0) {
const content = readFileContent(files[0]); const content = await readFileContent(files[0]);
activeFilePath = files[0]; activeFilePath = files[0];
startWatching(files[0]); startWatching(files[0]);
mainWindow.setTitle(`MarkLite - ${path.basename(files[0])}`); mainWindow.setTitle(`MarkLite - ${path.basename(files[0])}`);
@@ -228,7 +239,7 @@ ipcMain.handle('dialog:openFile', async () => {
ipcMain.handle('file:read', async (event, filePath) => { ipcMain.handle('file:read', async (event, filePath) => {
try { try {
const content = readFileContent(filePath); const content = await readFileContent(filePath);
return { success: true, content }; return { success: true, content };
} catch (err) { } catch (err) {
return { success: false, error: err.message }; return { success: false, error: err.message };
@@ -277,8 +288,8 @@ ipcMain.handle('file:getCurrentPath', () => {
ipcMain.handle('file:stats', async (event, filePath) => { ipcMain.handle('file:stats', async (event, filePath) => {
try { try {
const stats = fs.statSync(filePath); const fileStat = await stat(filePath);
return { success: true, size: stats.size, mtime: stats.mtime.toISOString() }; return { success: true, size: fileStat.size, mtime: fileStat.mtime.toISOString() };
} catch (err) { } catch (err) {
return { success: false, error: err.message }; return { success: false, error: err.message };
} }
@@ -287,7 +298,7 @@ ipcMain.handle('file:stats', async (event, filePath) => {
ipcMain.handle('file:reload', async () => { ipcMain.handle('file:reload', async () => {
if (!activeFilePath) return { success: false, error: '没有打开的文件' }; if (!activeFilePath) return { success: false, error: '没有打开的文件' };
try { try {
const content = readFileContent(activeFilePath); const content = await readFileContent(activeFilePath);
return { success: true, content, filePath: activeFilePath }; return { success: true, content, filePath: activeFilePath };
} catch (err) { } catch (err) {
return { success: false, error: err.message }; return { success: false, error: err.message };
+129 -1
View File
@@ -321,12 +321,116 @@
// ===== Scroll Sync ===== // ===== Scroll Sync =====
let previewPositions = null; let previewPositions = null;
let lineToPreviewMap = null; // 行号 → 预览像素位置的映射
function cachePreviewPositions() { function cachePreviewPositions() {
previewPositions = []; previewPositions = [];
for (let i = 0; i < preview.children.length; i++) { for (let i = 0; i < preview.children.length; i++) {
previewPositions.push(preview.children[i].offsetTop); previewPositions.push(preview.children[i].offsetTop);
} }
// 构建行号→预览位置的映射表
buildLineToPreviewMap();
}
// 通过分析原始 Markdown 文本,建立编辑器行号到预览 DOM 位置的映射
function buildLineToPreviewMap() {
if (!previewPositions || previewPositions.length === 0) {
lineToPreviewMap = null;
return;
}
const text = editor.value;
const lines = text.split('\n');
const totalLines = lines.length;
// 找出每个"块级元素"在编辑器中的起始行
// 块级元素:标题、段落、代码块、列表、引用、表格、水平线
const blockStartLines = [];
let inCodeBlock = false;
for (let i = 0; i < totalLines; i++) {
const line = lines[i];
const trimmed = line.trimStart();
// 围栏代码块边界
if (trimmed.startsWith('```')) {
if (!inCodeBlock) {
blockStartLines.push(i);
inCodeBlock = true;
} else {
inCodeBlock = false;
}
continue;
}
if (inCodeBlock) continue;
// 块级元素起始行
if (
trimmed.startsWith('#') || // 标题
trimmed.startsWith('- ') || // 无序列表
trimmed.startsWith('* ') || // 无序列表
trimmed.startsWith('+ ') || // 无序列表
/^\d+\.\s/.test(trimmed) || // 有序列表
trimmed.startsWith('> ') || // 引用
trimmed.startsWith('|') || // 表格
trimmed.startsWith('---') || // 水平线
trimmed.startsWith('***') || // 水平线
trimmed.startsWith('___') || // 水平线
(trimmed === '' && i > 0 && lines[i - 1].trim() === '') // 连续空行=段落分隔
) {
blockStartLines.push(i);
}
}
// 去重并排序
const uniqueStarts = [...new Set(blockStartLines)].sort((a, b) => a - b);
// 将编辑器行号映射到预览 DOM 位置
// 每个块的起始行对应一个预览元素,用线性插值填充中间行
lineToPreviewMap = new Float32Array(totalLines);
if (uniqueStarts.length === 0) {
// 没有找到块边界,回退到线性映射
for (let i = 0; i < totalLines; i++) {
lineToPreviewMap[i] = (i / Math.max(1, totalLines - 1)) * previewPositions[previewPositions.length - 1];
}
return;
}
// 将块起始行映射到预览 DOM 索引
const domCount = previewPositions.length;
for (let i = 0; i < uniqueStarts.length; i++) {
const lineIdx = uniqueStarts[i];
const domIdx = Math.min(Math.floor((i / uniqueStarts.length) * domCount), domCount - 1);
lineToPreviewMap[lineIdx] = previewPositions[domIdx];
}
// 线性插值填充所有行
for (let i = 0; i < totalLines; i++) {
if (lineToPreviewMap[i] !== 0 && uniqueStarts.includes(i)) continue;
// 找到前后最近的已映射行
let prevLine = -1, nextLine = -1;
for (let j = i - 1; j >= 0; j--) {
if (lineToPreviewMap[j] !== 0 || uniqueStarts.includes(j)) { prevLine = j; break; }
}
for (let j = i + 1; j < totalLines; j++) {
if (lineToPreviewMap[j] !== 0 || uniqueStarts.includes(j)) { nextLine = j; break; }
}
if (prevLine === -1 && nextLine === -1) {
lineToPreviewMap[i] = 0;
} else if (prevLine === -1) {
lineToPreviewMap[i] = lineToPreviewMap[nextLine];
} else if (nextLine === -1) {
lineToPreviewMap[i] = lineToPreviewMap[prevLine];
} else {
// 线性插值
const ratio = (i - prevLine) / (nextLine - prevLine);
lineToPreviewMap[i] = lineToPreviewMap[prevLine] + ratio * (lineToPreviewMap[nextLine] - lineToPreviewMap[prevLine]);
}
}
} }
function syncScroll() { function syncScroll() {
@@ -334,17 +438,23 @@
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8; const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8;
const scrollTop = editor.scrollTop; const scrollTop = editor.scrollTop;
const topLine = Math.floor(scrollTop / lineHeight); const topLine = Math.floor(scrollTop / lineHeight);
const totalLines = editor.value.split('\n').length;
if (!previewPositions || previewPositions.length === 0) cachePreviewPositions(); if (!previewPositions || previewPositions.length === 0) cachePreviewPositions();
if (!previewPositions || previewPositions.length === 0) return; if (!previewPositions || previewPositions.length === 0) return;
// 使用行号→预览位置映射表
if (lineToPreviewMap && topLine >= 0 && topLine < lineToPreviewMap.length) {
previewPanel.scrollTop = lineToPreviewMap[topLine];
} else {
// fallback: 线性比例映射
const totalLines = editor.value.split('\n').length;
const ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0; const ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0;
const targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1); const targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1);
if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) { if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) {
previewPanel.scrollTop = previewPositions[targetIndex]; previewPanel.scrollTop = previewPositions[targetIndex];
} }
} }
}
// ===== Schedule Update ===== // ===== Schedule Update =====
function scheduleUpdate() { function scheduleUpdate() {
@@ -549,6 +659,15 @@
updateTitle(); updateTitle();
} }
// ===== File Size Limit =====
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// ===== File Operations ===== // ===== File Operations =====
async function handleOpenFile() { async function handleOpenFile() {
if (typeof window.electronAPI !== 'undefined') { if (typeof window.electronAPI !== 'undefined') {
@@ -565,6 +684,10 @@
Array.from(e.target.files).forEach(file => { Array.from(e.target.files).forEach(file => {
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase(); const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
if (!['.md', '.markdown', '.txt'].includes(ext)) return; if (!['.md', '.markdown', '.txt'].includes(ext)) return;
if (file.size > MAX_FILE_SIZE) {
alert(`"${file.name}" 过大(${formatFileSize(file.size)}),暂不支持超过 10MB 的文件`);
return;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (ev) => { reader.onload = (ev) => {
createNewTab(file.name, ev.target.result); createNewTab(file.name, ev.target.result);
@@ -725,6 +848,11 @@
const filePath = file.path; const filePath = file.path;
const ext = filePath ? filePath.substring(filePath.lastIndexOf('.')).toLowerCase() : ''; const ext = filePath ? filePath.substring(filePath.lastIndexOf('.')).toLowerCase() : '';
if (!allowedExts.includes(ext)) continue; if (!allowedExts.includes(ext)) continue;
// Electron 主进程已有文件大小检查,此处为浏览器模式兜底
if (file.size > MAX_FILE_SIZE) {
alert(`"${file.name}" 过大(${formatFileSize(file.size)}),暂不支持超过 10MB 的文件`);
continue;
}
if (filePath && typeof window.electronAPI !== 'undefined') { if (filePath && typeof window.electronAPI !== 'undefined') {
const result = await window.electronAPI.readFile(filePath); const result = await window.electronAPI.readFile(filePath);
if (result.success) { if (result.success) {