Slate Node API 完全指南:深入理解节点树、检索方法、文本方法与检查方法
Slate Node API 完全指南深入理解节点树、检索方法、文本方法与检查方法【免费下载链接】slateA completely customizable framework for building rich text editors. (Currently in beta.)项目地址: https://gitcode.com/gh_mirrors/sl/slateSlate 将整个富文本文档建模为一棵递归嵌套的节点树而Node就是贯穿这棵树所有节点的核心接口。本文以官方 API 文档 docs/api/nodes/node.md 为主体结合 packages/slate/src/interfaces/node.ts 的源码实现与 packages/slate/test/interfaces/Node 的测试用例系统讲解Node的静态方法族检索方法、文本方法与检查方法。读完本文你将掌握在 Slate 文档树中按路径定位节点、遍历子孙/祖先、抽取片段、拼接纯文本以及做类型守卫的完整手段并理解这些 API 背后的遍历算法与边界处理。Node 类型体系Editor、Element 与 TextNode是一个联合类型union type它代表了 Slate 文档树中可能出现的所有节点种类type Node Editor | Element | Text type Descendant Element | Text type Ancestor Editor | ElementEditor树顶层的根节点包含整篇文档的内容Element文档的中间层容器节点承载你业务域里的语义段落、引用、链接、表格等核心约束是必须带有children属性Text树最底层的叶子节点持有真正的文本字符串与格式化标记如bold: true。Descendant后代与Ancestor祖先两个联合类型是Node的收窄narrowing便利类型例如Node.ancestor、Node.parent返回AncestorNode.descendant、Node.child返回Descendant。这三者及其关系可进一步参考 docs/concepts/02-nodes.md 与 docs/api/nodes/README.md。此外当遍历节点树时返回值通常是NodeEntry——一个由[Node, Path]组成的二元组Path表示该节点相对根节点的位置如[0, 1]详见 docs/api/nodes/node-entry.md。路径Path与选择Selection等定位概念的完整讲解见 docs/concepts/03-locations.md。检索方法Retrieval methods检索方法负责在节点树中按路径定位、遍历和提取节点是NodeAPI 中数量最多、使用最频繁的一类。定点获取get/getIf/hasNode.get(root: Node, path: Path): Node Node.getIf(root: Node, path: Path): Node | undefined Node.has(root: Node, path: Path): booleanNode.get返回指定path指向的后代节点当path为空数组[]时返回根节点自身。源码中它委托给getIf若结果不存在则抛出Cannot find a descendant at path [...]错误见 packages/slate/src/interfaces/node.ts。Node.getIf是get的安全版本路径无效时返回undefined而不会抛错适合先探测再处理的场景const node Node.getIf(root, [0, 1]) if (node) { // node exists at path [0, 1] } else { // no node exists at path [0, 1] }其内部实现是逐级沿children下钻遇到文本节点不能再下钻或下标越界时立即返回undefined见 node.ts。测试用例getIf/undefined.tsx、getIf/root.tsx覆盖了不存在路径与根路径两种情形。Node.has与getIf使用几乎相同的遍历逻辑但返回布尔值仅用于该路径下是否存在后代节点的判断。父子定位parent/child/ancestor/descendant/leafNode.parent(root: Node, path: Path): Ancestor Node.child(root: Node, index: number): Descendant Node.ancestor(root: Node, path: Path): Ancestor Node.descendant(root: Node, path: Path): Descendant Node.leaf(root: Node, path: Path): TextNode.parent返回指定路径节点的父节点。实现上先取Path.parent(path)再用Node.get获取若父路径指向文本节点即传入的路径本身不存在会抛出明确错误见 node.ts。Node.child返回某节点在指定index下的子节点。它对文本节点调用或index非数字、下标越界都会抛错见 node.ts。Node.ancestor断言路径指向的是祖先节点Editor 或 Element若指向文本节点则抛错见 node.ts。Node.descendant与ancestor相反断言路径指向的不是根 Editor 节点见 node.ts。Node.leaf进一步断言路径指向的是叶子文本节点否则抛错——它常用于确保拿到的节点可承载光标偏移见 node.ts。遍历方法nodes/descendants/elements/texts这四个方法都返回生成器Generator每次迭代产出一个NodeEntryNode.nodes(root: Node, options?): GeneratorNodeEntry Node.descendants(root: Node, options?): GeneratorNodeEntryDescendant Node.elements(root: Node, options?): GeneratorElementEntry Node.texts(root: Node, options?): GeneratorNodeEntryText公共选项为{from?: Path, to?: Path, reverse?: boolean, pass?: (node: NodeEntry boolean)}from/to限定遍历的起止路径含边界测试用例见nodes/to.tsx、texts/from.tsx——Node.texts(value, { from: [0, 1] })只产出[text keyb /, [0, 1]]reverse: true自底向上、从右到左遍历见descendants/reverse.tsx、texts/reverse.tsxpass谓词函数返回true表示穿透该节点、继续下钻到它的子树。测试用例nodes/pass.tsx展示了如何用{ pass: ([n]) !!n.pass }让遍历跳过不满足条件的子树。Node.nodes是四者的遍历引擎见 node.ts它维护一个visited集合避免重复访问按深度优先序先下钻到最深的子树再通过Path.next/Path.previous在兄弟间移动最后回溯到Path.parent直至回到根节点结束。descendants只是在nodes基础上过滤掉根路径[]因为根是 Editor不属于 Descendant见 node.tselements过滤出Node.isElement(node)的条目若根节点自身是 Element 也会被包含texts过滤出文本叶子节点。一个完整的nodes遍历示例来自测试用例nodes/all.tsx// 输入editor element [text a, text b] Array.from(Node.nodes(value)) // 输出 // [ [editor, []], [element, [0]], [text a, [0, 0]], [text b, [0, 1]] ]沿路径行走ancestors/levels/children/commonNode.ancestors(root: Node, path: Path, options?): GeneratorNodeEntryAncestor Node.levels(root: Node, path: Path, options?): GeneratorNodeEntry Node.children(root: Node, path: Path, options?): GeneratorNodeEntryDescendant Node.common(root: Node, path: Path, another: Path): NodeEntryNode.ancestors产出指定路径之上的所有祖先节点默认自顶向下从最高层祖先到最低层传{reverse: true}则自底向上。它内部调用Path.ancestors枚举祖先路径再对每条路径执行Node.ancestor见 node.ts。测试用例ancestors/reverse.tsx验证了逆向顺序。Node.levels与ancestors类似但产出的是从根到指定路径这一整条分支上的所有节点含根与目标节点本身见 node.ts。同样支持reverse选项。Node.children迭代某路径节点的所有直接子节点。reverse: true时从最后一个子节点向前迭代子节点路径通过path.concat(index)拼接得到见 node.ts。Node.common返回两条路径的公共祖先节点条目。它调用Path.common(path, another)求出公共路径后再Node.get。例如在 docs/concepts/03-locations.md 的示例中用Node.common(editor, range.anchor.path, range.focus.path)定位选区两端的最低公共祖先再结合Editor.above找到包含整个选区的公共块。边界与切片first/last/fragmentNode.first(root: Node, path: Path): NodeEntry Node.last(root: Node, path: Path): NodeEntry Node.fragment(root: Node, range: Range): Descendant[]Node.first返回从指定路径开始、沿最左侧分支下钻到第一个叶子文本节点或空元素的条目见 node.tsNode.last则沿最右侧分支下钻见 node.ts。二者常用于定位某元素的文本起点/终点。Node.fragment返回range所覆盖内容的切片副本。源码实现非常精巧先取出Range.edges(range)的两端再对整棵树做逆向遍历剪掉完全不在 range 内的分支removeChildren并在命中end.path/start.path时用modifyLeaf分别截断文本的头部与尾部最后返回newRoot.children见 node.ts。它正是复制选区内容这类能力的底层支撑。属性抽取extractPropsNode.extractProps(node: Node): NodeProps抽取节点上除内容字段之外的所有属性Element 节点剔除childrenText 节点剔除text。源码中分别通过解构剩余rest实现见 node.ts返回类型NodeProps定义见同文件末尾type NodeProps | OmitEditor, children | OmitElement, children | OmitText, text// 对于 Element 节点 const element { type: paragraph, align: center, children: [{ text: Try it out for yourself! }], } const props Node.extractProps(element) // 返回: { type: paragraph, align: center } // 对于 Text 节点 const text { text: Hello, bold: true } const props Node.extractProps(text) // 返回: { bold: true }该 API 常与Transforms.setNodes配合用于保留节点非内容属性、仅修改内容的场景。文本方法Text methods文本方法专注于节点的纯文本内容与叶子文本节点。Node.stringNode.string(root: Node): string返回节点内容的拼接文本。递归实现为文本节点直接返回text否则将所有子节点的Node.string结果无分隔拼接见 node.ts。注意两点它不会在块级节点之间插入空格或换行。测试用例string/across-elements.tsx中两个元素共四个文本节点one/two/three/four拼接结果为onetwothreefour而不是one two three four因此它不是面向用户展示的字符串而是用于基于偏移量offset计算的内部字符串。若需要渲染文本应使用 packages/slate/src/editor/string.ts 中的Editor.string。Node.textsNode.texts(root: Node, options?): GeneratorNodeEntryText产出根节点内所有叶子文本节点的条目选项与nodes一致from/to/reverse/pass。它基于Node.nodes过滤文本节点实现见 node.ts是遍历文档中每一段真实文本的标准入口例如实现全文搜索高亮时即可用它逐段扫描。检查方法Check methods检查方法用于判断节点的类型、存在性以及属性匹配。Node.isNode/Node.isNodeListNode.isNode(value: any, options?: {deep?: boolean}): value is Node Node.isNodeList(value: any, options?: {deep?: boolean}): value is Node[]Node.isNode判断值是否实现了Node接口。实现上是Text.isText(value) || Element.isElement(value, {deep}) || Editor.isEditor(value, {deep})三者之一见 node.ts。Node.isNodeList判断值是否为Node数组Array.isArray(value) value.every(val Node.isNode(val, {deep}))见 node.ts。测试用例覆盖了空数组、全 Element、全 Text、混合数组以及含非节点元素的否定情形。二者都接受{deep?: boolean}选项深检查会递归验证整棵子树而非仅检查顶层结构。Node.matchesNode.matches(root: Node, props: PartialNode): boolean判断节点是否匹配一组属性props。源码逻辑为若节点是 Element 且props是合法 Element 属性集则委托Element.matches若节点是 Text 且props是合法 Text 属性集则委托Text.matches见 node.ts。它常与Editor.nodes/Transforms的match选项配合用于按条件批量定位或修改节点。类型守卫补充源码中Node还提供了四个文档未单独列出的类型守卫见 node.tsNode.isAncestor(node): node is Ancestor——即不是文本节点Node.isEditor(node): node is Editor——通过typeof node.apply function判别Editor 必有apply方法Node.isElement(node): node is Element——children是数组且不是 Editor无apply函数Node.isText(node): node is Text——typeof node.text string。这些守卫是ancestor、descendant、elements、texts等方法的判断基石也是你编写自定义插件时最常用的类型收窄工具。实践建议何时使用哪个方法只需取一个节点用get确信存在或getIf不确定先探测再处理用has或getIf避免异常遍历整棵子树nodes全量、descendants排除根、elements只要容器、texts只要叶子沿路径向上parent取直接父、ancestors取全部祖先、levels取根到目标的整条分支、common取两条路径的交汇点求内容文本与切片string用于偏移计算fragment用于复制选区内容做类型判断与匹配isNode/isNodeList/matches配合插件或批量变换使用。这些方法在 Slate 内部的Transforms、规范化normalization与选区处理中大量复用。理解NodeAPI 的语义与实现是深入阅读 packages/slate/src 其余模块、乃至编写自定义插件与序列化逻辑的基础。【免费下载链接】slateA completely customizable framework for building rich text editors. (Currently in beta.)项目地址: https://gitcode.com/gh_mirrors/sl/slate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考