refactor: renderer.js拆分为6个模块(core/tabs/editor/search/sidebar/markdown)

This commit is contained in:
thzxx
2026-05-21 13:34:49 +08:00
parent b185771c41
commit e071a26db6
8 changed files with 1232 additions and 1664 deletions
+454
View File
@@ -0,0 +1,454 @@
// MarkLite — Core Module: state, DOM refs, settings, utilities, init orchestration
(function () {
'use strict';
// ===== Shared Namespace =====
const ML = {};
window.MarkLite = ML;
// ===== DOM Elements =====
ML.editor = document.getElementById('editor');
ML.preview = document.getElementById('preview');
ML.lineNumbers = document.getElementById('line-numbers');
ML.welcomeScreen = document.getElementById('welcome-screen');
ML.dropOverlay = document.getElementById('drop-overlay');
ML.modifiedBanner = document.getElementById('modified-banner');
ML.statusText = document.getElementById('status-text');
ML.statusCursor = document.getElementById('status-cursor');
ML.statusSize = document.getElementById('status-size');
ML.statusSizeDivider = document.querySelector('.status-size-divider');
ML.resizer = document.getElementById('resizer');
ML.editorPanel = document.getElementById('editor-panel');
ML.previewPanel = document.getElementById('preview-panel');
ML.mainContent = document.getElementById('main-content');
ML.contentWrapper = document.getElementById('content-wrapper');
ML.tabList = document.getElementById('tab-list');
// Sidebar
ML.sidebar = document.getElementById('sidebar');
ML.sidebarTree = document.getElementById('sidebar-tree');
ML.btnOpenFolder = document.getElementById('btn-open-folder');
// Search
ML.searchBar = document.getElementById('search-bar');
ML.searchInput = document.getElementById('search-input');
ML.replaceInput = document.getElementById('replace-input');
ML.searchCount = document.getElementById('search-count');
ML.replaceRow = document.getElementById('replace-row');
ML.btnCase = document.getElementById('btn-case');
ML.btnRegex = document.getElementById('btn-regex');
ML.btnNext = document.getElementById('btn-next');
ML.btnPrev = document.getElementById('btn-prev');
ML.btnSearchClose = document.getElementById('btn-search-close');
ML.btnReplace = document.getElementById('btn-replace');
ML.btnReplaceAll = document.getElementById('btn-replace-all');
// Buttons
ML.btnOpen = document.getElementById('btn-open');
ML.btnSave = document.getElementById('btn-save');
ML.btnSplit = document.getElementById('btn-split');
ML.btnEditor = document.getElementById('btn-editor');
ML.btnPreview = document.getElementById('btn-preview');
ML.btnWelcomeOpen = document.getElementById('btn-welcome-open');
ML.btnWelcomeNew = document.getElementById('btn-welcome-new');
ML.btnDark = document.getElementById('btn-dark');
ML.btnReload = document.getElementById('btn-reload');
ML.btnDismiss = document.getElementById('btn-dismiss');
ML.btnNewTab = document.getElementById('btn-new-tab');
// ===== Shared State =====
ML.viewMode = 'split';
ML.updateTimer = null;
ML.lineCountCache = 0;
ML.currentRootPath = null;
ML.MAX_FILE_SIZE = 20 * 1024 * 1024;
// ===== Settings =====
ML.loadSettings = function () {
try { return JSON.parse(localStorage.getItem('marklite-settings') || '{}'); }
catch { return {}; }
};
ML.saveSetting = function (key, value) {
try {
const s = ML.loadSettings();
s[key] = value;
localStorage.setItem('marklite-settings', JSON.stringify(s));
} catch { /* ignore */ }
};
// ===== Utilities =====
ML.getFileName = function (filePath) {
return filePath.split(/[/\\]/).pop();
};
ML.formatBytes = function (bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
// Character width measurement
var _charWidthCanvas = document.createElement('canvas');
var _charWidthCtx = _charWidthCanvas.getContext('2d');
var _cachedCharWidth = null;
ML.getCharWidth = function () {
if (_cachedCharWidth !== null) return _cachedCharWidth;
var style = getComputedStyle(ML.editor);
_charWidthCtx.font = style.fontSize + ' ' + style.fontFamily;
_cachedCharWidth = _charWidthCtx.measureText('M').width;
return _cachedCharWidth;
};
new MutationObserver(function () { _cachedCharWidth = null; })
.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
// Toast notification
ML.showToast = function (message, duration) {
duration = duration || 3000;
var toast = document.getElementById('toast-notification');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast-notification';
document.getElementById('app').appendChild(toast);
}
toast.textContent = message;
toast.classList.add('show');
clearTimeout(toast._timer);
toast._timer = setTimeout(function () { toast.classList.remove('show'); }, duration);
};
// ===== Dark Mode =====
ML.initDarkMode = function () {
var settings = ML.loadSettings();
var iconDark = document.getElementById('icon-dark');
var iconLight = document.getElementById('icon-light');
function apply(isDark) {
document.documentElement.classList.toggle('dark', isDark);
iconDark.style.display = isDark ? 'none' : '';
iconLight.style.display = isDark ? '' : 'none';
}
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
apply(settings.darkMode !== undefined ? !!settings.darkMode : prefersDark);
ML.btnDark.addEventListener('click', function () {
var isDark = !document.documentElement.classList.contains('dark');
apply(isDark);
ML.saveSetting('darkMode', isDark);
});
};
// ===== File Operations =====
ML.handleOpenFile = async function () {
if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.openFile();
if (result && !result.error) {
ML.createNewTab(result.filePath, result.content);
}
} else {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.md,.markdown,.txt';
input.multiple = true;
input.onchange = function (e) {
Array.from(e.target.files).forEach(function (file) {
var ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
if (!['.md', '.markdown', '.txt'].includes(ext)) return;
if (file.size > ML.MAX_FILE_SIZE) {
alert('"' + file.name + '" 过大(' + ML.formatBytes(file.size) + '),暂不支持超过 20MB 的文件');
return;
}
var reader = new FileReader();
reader.onload = function (ev) { ML.createNewTab(file.name, ev.target.result); };
reader.readAsText(file);
});
};
input.click();
}
};
ML.handleSave = async function () {
var tab = ML.getActiveTab();
if (!tab) return;
if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.saveFile({ filePath: tab.filePath, content: ML.editor.value });
if (result.success) {
tab.filePath = result.filePath;
tab.content = ML.editor.value;
tab.isModified = false;
ML.renderTabBar();
ML.updateTitle();
ML.updateSidebarHighlight();
ML.statusText.textContent = '已保存';
setTimeout(function () { ML.updateTitle(); }, 2000);
}
} else {
var blob = new Blob([ML.editor.value], { type: 'text/markdown' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = tab.filePath || 'untitled.md';
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
tab.isModified = false;
ML.renderTabBar();
ML.updateTitle();
}
};
ML.handleSaveAs = async function () {
var tab = ML.getActiveTab();
if (!tab) return;
if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.saveFileAs({ content: ML.editor.value });
if (result.success) {
tab.filePath = result.filePath;
tab.content = ML.editor.value;
tab.isModified = false;
ML.renderTabBar();
ML.updateTitle();
ML.updateSidebarHighlight();
ML.statusText.textContent = '已保存';
setTimeout(function () { ML.updateTitle(); }, 2000);
}
} else {
ML.handleSave();
}
};
// ===== View Mode =====
ML.setViewMode = function (mode) {
ML.viewMode = mode;
var app = document.getElementById('app');
app.classList.remove('mode-editor', 'mode-preview');
ML.btnSplit.classList.remove('active');
ML.btnEditor.classList.remove('active');
ML.btnPreview.classList.remove('active');
switch (mode) {
case 'split': ML.btnSplit.classList.add('active'); break;
case 'editor': ML.btnEditor.classList.add('active'); app.classList.add('mode-editor'); break;
case 'preview': ML.btnPreview.classList.add('active'); app.classList.add('mode-preview'); ML.renderMarkdown(ML.editor.value); break;
}
ML.saveSetting('viewMode', mode);
};
// ===== Resizer =====
ML.initResizer = function () {
var settings = ML.loadSettings();
if (settings.splitRatio) {
ML.editorPanel.style.flex = '0 0 ' + settings.splitRatio + '%';
ML.previewPanel.style.flex = '0 0 ' + (100 - settings.splitRatio) + '%';
}
var isResizing = false;
ML.resizer.addEventListener('mousedown', function (e) {
isResizing = true;
ML.resizer.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!isResizing) return;
var rect = ML.contentWrapper.getBoundingClientRect();
var pct = Math.max(20, Math.min(80, ((e.clientX - rect.left) / rect.width) * 100));
ML.editorPanel.style.flex = '0 0 ' + pct + '%';
ML.previewPanel.style.flex = '0 0 ' + (100 - pct) + '%';
ML.saveSetting('splitRatio', pct);
});
document.addEventListener('mouseup', function () {
if (isResizing) {
isResizing = false;
ML.resizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
};
// ===== Drag & Drop =====
ML.initDragDrop = function () {
var dragCounter = 0;
var allowedExts = ['.md', '.markdown', '.txt'];
document.addEventListener('dragenter', function (e) { e.preventDefault(); e.stopPropagation(); dragCounter++; ML.dropOverlay.classList.remove('hidden'); });
document.addEventListener('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); dragCounter--; if (dragCounter === 0) ML.dropOverlay.classList.add('hidden'); });
document.addEventListener('dragover', function (e) { e.preventDefault(); e.stopPropagation(); });
document.addEventListener('drop', async function (e) {
e.preventDefault(); e.stopPropagation(); dragCounter = 0; ML.dropOverlay.classList.add('hidden');
var files = e.dataTransfer.files;
var rejected = 0;
for (var _i = 0; _i < files.length; _i++) {
var file = files[_i];
var filePath = file.path;
var ext = filePath ? filePath.substring(filePath.lastIndexOf('.')).toLowerCase() : '';
if (!allowedExts.includes(ext)) { rejected++; continue; }
if (file.size > ML.MAX_FILE_SIZE) { ML.showToast('"' + file.name + '" 过大(' + ML.formatBytes(file.size) + '),暂不支持超过 20MB 的文件'); continue; }
if (filePath && typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.readFile(filePath);
if (result.success) ML.createNewTab(filePath, result.content);
} else {
var reader = new FileReader();
reader.onload = function (ev) { ML.createNewTab(file.name, ev.target.result); };
reader.readAsText(file);
}
}
if (rejected > 0) ML.showToast('仅支持 .md / .markdown / .txt 文件,已忽略 ' + rejected + ' 个文件');
});
};
// ===== Keyboard Shortcuts =====
ML.initKeyboard = function () {
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 'o') { e.preventDefault(); ML.handleOpenFile(); }
if (e.ctrlKey && e.key === 's' && !e.shiftKey) { e.preventDefault(); ML.handleSave(); }
if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); ML.handleSaveAs(); }
if (e.ctrlKey && e.key === '1') { e.preventDefault(); ML.setViewMode('split'); }
if (e.ctrlKey && e.key === '2') { e.preventDefault(); ML.setViewMode('editor'); }
if (e.ctrlKey && e.key === '3') { e.preventDefault(); ML.setViewMode('preview'); }
if (e.ctrlKey && e.key === 't') { e.preventDefault(); ML.createNewTab(null, ''); }
if (e.ctrlKey && e.key === 'w') { e.preventDefault(); if (ML.activeTabId) ML.closeTab(ML.activeTabId); }
if (e.ctrlKey && e.key === 'Tab') {
e.preventDefault();
if (ML.tabs.length > 1) {
if (e.shiftKey) {
if (ML.mruTabStack.length > 0) {
var targetId = ML.mruTabStack.shift();
if (ML.tabs.find(function (t) { return t.id === targetId; })) ML.switchToTab(targetId);
}
} else {
var idx = ML.getTabIndex(ML.activeTabId);
var next = (idx + 1) % ML.tabs.length;
ML.switchToTab(ML.tabs[next].id);
}
}
}
if (e.ctrlKey && e.key === 'f') { e.preventDefault(); ML.openSearch(false); }
if (e.ctrlKey && e.key === 'h') { e.preventDefault(); ML.openSearch(true); }
if (e.key === 'Escape' && !ML.searchBar.classList.contains('hidden')) { e.preventDefault(); ML.closeSearch(); }
if (e.key === 'Enter' && document.activeElement === ML.searchInput) { e.preventDefault(); e.shiftKey ? ML.findPrev() : ML.findNext(); }
if (e.altKey && e.key === 'c') { e.preventDefault(); ML.toggleSearchOption('case'); }
if (e.altKey && e.key === 'r') { e.preventDefault(); ML.toggleSearchOption('regex'); }
if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); ML.replaceAll(); }
if (e.ctrlKey && e.shiftKey && e.key === 'G') { e.preventDefault(); ML.replaceCurrent(); }
});
};
// ===== Tab Key Support =====
ML.initEditorTab = function () {
ML.editor.addEventListener('keydown', function (e) {
if (e.key === 'Tab') {
e.preventDefault();
var start = ML.editor.selectionStart;
var end = ML.editor.selectionEnd;
if (start !== end) {
var lines = ML.editor.value.substring(start, end).split('\n');
var indented = lines.map(function (line) { return ' ' + line; }).join('\n');
ML.editor.setRangeText(indented, start, end, 'end');
} else {
ML.editor.setRangeText(' ', start, end, 'end');
}
ML.scheduleUpdate();
}
});
};
// ===== External File Modification =====
ML.initFileWatch = function () {
if (typeof window.electronAPI === 'undefined') return;
window.electronAPI.onExternalModification(function (filePath) {
var tab = ML.getActiveTab();
if (tab && tab.filePath === filePath) ML.modifiedBanner.classList.remove('hidden');
});
ML.btnReload.addEventListener('click', async function () {
var tab = ML.getActiveTab();
if (!tab || !tab.filePath) return;
var result = await window.electronAPI.reloadFile();
if (result.success) {
ML.editor.value = result.content;
tab.content = result.content;
tab.isModified = false;
ML.lineCountCache = 0;
ML.updateLineNumbers();
ML.renderMarkdown(result.content);
ML.updateCursorPosition();
ML.updateFileSize();
ML.cachePreviewPositions();
ML.renderTabBar();
ML.updateTitle();
}
ML.modifiedBanner.classList.add('hidden');
});
ML.btnDismiss.addEventListener('click', function () { ML.modifiedBanner.classList.add('hidden'); });
};
// ===== Unsaved Changes Warning =====
ML.initUnsavedWarning = function () {
var hasUnsaved = function () { return ML.tabs.some(function (t) { return t.isModified; }); };
if (typeof window.electronAPI === 'undefined') {
window.addEventListener('beforeunload', function (e) { if (hasUnsaved()) { e.preventDefault(); e.returnValue = ''; } });
return;
}
window.electronAPI.onConfirmClose(function () {
if (!hasUnsaved()) { window.electronAPI.forceClose(); return; }
var shouldClose = confirm('有文件尚未保存,确定要关闭吗?');
if (shouldClose) window.electronAPI.forceClose();
else window.electronAPI.cancelClose();
});
};
// ===== Event Listeners =====
ML.initEventListeners = function () {
ML.editor.addEventListener('input', function () {
var tab = ML.getActiveTab();
if (tab) { tab.isModified = true; tab.content = ML.editor.value; ML.setModified(true); }
ML.scheduleUpdate();
});
ML.editor.addEventListener('scroll', function () {
ML.syncScroll();
ML.lineNumbers.scrollTop = ML.editor.scrollTop;
if (ML.lineCountCache > 2000) ML.renderVirtualLineNumbers(ML.lineCountCache);
});
ML.editor.addEventListener('click', ML.updateCursorPosition);
ML.editor.addEventListener('keyup', ML.updateCursorPosition);
ML.editor.addEventListener('select', ML.updateCursorPosition);
ML.btnOpen.addEventListener('click', ML.handleOpenFile);
ML.btnSave.addEventListener('click', ML.handleSave);
ML.btnSplit.addEventListener('click', function () { ML.setViewMode('split'); });
ML.btnEditor.addEventListener('click', function () { ML.setViewMode('editor'); });
ML.btnPreview.addEventListener('click', function () { ML.setViewMode('preview'); });
ML.btnNewTab.addEventListener('click', function () { ML.createNewTab(null, ''); });
ML.btnWelcomeOpen.addEventListener('click', ML.handleOpenFile);
ML.btnWelcomeNew.addEventListener('click', function () { ML.createNewTab(null, ''); });
if (typeof window.electronAPI !== 'undefined') {
window.electronAPI.removeAllListeners('file:openInTab');
window.electronAPI.removeAllListeners('menu:save');
window.electronAPI.removeAllListeners('menu:saveAs');
window.electronAPI.removeAllListeners('menu:viewMode');
window.electronAPI.onFileOpenInTab(function (data) { ML.createNewTab(data.filePath, data.content); });
window.electronAPI.onMenuSave(function () { ML.handleSave(); });
window.electronAPI.onMenuSaveAs(function () { ML.handleSaveAs(); });
window.electronAPI.onViewModeChange(function (mode) { ML.setViewMode(mode); });
}
};
// ===== Init =====
ML.init = function () {
ML.initMarked();
ML.initDarkMode();
ML.initResizer();
ML.initDragDrop();
ML.initKeyboard();
ML.initEditorTab();
ML.initPreviewLinks();
ML.initEventListeners();
ML.initFileWatch();
ML.initUnsavedWarning();
ML.initSidebar();
ML.initSearchBar();
var settings = ML.loadSettings();
ML.setViewMode(settings.viewMode || 'split');
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ML.init);
} else {
ML.init();
}
})();
+147
View File
@@ -0,0 +1,147 @@
// MarkLite — Editor Module: line numbers, cursor, file size, scroll sync
(function () {
'use strict';
var ML = window.MarkLite;
// ===== Line Numbers =====
function countNewlines(text) {
var count = 1;
for (var i = 0, len = text.length; i < len; i++) {
if (text.charCodeAt(i) === 10) count++;
}
return count;
}
ML.updateLineNumbers = function () {
var count = countNewlines(ML.editor.value);
if (count === ML.lineCountCache) return;
ML.lineCountCache = count;
if (count > 2000) {
ML.renderVirtualLineNumbers(count);
} else {
var arr = new Array(count);
for (var i = 0; i < count; i++) arr[i] = '<div class="line-num">' + (i + 1) + '</div>';
ML.lineNumbers.innerHTML = arr.join('');
}
};
ML.renderVirtualLineNumbers = function (totalLines) {
var lineHeight = 20.8;
var scrollTop = ML.editor.scrollTop;
var viewportHeight = ML.editor.clientHeight;
var buffer = 20;
var startLine = Math.max(0, Math.floor(scrollTop / lineHeight) - buffer);
var endLine = Math.min(totalLines, Math.ceil((scrollTop + viewportHeight) / lineHeight) + buffer);
var visibleCount = endLine - startLine;
var topPadding = startLine * lineHeight;
var bottomPadding = (totalLines - endLine) * lineHeight;
var arr = new Array(visibleCount + 2);
arr[0] = '<div style="height:' + topPadding + 'px"></div>';
for (var i = 0; i < visibleCount; i++) arr[i + 1] = '<div class="line-num">' + (startLine + i + 1) + '</div>';
arr[visibleCount + 1] = '<div style="height:' + bottomPadding + 'px"></div>';
ML.lineNumbers.innerHTML = arr.join('');
};
// ===== Cursor Position =====
ML.updateCursorPosition = function () {
var text = ML.editor.value;
var pos = ML.editor.selectionStart;
var textBefore = text.substring(0, pos);
var lines = textBefore.split('\n');
ML.statusCursor.textContent = '行 ' + lines.length + ', 列 ' + (lines[lines.length - 1].length + 1);
};
// ===== File Size =====
ML.updateFileSize = function () {
var bytes = new Blob([ML.editor.value]).size;
ML.statusSize.textContent = ML.formatBytes(bytes);
ML.statusSizeDivider.classList.add('visible');
};
// ===== Scroll Sync =====
var previewPositions = null;
var lineToPreviewMap = null;
ML.cachePreviewPositions = function () {
previewPositions = [];
for (var i = 0; i < ML.preview.children.length; i++) previewPositions.push(ML.preview.children[i].offsetTop);
buildLineToPreviewMap();
};
function buildLineToPreviewMap() {
if (!previewPositions || previewPositions.length === 0) { lineToPreviewMap = null; return; }
var text = ML.editor.value;
var lines = text.split('\n');
var totalLines = lines.length;
var blockStartLines = [];
var inCodeBlock = false;
for (var i = 0; i < totalLines; i++) {
var trimmed = lines[i].trimStart();
if (trimmed.startsWith('```')) {
if (!inCodeBlock) { blockStartLines.push(i); inCodeBlock = true; }
else inCodeBlock = false;
continue;
}
if (inCodeBlock) continue;
if (trimmed.startsWith('#') || trimmed.startsWith('- ') || trimmed.startsWith('* ') || trimmed.startsWith('+ ') ||
/^\d+\.\s/.test(trimmed) || trimmed.startsWith('> ') || trimmed.startsWith('|') ||
trimmed.startsWith('---') || trimmed.startsWith('***') || trimmed.startsWith('___') ||
(trimmed === '' && i > 0 && lines[i - 1].trim() === '')) {
blockStartLines.push(i);
}
}
var uniqueStartsArr = [].concat(_toConsumableArray(new Set(blockStartLines))).sort(function (a, b) { return a - b; });
var uniqueStartsSet = new Set(uniqueStartsArr);
lineToPreviewMap = new Float32Array(totalLines);
if (uniqueStartsArr.length === 0) {
for (var _i = 0; _i < totalLines; _i++) lineToPreviewMap[_i] = (_i / Math.max(1, totalLines - 1)) * previewPositions[previewPositions.length - 1];
return;
}
var domCount = previewPositions.length;
for (var _i2 = 0; _i2 < uniqueStartsArr.length; _i2++) {
var lineIdx = uniqueStartsArr[_i2];
var domIdx = Math.min(Math.floor((_i2 / uniqueStartsArr.length) * domCount), domCount - 1);
lineToPreviewMap[lineIdx] = previewPositions[domIdx];
}
for (var _i3 = 0; _i3 < totalLines; _i3++) {
if (lineToPreviewMap[_i3] !== 0 && uniqueStartsSet.has(_i3)) continue;
var prevLine = -1, nextLine = -1;
for (var j = _i3 - 1; j >= 0; j--) { if (lineToPreviewMap[j] !== 0 || uniqueStartsSet.has(j)) { prevLine = j; break; } }
for (var _j = _i3 + 1; _j < totalLines; _j++) { if (lineToPreviewMap[_j] !== 0 || uniqueStartsSet.has(_j)) { nextLine = _j; break; } }
if (prevLine === -1 && nextLine === -1) lineToPreviewMap[_i3] = 0;
else if (prevLine === -1) lineToPreviewMap[_i3] = lineToPreviewMap[nextLine];
else if (nextLine === -1) lineToPreviewMap[_i3] = lineToPreviewMap[prevLine];
else { var ratio = (_i3 - prevLine) / (nextLine - prevLine); lineToPreviewMap[_i3] = lineToPreviewMap[prevLine] + ratio * (lineToPreviewMap[nextLine] - lineToPreviewMap[prevLine]); }
}
}
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } return Array.from(arr); }
ML.syncScroll = function () {
if (ML.viewMode !== 'split') return;
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
var scrollTop = ML.editor.scrollTop;
var topLine = Math.floor(scrollTop / lineHeight);
if (!previewPositions || previewPositions.length === 0) ML.cachePreviewPositions();
if (!previewPositions || previewPositions.length === 0) return;
if (lineToPreviewMap && topLine >= 0 && topLine < lineToPreviewMap.length) {
ML.previewPanel.scrollTop = lineToPreviewMap[topLine];
} else {
var totalLines = ML.editor.value.split('\n').length;
var ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0;
var targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1);
if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) ML.previewPanel.scrollTop = previewPositions[targetIndex];
}
};
// ===== Schedule Update =====
ML.scheduleUpdate = function () {
if (ML.updateTimer) clearTimeout(ML.updateTimer);
ML.updateTimer = setTimeout(function () {
ML.renderMarkdown(ML.editor.value);
ML.updateLineNumbers();
ML.updateFileSize();
ML.cachePreviewPositions();
}, 150);
};
})();
+6 -1
View File
@@ -210,6 +210,11 @@
<script src="../lib/marked.min.js"></script>
<script src="../lib/highlight.min.js"></script>
<script src="renderer.js"></script>
<script src="markdown.js"></script>
<script src="editor.js"></script>
<script src="tabs.js"></script>
<script src="search.js"></script>
<script src="sidebar.js"></script>
<script src="core.js"></script>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
// MarkLite — Markdown Module: marked.js init, sanitizer, rendering, preview links
(function () {
'use strict';
var ML = window.MarkLite;
// ===== HTML Sanitizer =====
var DANGEROUS_TAGS = new Set(['script', 'iframe', 'object', 'embed', 'applet', 'form', 'base', 'meta', 'link', 'style']);
var DANGEROUS_ATTRS = new Set([
'onabort', 'onanimationend', 'onanimationiteration', 'onanimationstart', 'onauxclick', 'onbeforecopy', 'onbeforecut',
'onbeforepaste', 'onblur', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncontextmenu',
'oncopy', 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover',
'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'onfocusin', 'onfocusout',
'onfullscreenchange', 'onfullscreenerror', 'ongotpointercapture', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress',
'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onlostpointercapture', 'onmousedown',
'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onpaste',
'onpause', 'onplay', 'onplaying', 'onpointercancel', 'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove',
'onpointerout', 'onpointerover', 'onpointerup', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll',
'onsearch', 'onseeked', 'onseeking', 'onselect', 'onselectionchange', 'onselectstart', 'onshow', 'onstalled', 'onsubmit',
'onsuspend', 'ontimeupdate', 'ontoggle', 'ontouchcancel', 'ontouchend', 'ontouchmove', 'ontouchstart', 'ontransitionend',
'onvolumechange', 'onwaiting', 'onwheel', 'onanimationend', 'oncontentvisibilityautostatechange', 'onscrollend',
'onscrollsnapchange', 'onscrollsnapchanging'
]);
function sanitizeHTML(html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var walker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT);
var toRemove = [];
while (walker.nextNode()) {
var el = walker.currentNode;
var tag = el.tagName.toLowerCase();
if (DANGEROUS_TAGS.has(tag)) { toRemove.push(el); continue; }
var attrs = Array.from(el.attributes);
for (var i = 0; i < attrs.length; i++) {
var name = attrs[i].name.toLowerCase();
if (DANGEROUS_ATTRS.has(name) || name.startsWith('on')) el.removeAttribute(attrs[i].name);
else if ((name === 'href' || name === 'action' || name === 'formaction' || name === 'xlink:href') && /^\s*javascript:/i.test(attrs[i].value)) el.removeAttribute(attrs[i].name);
else if (name === 'style' && /expression\s*\(|url\s*\(\s*['"]?\s*javascript:/i.test(attrs[i].value)) el.removeAttribute(attrs[i].name);
}
}
for (var _i = 0; _i < toRemove.length; _i++) toRemove[_i].parentNode.removeChild(toRemove[_i]);
return doc.body.innerHTML;
}
// ===== Rendering =====
ML.renderMarkdown = function (text) {
if (typeof marked === 'undefined') { ML.preview.textContent = '错误: Markdown 解析库未加载'; ML.preview.style.color = 'red'; return; }
try {
var raw = marked.parse(text);
ML.preview.innerHTML = sanitizeHTML(raw);
ML.preview.style.color = '';
} catch (e) { ML.preview.textContent = '渲染错误: ' + e.message; ML.preview.style.color = 'red'; }
};
// ===== Init marked.js =====
ML.initMarked = function () {
if (typeof marked === 'undefined') return;
var renderer = new marked.Renderer();
renderer.code = function (codeOrToken, legacyLang) {
var text = (typeof codeOrToken === 'object' && codeOrToken !== null) ? codeOrToken.text : codeOrToken;
var lang = (typeof codeOrToken === 'object' && codeOrToken !== null) ? codeOrToken.lang : legacyLang;
var highlighted = text;
if (typeof hljs !== 'undefined') {
if (lang && hljs.getLanguage(lang)) { try { highlighted = hljs.highlight(text, { language: lang }).value; } catch (e) { /* */ } }
else { try { highlighted = hljs.highlightAuto(text).value; } catch (e) { /* */ } }
}
var langClass = lang ? ' language-' + lang : '';
return '<pre><code class="hljs' + langClass + '">' + highlighted + '</code></pre>';
};
renderer.image = function (hrefOrToken, title, text) {
var href = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.href : hrefOrToken;
var alt = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.text : text;
var titleAttr = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.title : title;
var src = href;
if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:') && !src.startsWith('file://')) {
var tab = ML.getActiveTab();
if (tab && tab.filePath) {
var dir = tab.filePath.replace(/[/\\][^/\\]+$/, '');
var sep = dir.includes('\\') ? '\\' : '/';
src = 'file://' + (dir + sep + src).replace(/\\/g, '/');
}
}
var titleStr = titleAttr ? ' title="' + titleAttr + '"' : '';
return '<img src="' + src + '" alt="' + alt + '"' + titleStr + '>';
};
marked.setOptions({ gfm: true, breaks: true, pedantic: false });
marked.use({ renderer: renderer });
};
// ===== Preview Links =====
ML.initPreviewLinks = function () {
ML.preview.addEventListener('click', function (e) {
var link = e.target.closest('a');
if (!link) return;
e.preventDefault();
var href = link.getAttribute('href');
if (!href) return;
if (href.startsWith('#')) { var target = ML.preview.querySelector(href); if (target) target.scrollIntoView({ behavior: 'smooth' }); return; }
try { var url = new URL(href); if (url.protocol !== 'http:' && url.protocol !== 'https:') return; } catch (err) { return; }
if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openExternal) window.electronAPI.openExternal(href);
else window.open(href, '_blank', 'noopener');
});
};
})();
-1663
View File
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
// MarkLite — Search & Replace Module
(function () {
'use strict';
var ML = window.MarkLite;
var searchMatches = [];
var searchIndex = -1;
var searchCaseSensitive = false;
var searchUseRegex = false;
ML.openSearch = function (showReplace) {
ML.searchBar.classList.remove('hidden');
ML.replaceRow.classList.toggle('hidden', !showReplace);
ML.searchInput.focus();
var selected = ML.editor.value.substring(ML.editor.selectionStart, ML.editor.selectionEnd);
if (selected && !selected.includes('\n')) ML.searchInput.value = selected;
doSearch();
};
ML.closeSearch = function () {
ML.searchBar.classList.add('hidden');
clearHighlights();
searchMatches = []; searchIndex = -1;
ML.searchCount.textContent = '';
ML.editor.focus();
};
ML.toggleSearchOption = function (type) {
if (type === 'case') { searchCaseSensitive = !searchCaseSensitive; ML.btnCase.classList.toggle('active', searchCaseSensitive); }
else { searchUseRegex = !searchUseRegex; ML.btnRegex.classList.toggle('active', searchUseRegex); }
doSearch();
};
function doSearch() {
clearHighlights();
var text = ML.searchInput.value;
if (!text) { searchMatches = []; searchIndex = -1; ML.searchCount.textContent = ''; return; }
searchMatches = findMatches(text);
if (searchMatches.length > 0) {
var cursor = ML.editor.selectionStart;
searchIndex = searchMatches.findIndex(function (m) { return m.start >= cursor; });
if (searchIndex === -1) searchIndex = 0;
scrollToMatch(searchIndex);
} else { searchIndex = -1; }
ML.searchCount.textContent = searchMatches.length > 0 ? (searchIndex + 1) + '/' + searchMatches.length : '无结果';
highlightMatches();
}
function findMatches(text) {
var content = ML.editor.value;
var matches = [];
if (searchUseRegex) {
try {
var flags = searchCaseSensitive ? 'g' : 'gi';
var regex = new RegExp(text, flags);
var m;
while ((m = regex.exec(content)) !== null) {
matches.push({ start: m.index, end: m.index + m[0].length });
if (m[0].length === 0) regex.lastIndex++;
}
} catch (e) { /* invalid regex */ }
} else {
var haystack = searchCaseSensitive ? content : content.toLowerCase();
var needle = searchCaseSensitive ? text : text.toLowerCase();
var idx = 0;
while ((idx = haystack.indexOf(needle, idx)) !== -1) {
matches.push({ start: idx, end: idx + needle.length });
idx += needle.length;
}
}
return matches;
}
function highlightMatches() {
clearHighlights();
if (searchMatches.length === 0) return;
var content = ML.editor.value;
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
var editorStyle = getComputedStyle(ML.editor);
var paddingTop = parseFloat(editorStyle.paddingTop);
var paddingLeft = parseFloat(editorStyle.paddingLeft);
var lineStarts = [0];
for (var i = 0; i < content.length; i++) { if (content.charCodeAt(i) === 10) lineStarts.push(i + 1); }
function posToXY(pos) {
var line = 0;
for (var _i = 0; _i < lineStarts.length; _i++) { if (lineStarts[_i] > pos) break; line = _i; }
return { top: line * lineHeight + paddingTop, left: (pos - lineStarts[line]) * ML.getCharWidth() + paddingLeft };
}
var overlay = document.createElement('div');
overlay.id = 'search-highlights';
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;z-index:1;';
var wrapper = document.getElementById('editor-wrapper');
wrapper.style.position = 'relative';
wrapper.appendChild(overlay);
for (var _i2 = 0; _i2 < searchMatches.length; _i2++) {
var m = searchMatches[_i2];
var startPos = posToXY(m.start);
var endPos = posToXY(m.end);
var isCurrent = _i2 === searchIndex;
var div = document.createElement('div');
div.className = 'search-hl' + (isCurrent ? ' current' : '');
div.style.cssText = 'position:absolute;top:' + startPos.top + 'px;left:' + startPos.left + 'px;height:' + lineHeight + 'px;width:' + Math.max(endPos.left - startPos.left, 8) + 'px;';
overlay.appendChild(div);
}
}
function clearHighlights() { var old = document.getElementById('search-highlights'); if (old) old.remove(); }
function scrollToMatch(index) {
if (index < 0 || index >= searchMatches.length) return;
searchIndex = index;
var m = searchMatches[index];
ML.editor.setSelectionRange(m.start, m.end);
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
var content = ML.editor.value;
var line = 0;
for (var i = 0; i < m.start; i++) { if (content.charCodeAt(i) === 10) line++; }
var matchTop = line * lineHeight;
var viewTop = ML.editor.scrollTop;
var viewBottom = viewTop + ML.editor.clientHeight;
if (matchTop < viewTop + 40 || matchTop > viewBottom - 40) ML.editor.scrollTop = matchTop - ML.editor.clientHeight / 2;
ML.searchCount.textContent = (index + 1) + '/' + searchMatches.length;
highlightMatches();
}
ML.findNext = function () { if (searchMatches.length === 0) return; scrollToMatch((searchIndex + 1) % searchMatches.length); };
ML.findPrev = function () { if (searchMatches.length === 0) return; scrollToMatch((searchIndex - 1 + searchMatches.length) % searchMatches.length); };
ML.replaceCurrent = function () {
if (searchMatches.length === 0 || searchIndex < 0) return;
var m = searchMatches[searchIndex];
ML.editor.setSelectionRange(m.start, m.end);
document.execCommand('insertText', false, ML.replaceInput.value);
doSearch();
};
ML.replaceAll = function () {
if (searchMatches.length === 0) return;
var replacement = ML.replaceInput.value;
for (var i = searchMatches.length - 1; i >= 0; i--) {
var m = searchMatches[i];
ML.editor.setSelectionRange(m.start, m.end);
document.execCommand('insertText', false, replacement);
}
doSearch();
};
ML.initSearchBar = function () {
ML.searchInput.addEventListener('input', doSearch);
ML.btnCase.addEventListener('click', function () { ML.toggleSearchOption('case'); });
ML.btnRegex.addEventListener('click', function () { ML.toggleSearchOption('regex'); });
ML.btnNext.addEventListener('click', ML.findNext);
ML.btnPrev.addEventListener('click', ML.findPrev);
ML.btnSearchClose.addEventListener('click', ML.closeSearch);
ML.btnReplace.addEventListener('click', ML.replaceCurrent);
ML.btnReplaceAll.addEventListener('click', ML.replaceAll);
};
})();
+208
View File
@@ -0,0 +1,208 @@
// MarkLite — Sidebar Module: file tree, independent files, resize handle
(function () {
'use strict';
var ML = window.MarkLite;
var expandedDirs = new Set();
var lastTreeData = null;
// ===== Sidebar Highlight =====
ML.updateSidebarHighlight = function () {
var tab = ML.getActiveTab();
var activePath = tab ? tab.filePath : null;
if (!ML.currentRootPath) {
ML.sidebarTree.innerHTML = '';
var indep = renderIndependentFiles();
if (indep) ML.sidebarTree.appendChild(indep);
return;
}
var oldSection = ML.sidebarTree.querySelector('.independent-files-section');
var newSection = renderIndependentFiles();
if (oldSection) oldSection.remove();
if (newSection) ML.sidebarTree.insertBefore(newSection, ML.sidebarTree.firstChild);
var treeItems = ML.sidebarTree.querySelectorAll('.tree-item:not(.independent-file-item)');
treeItems.forEach(function (item) {
var itemPath = item.dataset.path;
if (itemPath) item.classList.toggle('active', itemPath === activePath);
});
};
// ===== Independent Files =====
function getIndependentFiles() {
if (!ML.tabs.length) return [];
return ML.tabs.filter(function (t) {
if (!t.filePath) return false;
if (!ML.currentRootPath) return true;
return !t.filePath.startsWith(ML.currentRootPath);
});
}
function renderIndependentFiles() {
var files = getIndependentFiles();
if (files.length === 0) return null;
var section = document.createElement('div');
section.className = 'independent-files-section';
var header = document.createElement('div');
header.className = 'independent-files-header';
header.textContent = '已打开的文件';
section.appendChild(header);
files.forEach(function (tab) {
var item = document.createElement('div');
item.className = 'tree-item independent-file-item';
item.style.paddingLeft = '8px';
item.dataset.path = tab.filePath;
var icon = document.createElement('span');
icon.className = 'tree-icon';
icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>';
item.appendChild(icon);
var name = document.createElement('span');
name.className = 'tree-name';
name.textContent = ML.getFileName(tab.filePath);
item.appendChild(name);
if (tab.isModified) {
var dot = document.createElement('span');
dot.className = 'independent-modified-dot';
dot.textContent = ' •';
item.appendChild(dot);
}
if (tab.id === ML.activeTabId) item.classList.add('active');
item.addEventListener('click', function (e) { e.stopPropagation(); ML.switchToTab(tab.id); });
section.appendChild(item);
});
return section;
}
// ===== Folder Operations =====
async function openFolderDialog() {
if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openFolderDialog) {
var result = await window.electronAPI.openFolderDialog();
if (result) loadFolder(result);
}
}
async function loadFolder(dirPath) {
if (typeof window.electronAPI === 'undefined') return;
ML.currentRootPath = dirPath;
var result = await window.electronAPI.readDirTree(dirPath);
if (result.success) {
renderTree(result.tree, dirPath);
ML.sidebar.classList.remove('collapsed');
window.electronAPI.watchDir(dirPath);
}
}
async function refreshTree() {
if (!ML.currentRootPath) return;
var result = await window.electronAPI.readDirTree(ML.currentRootPath);
if (result.success) renderTree(result.tree, ML.currentRootPath);
}
// ===== Tree Rendering =====
function renderTreeOriginal(tree, rootPath) {
ML.sidebarTree.innerHTML = '';
var rootNode = { name: ML.getFileName(rootPath) || rootPath, path: rootPath, type: 'dir', children: tree };
expandedDirs.add(rootPath);
var fragment = document.createDocumentFragment();
buildTreeDOM(fragment, rootNode, 0);
ML.sidebarTree.appendChild(fragment);
}
function renderTree(tree, rootPath) {
lastTreeData = tree;
renderTreeOriginal(tree, rootPath);
var folderHeader = document.createElement('div');
folderHeader.className = 'sidebar-section-header';
folderHeader.textContent = '文件夹目录树';
ML.sidebarTree.insertBefore(folderHeader, ML.sidebarTree.firstChild);
var indepSection = renderIndependentFiles();
if (indepSection) ML.sidebarTree.insertBefore(indepSection, ML.sidebarTree.firstChild);
}
function buildTreeDOM(parent, node, depth) {
var item = document.createElement('div');
item.className = 'tree-item';
item.style.paddingLeft = (8 + depth * 16) + 'px';
if (node.type === 'dir') {
var isExpanded = expandedDirs.has(node.path);
var arrow = document.createElement('span');
arrow.className = 'tree-arrow' + (isExpanded ? ' expanded' : '');
arrow.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"></polyline></svg>';
item.appendChild(arrow);
var icon = document.createElement('span');
icon.className = 'tree-icon';
icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
item.appendChild(icon);
var name = document.createElement('span');
name.className = 'tree-name';
name.textContent = node.name;
item.appendChild(name);
parent.appendChild(item);
if (isExpanded && node.children) {
node.children.forEach(function (child) { buildTreeDOM(parent, child, depth + 1); });
}
item.addEventListener('click', function (e) {
e.stopPropagation();
if (expandedDirs.has(node.path)) expandedDirs.delete(node.path);
else expandedDirs.add(node.path);
renderTree(lastTreeData || [], ML.currentRootPath);
});
} else {
var spacer = document.createElement('span');
spacer.style.width = '16px';
spacer.style.flexShrink = '0';
item.appendChild(spacer);
var fileIcon = document.createElement('span');
fileIcon.className = 'tree-icon';
fileIcon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>';
item.appendChild(fileIcon);
var fileName = document.createElement('span');
fileName.className = 'tree-name';
fileName.textContent = node.name;
item.appendChild(fileName);
var tab = ML.getActiveTab();
if (tab && tab.filePath === node.path) item.classList.add('active');
item.dataset.path = node.path;
parent.appendChild(item);
item.addEventListener('click', async function (e) {
e.stopPropagation();
var existing = ML.tabs.find(function (t) { return t.filePath === node.path; });
if (existing) { ML.switchToTab(existing.id); }
else if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.readFile(node.path);
if (result.success) ML.createNewTab(node.path, result.content);
}
});
}
}
// ===== Init =====
ML.initSidebar = function () {
var settings = ML.loadSettings();
if (settings.sidebarCollapsed) ML.sidebar.classList.add('collapsed');
if (settings.sidebarWidth) ML.sidebar.style.width = settings.sidebarWidth + 'px';
// Resize handle
var handle = document.createElement('div');
handle.className = 'sidebar-resize-handle';
ML.sidebar.appendChild(handle);
var isResizing = false;
handle.addEventListener('mousedown', function (e) { isResizing = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); });
document.addEventListener('mousemove', function (e) {
if (!isResizing) return;
var newWidth = Math.max(180, Math.min(500, e.clientX));
ML.sidebar.style.width = newWidth + 'px';
ML.saveSetting('sidebarWidth', newWidth);
});
document.addEventListener('mouseup', function () { if (isResizing) { isResizing = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; } });
ML.btnOpenFolder.addEventListener('click', function () { if (typeof window.electronAPI !== 'undefined') openFolderDialog(); });
if (typeof window.electronAPI !== 'undefined') {
var dirRefreshTimer = null;
window.electronAPI.onDirChanged(function () {
if (!ML.currentRootPath) return;
if (dirRefreshTimer) clearTimeout(dirRefreshTimer);
dirRefreshTimer = setTimeout(function () { refreshTree(); }, 300);
});
}
};
})();
+156
View File
@@ -0,0 +1,156 @@
// MarkLite — Tabs Module
(function () {
'use strict';
var ML = window.MarkLite;
// ===== Tab State =====
ML.tabs = [];
ML.activeTabId = null;
var tabIdCounter = 0;
ML.mruTabStack = [];
ML.createTab = function (filePath, content) {
var id = ++tabIdCounter;
return { id: id, filePath: filePath || null, content: content || '', isModified: false, scrollTop: 0, scrollLeft: 0, selectionStart: 0, selectionEnd: 0, previewScrollTop: 0 };
};
ML.getActiveTab = function () { return ML.tabs.find(function (t) { return t.id === ML.activeTabId; }) || null; };
ML.getTabIndex = function (tabId) { return ML.tabs.findIndex(function (t) { return t.id === tabId; }); };
ML.saveCurrentTabState = function () {
var tab = ML.getActiveTab();
if (!tab) return;
tab.content = ML.editor.value;
tab.scrollTop = ML.editor.scrollTop;
tab.scrollLeft = ML.editor.scrollLeft;
tab.selectionStart = ML.editor.selectionStart;
tab.selectionEnd = ML.editor.selectionEnd;
tab.previewScrollTop = ML.previewPanel.scrollTop;
};
ML.loadTabState = function (tab) {
ML.editor.focus(); ML.editor.select();
var inserted = document.execCommand('insertText', false, tab.content);
if (!inserted) ML.editor.value = tab.content;
ML.lineCountCache = 0;
ML.updateLineNumbers();
ML.renderMarkdown(tab.content);
ML.updateFileSize();
ML.cachePreviewPositions();
requestAnimationFrame(function () {
ML.editor.scrollTop = tab.scrollTop;
ML.editor.scrollLeft = tab.scrollLeft;
ML.lineNumbers.scrollTop = tab.scrollTop;
ML.editor.setSelectionRange(tab.selectionStart, tab.selectionEnd);
ML.previewPanel.scrollTop = tab.previewScrollTop;
ML.updateCursorPosition();
});
ML.setModified(tab.isModified);
if (typeof window.electronAPI !== 'undefined') window.electronAPI.tabSwitched(tab.filePath);
};
ML.setModified = function (modified) {
var tab = ML.getActiveTab();
if (tab) {
tab.isModified = modified;
var tabEl = ML.tabList.querySelector('.tab-item[data-tab-id="' + tab.id + '"]');
if (tabEl) tabEl.classList.toggle('modified', modified);
}
ML.updateTitle();
};
ML.createNewTab = function (filePath, content) {
if (filePath) {
var existing = ML.tabs.find(function (t) { return t.filePath === filePath; });
if (existing) { ML.switchToTab(existing.id); return existing; }
}
var tab = ML.createTab(filePath, content);
ML.tabs.push(tab);
ML.switchToTab(tab.id);
return tab;
};
ML.switchToTab = function (tabId) {
if (tabId === ML.activeTabId) return;
ML.saveCurrentTabState();
if (ML.activeTabId) {
ML.mruTabStack = ML.mruTabStack.filter(function (id) { return id !== ML.activeTabId; });
ML.mruTabStack.unshift(ML.activeTabId);
}
ML.activeTabId = tabId;
var tab = ML.getActiveTab();
if (!tab) return;
ML.loadTabState(tab);
ML.renderTabBar();
ML.updateTitle();
ML.updateSidebarHighlight();
ML.welcomeScreen.classList.add('hidden');
};
ML.closeTab = function (tabId) {
var index = ML.getTabIndex(tabId);
if (index === -1) return;
var tab = ML.tabs[index];
if (tab.isModified) {
var name = tab.filePath ? ML.getFileName(tab.filePath) : '未命名';
if (!confirm('"' + name + '" 尚未保存,确定要关闭吗?')) return;
}
ML.tabs.splice(index, 1);
ML.mruTabStack = ML.mruTabStack.filter(function (id) { return id !== tabId; });
if (tabId === ML.activeTabId) {
if (ML.tabs.length === 0) {
ML.activeTabId = null;
ML.editor.value = '';
ML.preview.innerHTML = '';
ML.lineNumbers.innerHTML = '';
ML.lineCountCache = 0;
ML.statusSize.textContent = '';
ML.statusSizeDivider.classList.remove('visible');
ML.welcomeScreen.classList.remove('hidden');
document.title = 'MarkLite';
ML.statusText.textContent = '就绪';
if (typeof window.electronAPI !== 'undefined') window.electronAPI.tabSwitched(null);
} else {
var newIndex = Math.min(index, ML.tabs.length - 1);
ML.activeTabId = ML.tabs[newIndex].id;
ML.loadTabState(ML.tabs[newIndex]);
}
}
ML.renderTabBar();
ML.updateTitle();
ML.updateSidebarHighlight();
};
ML.renderTabBar = function () {
ML.tabList.innerHTML = '';
ML.tabs.forEach(function (tab) {
var el = document.createElement('div');
el.className = 'tab-item' + (tab.id === ML.activeTabId ? ' active' : '') + (tab.isModified ? ' modified' : '');
el.dataset.tabId = tab.id;
var name = document.createElement('span');
name.className = 'tab-name';
name.textContent = tab.filePath ? ML.getFileName(tab.filePath) : '未命名';
el.appendChild(name);
var close = document.createElement('button');
close.className = 'tab-close';
close.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
close.addEventListener('click', function (e) { e.stopPropagation(); ML.closeTab(tab.id); });
el.appendChild(close);
el.addEventListener('click', function () { ML.switchToTab(tab.id); });
ML.tabList.appendChild(el);
});
};
ML.updateTitle = function () {
var tab = ML.getActiveTab();
if (tab) {
var name = tab.filePath ? ML.getFileName(tab.filePath) : '未命名';
var mod = tab.isModified ? ' *' : '';
document.title = 'MarkLite - ' + name + mod;
ML.statusText.textContent = name;
} else {
document.title = 'MarkLite';
ML.statusText.textContent = '就绪';
}
};
})();