Skip to content

Commit

Permalink
25/02/22
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Feb 22, 2025
1 parent 7ebb526 commit ea3bdf9
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 4 deletions.
1 change: 1 addition & 0 deletions .scripts/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export const docs: Record<string, string[]> = {
"RichText/WrapNode数据结构与操作变换",
"RichText/TS类型扩展与节点类型检查",
"RichText/Node节点与Path路径映射",
"RichText/Decorator装饰器渲染调度",
],
Patterns: [
"Patterns/简单工厂模式",
Expand Down
14 changes: 13 additions & 1 deletion Backup/基于MVC模式的编辑器架构设计.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@
- [从零实现富文本编辑器-基于MVC模式的编辑器架构设计]()

## 精简的编辑器
我们先前已经提到了`ContentEditable`属性以及`execCommand`命令,通过`document.execCommand`来执行命令修改`HTML`的方案虽然简单,但是很明显其可控性比较差。`execCommand`命令的行为在各个浏览器的表现是不一致的,这也是之前我们提到的浏览器兼容行为的一种,然而这些行为我们也没有任何办法去控制,这都是其默认的行为。
在整套系统架构的设计中,最重要的核心理念便是**状态同步**,如果以状态模型为基准,那么我们需要维护的状态同步就可以归纳为下面的两方面:

- 将用户操作状态同步到状态模型中,当用户操作文本状态时,例如用户的选区操作、输入操作、删除操作等,需要将变更操作到状态模型中。
- 将状态模型状态同步到视图层中,当在控制层中执行命令时,需要将经过变更后生成的新状态模型同步到视图层中,保证数据状态与视图的一致。

其实这两个状态同步是个正向依赖的过程,用户操作形成的状态同步到状态模型,状态模型的变更同步到视图层,视图层则又是用户操作的基础。举个例子,当用户通过拖拽选择部分文本时,需要将其选中的范围同步到状态模型。当此时执行删除操作时,需要将数据中的这部分文本删除,之后再刷新视图的到新的`DOM`结构。下次循环就需要继续保证状态的同步,然后执行输入、刷新视图等操作。

由此我们的目标主要是状态同步,虽然看起来仅有简单的两个原则,但是这件事做起来并没有那么简单。当我们执行状态同步时,是非常依赖浏览器的相关`API`的,例如选区、输入、键盘等事件。然而此时我们必须要处理浏览器的相关问题,例如截止目前`ContentEditable`无法真正阻止`IME`的输入,`EditContext`的兼容性也还有待提升,这些都是我们需要处理的问题。

实际上当我们用到了越多的浏览器`API`实现,我们就需要考虑越多的浏览器兼容性问题。因此富文本编辑器的实现才会出现很多非`ContentEditable`的实现,例如如钉钉文档的自绘选区、`Google Doc``Canvas`文档绘制等。但是这样虽然能够降低部分浏览器`API`的依赖,但是也无法真正完全脱离浏览器的实现,因此即使是`Canvas`绘制的文档,也必须要依赖浏览器的`API`来实现输入、位置计算等等。

回到我们的精简编辑器模型,先前的文章已经提到了`ContentEditable`属性以及`execCommand`命令,通过`document.execCommand`来执行命令修改`HTML`的方案虽然简单,但是很明显其可控性比较差。`execCommand`命令的行为在各个浏览器的表现是不一致的,这也是之前我们提到的浏览器兼容行为的一种,然而这些行为我们也没有任何办法去控制,这都是其默认的行为。

```html
<div>
Expand Down Expand Up @@ -307,3 +318,4 @@ Utils
## 参考

- <https://www.oschina.net/translate/why-contenteditable-is-terrible>
- <https://stackoverflow.com/questions/78268797/how-to-prevent-ime-input-method-editor-to-mutate-the-contenteditable-element>
105 changes: 105 additions & 0 deletions I18N/RichText/Decorator装饰器渲染调度.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Decorator Rendering Dispatcher
Previously, we discussed the design of data structures based on a document editor and talked about the document editor type system implemented based on `slate`. Now, let's dive into the implementation of decorators in the `slate` editor. Decorators play a crucial role in `slate`, allowing us to easily handle the rendering of `range` during editor dispatch.

* Online Editor: <https://windrunnermax.github.io/DocEditor>
* Open Source Repository: <https://github.com/WindrunnerMax/DocEditor>

Related articles about the `slate` document editor project:

* [Building a Document Editor with Slate](https://juejin.cn/post/7265516410490830883)
* [Slate Document Editor - WrapNode Data Structure and Operation Transformation](https://juejin.cn/spost/7385752495535603727)
* [Slate Document Editor - TS Type Extension and Node Type Checking](https://juejin.cn/spost/7399453742346551332)
* [Slate Document Editor - Decorator Rendering Dispatcher]()

## Decorate
In `slate`, `decoration` is a particularly interesting feature. Imagine a scenario where we need to highlight code blocks. We can implement this in several ways: the first approach involves directly parsing the content of the code block, extracting the keyword categories, and storing them in the data structure to render the highlight information during rendering. However, this increases the size of the data structure. The second approach involves storing only the code information and parsing it into `Marks` during frontend rendering when highlighting is needed. This approach adds a bit of complexity because we may need to mark it as a non-collaborative operation and as pure client-side `Op` that does not require server storage. The third method involves using `decoration`, where `slate` essentially streamlines the second approach by rendering additional `Marks` without altering the data structure.

Of course, decorators are not limited to code block highlighting. Any content that should not be expressed in the data structure but needs to be displayed during rendering requires the use of `decoration`. A clear example is the search feature. When implementing a search function in the editor, we need to mark the found content, which can be achieved using `decoration`. Alternatively, we would have to draw virtual layers to accomplish this. Similarly, for implementing user-friendly hyperlink parsing functionality, such as automatically converting pasted links into hyperlink nodes, decorators can be leveraged.

During my recent testing of the `search-highlighting example` on the `slate` official website, the search worked well for single-node searches like `adds`. However, highlighting across multiple nodes was less effective. Details can be found at `https://github.com/ianstormtaylor/slate/pull/5670`.
This reveals some challenges when `decoration` handles cross-node processing. For instance, when searching for `123` or `12345`, the decorations are rendered correctly, but searching for `123456` with a `range` constructed as `path: [0], offset: [0-6]` results in mislabeling content because we are crossing the boundary of the `[0]` node.

By examining the related code for searching, we can see that the parent's `decorate` results are passed down for subsequent rendering. At this level, the passing `decorate` function is called to generate new `decorations`. It is crucial to note that if the parent's `decorations` and the range of the current node intersect, the content will continue to be passed down. The crux of the matter lies here. Considering our scenario with the content mentioned earlier as an example, if we want to find the index of `123456` at this point, just searching within the `text: 12345` node is insufficient. We must concatenate the content of all text nodes in the higher array and then search to accurately locate the index position.

```js
// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/hooks/use-children.tsx#L21
const useChildren = (props: {
decorations: Range[]
// ...
}) => {
// ...

for (let i = 0; i < node.children.length; i++) {
// ...
const ds = decorate([n, p])
for (const dec of decorations) {
const d = Range.intersection(dec, range)
if (d) {
ds.push(d)
}
}
// ...
}
// ...
}
```

At this point, it is clear that we need to specify that the node we invoke `decorate` on is the parent element. When the parent node is passed to the `text` node we need to process, we use `Range.intersection` to determine if there is an intersection. The strategy for determining the intersection is actually simple. We have provided two examples below, one where there is an intersection and another where there isn't. Essentially, we only need to check the final state of the two nodes.

```js
// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/range.ts#L118

// start1 end1 start2 end2
// end1 start2
// end1 < start2 ===> No intersection

// start1 start2 end1 end2
// start2 end1
// start2 < end1 ===> Intersection [start2, end1]
```

So, can we solve this problem by modifying the logic part of `Range.intersection` in the `decorate` code? Specifically, when the content we find exceeds the original `range`, we should trim the part that needs to be decorated and discard the rest. In fact, this logic was found to be correct during our analysis earlier, that is, when we search for `'123456'`, we are able to fully display the portion `'12345'`. Based on the earlier analysis, in this current iteration, our nodes are all at `path: [0]`. This part of the code will trim the part of the code from `start: 0` to `end: 5` of the `range` and render it.

However, continuing to find the part `'6'` within the next `text range` is not as simple because in the previous search, the actual `range` was `path: [0], offset: [0-6]`, while the basic `range` of the second `text` was `path: [1], offset: [0-5]`. Based on these conditions, we find that there is no intersection. Therefore, if we need to handle this here, we would need to obtain the previous `range`, or even traverse back through many nodes in the case of spanning multiple nodes. When there are many `decorations`, we would need to check all the nodes because at this point, we do not know if the previous node has exceeded the length of the node itself. In such cases, the computational load here might be relatively high, potentially leading to performance issues.

Hence, it is better to start by constructing the `range` during parsing. When crossing nodes, we need to split the content we find into multiple `ranges`, and then insert marks for each `range`. Referring to the data above, the search result now consists of two parts: `path: [0], offset: [0, 5]` and `path: [1], offset: [0, 1]`. In this scenario, we can handle intersections properly when using `Range.intersection`. At this point, our `path` is fully aligned, and even if the content completely spans, that is, when the search content crosses more than one node, we can still handle it using this method.

In addition, when using decorators in scheduling, it is important to pay attention to the value of the `RenderLeafProps` parameter in the `renderLeaf` function, as there are two types of text content here, namely `leaf: Text;` and `text: Text;` which are fundamental `TextInterface` types. When we render content using `renderLeaf`, such as highlighting the rendering of the `mark` node within a code block, we actually need to base the rendering on the `leaf` rather than the `text`. For example, when rendering `mark` and `bold` styles overlap, both of these nodes need to be based on the `leaf`.

The reason for this is that `decorations` in Slate are split into multiple `leaves` based on the `text` node, and then these `leaves` are passed to `renderLeaf` for rendering. So in essence, the `text` attribute is the original value, while the `leaf` attribute is a more granular node. When scheduling `renderLeaf`, it is also rendered based on the granularity of `leaf`. Of course, when not using decorators, the node types of these two attributes are equivalent.

```js
// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/components/text.tsx#L39
const leaves = SlateText.decorations(text, decorations)
const key = ReactEditor.findKey(editor, text)
const children = []

for (let i = 0; i < leaves.length; i++) {
const leaf = leaves[i]

children.push(
<Leaf
isLast={isLast && i === leaves.length - 1}
key={`${key.id}-${i}`}
renderPlaceholder={renderPlaceholder}
leaf={leaf}
text={text}
parent={parent}
renderLeaf={renderLeaf}
/>
)
}
```

## Summary
Here we mainly discussed the implementation of the `decoration` decorator in Slate, as well as the potential issues that may arise in practical use. Particularly in cases involving multiple nodes, we need to split the `range` into multiple `ranges` and process them separately. We also analyzed the source code to delve into the implementation of related issues. In the upcoming articles, we will primarily delve into discussing how `Path` is expressed in `slate` and how to correctly maintain `Path` paths and `Element` content rendering in `React`.

## Daily Quiz

- <https://github.com/WindRunnerMax/EveryDay>

## References

- <https://docs.slatejs.org/>
- <https://github.com/WindRunnerMax/DocEditor>
- <https://github.com/ianstormtaylor/slate/blob/25be3b/>
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
如果觉得还不错,点个`star`吧 😁

<!-- Summary Start -->
版本库中共有`493`篇文章,总计`93141`行,`1102073`字,`3060315`字符。
版本库中共有`493`篇文章,总计`93350`行,`1105367`字,`3067511`字符。
<!-- Summary End -->

这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字`EveryDay`就是希望激励我能够每天学习,下面的文章就是从`2020.02.25`开始积累的文章,都是参考众多文章归纳整理学习而写的。文章包括了`HTML`基础、`CSS`基础、`JavaScript`基础与拓展、`Browser`浏览器相关、`Vue`使用与分析、`React`使用与分析、`Plugin`插件相关、`RichText`富文本、`Patterns`设计模式、`Linux`命令、`LeetCode`题解等类别,内容都是比较基础的,毕竟我也还是个小白。此外基本上每个示例都是本着能够即时运行为目标的,新建一个`HTML`文件复制之后即可在浏览器运行或者直接可以在`console`中运行。
Expand Down Expand Up @@ -311,6 +311,7 @@
* [WrapNode数据结构与操作变换](RichText/WrapNode数据结构与操作变换.md) [(*en-us*)](I18N/RichText/WrapNode数据结构与操作变换.md)
* [TS类型扩展与节点类型检查](RichText/TS类型扩展与节点类型检查.md) [(*en-us*)](I18N/RichText/TS类型扩展与节点类型检查.md)
* [Node节点与Path路径映射](RichText/Node节点与Path路径映射.md) [(*en-us*)](I18N/RichText/Node节点与Path路径映射.md)
* [Decorator装饰器渲染调度](RichText/Decorator装饰器渲染调度.md) [(*en-us*)](I18N/RichText/Decorator装饰器渲染调度.md)

## Patterns
* [简单工厂模式](Patterns/简单工厂模式.md) [(*en-us*)](I18N/Patterns/简单工厂模式.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Slate文档编辑器-Decorator装饰器渲染调度
# Decorator装饰器渲染调度
在之前我们聊到了基于文档编辑器的数据结构设计,聊了聊基于`slate`实现的文档编辑器类型系统,那么当前我们来研究一下`slate`编辑器中的装饰器实现。装饰器在`slate`中是非常重要的实现,可以为我们方便地在编辑器渲染调度时处理`range`的渲染。

* 在线编辑: <https://windrunnermax.github.io/DocEditor>
Expand Down
5 changes: 4 additions & 1 deletion Timeline.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Timeline

前端笔记系列共有 429 篇文章,总计 76769 行, 880536 字, 2441026 字符。
前端笔记系列共有 430 篇文章,总计 76913 行, 883025 字, 2446901 字符。

### 2025-02-22
第 430 题:[Decorator装饰器渲染调度](RichText/Decorator装饰器渲染调度.md)

### 2025-02-16
第 429 题:[du-磁盘占用管理](Linux/du-磁盘占用管理.md)
Expand Down

0 comments on commit ea3bdf9

Please sign in to comment.