diff --git a/DESIGN.md b/DESIGN.md
index b4ce514..9fbe047 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,8 +1,8 @@
-# MarkLite v0.1.1 — 架构设计文档
+# MarkLite v0.3.10 — 架构设计文档
## 1. 项目概述
-MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 CodeMirror 6 编辑器、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
+MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 Milkdown v7 WYSIWYG 编辑器 + 源码编辑模式、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
### 1.1 核心原则
@@ -20,7 +20,7 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
| 桌面框架 | Electron | v28 | 跨平台桌面应用框架 |
| 前端框架 | React | v18 | 函数组件 + Hooks |
| 类型系统 | TypeScript | v5.6 | 全量类型安全 |
-| 编辑器 | CodeMirror | v6 | 现代化代码编辑器 |
+| 编辑器 | Milkdown | v7 | WYSIWYG 编辑器 + 源码编辑模式 |
| 状态管理 | Zustand | v5 | 轻量级状态管理 |
| 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 |
| Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线 |
@@ -44,29 +44,36 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
┌───────────────────────▼──────────────────────────────────────┐
│ Preload Script (src/preload/) 1 文件 │
│ index.ts contextBridge 类型安全暴露 │
-│ electronAPI 22 个方法/事件的类型安全接口 │
+│ electronAPI 18 个方法/事件的类型安全接口 │
└───────────────────────┬──────────────────────────────────────┘
│
┌───────────────────────▼──────────────────────────────────────┐
│ Renderer Process (src/renderer/) React 18 │
│ │
-│ components/ (11) Toolbar · TabBar · Editor · Preview │
-│ Sidebar · StatusBar · WelcomeScreen │
-│ Toast · ModifiedBanner · DropOverlay │
+│ components/ (23) Toolbar · TabBar · Editor · EditorToolbar │
+│ SourceEditor · Preview · Sidebar · FileTree│
+│ OutlinePanel · StatusBar · WelcomeScreen │
+│ Toast · ConfirmDialog · ModifiedBanner │
+│ SearchReplace · DropOverlay · ErrorBoundary│
+│ AboutDialog · LoadingSpinner · Icons │
│ │
│ stores/ (3) tabStore · editorStore · sidebarStore │
-│ hooks/ (6) useTheme · useSettings · useKeyboard │
-│ useDragDrop · useFileWatch · useUnsaved │
-│ lib/ (3) markdown · fileUtils · constants │
-│ db/ (4) schema · tab · settings · recentFiles │
-│ types/ (6) tab · file · settings · ipc · index │
+│ hooks/ (19) useTheme · useSettings · useSettingsInit │
+│ useKeyboard · useDragDrop · useFileWatch │
+│ useAutoSave · useIpcListeners ... │
+│ lib/ (4) markdown · fileUtils · errorHandler │
+│ constants │
+│ db/ (4) schema · tabRepository · settingsRepo │
+│ recentFilesRepository │
+│ types/ (5) tab · file · settings · ipc · index │
│ styles/ (3) variables · global · markdown-body │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
-│ Shared (src/shared/) 2 文件 │
+│ Shared (src/shared/) 3 文件 │
│ ipc-channels.ts IPC 通道名常量 │
│ types.ts 共享类型定义 │
+│ constants.ts 共享常量 (版本号、文件大小限制等) │
└──────────────────────────────────────────────────────────────┘
```
@@ -205,21 +212,30 @@ db.version(1).stores({
| `window:confirmClose` | 无 | 请求确认关闭 |
| `sidebar:dirChanged` | 无 | 目录结构变化 |
-## 6. 编辑器架构 — CodeMirror 6
+## 6. 编辑器架构 — Milkdown v7 (WYSIWYG) + SourceEditor (textarea)
-### 6.1 功能特性
+### 6.1 双编辑模式
-- Markdown 语法高亮
-- 行号显示
-- 代码折叠 (foldGutter)
-- 括号匹配 (bracketMatching)
-- 搜索替换 (search) — 中文本地化
-- 历史记录 (history) — 支持 undo/redo
-- Tab 缩进 (indentWithTab)
-- 自动换行 (lineWrapping)
-- 暗色主题 (oneDark)
+- **编辑模式 (Milkdown)**:基于 ProseMirror 的 WYSIWYG Markdown 编辑器,支持格式化工具栏(粗体/斜体/删除线/标题/列表/引用/代码块/链接/图片/分割线)、搜索替换面板(含正则支持)、自动配对括号/引号、undo/redo
+- **源码模式 (SourceEditor)**:原生 textarea 控制 Markdown 原文,支持 Tab 缩进、Ctrl+B/I 快捷键
-### 6.2 滚动与选区持久化
+两种模式共享同一 tabStore 数据源,可随时切换。
+
+### 6.2 插件体系
+
+| 插件 | 说明 |
+|------|------|
+| commonmark | 基础 Markdown 语法 |
+| gfm | GitHub Flavored Markdown(表格/任务列表/删除线等) |
+| history | undo/redo |
+| listener | 内容变更监听 |
+| indent | Tab 缩进 |
+| trailing | 尾随换行 |
+| clipboard | 剪贴板增强 |
+| searchPlugin (自研) | 搜索高亮装饰 |
+| autoPairPlugin (自研) | 自动配对括号/引号 |
+
+### 6.3 滚动与选区持久化
切换标签时自动保存/恢复:
- 滚动位置 (`scrollTop`)
diff --git a/README.md b/README.md
index 1ac84d6..09139ab 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -30,7 +30,7 @@
|------|------|
| 📑 **多标签页** | 同时打开多个文件,Ctrl+T 新建、Ctrl+W 关闭、Ctrl+Tab MRU 切换 |
| 📂 **文件打开** | 按钮打开 / 拖拽打开 / 文件关联(双击 .md) / 命令行参数 |
-| ✏️ **实时编辑** | 左侧编辑器(CodeMirror 6),支持 Tab 缩进、行号显示、折叠、括号匹配 |
+| ✏️ **双编辑模式** | Milkdown WYSIWYG 编辑器(格式化工具栏/搜索替换/正则/自动配对)+ 源码模式(textarea)|
| 👁 **实时预览** | 右侧预览面板,基于 unified/rehype 管线渲染,编辑即更新 |
| 🔤 **代码高亮** | 基于 rehype-highlight,支持 180+ 种编程语言语法高亮 |
| 🎨 **两种视图** | 编辑模式 / 预览模式,自由切换 |
@@ -131,6 +131,7 @@ npm run test:coverage
| `Ctrl + Shift + S` | 另存为 |
| `Ctrl + 1` | 编辑模式 |
| `Ctrl + 2` | 预览模式 |
+| `Ctrl + 3` | 源码模式 |
| `Ctrl + F` | 搜索 |
| `Ctrl + H` | 搜索并替换 |
| `Ctrl + B` | 粗体 |
@@ -145,7 +146,7 @@ npm run test:coverage
| 桌面框架 | [Electron](https://www.electronjs.org/) v28 | 跨平台桌面应用框架 |
| 前端框架 | [React](https://react.dev/) v18 | 函数组件 + Hooks |
| 类型系统 | [TypeScript](https://www.typescriptlang.org/) v5.6 | 全量类型安全 |
-| 编辑器 | [CodeMirror](https://codemirror.net/) v6 | 现代化代码编辑器 |
+| 编辑器 | [Milkdown](https://milkdown.dev/) v7 | WYSIWYG 编辑器 + 源码模式 |
| 状态管理 | [Zustand](https://zustand-demo.pmnd.rs/) v5 | 轻量级状态管理 |
| 持久化 | [Dexie.js](https://dexie.org/) v4 (IndexedDB) | 标签页状态 & 用户设置持久化 |
| Markdown 解析 | [unified](https://unifiedjs.com/) / [remark](https://remark.js.org/) / [rehype](https://rehype.js.org/) | 插件化 Markdown 渲染管线 |
@@ -182,17 +183,25 @@ MarkLite/
│ │ ├── main.tsx # React 入口
│ │ ├── App.tsx # 根组件:布局编排、全局事件
│ │ │
-│ │ ├── components/ # UI 组件
+│ │ ├── components/ # UI 组件 (23个)
│ │ │ ├── Toolbar/ # 工具栏
-│ │ │ ├── TabBar/ # 标签页栏
-│ │ │ ├── Editor/ # CodeMirror 6 编辑器
+│ │ │ ├── TabBar/ # 标签页栏 (含拖拽排序)
+│ │ │ ├── Editor/ # Milkdown 编辑器 + 工具栏
│ │ │ ├── Preview/ # Markdown 预览面板
+│ │ │ ├── SourceEditor/ # 源码编辑模式 (textarea)
│ │ │ ├── Sidebar/ # 侧边栏文件树
-│ │ │ ├── StatusBar/ # 状态栏
+│ │ │ ├── FileTree/ # 递归文件树
+│ │ │ ├── OutlinePanel/ # 文档大纲 (活跃标题高亮)
+│ │ │ ├── StatusBar/ # 状态栏 (自动保存开关)
+│ │ │ ├── SearchReplace/ # 搜索替换面板 (支持正则)
│ │ │ ├── WelcomeScreen/ # 欢迎屏幕
│ │ │ ├── Toast/ # Toast 通知
+│ │ │ ├── ConfirmDialog/ # 确认对话框
│ │ │ ├── ModifiedBanner/ # 文件外部修改提示
│ │ │ ├── DropOverlay/ # 拖拽文件覆盖层
+│ │ │ ├── ErrorBoundary/ # 错误边界
+│ │ │ ├── AboutDialog/ # 关于对话框
+│ │ │ ├── LoadingSpinner/ # 加载指示器
│ │ │ └── Icons.tsx # SVG 图标库
│ │ │
│ │ ├── stores/ # Zustand 状态管理
@@ -200,17 +209,23 @@ MarkLite/
│ │ │ ├── editorStore.ts # 编辑器状态
│ │ │ └── sidebarStore.ts # 侧边栏状态
│ │ │
-│ │ ├── hooks/ # 自定义 Hooks
+│ │ ├── hooks/ # 自定义 Hooks (19个)
│ │ │ ├── useTheme.ts # 暗色/亮色主题
-│ │ │ ├── useSettings.ts # 用户设置
+│ │ │ ├── useSettings.ts # 视图模式
+│ │ │ ├── useSettingsInit.ts # 设置初始加载
│ │ │ ├── useKeyboard.ts # 全局快捷键
│ │ │ ├── useDragDrop.ts # 拖拽打开
│ │ │ ├── useFileWatch.ts # 外部修改监听
-│ │ │ └── useUnsavedWarning.ts # 未保存提醒
+│ │ │ ├── useUnsavedWarning.ts # 未保存提醒
+│ │ │ ├── useAutoSave.ts # 自动保存
+│ │ │ ├── useAutoExpandDir.ts # 自动展开目录
+│ │ │ ├── useActiveHeading.ts # 活跃标题追踪
+│ │ │ └── ... # 文件操作、IPC监听等
│ │ │
│ │ ├── lib/ # 工具库
│ │ │ ├── markdown.ts # Markdown 渲染管线
│ │ │ ├── fileUtils.ts # 文件工具函数
+│ │ │ ├── errorHandler.ts # 错误处理
│ │ │ └── constants.ts # 常量定义
│ │ │
│ │ ├── db/ # IndexedDB 持久化层
diff --git a/package.json b/package.json
index 32aa2da..e09e776 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "marklite",
- "version": "0.3.8",
+ "version": "0.3.10",
"description": "Lightweight Markdown Editor for Windows",
"main": "./dist/main/index.js",
"scripts": {
diff --git a/src/preload/index.ts b/src/preload/index.ts
index d301e9b..21db7d0 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -1,12 +1,14 @@
import { contextBridge, ipcRenderer, shell } from 'electron'
import { IPC_CHANNELS } from '../shared/ipc-channels'
+import type { ElectronAPI } from '../renderer/types/ipc'
-contextBridge.exposeInMainWorld('electronAPI', {
+// C-02: 运行时实现受 ElectronAPI 类型约束,编译期保证 preload 与渲染进程契约一致
+const api: ElectronAPI = {
// File operations
openFile: () => ipcRenderer.invoke(IPC_CHANNELS.DIALOG_OPEN_FILE),
readFile: (filePath: string) => ipcRenderer.invoke(IPC_CHANNELS.FILE_READ, filePath),
- saveFile: (data: { filePath: string | null; content: string }) => ipcRenderer.invoke(IPC_CHANNELS.FILE_SAVE, data),
- saveFileAs: (data: { content: string }) => ipcRenderer.invoke(IPC_CHANNELS.FILE_SAVE_AS, data),
+ saveFile: (data) => ipcRenderer.invoke(IPC_CHANNELS.FILE_SAVE, data),
+ saveFileAs: (data) => ipcRenderer.invoke(IPC_CHANNELS.FILE_SAVE_AS, data),
getCurrentPath: () => ipcRenderer.invoke(IPC_CHANNELS.FILE_GET_CURRENT_PATH),
getFileStats: (filePath: string) => ipcRenderer.invoke(IPC_CHANNELS.FILE_STATS, filePath),
reloadFile: () => ipcRenderer.invoke(IPC_CHANNELS.FILE_RELOAD),
@@ -37,39 +39,26 @@ contextBridge.exposeInMainWorld('electronAPI', {
unwatchDir: () => ipcRenderer.invoke(IPC_CHANNELS.DIR_UNWATCH),
// Events from main process — 返回取消订阅函数
- onFileOpenInTab: (callback: (data: { filePath: string; content: string }) => void) => {
+ onFileOpenInTab: (callback) => {
const handler = (_event: Electron.IpcRendererEvent, data: { filePath: string; content: string }) => callback(data)
ipcRenderer.on(IPC_CHANNELS.FILE_OPEN_IN_TAB, handler)
return () => { ipcRenderer.removeListener(IPC_CHANNELS.FILE_OPEN_IN_TAB, handler) }
},
- onMenuSave: (callback: () => void) => {
- const handler = () => callback()
- ipcRenderer.on(IPC_CHANNELS.MENU_SAVE, handler)
- return () => { ipcRenderer.removeListener(IPC_CHANNELS.MENU_SAVE, handler) }
- },
- onMenuSaveAs: (callback: () => void) => {
- const handler = () => callback()
- ipcRenderer.on(IPC_CHANNELS.MENU_SAVE_AS, handler)
- return () => { ipcRenderer.removeListener(IPC_CHANNELS.MENU_SAVE_AS, handler) }
- },
- onViewModeChange: (callback: (mode: string) => void) => {
- const handler = (_event: Electron.IpcRendererEvent, mode: string) => callback(mode)
- ipcRenderer.on(IPC_CHANNELS.MENU_VIEW_MODE, handler)
- return () => { ipcRenderer.removeListener(IPC_CHANNELS.MENU_VIEW_MODE, handler) }
- },
- onExternalModification: (callback: (filePath: string) => void) => {
+ onExternalModification: (callback) => {
const handler = (_event: Electron.IpcRendererEvent, filePath: string) => callback(filePath)
ipcRenderer.on(IPC_CHANNELS.FILE_EXTERNALLY_MODIFIED, handler)
return () => { ipcRenderer.removeListener(IPC_CHANNELS.FILE_EXTERNALLY_MODIFIED, handler) }
},
- onDirChanged: (callback: () => void) => {
+ onDirChanged: (callback) => {
const handler = () => callback()
ipcRenderer.on(IPC_CHANNELS.SIDEBAR_DIR_CHANGED, handler)
return () => { ipcRenderer.removeListener(IPC_CHANNELS.SIDEBAR_DIR_CHANGED, handler) }
},
- onConfirmClose: (callback: () => void) => {
+ onConfirmClose: (callback) => {
const handler = () => callback()
ipcRenderer.on(IPC_CHANNELS.WINDOW_CONFIRM_CLOSE, handler)
return () => { ipcRenderer.removeListener(IPC_CHANNELS.WINDOW_CONFIRM_CLOSE, handler) }
}
-})
+}
+
+contextBridge.exposeInMainWorld('electronAPI', api)
diff --git a/src/preload/preload.d.ts b/src/preload/preload.d.ts
index cb3b537..e337233 100644
--- a/src/preload/preload.d.ts
+++ b/src/preload/preload.d.ts
@@ -1,55 +1,11 @@
/**
* DX-02: Preload 类型安全声明
- * 为 window.electronAPI 提供完整的 TypeScript 类型定义,
- * 确保渲染进程访问 API 时有完整的类型提示和编译时检查。
+ * ElectronAPI 类型现在由 ../renderer/types/ipc.ts 集中定义,
+ * preload/index.ts 引入该类型做编译期契约检查。
+ * 本文件仅保留全局 Window 增强声明。
*/
-import type {
- OpenFileResponse,
- ReadFileResult,
- SaveFilePayload,
- SaveAsPayload,
- SaveFileResult,
- ReloadFileResult,
- FileStatsResult,
- ReadDirTreeResult,
-} from '../shared/types'
-
-interface ElectronAPI {
- // File operations
- openFile: () => Promise
- readFile: (filePath: string) => Promise
- saveFile: (data: SaveFilePayload) => Promise
- saveFileAs: (data: SaveAsPayload) => Promise
- getCurrentPath: () => Promise
- getFileStats: (filePath: string) => Promise
- reloadFile: () => Promise
-
- // Tab management
- tabSwitched: (filePath: string | null) => Promise
-
- // Window control
- forceClose: () => Promise
- cancelClose: () => Promise
-
- // Shell
- openExternal: (url: string) => void
-
- // File Tree (Sidebar)
- readDirTree: (dirPath: string) => Promise
- openFolderDialog: () => Promise<{ canceled: boolean; filePaths: string[] }>
- watchDir: (dirPath: string) => Promise
- unwatchDir: () => Promise
-
- // Events from main process — 返回取消订阅函数
- onFileOpenInTab: (callback: (data: { filePath: string; content: string }) => void) => () => void
- onMenuSave: (callback: () => void) => () => void
- onMenuSaveAs: (callback: () => void) => () => void
- onViewModeChange: (callback: (mode: string) => void) => () => void
- onExternalModification: (callback: (filePath: string) => void) => () => void
- onDirChanged: (callback: () => void) => () => void
- onConfirmClose: (callback: () => void) => () => void
-}
+import type { ElectronAPI } from '../renderer/types/ipc'
declare global {
interface Window {
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index bb9ced8..c5896dc 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useTabStore } from './stores/tabStore'
+import { flushSaveToDB } from './stores/tabStore'
import { useEditorStore } from './stores/editorStore'
import { useTheme } from './hooks/useTheme'
import { useSettings } from './hooks/useSettings'
@@ -51,7 +52,7 @@ export function App() {
const { handleOpenFile, handleSave, handleSaveAs, handleOpenRecent } = useFileOperations(showToast)
useDragDrop(showToast)
useFileWatch()
- const { isAutoSaving, autoSaveEnabled } = useAutoSave()
+ const { isAutoSaving, autoSaveEnabled, toggleAutoSave } = useAutoSave()
// UX-01: 传入 confirm 函数替代原生 confirm()
const handleConfirmClose = useCallback(async (message: string): Promise => {
@@ -64,9 +65,9 @@ export function App() {
})
}, [confirm])
- useUnsavedWarning(() => tabs.some(t => t.isModified), handleConfirmClose)
+ useUnsavedWarning(() => tabs.some(t => t.isModified), handleConfirmClose, flushSaveToDB)
useKeyboard(handleOpenFile, handleSave, handleSaveAs)
- useIpcListeners(handleSave, handleSaveAs)
+ useIpcListeners()
useEffect(() => {
if (!window.electronAPI) return
@@ -117,7 +118,7 @@ export function App() {
)}
-
+
{showAbout && }
diff --git a/src/renderer/components/Editor/EditorToolbar.tsx b/src/renderer/components/Editor/EditorToolbar.tsx
index e9bd26d..3caaaa1 100644
--- a/src/renderer/components/Editor/EditorToolbar.tsx
+++ b/src/renderer/components/Editor/EditorToolbar.tsx
@@ -10,6 +10,7 @@ import {
wrapInBulletListCommand,
wrapInOrderedListCommand,
createCodeBlockCommand,
+ toggleInlineCodeCommand,
insertImageCommand,
toggleLinkCommand,
insertHrCommand
@@ -63,7 +64,7 @@ export const EditorToolbar = React.memo(function EditorToolbar({ action }: Edito