diff --git a/.scripts/docs.ts b/.scripts/docs.ts index 5b7fc35..5fe9670 100644 --- a/.scripts/docs.ts +++ b/.scripts/docs.ts @@ -290,6 +290,7 @@ export const docs: Record = { "RichText/初探富文本之搜索替换算法", "RichText/基于slate构建文档编辑器", "RichText/WrapNode数据结构与操作变换", + "RichText/TS类型扩展与节点类型检查", "RichText/Node节点与Path路径映射", ], Patterns: [ diff --git "a/Backup/\344\273\216\351\233\266\350\256\276\350\256\241\345\256\236\347\216\260\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221\345\231\250.md" "b/Backup/\344\273\216\351\233\266\350\256\276\350\256\241\345\256\236\347\216\260\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221\345\231\250.md" index 287752f..d7e76b3 100644 --- "a/Backup/\344\273\216\351\233\266\350\256\276\350\256\241\345\256\236\347\216\260\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221\345\231\250.md" +++ "b/Backup/\344\273\216\351\233\266\350\256\276\350\256\241\345\256\236\347\216\260\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221\345\231\250.md" @@ -122,6 +122,15 @@ ``` +除此之外,编辑器自然是需要跟字符打交道的,那么在`js`表现出来的`Unicode`编码实现中,`emoji`就是最常见且容易出问题的表达。除了其单个长度为`2`这种情况外,组合的`emoji`也是使用独特的零宽连字符`\u200d`来表示的。 + +```js +"🎨".length +// 2 +"🧑" + "\u200d" + "🎨" +// 🧑‍🎨 +``` + ### 数据结构设计 编辑器数据结构的设计是影响面非常广的事情,无论是在维护编辑器的文本内容、块结构嵌套、序列化反序列化等,还是平台应用层面上的`diff`算法、查找替换、协同算法等,以及后端服务的数据转换、导出`md/word/pdf`、数据存储等,都会涉及到编辑器的数据结构设计。 diff --git "a/I18N/RichText/TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" "b/I18N/RichText/TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" new file mode 100644 index 0000000..7d099be --- /dev/null +++ "b/I18N/RichText/TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" @@ -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 + +- + +## References + +- +- +- diff --git a/README.md b/README.md index 21d4046..ef30198 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 如果觉得还不错,点个`star`吧 😁 -版本库中共有`492`篇文章,总计`93008`行,`1100133`字,`3056129`字符。 +版本库中共有`492`篇文章,总计`93017`行,`1100225`字,`3056302`字符。 这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字`EveryDay`就是希望激励我能够每天学习,下面的文章就是从`2020.02.25`开始积累的文章,都是参考众多文章归纳整理学习而写的。文章包括了`HTML`基础、`CSS`基础、`JavaScript`基础与拓展、`Browser`浏览器相关、`Vue`使用与分析、`React`使用与分析、`Plugin`插件相关、`RichText`富文本、`Patterns`设计模式、`Linux`命令、`LeetCode`题解等类别,内容都是比较基础的,毕竟我也还是个小白。此外基本上每个示例都是本着能够即时运行为目标的,新建一个`HTML`文件复制之后即可在浏览器运行或者直接可以在`console`中运行。 @@ -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 diff --git "a/Backup/Slate\346\226\207\346\241\243\347\274\226\350\276\221\345\231\250-TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" "b/RichText/TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" similarity index 99% rename from "Backup/Slate\346\226\207\346\241\243\347\274\226\350\276\221\345\231\250-TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" rename to "RichText/TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" index 13832f9..c16dd8b 100644 --- "a/Backup/Slate\346\226\207\346\241\243\347\274\226\350\276\221\345\231\250-TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" +++ "b/RichText/TS\347\261\273\345\236\213\346\211\251\345\261\225\344\270\216\350\212\202\347\202\271\347\261\273\345\236\213\346\243\200\346\237\245.md" @@ -1,4 +1,4 @@ -# Slate文档编辑器-TS类型扩展与节点类型检查 +# TS类型扩展与节点类型检查 在之前我们基于`slate`实现的文档编辑器探讨了`WrapNode`数据结构与操作变换,主要是对于嵌套类型的数据结构类型需要关注的`Normalize`与`Transformers`,那么接下来我们更专注于文档编辑器的数据结构设计,聊聊基于`slate`实现的文档编辑器类型系统。 diff --git a/Timeline.md b/Timeline.md index e9f3d8f..b623759 100644 --- a/Timeline.md +++ b/Timeline.md @@ -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)