Skip to content

Commit

Permalink
25/02/01
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Feb 1, 2025
1 parent 6684729 commit f62f017
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 3 deletions.
1 change: 1 addition & 0 deletions .scripts/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export const docs: Record<string, string[]> = {
"RichText/初探富文本之搜索替换算法",
"RichText/基于slate构建文档编辑器",
"RichText/WrapNode数据结构与操作变换",
"RichText/TS类型扩展与节点类型检查",
"RichText/Node节点与Path路径映射",
],
Patterns: [
Expand Down
9 changes: 9 additions & 0 deletions Backup/从零设计实现富文本编辑器.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@
</div>
```

除此之外,编辑器自然是需要跟字符打交道的,那么在`js`表现出来的`Unicode`编码实现中,`emoji`就是最常见且容易出问题的表达。除了其单个长度为`2`这种情况外,组合的`emoji`也是使用独特的零宽连字符`\u200d`来表示的。

```js
"🎨".length
// 2
"🧑" + "\u200d" + "🎨"
// 🧑‍🎨
```

### 数据结构设计
编辑器数据结构的设计是影响面非常广的事情,无论是在维护编辑器的文本内容、块结构嵌套、序列化反序列化等,还是平台应用层面上的`diff`算法、查找替换、协同算法等,以及后端服务的数据转换、导出`md/word/pdf`、数据存储等,都会涉及到编辑器的数据结构设计。

Expand Down
182 changes: 182 additions & 0 deletions I18N/RichText/TS类型扩展与节点类型检查.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# TS Type Extension and Node Type Checking

Previously, we discussed the `WrapNode` data structure and operations transformation based on `slate` to address the normalization and transformers required for nested data structure types. Now, let's delve deeper into the data structure design of the document editor and talk about the type system implemented based on `slate`.

- Online Editing: [DocEditor Online](https://windrunnermax.github.io/DocEditor)
- Open Source Repository: [GitHub - DocEditor](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)

## TS Type Extension
When incorporating `slate` into TypeScript (`TS`), you may notice that when calling `createEditor` to instantiate the editor, there is no generic type defined for input. Does this imply that `slate` cannot define types in `TS` and one must resort to using `as` for type assertions during property handling? This approach is not suitable for a mature editor engine. For a rich text editor, defining types in `TS` is crucial. We can perceive rich text as text with attributes. Unclear attribute definitions can lead to maintenance challenges. Maintaining rich text content is already problematic, hence defining types becomes necessary.

Before exploring type extensions in `slate`, let's first look at `declare module` and `interface` declarations provided by TypeScript. The distinction between interfaces defined by using `type` and `interface` is significant. Apart from `type` being able to define union types, a key difference is that an `interface` can be merged, allowing for extension of defined interfaces. On the other hand, `type` cannot redefine type declarations within the same module.

```js
interface A { a: string }
interface A { b: number }
const a: A = { a: "", b: 1 }

type B = { a: string }
// type B = { b: number }
const b = { a: "" }
```

Initially, the difference between `interface` and `type` might seem minor and almost interchangeable. However, in practical applications, the merging feature of interfaces is crucial, especially in scenarios involving `declare module`. It can be used to extend type declarations for existing libraries, thereby adding extra type definitions.

```js
declare module "some-library" {
export interface A {
a: number;
}
export function fn(): void;
}
```
By utilizing the merging feature of `declare module + interface`, we can extend the type definitions of modules. This is similar to how `slate` extends classes, where we inject the required basic types by extending the module with interfaces. Our basic types are defined using the `interface` keyword, allowing continuous extension of types through the `declare module`. One crucial aspect to note is that once we implement type extensions through `declare module`, we can dynamically load the extended types as needed. This means that the corresponding type declarations are only loaded when the defined types are actually referenced, enabling on-demand type extension.
```js
// packages/delta/src/interface.ts
import type { BaseEditor } from "slate";

declare module "slate" {
interface CustomTypes {
Editor: BaseEditor;
Element: BlockElement;
Text: TextElement;
}
}

export interface BlockElement {
text?: never;
children: BaseNode[];
[key: string]: unknown;
}

export interface TextElement {
text: string;
children?: never;
[key: string]: unknown;
}
```
Actually, I highly recommend defining types in separate files. Otherwise, when we use the feature to jump to type definitions in the IDE and look at the complete `slate` type file, we will find that it jumps to the location of the file we defined above. Similarly, when we separate `slate` and its related modules into separate packages, we may find that the type jumps are not that convenient. Even if our goal is not to extend type definitions, we might end up jumping to the location of our `declare module` file. Therefore, after separating them out, handle declaration files as separate modules, so there won't be any positioning issues. Below is an example of our declared code block format type definitions.
```js
// packages/plugin/src/codeblock/types/index.ts
declare module "doc-editor-delta/dist/interface" {
interface BlockElement {
[CODE_BLOCK_KEY]?: boolean;
[CODE_BLOCK_CONFIG]?: { language: string };
}
interface TextElement {
[CODE_BLOCK_TYPE]?: string;
}
}

export const CODE_BLOCK_KEY = "code-block";
export const CODE_BLOCK_TYPE = "code-block-type";
export const CODE_BLOCK_CONFIG = "code-block-config";
```
## Node Type Checking
After expanding the types in `TS`, we also need to pay attention to actual node type checking. After all, `TS` only helps with static type checking. When compiled to `Js` and running in the browser, there usually won't be any additional code injection. However, in our usual usage, we clearly need these type checks. For instance, we might need to check if the current selection node is correctly positioned on a `Text` node to respond accordingly to user actions.
Let's organize the node types in `slate`. Based on the `CustomTypes` exported by `slate`, we can determine that the most basic types are just `Element` and `Text`, which I prefer to rename as `BlockElement` and `TextElement`. Due to the complexity of our business, we often need to extend two more types, `InlineElement` and `TextBlockElement`.
Let's start with `BlockElement`, the basic block element defined in `slate`. For example, the outermost nesting structure of our code block needs to be a `BlockElement`. Due to possible complex nesting structures like nested column structures in tables or nested code block structures within columns, we can understand `BlockElement` as the required structure for nesting. Checking whether a node is of type `BlockElement` can be done directly using the `Editor.isBlock` method.
```js
export interface BlockElement {
text?: never;
children: BaseNode[];
[key: string]: unknown;
}

export const isBlock = (editor: Editor, node: Node | null): node is BlockElement => {
if (!node) return false;
return Editor.isBlock(editor, node);
};

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/editor.ts#L590
export const Editor = {
isBlock(editor: Editor, value: any): value is Element {
return Element.isElement(value) && !editor.isInline(value)
}
}
```
`TextElement` is defined as a text node, and it's worth noting that attributes can also be added here, known as `markers` in `slate`. Regardless of the nested node types or the leaf nodes of the tree data structure, it must be a text node. Similarly, checking for `TextElement` can be done through the `isText` method on `Text`.
```js
export interface TextElement {
text: string;
children?: never;
[key: string]: unknown;
}

export const isText = (node: Node): node is TextElement => Text.isText(node);
```
```js
// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/text.ts#L53
export const Text = {
isText(value: any): value is Text {
return isPlainObject(value) && typeof value.text === 'string'
},
}
```
If we carefully examine the conditions for determining `BlockElement` in `slate`, we can see that besides calling the `Element.isElement` method, it also checks the `editor.isInline` method. Since `InlineElement` in `slate` is a special kind of `BlockElement`, we also need to independently implement type checking for it. The way to check for `InlineElement` needs to be implemented by the `editor.isInline` method, which actually needs to be defined by us during `with`.
```js
export interface InlineElement {
text?: never;
children: TextElement[];
[key: string]: unknown;
}

export const isInline = (editor: Editor, node: Node): node is InlineElement => {
return editor.isInline(node);
}
```
In business scenarios, we often need to determine the actual rendering node of a text paragraph, `TextBlockElement`. This determination is also very important because it identifies the current node as the paragraph we actually want to render. This is very useful in data conversion scenarios. When we need to re-parse data, when encountering a text paragraph, we can determine that the current block-level structure parsing can end and we can proceed to build the content composition of the paragraph. Since a paragraph may contain both `TextElement` and `InlineElement` nodes, our check needs to consider both cases. Under normal conditions, we usually only need to check if the first node meets the criteria. In development mode, we can try comparing the structures of all nodes to determine and verify dirty data.
```js
export interface TextBlockElement {
text?: never;
children: (TextElement | InlineElement)[];
[key: string]: unknown;
}

export const isTextBlock = (editor: Editor, node: Node): node is TextBlockElement => {
if (!isBlock(editor, node)) return false;
const firstNode = node.children[0];
const result = firstNode && (isText(firstNode) || isInline(editor, firstNode));
if (process.env.NODE_ENV === "development") {
const strictInspection = node.children.every(child => isText(firstNode) || isInline(editor, firstNode));
if (result !== strictInspection) {
console.error("Fatal Error: Text Block Check Fail", node);
}
}
return result;
};
```
## Summary
In this discussion, we focused on the data structure design of document editors and talked about the type system of document editors implemented based on `slate`. In `slate`, there are many additional concepts and operations to pay attention to, such as `Range`, `Operation`, `Editor`, `Element`, `Path`, etc. In the upcoming articles, we will mainly discuss the representation of `Path` in `slate`, and how to control the content expression in `React` and properly maintain the `Path` path and `Element` content rendering. Moreover, we will also delve into the design and implementation of table modules.
## Question of the Day
- <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 -->
版本库中共有`492`篇文章,总计`93008`行,`1100133`字,`3056129`字符。
版本库中共有`492`篇文章,总计`93017`行,`1100225`字,`3056302`字符。
<!-- Summary End -->

这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字`EveryDay`就是希望激励我能够每天学习,下面的文章就是从`2020.02.25`开始积累的文章,都是参考众多文章归纳整理学习而写的。文章包括了`HTML`基础、`CSS`基础、`JavaScript`基础与拓展、`Browser`浏览器相关、`Vue`使用与分析、`React`使用与分析、`Plugin`插件相关、`RichText`富文本、`Patterns`设计模式、`Linux`命令、`LeetCode`题解等类别,内容都是比较基础的,毕竟我也还是个小白。此外基本上每个示例都是本着能够即时运行为目标的,新建一个`HTML`文件复制之后即可在浏览器运行或者直接可以在`console`中运行。
Expand Down Expand Up @@ -307,6 +307,7 @@
* [初探富文本之搜索替换算法](RichText/初探富文本之搜索替换算法.md) [(*en-us*)](I18N/RichText/初探富文本之搜索替换算法.md)
* [基于slate构建文档编辑器](RichText/基于slate构建文档编辑器.md) [(*en-us*)](I18N/RichText/基于slate构建文档编辑器.md)
* [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)

## Patterns
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Slate文档编辑器-TS类型扩展与节点类型检查
# TS类型扩展与节点类型检查

在之前我们基于`slate`实现的文档编辑器探讨了`WrapNode`数据结构与操作变换,主要是对于嵌套类型的数据结构类型需要关注的`Normalize``Transformers`,那么接下来我们更专注于文档编辑器的数据结构设计,聊聊基于`slate`实现的文档编辑器类型系统。

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

前端笔记系列共有 425 篇文章,总计 75960 行, 869538 字, 2412621 字符。
前端笔记系列共有 426 篇文章,总计 76142 行, 871945 字, 2418959 字符。

### 2025-02-01
第 426 题:[TS类型扩展与节点类型检查](RichText/TS类型扩展与节点类型检查.md)

### 2025-01-25
第 425 题:[Canvas编辑器之图形状态管理](Plugin/Canvas编辑器之图形状态管理.md)
Expand Down

0 comments on commit f62f017

Please sign in to comment.