Day 26. slate × Normalizing × normalizeNode

https://ithelp.ithome.com.tw/upload/images/20211011/20139359cwqxqYM4C9.png

Slate 正规化的相关功能由两个主要函式:

  • interfaces/editor.ts 的 normalize method
  • create-editor 编辑器的 normalizeNode action

以及两个辅助函式: interfaces/editor.ts 里的 withoutNormalizingisNormalizing 来完成。

我们在 Day24 也有提到过, normalizeNode action 是主要负责执行节点正规化运算的函式,也就是实作那些 Slate Built-In constraints 的地方,也是开发者自定义 custom constraints 时要 override 的 function 。

normalize method 则负责呼叫 normalizeNode action 以及呼叫前的预处理,它同时也是我们先前所提到过,会事先完整遍历过 document 里的 Element 节点并执行第 1. 的正规化,确保它们都拥有至少一个子层节点的地方。

https://ithelp.ithome.com.tw/upload/images/20211011/201393592SymzpzR0H.png

今天先让我们从相对单纯的 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 type
    • node 不存在子层节点
    • 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 的吧!

Built-in constraints Implementation


  1. 所有的 Element 节点内必须含有至少一个 Text 子节点。在进行正规化时如果遭遇到不符合此规范的 Element 节点,会加入一个空的 Text 节点进入它的子层。
  2. 将两个相邻且拥有完全相同的 properties 的 Text nodes 合并成同一个节点(不会删减文字内容,只是单纯做节点合并而已)。
  3. Block type 节点只能在 Block type 或者 Inline-Block type & Text 节点之中,选择一种作为它的子层节点。举例: paragraph block 不能同时将另外的 paragraph block element 以及 link inline-block element 作为它的子层节点。 Slate 会以子层的第一个顺位的节点作为判断可接受类别的依据,不符合规范的子层节点将直接被移除。
  4. 保持所有的 Inline-Block 节点都被环绕在 Text 节点之间, Slate 会透过『插入空的 Text 节点』来修正违反此 constraint 的情形。
  5. 最顶层的 Editor 节点只能将 Block type 节点作为其子层节点,任何子层的 Inline type 与 Text 节点都会直接被移除。

1st constraint


单纯去判断 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
}

2nd constraint


透过针对子层的 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)) {
	// ...
}

childprev 的内容相等 → 执行『节点合并』 & n - 1

if (Text.equals(child, prev, { loose: true })) {
  Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
  n--
}
// ... else if implementation

prev 为空字串节点 → 直接移除 prevn - 1

else if (prev.text === '') {
  Transforms.removeNodes(editor, {
    at: path.concat(n - 1),
    voids: true,
  })
  n--
}
// ... else if implementation

child 为最後一个顺位的节点且为空字串则直接移除 childn - 1

else if (isLast && child.text === '') {
  Transforms.removeNodes(editor, {
    at: path.concat(n),
    voids: true,
  })
  n--
}

3rd constraint & 5th constraint


利用 shouldHaveInlinesisInlineOrText 决定是否移除当前 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--
	}
}

4th constraint


检查 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 的实作。

如果把这句话摆在开头的话应该就没有读者会看这一篇的内容了吧 XD

它除了善用了 JS 函式相关的特性与 WeakMap 做搭配,处理了效能的优化之外,我们在 Day24 里提到的 Slate Normalizing 相关特性也几乎实作於其中。

只不过还是要偷抱怨一下,因为使用到的函式彼此之间的关系错综复杂,笔者当初在研读时也是 cmd + D 关联来关联去的,迷路了好几次才终於看到出口 ?

明天的篇章除了介绍程序码的实作之外,我们也会在明天文章的最後摆上延续 Day10 Slate 完整的运作流程图,为整个 Normalizing 篇章做一个收尾。

我们一样明天再见罗~

铁人赛倒数 4 天

<<:  DAY29 - 把LineBot或网站架在Heroku上

>>:  [Day 28] 来做一个人脸互动的程序吧!

D10 - 点一笼热呼呼的小笼闭包 Closure

前言 闭包,一个完全无法从字面意思了解的专有名词,若是改叫小笼闭包,是不是马上联想到这个画面 一个个...

Day03:资料结构 - 列表(List)

什麽是资料结构? 前一天有提到空间复杂度(Space Complexity),简单的复习一下,空间复...

[想试试看JavaScript ] 函式 回传值

函式 回传值 函式还有一个设定叫做回传值,回传值可以做什麽呢? 就是当我函式计算完之後,我想将函式计...

16. STM32-I²C EEPROM DataSheet

上一篇介绍过了I2C的基本原理以及相关的函数,这一篇会介绍EEPROM来做为I2C实作的示范。 什麽...

[前端暴龙机,Vue2.x 进化 Vue3 ] Day15.组件介绍

当我们有时候某个功能的重复运用性较高,但每次都还要再写一个一模一样的功能,是不是很麻烦呢? 那麽这时...