Initial commit: MarkLite Markdown Reader
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
// 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') {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 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 =====
|
||||
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);
|
||||
}
|
||||
|
||||
// ===== 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();
|
||||
dragCounter++;
|
||||
dropOverlay.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter--;
|
||||
if (dragCounter === 0) {
|
||||
dropOverlay.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 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 =====
|
||||
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;
|
||||
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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user