From 76e1699ee7f268d984dde82d2fdea28de4a66423 Mon Sep 17 00:00:00 2001 From: thzxx Date: Mon, 18 May 2026 10:46:22 +0800 Subject: [PATCH] fix: resolve critical issues found in code review - Fix marked.js v12 code highlighting (removed deprecated highlight option, use custom renderer.code) - Fix open-file race condition (handle file association before window ready) - Fix Tab key undo history (use execCommand/setRangeText instead of direct value assignment) - Fix drag-drop file path tracking (use IPC readFile instead of FileReader) - Fix CSP security (remove unsafe-inline from script-src) - Improve scroll sync (line-based instead of percentage-based) - Deduplicate file open dialog logic - Fix .gitignore duplicate entry --- .gitignore | 1 - main.js | 65 +++++++++++++++++----------- renderer/index.html | 2 +- renderer/renderer.js | 101 ++++++++++++++++++++++++++++--------------- 4 files changed, 105 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index e090697..e7ff74d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Dependencies node_modules/ -dist/ package-lock.json # OS files diff --git a/main.js b/main.js index 9e93156..7db877e 100644 --- a/main.js +++ b/main.js @@ -4,6 +4,7 @@ const fs = require('fs'); let mainWindow = null; let currentFilePath = null; +let pendingFilePath = null; // For file association race condition function createWindow() { // Create the browser window @@ -120,7 +121,7 @@ function buildMenu() { Menu.setApplicationMenu(menu); } -async function handleOpenFile() { +async function showOpenDialog() { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [ @@ -128,9 +129,16 @@ async function handleOpenFile() { { name: '所有文件', extensions: ['*'] } ] }); - if (!result.canceled && result.filePaths.length > 0) { - openFile(result.filePaths[0]); + return result.filePaths[0]; + } + return null; +} + +async function handleOpenFile() { + const filePath = await showOpenDialog(); + if (filePath) { + openFile(filePath); } } @@ -147,16 +155,8 @@ function openFile(filePath) { // IPC Handlers ipcMain.handle('dialog:openFile', async () => { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] }, - { name: '所有文件', extensions: ['*'] } - ] - }); - - if (!result.canceled && result.filePaths.length > 0) { - const filePath = result.filePaths[0]; + const filePath = await showOpenDialog(); + if (filePath) { const content = fs.readFileSync(filePath, 'utf-8'); currentFilePath = filePath; mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); @@ -227,7 +227,7 @@ ipcMain.handle('file:getCurrentPath', () => { }); // Handle file open from command line or file association -function handleFileOpen() { +function getCommandLineFile() { const args = process.argv.slice(1); if (args.length > 0 && !args[0].startsWith('--')) { const filePath = args[0]; @@ -240,24 +240,37 @@ function handleFileOpen() { // App lifecycle app.whenReady().then(() => { + // Register open-file handler BEFORE creating window (fixes race condition) + app.on('open-file', (event, filePath) => { + event.preventDefault(); + if (mainWindow && mainWindow.webContents.isLoading()) { + // Window is still loading, queue the file + pendingFilePath = filePath; + } else if (mainWindow) { + openFile(filePath); + } else { + // Window not created yet, store for later + pendingFilePath = filePath; + } + }); + createWindow(); // Check for file passed via command line - const fileToOpen = handleFileOpen(); + const fileToOpen = getCommandLineFile(); if (fileToOpen) { - // Wait for window to be ready - mainWindow.webContents.on('did-finish-load', () => { - openFile(fileToOpen); - }); + pendingFilePath = fileToOpen; } - // Handle macOS open-file event - app.on('open-file', (event, filePath) => { - event.preventDefault(); - if (mainWindow) { - openFile(filePath); - } - }); + // Open pending file once window is fully ready + if (pendingFilePath) { + mainWindow.webContents.on('did-finish-load', () => { + if (pendingFilePath) { + openFile(pendingFilePath); + pendingFilePath = null; + } + }); + } }); app.on('window-all-closed', () => { diff --git a/renderer/index.html b/renderer/index.html index 11b579d..c65a81c 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -3,7 +3,7 @@ - + MarkLite diff --git a/renderer/renderer.js b/renderer/renderer.js index 61ce822..4d06a23 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -34,29 +34,27 @@ // ===== Initialize marked.js ===== function initMarked() { if (typeof marked !== 'undefined') { + // Custom renderer for code block highlighting (compatible with marked v12+) + const renderer = new marked.Renderer(); + renderer.code = function ({ text, lang }) { + let highlighted = text; + if (typeof hljs !== 'undefined') { + if (lang && hljs.getLanguage(lang)) { + try { highlighted = hljs.highlight(text, { language: lang }).value; } catch (e) { /* fallback */ } + } else { + try { highlighted = hljs.highlightAuto(text).value; } catch (e) { /* fallback */ } + } + } + const langClass = lang ? ` language-${lang}` : ''; + return `
${highlighted}
`; + }; + marked.setOptions({ gfm: true, breaks: true, - pedantic: false, - highlight: function (code, lang) { - if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(code, { language: lang }).value; - } catch (e) { - // fall through - } - } - // Auto detect if no language specified - if (typeof hljs !== 'undefined') { - try { - return hljs.highlightAuto(code).value; - } catch (e) { - // fall through - } - } - return code; - } + pedantic: false }); + marked.use({ renderer }); } } @@ -96,13 +94,25 @@ statusCursor.textContent = `行 ${line}, 列 ${col}`; } - // ===== Sync Scroll ===== + // ===== Sync Scroll (line-based for more accurate sync) ===== function syncScroll() { if (viewMode !== 'split') return; - const editorEl = editor; - const previewEl = previewPanel; - const scrollPercent = editorEl.scrollTop / (editorEl.scrollHeight - editorEl.clientHeight); - previewEl.scrollTop = scrollPercent * (previewEl.scrollHeight - previewEl.clientHeight); + + // Calculate which line is at the top of the editor viewport + const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8; + const scrollTop = editor.scrollTop; + const topLine = Math.floor(scrollTop / lineHeight); + + // Calculate corresponding scroll position in preview + const previewChildren = preview.children; + if (!previewChildren.length) return; + + // Find the element that corresponds to the current top line + // Approximate: map editor line ratio to preview element positions + const totalLines = editor.value.split('\n').length; + const ratio = totalLines > 0 ? topLine / totalLines : 0; + const targetScrollTop = ratio * (previewPanel.scrollHeight - previewPanel.clientHeight); + previewPanel.scrollTop = targetScrollTop; } // ===== Update Preview ===== @@ -287,12 +297,14 @@ document.addEventListener('dragenter', (e) => { e.preventDefault(); + e.stopPropagation(); dragCounter++; dropOverlay.classList.remove('hidden'); }); document.addEventListener('dragleave', (e) => { e.preventDefault(); + e.stopPropagation(); dragCounter--; if (dragCounter === 0) { dropOverlay.classList.add('hidden'); @@ -301,21 +313,36 @@ document.addEventListener('dragover', (e) => { e.preventDefault(); + e.stopPropagation(); }); - document.addEventListener('drop', (e) => { + document.addEventListener('drop', async (e) => { e.preventDefault(); + e.stopPropagation(); dragCounter = 0; dropOverlay.classList.add('hidden'); const files = e.dataTransfer.files; if (files.length > 0) { const file = files[0]; - const reader = new FileReader(); - reader.onload = (ev) => { - setEditorContent(ev.target.result, file.name); - }; - reader.readAsText(file); + const filePath = file.path; // Electron provides the real file path + + if (filePath && typeof window.electronAPI !== 'undefined') { + // Use IPC to read file through main process (tracks currentFilePath) + const result = await window.electronAPI.readFile(filePath); + if (result.success) { + setEditorContent(result.content, filePath); + } else { + statusText.textContent = '打开失败: ' + result.error; + } + } else { + // Fallback: use FileReader (browser mode) + const reader = new FileReader(); + reader.onload = (ev) => { + setEditorContent(ev.target.result, file.name); + }; + reader.readAsText(file); + } } }); } @@ -356,16 +383,18 @@ }); } - // ===== Tab key support in editor ===== + // ===== Tab key support in editor (preserves undo history) ===== function initEditorTab() { editor.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); - const start = editor.selectionStart; - const end = editor.selectionEnd; - const value = editor.value; - editor.value = value.substring(0, start) + ' ' + value.substring(end); - editor.selectionStart = editor.selectionEnd = start + 4; + // Use insertText command to preserve undo/redo history + if (!document.execCommand('insertText', false, ' ')) { + // Fallback for environments where execCommand is unsupported + const start = editor.selectionStart; + const end = editor.selectionEnd; + editor.setRangeText(' ', start, end, 'end'); + } scheduleUpdate(); } });