Initial commit: MarkLite Markdown Reader

This commit is contained in:
thzxx
2026-05-18 10:40:11 +08:00
commit fda3e5987d
13 changed files with 2967 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# Dependencies
node_modules/
dist/
package-lock.json
# OS files
.DS_Store
Thumbs.db
desktop.ini
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Build output
dist/
out/
*.exe
*.msi
*.dmg
# Logs
*.log
npm-debug.log*
+157
View File
@@ -0,0 +1,157 @@
# MarkLite - 设计文档
## 1. 项目概述
MarkLite 是一款轻量级的 Windows 本地 Markdown 阅读器桌面应用程序。基于 Electron 框架构建,提供简洁现代的用户界面,支持 Markdown 文件的打开、编辑和实时预览。
## 2. 技术架构
### 2.1 技术栈
| 组件 | 技术选型 | 说明 |
|------|----------|------|
| 桌面框架 | Electron | 跨平台桌面应用框架 |
| 前端 | HTML + CSS + JavaScript | 原生前端技术,无框架依赖 |
| Markdown解析 | marked.js v12.0.2 | 高性能 Markdown 解析库 |
| 代码高亮 | highlight.js v11.9.0 | 代码块语法高亮 |
| 打包 | electron-builder | 生成 Windows exe 安装包 |
### 2.2 进程架构
```
┌─────────────────────────────────────────┐
│ Main Process (main.js) │
│ - 窗口管理 │
│ - 文件系统操作 (fs) │
│ - 原生菜单 │
│ - IPC 通信主端 │
└──────────────┬──────────────────────────┘
│ IPC (contextBridge)
┌──────────────▼──────────────────────────┐
│ Preload Script (preload.js) │
│ - 安全的 API 桥接 │
│ - exposeInMainWorld │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│ Renderer Process (renderer/) │
│ - UI 渲染 │
│ - Markdown 编辑与预览 │
│ - 拖拽文件处理 │
│ - 快捷键处理 │
└─────────────────────────────────────────┘
```
## 3. 功能设计
### 3.1 核心功能
1. **文件打开**
- 菜单栏 → 文件 → 打开(支持 .md / .txt / .markdown
- 拖拽文件到窗口打开
- 支持 Windows 文件关联(双击 .md 文件打开)
2. **文件保存**
- Ctrl+S 快捷键保存
- 菜单栏 → 文件 → 保存
- 另存为功能
3. **实时预览**
- 左侧编辑器 + 右侧预览(默认模式)
- 纯预览模式(隐藏编辑器)
- 编辑时实时更新预览
4. **视图模式**
- 编辑+预览(Split View
- 纯编辑模式
- 纯预览模式
### 3.2 UI 设计
#### 色彩方案
- **主色调**: #1a73e8(蓝色)
- **背景色**: #ffffff(白色)
- **侧边栏**: #f8f9fa(浅灰)
- **文字色**: #333333(深灰)
- **代码块背景**: #f6f8fa
- **边框色**: #e1e4e8
#### 字体
- **UI字体**: system-ui, -apple-system, "Segoe UI", sans-serif
- **编辑器字体**: "Cascadia Code", "Fira Code", "Consolas", monospace
- **预览字体**: system-ui, -apple-system, "Segoe UI", sans-serif
#### 布局
```
┌──────────────────────────────────────────────┐
│ 📄 MarkLite - filename.md ─ □ ✕ │
├──────────────────────────────────────────────┤
│ 📁 打开 │ 💾 保存 │ 📝 编辑 │ 👁 预览 │ 分屏 │
├─────────────────┬────────────────────────────┤
│ │ │
│ Editor Area │ Preview Area │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
├─────────────────┴────────────────────────────┤
│ 就绪 │ UTF-8 │ Markdown │ 行: 1, 列: 1 │
└──────────────────────────────────────────────┘
```
## 4. 文件结构
```
MarkLite/
├── package.json # 项目配置与依赖
├── main.js # Electron 主进程
├── preload.js # 预加载脚本(IPC 桥接)
├── renderer/
│ ├── index.html # 主页面
│ ├── style.css # 样式表
│ └── renderer.js # 渲染进程逻辑
├── lib/
│ ├── marked.min.js # Markdown 解析库
│ ├── highlight.min.js # 代码高亮库
│ └── highlight-github.css # 代码高亮主题
├── assets/
│ └── icon.ico # 应用图标
├── DESIGN.md # 设计文档(本文件)
├── README.md # 项目说明
└── .gitignore # Git 忽略文件
```
## 5. IPC 通信设计
| 通道 | 方向 | 说明 |
|------|------|------|
| `dialog:openFile` | Renderer → Main | 打开文件对话框 |
| `file:read` | Renderer → Main | 读取文件内容 |
| `file:save` | Renderer → Main | 保存文件内容 |
| `file:saveAs` | Renderer → Main | 另存为 |
| `menu:action` | Main → Renderer | 菜单操作通知 |
| `window:setTitle` | Renderer → Main | 设置窗口标题 |
## 6. Markdown 渲染支持
支持标准 Markdown 和 GFMGitHub Flavored Markdown):
- 标题(h1-h6
- 段落、换行
- **粗体**、*斜体*、~~删除线~~
- 有序/无序列表
- 任务列表(- [x]
- 代码块(围栏式 + 缩进式)
- 行内代码
- 链接、图片
- 表格
- 引用块
- 水平线
- HTML 内联
## 7. 构建与发布
使用 `electron-builder` 打包:
- 输出格式:NSIS 安装包(.exe)
- 目标平台:Windows x64
- 应用图标:assets/icon.ico
+69
View File
@@ -0,0 +1,69 @@
# MarkLite
一款轻量级的 Windows 本地 Markdown 阅读器桌面应用程序。
## 功能特性
- 📝 Markdown 文件打开与编辑
- 👁 实时预览(编辑+预览 / 纯预览模式)
- 🎨 现代化简洁 UI 设计
- 📂 支持拖拽打开文件
- 💾 文件保存功能
- 🔤 代码语法高亮
- ⌨️ 快捷键支持
## 技术栈
- **Electron** - 桌面应用框架
- **marked.js** - Markdown 解析
- **highlight.js** - 代码高亮
- **HTML + CSS + JavaScript** - 前端技术
## 开发
```bash
# 安装依赖
npm install
# 启动开发模式
npm start
```
## 打包
```bash
# 打包为 Windows exe 安装包
npm run build
```
## 快捷键
| 快捷键 | 功能 |
|--------|------|
| Ctrl+O | 打开文件 |
| Ctrl+S | 保存文件 |
| Ctrl+Shift+S | 另存为 |
| Ctrl+1 | 编辑+预览模式 |
| Ctrl+2 | 纯编辑模式 |
| Ctrl+3 | 纯预览模式 |
## 项目结构
```
MarkLite/
├── package.json # 项目配置
├── main.js # Electron 主进程
├── preload.js # 预加载脚本
├── renderer/ # 渲染进程
│ ├── index.html
│ ├── style.css
│ └── renderer.js
├── lib/ # 第三方库
├── assets/ # 资源文件
├── DESIGN.md # 设计文档
└── README.md # 项目说明
```
## 许可证
MIT
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

+10
View File
@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
+1213
View File
File diff suppressed because one or more lines are too long
+6
View File
File diff suppressed because one or more lines are too long
+273
View File
@@ -0,0 +1,273 @@
const { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow = null;
let currentFilePath = null;
function createWindow() {
// Create the browser window
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
icon: path.join(__dirname, 'assets', 'icon.ico'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'default',
show: false
});
// Load the index.html
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
// Show window when ready
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Build menu
buildMenu();
mainWindow.on('closed', () => {
mainWindow = null;
});
}
function buildMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '打开文件',
accelerator: 'CmdOrCtrl+O',
click: () => handleOpenFile()
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => mainWindow.webContents.send('menu:save')
},
{
label: '另存为',
accelerator: 'CmdOrCtrl+Shift+S',
click: () => mainWindow.webContents.send('menu:saveAs')
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => app.quit()
}
]
},
{
label: '视图',
submenu: [
{
label: '编辑 + 预览',
accelerator: 'CmdOrCtrl+1',
click: () => mainWindow.webContents.send('menu:viewMode', 'split')
},
{
label: '纯编辑',
accelerator: 'CmdOrCtrl+2',
click: () => mainWindow.webContents.send('menu:viewMode', 'editor')
},
{
label: '纯预览',
accelerator: 'CmdOrCtrl+3',
click: () => mainWindow.webContents.send('menu:viewMode', 'preview')
},
{ type: 'separator' },
{
label: '开发者工具',
accelerator: 'F12',
click: () => mainWindow.webContents.toggleDevTools()
},
{ type: 'separator' },
{
label: '重新加载',
accelerator: 'CmdOrCtrl+R',
click: () => mainWindow.webContents.reload()
}
]
},
{
label: '帮助',
submenu: [
{
label: '关于 MarkLite',
click: () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: '关于 MarkLite',
message: 'MarkLite v1.0.0',
detail: '一款轻量级的 Markdown 阅读器。\n\n技术栈:Electron + marked.js + highlight.js'
});
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
async function handleOpenFile() {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] },
{ name: '所有文件', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
openFile(result.filePaths[0]);
}
}
function openFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
currentFilePath = filePath;
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
mainWindow.webContents.send('file:opened', { filePath, content });
} catch (err) {
dialog.showErrorBox('错误', `无法读取文件: ${err.message}`);
}
}
// IPC Handlers
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
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');
currentFilePath = filePath;
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
return { filePath, content };
}
return null;
});
ipcMain.handle('file:read', async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return { success: true, content };
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.handle('file:save', async (event, { filePath, content }) => {
try {
if (filePath) {
fs.writeFileSync(filePath, content, 'utf-8');
currentFilePath = filePath;
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
return { success: true, filePath };
} else {
// No file path, do save as
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: 'Markdown 文件', extensions: ['md'] },
{ name: '文本文件', extensions: ['txt'] }
]
});
if (!result.canceled) {
fs.writeFileSync(result.filePath, content, 'utf-8');
currentFilePath = result.filePath;
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
return { success: true, filePath: result.filePath };
}
return { success: false, canceled: true };
}
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.handle('file:saveAs', async (event, { content }) => {
try {
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: 'Markdown 文件', extensions: ['md'] },
{ name: '文本文件', extensions: ['txt'] }
]
});
if (!result.canceled) {
fs.writeFileSync(result.filePath, content, 'utf-8');
currentFilePath = result.filePath;
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
return { success: true, filePath: result.filePath };
}
return { success: false, canceled: true };
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.handle('file:getCurrentPath', () => {
return currentFilePath;
});
// Handle file open from command line or file association
function handleFileOpen() {
const args = process.argv.slice(1);
if (args.length > 0 && !args[0].startsWith('--')) {
const filePath = args[0];
if (fs.existsSync(filePath)) {
return filePath;
}
}
return null;
}
// App lifecycle
app.whenReady().then(() => {
createWindow();
// Check for file passed via command line
const fileToOpen = handleFileOpen();
if (fileToOpen) {
// Wait for window to be ready
mainWindow.webContents.on('did-finish-load', () => {
openFile(fileToOpen);
});
}
// Handle macOS open-file event
app.on('open-file', (event, filePath) => {
event.preventDefault();
if (mainWindow) {
openFile(filePath);
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
+67
View File
@@ -0,0 +1,67 @@
{
"name": "marklite",
"version": "1.0.0",
"description": "Lightweight Markdown Reader for Windows",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder --win",
"build:portable": "electron-builder --win portable"
},
"author": "MarkLite",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1"
},
"build": {
"appId": "com.marklite.app",
"productName": "MarkLite",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"renderer/**/*",
"lib/**/*",
"assets/**/*"
],
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"icon": "assets/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "MarkLite"
},
"fileAssociations": [
{
"ext": "md",
"name": "Markdown",
"description": "Markdown File",
"icon": "assets/icon.ico"
},
{
"ext": "markdown",
"name": "Markdown",
"description": "Markdown File",
"icon": "assets/icon.ico"
},
{
"ext": "txt",
"name": "Text",
"description": "Text File",
"icon": "assets/icon.ico"
}
]
}
}
+19
View File
@@ -0,0 +1,19 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// File operations
openFile: () => ipcRenderer.invoke('dialog:openFile'),
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
saveFile: (data) => ipcRenderer.invoke('file:save', data),
saveFileAs: (data) => ipcRenderer.invoke('file:saveAs', data),
getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'),
// Menu events
onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)),
onMenuSave: (callback) => ipcRenderer.on('menu:save', () => callback()),
onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()),
onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)),
// Remove listeners
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
});
+137
View File
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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:;">
<title>MarkLite</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="../lib/highlight-github.css">
</head>
<body>
<div id="app">
<!-- Toolbar -->
<div id="toolbar">
<div class="toolbar-left">
<button id="btn-open" class="toolbar-btn" title="打开文件 (Ctrl+O)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
<span>打开</span>
</button>
<button id="btn-save" class="toolbar-btn" title="保存文件 (Ctrl+S)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
<span>保存</span>
</button>
<div class="toolbar-divider"></div>
<button id="btn-split" class="toolbar-btn active" title="编辑+预览 (Ctrl+1)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="3" x2="12" y2="21"></line>
</svg>
<span>分屏</span>
</button>
<button id="btn-editor" class="toolbar-btn" title="纯编辑 (Ctrl+2)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
<span>编辑</span>
</button>
<button id="btn-preview" class="toolbar-btn" title="纯预览 (Ctrl+3)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span>预览</span>
</button>
</div>
</div>
<!-- Main content area -->
<div id="main-content">
<!-- Editor panel -->
<div id="editor-panel">
<div id="editor-wrapper">
<div id="line-numbers"></div>
<textarea id="editor" spellcheck="false" placeholder="在此输入 Markdown 内容,或拖拽 .md 文件到窗口打开..."></textarea>
</div>
</div>
<!-- Resizer -->
<div id="resizer"></div>
<!-- Preview panel -->
<div id="preview-panel">
<div id="preview" class="markdown-body"></div>
</div>
</div>
<!-- Status bar -->
<div id="statusbar">
<div class="status-left">
<span id="status-text">就绪</span>
</div>
<div class="status-right">
<span id="status-encoding">UTF-8</span>
<span class="status-divider">|</span>
<span id="status-lang">Markdown</span>
<span class="status-divider">|</span>
<span id="status-cursor">行 1, 列 1</span>
</div>
</div>
<!-- Drop overlay -->
<div id="drop-overlay" class="hidden">
<div class="drop-content">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<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>
<p>释放文件以打开</p>
</div>
</div>
<!-- Welcome screen -->
<div id="welcome-screen">
<div class="welcome-content">
<div class="welcome-icon">
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="5" width="80" height="90" rx="8" fill="#f0f6ff" stroke="#1a73e8" stroke-width="2"/>
<text x="50" y="45" text-anchor="middle" fill="#1a73e8" font-family="system-ui" font-weight="bold" font-size="32">M↓</text>
<text x="50" y="70" text-anchor="middle" fill="#5f6368" font-family="system-ui" font-size="12">MarkLite</text>
</svg>
</div>
<h1>欢迎使用 MarkLite</h1>
<p>一款轻量级的 Markdown 阅读器</p>
<div class="welcome-actions">
<button id="btn-welcome-open" class="welcome-btn primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
打开文件
</button>
<button id="btn-welcome-new" class="welcome-btn secondary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
新建文件
</button>
</div>
<div class="welcome-tips">
<p>💡 提示:可以直接拖拽 .md 文件到窗口打开</p>
<p>⌨️ 快捷键:Ctrl+O 打开 | Ctrl+S 保存 | Ctrl+1/2/3 切换视图</p>
</div>
</div>
</div>
</div>
<script src="../lib/marked.min.js"></script>
<script src="../lib/highlight.min.js"></script>
<script src="renderer.js"></script>
</body>
</html>
+441
View File
@@ -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();
}
})();
+548
View File
@@ -0,0 +1,548 @@
/* ===== Reset & Base ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #1a73e8;
--primary-light: #e8f0fe;
--primary-dark: #1557b0;
--bg: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #f1f3f4;
--text: #333333;
--text-secondary: #5f6368;
--text-tertiary: #9aa0a6;
--border: #e1e4e8;
--border-light: #f0f0f0;
--code-bg: #f6f8fa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1);
--radius: 6px;
--toolbar-height: 44px;
--statusbar-height: 28px;
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
}
html, body {
height: 100%;
font-family: var(--font-ui);
font-size: 14px;
color: var(--text);
background: var(--bg);
overflow: hidden;
user-select: none;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
/* ===== Toolbar ===== */
#toolbar {
height: var(--toolbar-height);
background: var(--bg);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 8px;
flex-shrink: 0;
z-index: 10;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 2px;
}
.toolbar-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-family: var(--font-ui);
border-radius: var(--radius);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.toolbar-btn:hover {
background: var(--bg-tertiary);
color: var(--text);
}
.toolbar-btn.active {
background: var(--primary-light);
color: var(--primary);
}
.toolbar-btn svg {
flex-shrink: 0;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: var(--border);
margin: 0 6px;
}
/* ===== Main Content ===== */
#main-content {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
/* ===== Editor Panel ===== */
#editor-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
border-right: 1px solid var(--border);
}
#editor-wrapper {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
#line-numbers {
width: 50px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-light);
padding: 12px 0;
text-align: right;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
color: var(--text-tertiary);
overflow: hidden;
user-select: none;
flex-shrink: 0;
}
#line-numbers .line-num {
padding-right: 12px;
height: 20.8px;
}
#editor {
flex: 1;
width: 100%;
padding: 12px 16px;
border: none;
outline: none;
resize: none;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
color: var(--text);
background: var(--bg);
tab-size: 4;
overflow-y: auto;
user-select: text;
}
#editor::placeholder {
color: var(--text-tertiary);
}
/* ===== Resizer ===== */
#resizer {
width: 4px;
background: var(--border);
cursor: col-resize;
transition: background 0.15s ease;
flex-shrink: 0;
}
#resizer:hover,
#resizer.active {
background: var(--primary);
}
/* ===== Preview Panel ===== */
#preview-panel {
flex: 1;
overflow-y: auto;
min-width: 0;
background: var(--bg);
}
#preview {
padding: 24px 32px;
max-width: 900px;
margin: 0 auto;
}
/* ===== Markdown Body Styles ===== */
.markdown-body {
font-family: var(--font-ui);
font-size: 15px;
line-height: 1.7;
color: var(--text);
word-wrap: break-word;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: var(--text);
}
.markdown-body h1 {
font-size: 2em;
padding-bottom: 0.3em;
border-bottom: 1px solid var(--border);
}
.markdown-body h2 {
font-size: 1.5em;
padding-bottom: 0.3em;
border-bottom: 1px solid var(--border-light);
}
.markdown-body h3 { font-size: 1.25em; }
.markdown-body h4 { font-size: 1em; }
.markdown-body h5 { font-size: 0.875em; }
.markdown-body h6 { font-size: 0.85em; color: var(--text-secondary); }
.markdown-body p {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body a {
color: var(--primary);
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body strong { font-weight: 600; }
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: var(--radius);
margin: 8px 0;
}
.markdown-body hr {
height: 2px;
background: var(--border);
border: none;
margin: 24px 0;
border-radius: 1px;
}
.markdown-body blockquote {
margin: 0 0 16px 0;
padding: 4px 16px;
border-left: 4px solid var(--primary);
color: var(--text-secondary);
background: var(--bg-secondary);
border-radius: 0 var(--radius) var(--radius) 0;
}
.markdown-body blockquote p:last-child {
margin-bottom: 0;
}
.markdown-body ul,
.markdown-body ol {
margin-top: 0;
margin-bottom: 16px;
padding-left: 2em;
}
.markdown-body li {
margin-top: 4px;
}
.markdown-body li + li {
margin-top: 4px;
}
.markdown-body code {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--code-bg);
padding: 2px 6px;
border-radius: 4px;
color: #e83e8c;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 16px;
padding: 16px;
background: var(--code-bg);
border-radius: var(--radius);
overflow-x: auto;
border: 1px solid var(--border-light);
}
.markdown-body pre code {
padding: 0;
background: transparent;
color: inherit;
font-size: 13px;
line-height: 1.5;
}
.markdown-body table {
border-collapse: collapse;
width: 100%;
margin-bottom: 16px;
overflow-x: auto;
display: block;
}
.markdown-body table th,
.markdown-body table td {
padding: 8px 16px;
border: 1px solid var(--border);
text-align: left;
}
.markdown-body table th {
font-weight: 600;
background: var(--bg-secondary);
}
.markdown-body table tr:nth-child(even) {
background: var(--bg-secondary);
}
.markdown-body input[type="checkbox"] {
margin-right: 6px;
accent-color: var(--primary);
}
/* ===== Status Bar ===== */
#statusbar {
height: var(--statusbar-height);
background: var(--bg-secondary);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
font-size: 12px;
color: var(--text-secondary);
flex-shrink: 0;
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 8px;
}
.status-divider {
color: var(--border);
}
/* ===== Drop Overlay ===== */
#drop-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 115, 232, 0.1);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
border: 3px dashed var(--primary);
margin: 8px;
border-radius: 12px;
}
#drop-overlay.hidden {
display: none;
}
.drop-content {
text-align: center;
color: var(--primary);
}
.drop-content svg {
margin-bottom: 12px;
opacity: 0.7;
}
.drop-content p {
font-size: 18px;
font-weight: 500;
}
/* ===== Welcome Screen ===== */
#welcome-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
z-index: 5;
}
#welcome-screen.hidden {
display: none;
}
.welcome-content {
text-align: center;
max-width: 480px;
padding: 40px;
}
.welcome-icon {
margin-bottom: 24px;
}
.welcome-content h1 {
font-size: 28px;
font-weight: 600;
color: var(--text);
margin-bottom: 8px;
}
.welcome-content > p {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 32px;
}
.welcome-actions {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 32px;
}
.welcome-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
border: none;
border-radius: var(--radius);
font-size: 14px;
font-family: var(--font-ui);
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.welcome-btn.primary {
background: var(--primary);
color: white;
}
.welcome-btn.primary:hover {
background: var(--primary-dark);
box-shadow: var(--shadow-lg);
}
.welcome-btn.secondary {
background: var(--bg-secondary);
color: var(--text);
border: 1px solid var(--border);
}
.welcome-btn.secondary:hover {
background: var(--bg-tertiary);
}
.welcome-tips {
text-align: left;
background: var(--bg-secondary);
border-radius: var(--radius);
padding: 16px 20px;
border: 1px solid var(--border-light);
}
.welcome-tips p {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.welcome-tips p:last-child {
margin-bottom: 0;
}
/* ===== View Modes ===== */
#app.mode-editor #preview-panel,
#app.mode-editor #resizer {
display: none;
}
#app.mode-preview #editor-panel,
#app.mode-preview #resizer {
display: none;
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* ===== Animations ===== */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.welcome-content {
animation: fadeIn 0.3s ease;
}