From 21d7b2da9fafed43d5454f4b7681507de2dc6329 Mon Sep 17 00:00:00 2001 From: thzxx Date: Mon, 18 May 2026 17:33:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20BOM=E5=A4=84=E7=90=86=E3=80=81=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E8=AF=BB=E5=8F=96=E3=80=8110MB=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E9=99=90=E5=88=B6=E3=80=81=E6=BB=9A=E5=8A=A8=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E7=B2=BE=E5=BA=A6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 29 ++++++--- renderer/renderer.js | 138 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 153 insertions(+), 14 deletions(-) diff --git a/main.js b/main.js index de064ea..9e59959 100644 --- a/main.js +++ b/main.js @@ -1,6 +1,9 @@ const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); +const { readFile, stat } = require('fs/promises'); + +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB let mainWindow = null; let activeFilePath = null; // Currently active tab's file path @@ -149,15 +152,23 @@ async function showOpenDialog() { return null; } -function readFileContent(filePath) { - return fs.readFileSync(filePath, 'utf-8'); +async function readFileContent(filePath) { + // 文件大小检查 + 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 BOM(Windows 某些编辑器会添加) + if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1); + return content; } // Open file in a new tab (renderer manages tabs) -function openFileInTab(filePath) { +async function openFileInTab(filePath) { if (!mainWindow || mainWindow.isDestroyed()) return; try { - const content = readFileContent(filePath); + const content = await readFileContent(filePath); activeFilePath = filePath; startWatching(filePath); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); @@ -213,7 +224,7 @@ ipcMain.handle('dialog:openFile', async () => { try { const files = await showOpenDialog(); if (files && files.length > 0) { - const content = readFileContent(files[0]); + const content = await readFileContent(files[0]); activeFilePath = files[0]; startWatching(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) => { try { - const content = readFileContent(filePath); + const content = await readFileContent(filePath); return { success: true, content }; } catch (err) { return { success: false, error: err.message }; @@ -277,8 +288,8 @@ ipcMain.handle('file:getCurrentPath', () => { ipcMain.handle('file:stats', async (event, filePath) => { try { - const stats = fs.statSync(filePath); - return { success: true, size: stats.size, mtime: stats.mtime.toISOString() }; + const fileStat = await stat(filePath); + return { success: true, size: fileStat.size, mtime: fileStat.mtime.toISOString() }; } catch (err) { return { success: false, error: err.message }; } @@ -287,7 +298,7 @@ ipcMain.handle('file:stats', async (event, filePath) => { ipcMain.handle('file:reload', async () => { if (!activeFilePath) return { success: false, error: '没有打开的文件' }; try { - const content = readFileContent(activeFilePath); + const content = await readFileContent(activeFilePath); return { success: true, content, filePath: activeFilePath }; } catch (err) { return { success: false, error: err.message }; diff --git a/renderer/renderer.js b/renderer/renderer.js index b6ab853..b89035e 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -321,12 +321,116 @@ // ===== Scroll Sync ===== let previewPositions = null; + let lineToPreviewMap = null; // 行号 → 预览像素位置的映射 function cachePreviewPositions() { previewPositions = []; for (let i = 0; i < preview.children.length; i++) { 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() { @@ -334,15 +438,21 @@ const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8; const scrollTop = editor.scrollTop; const topLine = Math.floor(scrollTop / lineHeight); - const totalLines = editor.value.split('\n').length; if (!previewPositions || previewPositions.length === 0) cachePreviewPositions(); if (!previewPositions || previewPositions.length === 0) return; - const ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0; - const targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1); - if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) { - previewPanel.scrollTop = previewPositions[targetIndex]; + // 使用行号→预览位置映射表 + 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 targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1); + if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) { + previewPanel.scrollTop = previewPositions[targetIndex]; + } } } @@ -549,6 +659,15 @@ 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 ===== async function handleOpenFile() { if (typeof window.electronAPI !== 'undefined') { @@ -565,6 +684,10 @@ Array.from(e.target.files).forEach(file => { const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase(); 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(); reader.onload = (ev) => { createNewTab(file.name, ev.target.result); @@ -725,6 +848,11 @@ const filePath = file.path; const ext = filePath ? filePath.substring(filePath.lastIndexOf('.')).toLowerCase() : ''; 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') { const result = await window.electronAPI.readFile(filePath); if (result.success) {