Skip to content

Commit c9a0426

Browse files
committed
fix: improve rune dimension handling in Muya component
This commit refines the logic for managing rune dimensions within the Muya component, ensuring that size adjustments are accurately applied to both the opening tag and its content. This enhancement contributes to better rendering and user experience by maintaining the integrity of rune placeholders during dynamic resizing.
1 parent a1bbd5e commit c9a0426

2 files changed

Lines changed: 1454 additions & 0 deletions

File tree

‎_todo/Muya设计-codex分析.md‎

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)