Files
MarkLite/renderer/renderer.js
T
thzxx 76e1699ee7 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
2026-05-18 10:46:22 +08:00

471 lines
14 KiB
JavaScript

// MarkLite Renderer Process
(function () {
'use strict';
// ===== DOM Elements =====
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const lineNumbers = document.getElementById('line-numbers');
const welcomeScreen = document.getElementById('welcome-screen');
const dropOverlay = document.getElementById('drop-overlay');
const statusText = document.getElementById('status-text');
const statusCursor = document.getElementById('status-cursor');
const resizer = document.getElementById('resizer');
const editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
const mainContent = document.getElementById('main-content');
// Buttons
const btnOpen = document.getElementById('btn-open');
const btnSave = document.getElementById('btn-save');
const btnSplit = document.getElementById('btn-split');
const btnEditor = document.getElementById('btn-editor');
const btnPreview = document.getElementById('btn-preview');
const btnWelcomeOpen = document.getElementById('btn-welcome-open');
const btnWelcomeNew = document.getElementById('btn-welcome-new');
// ===== State =====
let currentFilePath = null;
let currentContent = '';
let viewMode = 'split'; // split, editor, preview
let isModified = false;
let updateTimer = null;
// ===== 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 `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`;
};
marked.setOptions({
gfm: true,
breaks: true,
pedantic: false
});
marked.use({ renderer });
}
}
// ===== Markdown Rendering =====
function renderMarkdown(text) {
if (typeof marked === 'undefined') {
preview.innerHTML = '<p style="color: red;">错误: Markdown 解析库未加载</p>';
return;
}
try {
const html = marked.parse(text);
preview.innerHTML = html;
} catch (e) {
preview.innerHTML = '<p style="color: red;">渲染错误: ' + e.message + '</p>';
}
}
// ===== Line Numbers =====
function updateLineNumbers() {
const lines = editor.value.split('\n');
const count = lines.length;
let html = '';
for (let i = 1; i <= count; i++) {
html += '<div class="line-num">' + i + '</div>';
}
lineNumbers.innerHTML = html;
}
// ===== Cursor Position =====
function updateCursorPosition() {
const text = editor.value;
const pos = editor.selectionStart;
const textBeforeCursor = text.substring(0, pos);
const lines = textBeforeCursor.split('\n');
const line = lines.length;
const col = lines[lines.length - 1].length + 1;
statusCursor.textContent = `行 ${line}, 列 ${col}`;
}
// ===== Sync Scroll (line-based for more accurate sync) =====
function syncScroll() {
if (viewMode !== 'split') return;
// 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 =====
function scheduleUpdate() {
if (updateTimer) clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
const text = editor.value;
renderMarkdown(text);
updateLineNumbers();
}, 150);
}
// ===== File Operations =====
function setModified(modified) {
isModified = modified;
const title = currentFilePath
? `MarkLite - ${getFileName(currentFilePath)}${modified ? ' *' : ''}`
: `MarkLite - 未命名${modified ? ' *' : ''}`;
document.title = title;
}
function getFileName(filePath) {
return filePath.split(/[/\\]/).pop();
}
function setEditorContent(text, filePath) {
editor.value = text;
currentContent = text;
currentFilePath = filePath || null;
isModified = false;
updateLineNumbers();
renderMarkdown(text);
updateCursorPosition();
if (filePath) {
document.title = `MarkLite - ${getFileName(filePath)}`;
statusText.textContent = getFileName(filePath);
} else {
document.title = 'MarkLite - 未命名';
statusText.textContent = '未命名';
}
// Hide welcome screen
welcomeScreen.classList.add('hidden');
}
async function handleOpenFile() {
if (typeof window.electronAPI !== 'undefined') {
const result = await window.electronAPI.openFile();
if (result) {
setEditorContent(result.content, result.filePath);
}
} else {
// Fallback: use file input
const input = document.createElement('input');
input.type = 'file';
input.accept = '.md,.markdown,.txt';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (ev) => {
setEditorContent(ev.target.result, file.name);
};
reader.readAsText(file);
}
};
input.click();
}
}
async function handleSave() {
if (typeof window.electronAPI !== 'undefined') {
const result = await window.electronAPI.saveFile({
filePath: currentFilePath,
content: editor.value
});
if (result.success) {
currentFilePath = result.filePath;
currentContent = editor.value;
setModified(false);
statusText.textContent = '已保存';
setTimeout(() => {
statusText.textContent = getFileName(currentFilePath) || '就绪';
}, 2000);
}
} else {
// Fallback: download file
const blob = new Blob([editor.value], { type: 'text/markdown' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (currentFilePath || 'untitled.md');
a.click();
URL.revokeObjectURL(a.href);
setModified(false);
}
}
async function handleSaveAs() {
if (typeof window.electronAPI !== 'undefined') {
const result = await window.electronAPI.saveFileAs({
content: editor.value
});
if (result.success) {
currentFilePath = result.filePath;
currentContent = editor.value;
setModified(false);
statusText.textContent = '已保存';
setTimeout(() => {
statusText.textContent = getFileName(currentFilePath) || '就绪';
}, 2000);
}
} else {
handleSave();
}
}
// ===== View Mode =====
function setViewMode(mode) {
viewMode = mode;
const app = document.getElementById('app');
// Remove all mode classes
app.classList.remove('mode-editor', 'mode-preview');
// Update button states
btnSplit.classList.remove('active');
btnEditor.classList.remove('active');
btnPreview.classList.remove('active');
switch (mode) {
case 'split':
btnSplit.classList.add('active');
break;
case 'editor':
btnEditor.classList.add('active');
app.classList.add('mode-editor');
break;
case 'preview':
btnPreview.classList.add('active');
app.classList.add('mode-preview');
// Update preview when switching to preview mode
renderMarkdown(editor.value);
break;
}
}
// ===== Resizer =====
let isResizing = false;
function initResizer() {
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
resizer.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const containerRect = mainContent.getBoundingClientRect();
const percentage = ((e.clientX - containerRect.left) / containerRect.width) * 100;
const clamped = Math.max(20, Math.min(80, percentage));
editorPanel.style.flex = `0 0 ${clamped}%`;
previewPanel.style.flex = `0 0 ${100 - clamped}%`;
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
resizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
}
// ===== Drag & Drop =====
function initDragDrop() {
let dragCounter = 0;
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');
}
});
document.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
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 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);
}
}
});
}
// ===== Keyboard Shortcuts =====
function initKeyboard() {
document.addEventListener('keydown', (e) => {
// Ctrl+O: Open
if (e.ctrlKey && e.key === 'o') {
e.preventDefault();
handleOpenFile();
}
// Ctrl+S: Save
if (e.ctrlKey && e.key === 's' && !e.shiftKey) {
e.preventDefault();
handleSave();
}
// Ctrl+Shift+S: Save As
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
handleSaveAs();
}
// Ctrl+1: Split view
if (e.ctrlKey && e.key === '1') {
e.preventDefault();
setViewMode('split');
}
// Ctrl+2: Editor only
if (e.ctrlKey && e.key === '2') {
e.preventDefault();
setViewMode('editor');
}
// Ctrl+3: Preview only
if (e.ctrlKey && e.key === '3') {
e.preventDefault();
setViewMode('preview');
}
});
}
// ===== Tab key support in editor (preserves undo history) =====
function initEditorTab() {
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
// 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();
}
});
}
// ===== Event Listeners =====
function initEventListeners() {
// Editor input
editor.addEventListener('input', () => {
setModified(true);
scheduleUpdate();
});
// Editor scroll sync
editor.addEventListener('scroll', syncScroll);
// Editor cursor position
editor.addEventListener('click', updateCursorPosition);
editor.addEventListener('keyup', updateCursorPosition);
editor.addEventListener('select', updateCursorPosition);
// Toolbar buttons
btnOpen.addEventListener('click', handleOpenFile);
btnSave.addEventListener('click', handleSave);
btnSplit.addEventListener('click', () => setViewMode('split'));
btnEditor.addEventListener('click', () => setViewMode('editor'));
btnPreview.addEventListener('click', () => setViewMode('preview'));
// Welcome buttons
btnWelcomeOpen.addEventListener('click', handleOpenFile);
btnWelcomeNew.addEventListener('click', () => {
setEditorContent('', null);
});
// Electron IPC events
if (typeof window.electronAPI !== 'undefined') {
window.electronAPI.onFileOpened((data) => {
setEditorContent(data.content, data.filePath);
});
window.electronAPI.onMenuSave(() => {
handleSave();
});
window.electronAPI.onMenuSaveAs(() => {
handleSaveAs();
});
window.electronAPI.onViewModeChange((mode) => {
setViewMode(mode);
});
}
}
// ===== Initialize =====
function init() {
initMarked();
initResizer();
initDragDrop();
initKeyboard();
initEditorTab();
initEventListeners();
updateLineNumbers();
setViewMode('split');
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();