本文继续探究富文本编辑器ProseMirror。

节点的类型

前文说道,ProseMirror的文档(document)要遵从范式(schema)的要求,就像编程语言里面的一个值都有一个相对应的类型一样。范式规定了文档的结构,同时也规定的文档的节点类型。在每个ProseMirror的范式中,最基本的节点类型是文本节点(TextNode),也就是只包含文字。文本节点的内容一定是平铺(inline)的,但是平铺类型的节点并不一定都是文本节点,我们可以把一个图像节点定义为平铺的:

  image: {
    inline: true,
  }

只要将节点的inline属性设置为true,那么这个节点就是平铺类型的,而不是默认的行列(block)类型。上面定义的image节点便是此种情况。同时,该image节点没有设置content属性,也就是不能包含其他子节点,所以也可以说这个image节点是叶子(leaf)节点。

ProseMirror中还有文本块(TextBlock)的概念。对于一个行列类型的节点,只要其包含的子节点集(Fragment)包含的都是平铺类型的节点,那么这个节点就算得上一个文本块,下面是一个例子:

  paragraph: {
    content: "(text | image)*",
  },

该paragraph节点只能包含文本节点和前面定义的图片节点。它的子节点都是平铺类型的,所以paragraph是一个文本块。此外,因为可以包含子内容,所以paragraph不是一个叶子节点。

一个节点可以既是行列节点,又是叶子节点:

  video: {
  }

上面的video默认是行列类型,同时又没有子节点。它显示的时候会独占一行,不会和其他的文字平铺在一起。

节点的索引

除了树形层次结构以外,ProseMirror会针对文档中所有节点的内容建立一个扁平的、线性的索引。来自doc.indexing的例子:

0   1 2 3 4    5
 <p> O n e </p>

5            6   7 8 9 10    11   12            13
 <blockquote> <p> T w o <img> </p> </blockquote>

索引是从左往右,从上往下递增的。其递增规则如下:

  • 文档开头,在第一个节点之前(上面例子的<p>之前),索引为0
  • 进入一个非叶子节点,索引加1(里面例子中的<p>之后)
  • 离开一个非叶子节点,索引加1(里面例子中的</p>之后)
  • 对于文本节点,每跨过一个字符索引加1
  • 每跨过一个叶子节点索引加1

节点的大小也反应了上述的索引规则。对于叶子节点,长度为1;对于文本节点,长度为所包含的字符数目;对于行列节点,就算内容为空,长度也至少为2。

索引解析

ProseMirror提供一个resolve方法,可以从一个索引值获取当前节点的更多信息,包括:

  • 从根结点开始,到当前节点的父节点的层级结构。每一层包括的信息有:
    • 当前层级节点
    • 当前层级节点的哪个子节点包含了被解析的索引
    • 包含了被解析索引的子节点自身的开始索引
  • 还有就是索引在当前节点内的偏移

标记类型

平铺类型的节点可以附加任意标记。默认情况下,可以添加任意标记,不过也可以在范式中作出规定,只允许某些平铺节点添加制定类型的标记。

所谓标记,主要是用来给平铺类型的节点加上各种格式,比如加粗、加斜体。另一个例子是给超链接文本加上可以点击的效果。在ProseMirror里面,标记不包括在节点层级中,而只是作用于平铺类型的节点。具有不同标记集合的节点是不能合并到一起的。以文本节点为例,如果一个文本节点原先包含一段不添加标记的文字,然后中间的一部分文字被加粗了(也就是增加了加粗标记),那么加粗的那部分文字就会分裂出来,形成一个新的带有加粗标记的文本。随之而来的是,前面以及后面的文字也会形成单独的文字节点。这样一来,一个文字节点就分裂成三个文字节点,前后两个文字节点其实是类似的,只不过中间隔着一段加粗文字,所以被迫分开。

这时候,如果对前面的文字节点也进行加粗操作,ProseMirror会把前面的节点和中间的节点合并,形成一个更大的文字节点。结果是三个文字节点又变成了两个。

(未完待续)

2019-10-21更新

ProseMirror提供了一个API可以在Node上调用resolve(pos)(此处pos是表示位移的一个数字)来返回一个ResolvedPos对象。ResolvedPos比pos提供了更丰富的信息。

在state.doc级别调用resolve(pos),可以获取全局的位置信息;如果只是在某个子节点调用resolve(pos),那么resolve所获得的信息就是相对这个子节点而言的。

也可以通过当前的selection来获取resolvedPos。比如:let { $from, $to } = state.selection。此处$from, $to是selection对象的部属,其类型是resolvedPos.

让我们来看一下,ResovledPos提供那些部属以及方法。假设返回resolve(pos)中的pos在一个文件节点中,由于文本节点被视作是扁平的,ResovledPos拥有的信息其实是关于其父节点的。

ResovledPos.pos返回resolve(pos)调用中的pos信息。ResovledPos.depth表示深度。非文本节点才有深度,所以如果是文本节点的话ResovledPos.depth表示的是其父节点(block类型)的深度。通过ResovledPos.node(ResovledPos.depth)可以返回其父节点(跟ResolvedPos.parent部属返回的结果一样)。ResovledPos.parentOffset用来表示当前位码在父节点中的偏移。

ResovledPos.index()返回当前节点在父节点中的索引。如果当前节点是其节点的子节点中排行第二,那么ResovledPos.index()返回1(从零开始)。ResovledPos.indexAfter()返回下一个兄弟节点的索引,(注意:下一个兄弟节点不一定存在,ResovledPos.indexAfter()更像是ResovledPos.index()+1)

ResovledPos.start()返回父节点内容的开始位置,ResovledPos.end()返回父节点的内容的结束位置。ResovledPos.before()等于ResovledPos.start()-1;ResovledPos.after()等于ResovledPos.end()+1。

由于当前节点是文本节点,ResovledPos.textOffset返回当前位置在文本节点中的偏移。ResovledPos.nodeBefore返回当前位置之前的文本节点的切片;ResovledPos.nodeAfter返回当前位置之后的文本节点的切片。

ResovledPos.marks()返回当前文本节点上应用的标记集合。ResovledPos.marksAcross($end?)则可以返回从此位置,到另外一个位置的范围内的标记集合。

ResovledPos.sharedDepth(pos: number)返回当前位置与另外一个位置的共有深度(多少共同的父节点)

ResovledPos的min()、max()、sameParent()可以和其他ResovledPos做比较。

ResovledPos.blockRange()可以返回到其他位置之间的范围(NodeRange)。

更多参考Resolved Positions

最后谈一下selection,通过state.doc.selection.content()可以返回一个slice,这个slice是从doc级别开始算起的,其openStart等于state.doc.selection.$from.depth, 其openEnd等于state.doc.selection.$to.depth

(更新完)