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
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let currentFilePath = null;
|
let currentFilePath = null;
|
||||||
|
let pendingFilePath = null; // For file association race condition
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Create the browser window
|
// Create the browser window
|
||||||
@@ -120,7 +121,7 @@ function buildMenu() {
|
|||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOpenFile() {
|
async function showOpenDialog() {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
@@ -128,9 +129,16 @@ async function handleOpenFile() {
|
|||||||
{ name: '所有文件', extensions: ['*'] }
|
{ name: '所有文件', extensions: ['*'] }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
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
|
// IPC Handlers
|
||||||
ipcMain.handle('dialog:openFile', async () => {
|
ipcMain.handle('dialog:openFile', async () => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const filePath = await showOpenDialog();
|
||||||
properties: ['openFile'],
|
if (filePath) {
|
||||||
filters: [
|
|
||||||
{ name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] },
|
|
||||||
{ name: '所有文件', extensions: ['*'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
|
||||||
const filePath = result.filePaths[0];
|
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
currentFilePath = filePath;
|
currentFilePath = filePath;
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
||||||
@@ -227,7 +227,7 @@ ipcMain.handle('file:getCurrentPath', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle file open from command line or file association
|
// Handle file open from command line or file association
|
||||||
function handleFileOpen() {
|
function getCommandLineFile() {
|
||||||
const args = process.argv.slice(1);
|
const args = process.argv.slice(1);
|
||||||
if (args.length > 0 && !args[0].startsWith('--')) {
|
if (args.length > 0 && !args[0].startsWith('--')) {
|
||||||
const filePath = args[0];
|
const filePath = args[0];
|
||||||
@@ -240,24 +240,37 @@ function handleFileOpen() {
|
|||||||
|
|
||||||
// App lifecycle
|
// App lifecycle
|
||||||
app.whenReady().then(() => {
|
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();
|
createWindow();
|
||||||
|
|
||||||
// Check for file passed via command line
|
// Check for file passed via command line
|
||||||
const fileToOpen = handleFileOpen();
|
const fileToOpen = getCommandLineFile();
|
||||||
if (fileToOpen) {
|
if (fileToOpen) {
|
||||||
// Wait for window to be ready
|
pendingFilePath = fileToOpen;
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
|
||||||
openFile(fileToOpen);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle macOS open-file event
|
// Open pending file once window is fully ready
|
||||||
app.on('open-file', (event, filePath) => {
|
if (pendingFilePath) {
|
||||||
event.preventDefault();
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
if (mainWindow) {
|
if (pendingFilePath) {
|
||||||
openFile(filePath);
|
openFile(pendingFilePath);
|
||||||
}
|
pendingFilePath = null;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:;">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https:;">
|
||||||
<title>MarkLite</title>
|
<title>MarkLite</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="../lib/highlight-github.css">
|
<link rel="stylesheet" href="../lib/highlight-github.css">
|
||||||
|
|||||||
+65
-36
@@ -34,29 +34,27 @@
|
|||||||
// ===== Initialize marked.js =====
|
// ===== Initialize marked.js =====
|
||||||
function initMarked() {
|
function initMarked() {
|
||||||
if (typeof marked !== 'undefined') {
|
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 `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`;
|
||||||
|
};
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
pedantic: false,
|
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
marked.use({ renderer });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +94,25 @@
|
|||||||
statusCursor.textContent = `行 ${line}, 列 ${col}`;
|
statusCursor.textContent = `行 ${line}, 列 ${col}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Sync Scroll =====
|
// ===== Sync Scroll (line-based for more accurate sync) =====
|
||||||
function syncScroll() {
|
function syncScroll() {
|
||||||
if (viewMode !== 'split') return;
|
if (viewMode !== 'split') return;
|
||||||
const editorEl = editor;
|
|
||||||
const previewEl = previewPanel;
|
// Calculate which line is at the top of the editor viewport
|
||||||
const scrollPercent = editorEl.scrollTop / (editorEl.scrollHeight - editorEl.clientHeight);
|
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8;
|
||||||
previewEl.scrollTop = scrollPercent * (previewEl.scrollHeight - previewEl.clientHeight);
|
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 =====
|
// ===== Update Preview =====
|
||||||
@@ -287,12 +297,14 @@
|
|||||||
|
|
||||||
document.addEventListener('dragenter', (e) => {
|
document.addEventListener('dragenter', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
dragCounter++;
|
dragCounter++;
|
||||||
dropOverlay.classList.remove('hidden');
|
dropOverlay.classList.remove('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('dragleave', (e) => {
|
document.addEventListener('dragleave', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
dragCounter--;
|
dragCounter--;
|
||||||
if (dragCounter === 0) {
|
if (dragCounter === 0) {
|
||||||
dropOverlay.classList.add('hidden');
|
dropOverlay.classList.add('hidden');
|
||||||
@@ -301,21 +313,36 @@
|
|||||||
|
|
||||||
document.addEventListener('dragover', (e) => {
|
document.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('drop', (e) => {
|
document.addEventListener('drop', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
dragCounter = 0;
|
dragCounter = 0;
|
||||||
dropOverlay.classList.add('hidden');
|
dropOverlay.classList.add('hidden');
|
||||||
|
|
||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
const reader = new FileReader();
|
const filePath = file.path; // Electron provides the real file path
|
||||||
reader.onload = (ev) => {
|
|
||||||
setEditorContent(ev.target.result, file.name);
|
if (filePath && typeof window.electronAPI !== 'undefined') {
|
||||||
};
|
// Use IPC to read file through main process (tracks currentFilePath)
|
||||||
reader.readAsText(file);
|
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() {
|
function initEditorTab() {
|
||||||
editor.addEventListener('keydown', (e) => {
|
editor.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const start = editor.selectionStart;
|
// Use insertText command to preserve undo/redo history
|
||||||
const end = editor.selectionEnd;
|
if (!document.execCommand('insertText', false, ' ')) {
|
||||||
const value = editor.value;
|
// Fallback for environments where execCommand is unsupported
|
||||||
editor.value = value.substring(0, start) + ' ' + value.substring(end);
|
const start = editor.selectionStart;
|
||||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
const end = editor.selectionEnd;
|
||||||
|
editor.setRangeText(' ', start, end, 'end');
|
||||||
|
}
|
||||||
scheduleUpdate();
|
scheduleUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user