|
| 1 | +这里的 Muya,实际上是 Coolma 抽出来的 `coolma-muya` Markdown 编辑器核心。它的核心思想可以概括成一句话: |
| 2 | + |
| 3 | +> Markdown 是唯一真源,编辑器把它转换成可编辑的结构树,再渲染成富文本 DOM;用户操作后再从结构树导回 Markdown。 |
| 4 | +
|
| 5 | +整体数据流大致是: |
| 6 | + |
| 7 | +```mermaid |
| 8 | +flowchart LR |
| 9 | + A["Markdown 源文本"] --> B["Lexer / markdownToState"] |
| 10 | + B --> C["ContentState:Block Tree"] |
| 11 | + C --> D["Inline Tokenizer"] |
| 12 | + D --> E["StateRender + Snabbdom"] |
| 13 | + E --> F["contenteditable DOM"] |
| 14 | + F --> G["input / keyboard / mouse 事件"] |
| 15 | + G --> C |
| 16 | + C --> H["ExportMarkdown"] |
| 17 | + H --> A |
| 18 | +``` |
| 19 | + |
| 20 | +### 1. 核心模型:Block Tree |
| 21 | + |
| 22 | +Muya 不直接把 HTML 当作编辑模型,而是在 `ContentState` 中维护一棵块级树: |
| 23 | + |
| 24 | +- `p`、`h1`、`blockquote`、`ul`、`table` 等是容器或段落块; |
| 25 | +- `span` 是真正承载文本的叶子块; |
| 26 | +- 每个 block 有稳定的 `key`; |
| 27 | +- 同时维护 `parent`、`preSibling`、`nextSibling` 和 `children`。 |
| 28 | + |
| 29 | +相关代码在: |
| 30 | + |
| 31 | +- [`contentState/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/contentState/index.js) |
| 32 | +- [`utils/importMarkdown.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/utils/importMarkdown.js) |
| 33 | + |
| 34 | +这样设计的好处是,回车、删除、列表缩进、表格单元格操作等,都可以直接操作结构树,而不是依赖浏览器不稳定的 `contenteditable` HTML。 |
| 35 | + |
| 36 | +例如: |
| 37 | + |
| 38 | +```text |
| 39 | +Markdown |
| 40 | +└── ul |
| 41 | + └── li |
| 42 | + ├── input[type=checkbox] |
| 43 | + └── span("item text") |
| 44 | +``` |
| 45 | + |
| 46 | +### 2. Markdown 解析分两层 |
| 47 | + |
| 48 | +Muya 的解析器大致分为两层: |
| 49 | + |
| 50 | +#### 块级解析 |
| 51 | + |
| 52 | +通过类似 Marked 的 Lexer,把 Markdown 解析成 heading、paragraph、list、table、code、blockquote 等块。 |
| 53 | + |
| 54 | +入口主要在: |
| 55 | + |
| 56 | +[`parser/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/parser/index.js) |
| 57 | + |
| 58 | +#### 行内解析 |
| 59 | + |
| 60 | +对于文本型 block,再运行 tokenizer,将文本切成: |
| 61 | + |
| 62 | +- `strong` |
| 63 | +- `em` |
| 64 | +- `del` |
| 65 | +- `inline_code` |
| 66 | +- `link` |
| 67 | +- `image` |
| 68 | +- `inline_math` |
| 69 | +- `emoji` |
| 70 | +- `html_tag` |
| 71 | +- `echo_anno` |
| 72 | + |
| 73 | +每个 token 都带有 `range`,因此编辑器可以把光标、搜索高亮和格式化范围映射回原始文本。 |
| 74 | + |
| 75 | +这也是它能实现“看起来像富文本,但本质仍然是 Markdown”的关键。 |
| 76 | + |
| 77 | +### 3. 渲染:Block → Token → VNode → DOM |
| 78 | + |
| 79 | +渲染入口是 `StateRender`: |
| 80 | + |
| 81 | +[`parser/render/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/parser/render/index.js) |
| 82 | + |
| 83 | +它会根据 block 是否有子节点,选择: |
| 84 | + |
| 85 | +- `renderContainerBlock` |
| 86 | +- `renderLeafBlock` |
| 87 | + |
| 88 | +叶子块会先 tokenizer,然后根据 token 类型调用对应 renderer。例如: |
| 89 | + |
| 90 | +- 数学公式 → KaTeX |
| 91 | +- 代码 → Prism |
| 92 | +- Mermaid / Flowchart / Vega → 延迟渲染 |
| 93 | +- 普通 Markdown → Snabbdom VNode |
| 94 | + |
| 95 | +Muya 使用 Snabbdom 做 DOM patch: |
| 96 | + |
| 97 | +[`parser/render/snabbdom.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/parser/render/snabbdom.js) |
| 98 | + |
| 99 | +它不是每次都重建整棵 DOM,而是提供: |
| 100 | + |
| 101 | +- 全量 render |
| 102 | +- partialRender |
| 103 | +- singleRender |
| 104 | + |
| 105 | +编辑普通段落时通常只更新局部 block,这对编辑性能很重要。 |
| 106 | + |
| 107 | +### 4. 编辑事件不是直接改 HTML |
| 108 | + |
| 109 | +键盘、输入、粘贴、拖拽等事件由独立控制器处理: |
| 110 | + |
| 111 | +- `keyboard.js` |
| 112 | +- `clipboard.js` |
| 113 | +- `dragDrop.js` |
| 114 | +- `mouseEvent.js` |
| 115 | +- `resize.js` |
| 116 | + |
| 117 | +输入事件的核心逻辑在: |
| 118 | + |
| 119 | +[`contentState/inputCtrl.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/contentState/inputCtrl.js) |
| 120 | + |
| 121 | +流程是: |
| 122 | + |
| 123 | +1. 从浏览器 Selection 读取光标位置; |
| 124 | +2. 从当前段落 DOM 读取文本; |
| 125 | +3. 对比旧 block 内容; |
| 126 | +4. 更新 block tree; |
| 127 | +5. 处理自动补全括号、Markdown 标记、IME 输入; |
| 128 | +6. 写入 History; |
| 129 | +7. 重新渲染局部 DOM; |
| 130 | +8. 通过 `change` 事件向外通知。 |
| 131 | + |
| 132 | +所以 DOM 只是输入和显示层,不是最终数据模型。 |
| 133 | + |
| 134 | +### 5. 光标和撤销系统 |
| 135 | + |
| 136 | +每个 block 的稳定 `key` 是光标系统的基础。光标保存为: |
| 137 | + |
| 138 | +```js |
| 139 | +{ |
| 140 | + start: { key, offset }, |
| 141 | + end: { key, offset } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +这样即使 DOM 被重新 patch,也能把光标恢复到对应 block。 |
| 146 | + |
| 147 | +撤销/重做由: |
| 148 | + |
| 149 | +[`contentState/history.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/contentState/history.js) |
| 150 | + |
| 151 | +负责。它保存的是: |
| 152 | + |
| 153 | +- block tree |
| 154 | +- cursor |
| 155 | +- renderRange |
| 156 | + |
| 157 | +而不是 DOM 快照。这比保存 HTML 更容易保证结构一致性。 |
| 158 | + |
| 159 | +### 6. 插件化 UI |
| 160 | + |
| 161 | +Muya 本身不是一个巨大的单体工具栏,而是通过插件注册: |
| 162 | + |
| 163 | +```js |
| 164 | +Muya.use(TablePicker) |
| 165 | +Muya.use(QuickInsert) |
| 166 | +Muya.use(CodePicker) |
| 167 | +Muya.use(EmojiPicker) |
| 168 | +Muya.use(ImageSelector) |
| 169 | +Muya.use(FormatPicker) |
| 170 | +``` |
| 171 | + |
| 172 | +构造 Muya 实例时,这些插件会被实例化并挂到实例上。 |
| 173 | + |
| 174 | +插件主要负责: |
| 175 | + |
| 176 | +- 浮层工具 |
| 177 | +- 快捷插入 |
| 178 | +- 表格操作 |
| 179 | +- 图片工具 |
| 180 | +- 格式工具 |
| 181 | +- 链接工具 |
| 182 | + |
| 183 | +事件通信通过 `EventCenter` 完成,而不是让各模块互相直接调用: |
| 184 | + |
| 185 | +[`eventHandler/event.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/eventHandler/event.js) |
| 186 | + |
| 187 | +这让编辑器内核和 UI 工具之间保持相对松耦合。 |
| 188 | + |
| 189 | +### 7. Coolma 的 Rune / Echo 扩展 |
| 190 | + |
| 191 | +Coolma 在 Muya 之上增加了两种自定义语法。 |
| 192 | + |
| 193 | +#### Rune:块级可交互组件 |
| 194 | + |
| 195 | +Rune 在 Markdown 中先保存为占位符,类似: |
| 196 | + |
| 197 | +```html |
| 198 | +<div |
| 199 | + data-rune-name="..." |
| 200 | + data-rune-id="..." |
| 201 | + data-rune-node-id="..." |
| 202 | + data-rune-value="..." |
| 203 | +> |
| 204 | + ... |
| 205 | +</div> |
| 206 | +``` |
| 207 | + |
| 208 | +渲染阶段再把占位符替换成 Vue 组件。 |
| 209 | + |
| 210 | +SFC 编译逻辑在: |
| 211 | + |
| 212 | +[`runeSfcRendererFactory.js`](E:/work-coolma/coolma/src/components/muya/runeSfcRendererFactory.js) |
| 213 | + |
| 214 | +它的设计特点: |
| 215 | + |
| 216 | +- 通过 `vue-template-compiler` 同步解析 SFC; |
| 217 | +- 编译结果按 `rune.id + template` 缓存; |
| 218 | +- 每个 Rune 使用独立的 `data-v-rune-*` scope; |
| 219 | +- SFC 通过 `input` 事件把值写回 Markdown 占位符。 |
| 220 | + |
| 221 | +#### Echo:行内注解语法 |
| 222 | + |
| 223 | +Echo 采用类似: |
| 224 | + |
| 225 | +```markdown |
| 226 | +@EchoName{value: '...'}() |
| 227 | +``` |
| 228 | + |
| 229 | +解析器把它识别为 `echo_anno` token,然后渲染成行内占位符。 |
| 230 | + |
| 231 | +Echo 的运行时开关通过修改共享的 `inlineRules.echo_anno` 实现: |
| 232 | + |
| 233 | +```js |
| 234 | +setEchoAnnoRule({ requireParens: true }) |
| 235 | +``` |
| 236 | + |
| 237 | +这意味着不需要重建 Muya,只需更新规则并触发重新渲染。 |
| 238 | + |
| 239 | +### 8. Vue 外层只是宿主集成层 |
| 240 | + |
| 241 | +Coolma 的 [`Muya.vue`](E:/work-coolma/coolma/src/components/muya/Muya.vue) 主要负责: |
| 242 | + |
| 243 | +- 创建 `new Muya(...)`; |
| 244 | +- 注入 Rune、Echo registry; |
| 245 | +- 提供图片选择和上传回调; |
| 246 | +- 监听 Muya 的 `change`、`selectionChange`; |
| 247 | +- 将内容同步到 Vuex / 当前笔记; |
| 248 | +- 处理主题、笔记切换和设置变化; |
| 249 | +- 管理动态 Vue renderer 的挂载和销毁。 |
| 250 | + |
| 251 | +也就是说: |
| 252 | + |
| 253 | +```text |
| 254 | +Muya:编辑器内核 |
| 255 | +Muya.vue:Coolma 业务适配层 |
| 256 | +Vuex / Echo / Rune:应用能力层 |
| 257 | +``` |
| 258 | + |
| 259 | +这种分层让 Muya 可以独立打包成 npm 包或 UMD bundle,而 Coolma 只负责注入自己的业务扩展。 |
| 260 | + |
| 261 | +### 9. 这个设计的主要取舍 |
| 262 | + |
| 263 | +优点: |
| 264 | + |
| 265 | +- Markdown 数据结构清晰、可持久化; |
| 266 | +- 复杂结构编辑比直接操作 HTML 更可靠; |
| 267 | +- block key 让光标和局部渲染更稳定; |
| 268 | +- Snabbdom 支持增量更新; |
| 269 | +- UI 插件和业务扩展比较容易接入; |
| 270 | +- Rune / Echo 可以扩展 Markdown 的表达能力。 |
| 271 | + |
| 272 | +代价: |
| 273 | + |
| 274 | +- Block tree、token range、DOM selection 三者之间需要持续同步; |
| 275 | +- 自定义语法要同时考虑解析、渲染、编辑、导出; |
| 276 | +- DOM 被 patch 后,光标恢复逻辑比较复杂; |
| 277 | +- 动态 Rune SFC 本质上是运行时编译,必须严格控制来源和缓存; |
| 278 | +- Muya 的 parser 规则是共享引用,运行时修改后需要主动触发重渲染。 |
| 279 | + |
| 280 | +如果从源码阅读顺序入手,我建议按这个顺序看: |
| 281 | + |
| 282 | +1. [`lib/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/index.js) |
| 283 | +2. [`lib/contentState/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/contentState/index.js) |
| 284 | +3. [`lib/utils/importMarkdown.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/utils/importMarkdown.js) |
| 285 | +4. [`lib/parser/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/parser/index.js) |
| 286 | +5. [`lib/parser/render/index.js`](E:/work-coolma/coolma/_plugins/coolma-muya/lib/parser/render/index.js) |
| 287 | +6. [`src/components/muya/Muya.vue`](E:/work-coolma/coolma/src/components/muya/Muya.vue) |
| 288 | + |
| 289 | +一句话总结:Muya 是一个“以 Markdown 为源、以 Block Tree 为编辑模型、以 Tokenizer 为格式层、以 Snabbdom 为渲染层、以事件中心和插件为扩展机制”的所见即所得 Markdown 编辑器。 |
0 commit comments