// 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 `
${highlighted}
`; }; marked.setOptions({ gfm: true, breaks: true, pedantic: false }); marked.use({ renderer }); } } // ===== Markdown Rendering ===== function renderMarkdown(text) { if (typeof marked === 'undefined') { preview.innerHTML = '

错误: Markdown 解析库未加载

'; return; } try { const html = marked.parse(text); preview.innerHTML = html; } catch (e) { preview.innerHTML = '

渲染错误: ' + e.message + '

'; } } // ===== Line Numbers ===== function updateLineNumbers() { const lines = editor.value.split('\n'); const count = lines.length; let html = ''; for (let i = 1; i <= count; i++) { html += '
' + i + '
'; } 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(); } })();