Slate 正规化的相关功能由两个主要函式:
normalize
methodnormalizeNode
action以及两个辅助函式: interfaces/editor.ts 里的 withoutNormalizing
、 isNormalizing
来完成。
我们在 Day24 也有提到过, normalizeNode
action 是主要负责执行节点正规化运算的函式,也就是实作那些 Slate Built-In constraints 的地方,也是开发者自定义 custom constraints 时要 override 的 function 。
而 normalize
method 则负责呼叫 normalizeNode
action 以及呼叫前的预处理,它同时也是我们先前所提到过,会事先完整遍历过 document 里的 Element
节点并执行第 1. 的正规化,确保它们都拥有至少一个子层节点的地方。
今天先让我们从相对单纯的 normalizeNode
里头的 code 下手
normalizeNode
action从函式的名称就能看出它是针对『单一节点』的正规化,呼叫函式所需带入的参数就只有一组:一组欲正规化的 NodeEntry
export type NodeEntry<T extends Node = Node> = [T, Path]
normalizeNode: (entry: NodeEntry) => void
在函式里头除了实作 Built-in constraints 的内容之外,还有抵挡不必进行正规化的节点的判断式,加快执行速度:
// There are no core normalizations for text nodes.
if (Text.isText(node)) {
return
}
以及一些在实作正规化的过程中会利用到的一些变数们:
shouldHaveInlines
判断子层节点应为 Block type 节点还是 Inline-Block type 节点的 boolean 变数,当节点为 Element
且符合下列四种情形时则为 true
,代表 node
的子层节点只接受 Inline-Block type 或 Text
节点:
node
为 Inline-Block typenode
不存在子层节点node
第一顺位的子层节点为 Text
node
第一顺位的子层节点为 Inline-Block type它主要被用於实作第 3. & 5. 的 Built-In constraint:
// Determine whether the node should have block or inline children.
const shouldHaveInlines = Editor.isEditor(node)
? false
: Element.isElement(node) &&
(editor.isInline(node) ||
node.children.length === 0 ||
Text.isText(node.children[0]) ||
editor.isInline(node.children[0]))
n
一个『子层节点指针』,用於指向因应不同的正规化需要更新节点时,实际更新的节点路径。
// Since we'll be applying operations while iterating, keep track of an
// index that accounts for any added/removed nodes.
let n = 0
在执行正规化时实际上需要更新的节点大多都为传入节点的子层节点,因此在程序码中段我们可以看到一个 for loop 遍历过一轮 node
的子层节点,去对每一个子层节点判断是否进行正规化。
但因为每次透过『插入节点』或『移除节点』执行正规化时,子层节点的 index 资料都会过期,不能代表它於该层级的节点顺位。
也因此我们需要 n 为我们记录每个子层节点要进行更新时的实际路径,我们用下方的简易举例让读者对它的用途更有画面一点
for (let i = 0; i < node.children.length; i++, n++) {
// ...
// remove node update
Transforms.removeNodes(
editor,
// node's path concat with n
{ at: path.concat(n) },
...
)
}
currentNode
当前执行正规化的节点:
for (let i = 0; i < node.children.length; i++, n++) {
const currentNode = Node.get(editor, path)
}
child
传入节点执行正规化前第 i
顺位的子层节点
for (let i = 0; i < node.children.length; i++, n++) {
const child = node.children[i] as Descendant
}
prev
实际执行更新的前一个 sibling 节点
for (let i = 0; i < node.children.length; i++, n++) {
const prev = currentNode.children[n - 1] as Descendant
}
isLast
i
是否为子层节点最後一个顺位
for (let i = 0; i < node.children.length; i++, n++) {
const isLast = i === node.children.length - 1
}
isInlineOrText
child
是否为 Inline-Block 或 Text
type
for (let i = 0; i < node.children.length; i++, n++) {
const isInlineOrText =
Text.isText(child) ||
(Element.isElement(child) && editor.isInline(child))
}
接着就让我们依照 Day 24 所列举的 Built-in constraints 的顺序,依序介绍里头是如何实现这些 constraints 的吧!
Element
节点内必须含有至少一个 Text
子节点。在进行正规化时如果遭遇到不符合此规范的 Element
节点,会加入一个空的 Text
节点进入它的子层。Text
nodes 合并成同一个节点(不会删减文字内容,只是单纯做节点合并而已)。Text
节点之中,选择一种作为它的子层节点。举例: paragraph
block 不能同时将另外的 paragraph
block element 以及 link
inline-block element 作为它的子层节点。 Slate 会以子层的第一个顺位的节点作为判断可接受类别的依据,不符合规范的子层节点将直接被移除。Text
节点之间, Slate 会透过『插入空的 Text
节点』来修正违反此 constraint 的情形。Editor
节点只能将 Block type 节点作为其子层节点,任何子层的 Inline type 与 Text 节点都会直接被移除。单纯去判断 Element
节点是否存在子层节点,若不存在则插入一个空的 Text
void 节点:
// Ensure that block and inline nodes have at least one text child.
if (Element.isElement(node) && node.children.length === 0) {
const child = { text: '' }
Transforms.insertNodes(editor, child, {
at: path.concat(0),
voids: true,
})
return
}
透过针对子层的 if else 判断来 child
变数的可能性缩减为只剩 Text
type 的可能性:
for (let i = 0; i < node.children.length; i++, n++) {
if (isInlineOrText !== shouldHaveInlines) {
// ...
}
else if (Element.isElement(child)) {
// ...
}
else {
// ... 2nd constraint implementation
}
}
同层的前一组 sibling 存在且为 Text
节点我们才需要继续进行正规化判断:
// Merge adjacent text nodes that are empty or match.
if (prev != null && Text.isText(prev)) {
// ...
}
若 child
与 prev
的内容相等 → 执行『节点合并』 & n - 1
:
if (Text.equals(child, prev, { loose: true })) {
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
n--
}
// ... else if implementation
若 prev
为空字串节点 → 直接移除 prev
& n - 1
:
else if (prev.text === '') {
Transforms.removeNodes(editor, {
at: path.concat(n - 1),
voids: true,
})
n--
}
// ... else if implementation
若 child
为最後一个顺位的节点且为空字串则直接移除 child
& n - 1
:
else if (isLast && child.text === '') {
Transforms.removeNodes(editor, {
at: path.concat(n),
voids: true,
})
n--
}
利用 shouldHaveInlines
与 isInlineOrText
决定是否移除当前 index 的子节点:
for (let i = 0; i < node.children.length; i++, n++) {
// Only allow block nodes in the top-level children and parent blocks
// that only contain block nodes. Similarly, only allow inline nodes in
// other inline nodes, or parent blocks that only contain inlines and
// text.
if (isInlineOrText !== shouldHaveInlines) {
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
n--
}
}
检查 Inline-Block type 子层节点的前一组 sibling ,若不存在或不为 Text
节点则插入一组 Text
void 节点:
for (let i = 0; i < node.children.length; i++, n++) {
// Ensure that inline nodes are surrounded by text nodes.
if (editor.isInline(child)) {
if (prev == null || !Text.isText(prev)) {
const newChild = { text: '' }
Transforms.insertNodes(editor, newChild, {
at: path.concat(n),
voids: true,
})
n++
}
// ... else if statement
}
}
如果该节点为同层节点的最後一个顺位则在它後方插入一组 Text
void 节点:
else if (isLast) {
const newChild = { text: '' }
Transforms.insertNodes(editor, newChild, {
at: path.concat(n + 1),
voids: true,
})
n++
}
下一篇就轮到深入 normalize
method 的实作了。
笔者个人认为下一篇的内容才是 Slate 处理 Normalizing 相关功能最精华的地方,反而不是这些 constraints 的实作。
它除了善用了 JS 函式相关的特性与 WeakMap 做搭配,处理了效能的优化之外,我们在 Day24 里提到的 Slate Normalizing 相关特性也几乎实作於其中。
cmd + D
关联来关联去的,迷路了好几次才终於看到出口 ?明天的篇章除了介绍程序码的实作之外,我们也会在明天文章的最後摆上延续 Day10 Slate 完整的运作流程图,为整个 Normalizing 篇章做一个收尾。
我们一样明天再见罗~
<<: DAY29 - 把LineBot或网站架在Heroku上
前言 闭包,一个完全无法从字面意思了解的专有名词,若是改叫小笼闭包,是不是马上联想到这个画面 一个个...
什麽是资料结构? 前一天有提到空间复杂度(Space Complexity),简单的复习一下,空间复...
函式 回传值 函式还有一个设定叫做回传值,回传值可以做什麽呢? 就是当我函式计算完之後,我想将函式计...
上一篇介绍过了I2C的基本原理以及相关的函数,这一篇会介绍EEPROM来做为I2C实作的示范。 什麽...
当我们有时候某个功能的重复运用性较高,但每次都还要再写一个一模一样的功能,是不是很麻烦呢? 那麽这时...