前面说到,ProseMirror采用类似VirtualDOM的方式来管理和协调用户交互以及DOM修改。本文将继续探讨这个话题。

我们可以给这个类似VirtualDOM的抽象层娶一个名字,叫做编辑浮层(Editing Surface),简称浮层。这个浮层要映射到DOM,所以必须也是树形结构,然后这个浮层在概念上又要贴合文本编辑。接下来让我们看看ProseMirror是怎么做得。

ProseMirror文档的节点

根据ProseMirror的说明文档doc.data_structures,ProseMirorr的核心数据是Node(节点),它包含以下属性:

  • type(NodeType类型),指定Node的类型
  • content (Fragment类型),包含了子节点
  • attrs (Object类型),包含改节点的属性
  • marks(Mark类型),包含应用于这个节点上的标记

首先,每个节点通过自身的content属性,可以包含一系列的子节点。这个节点和节点之间就形成了一个树状结构,跟DOM的结构类似。

既然ProseMirror的节点是用来模拟DOM的元素和文本的,所以ProseMirror的节点也有两种类型,一种是平铺(inline)类型,另一种是行列(block)类型(也可以叫做块状类型)。通常而言,文本节点(text node)是属于平铺类型的节点,它的内容从左到右,从上到下平铺开来;然而包含文本的段落节点(paragraph node)是属于行列类型,它把文字以段落的方式分隔开来。简而言之,平铺类型的节点是用来包含具体内容,而行列类型的节点一般是用来调整文档的结构。

一个最简单的ProseMirror文档,甚至可以只包含文本节点(平铺类型),而不包含任何段落(行列类型)。这有现实的意义,比如你可以把ProseMirror编辑器挂载(Mount)到一个<span></span>节点上编辑其中的内容(因为span元素只能包含平铺类型的内容)。

通常情况下,ProseMirror编辑器挂载的DOM元素都是行列类型的,比如<div></div>。这时候ProseMirror可以在文档内支持更复杂的节点类型,比如:段落(Paragraph)、引用(Blackquote)等等这些行列类型。

一个关于ProseMirror文档结构的例子

这个例子来自doc.indexing

<blockquote><p>Two<img src="..."></p></blockquote>

上面的html对应的ProseMirror文档的组织方式是下面这样子的:

  • 第一级:blockquote节点(对应blockquote元素)
  • 第二级,也就是blockquote节点的子节点:paragraph(对应p元素)
  • 第三级,也就是paragraph对于的子节点:Two(文本节点)和<img src="...">(图片节点)

上面的第一级节点(blockquote)和第二级节点(paragraph)都是行列类型的,第三级节点(文本节点和图片节点)则是平铺类型的。

使用schema定义文档结构

上个章节的ProseMirror文档示例是通过下面的Schema描述的

import {Schema} from "prosemirror-model"

const schema = new Schema({
  doc: {
    content: "blockquote"
  },
  blockquote: {
    content: "paragraph",
    defining: true,
    parseDOM: [{tag: "blockquote"}],
    toDOM() {return ["blockquote", 0]}
  },
  paragraph: {
    content: "inline*",
    parseDOM: [{tag: "p"}],
    toDOM() {return ["p", 0]}
  }
  text: {
    inline: true
  },
  image: {
    inline: true,
    attrs: {
      src: {},
      alt: {default: null}
    },
    parseDOM: [{tag: "img[src]", getAttrs(dom) {
      return {
        src: dom.getAttribute("src"),
        alt: dom.getAttribute("alt")
      }
    }}],
    toDOM(node) {return ["img", node.attrs]}
  }
})

对上面的schema做一些解释,首先schema的最顶层节点一般是doc,用来表示该ProseMirror编辑器所挂载的DOM节点。该scheme定义了文档的结构:

  • doc下面只包含了一个元素,那就是blockquote
  • blockquote的内容只能为一个paragraph
  • paragraph的内容可以为text或者image

另外值得注意的是,除了顶层节点doctext节点之外,其他节点都定义parseDOM属性和toDOM方法,用来描述ProseMirror文档和DOM之间的转化。顶层节点doc本来就挂载在DOM中,无需定义这些方法;text节点直接对应DOM的文本节点,所以也无需表明。

更多关于Schema的,请参考ProseMirror的官方文档Schema

小结

ProseMirror文档是根据相应的范式(Schema)派生出来的,范式定义了文档内有多少类型的节点,节点与节点之间的层级结构,以及与HTML的DOM之间的映射。这个就像是HTML规范与HTML文档之间的关系,HTML规范说明了什么样的文档可以出现在HTML文档中,以及它们的层级关系(比如<body>要包含在<html>中)。ProseMirror文档是要在HTML的DOM上建立一层编辑浮面,所以它应该模仿HTML的DOM结构,同时又允许做一定的简化。

ProseMirror用来描述Schema的方式非常像一个书写一个正则表达式。事实上,ProseMirror也是用类似解析正则表达式的技术(NFA/DFA)来解析Schema的。

(未完待续)

2019-10-20更新

HTML文档可以看出是HTML元素标签构成的一棵树,将这棵树摊开摆在桌子上,你看到的是充满字符的一个文件。HTML源代码可以看出是HTML树序列化成字符串的一种形式,使其可以易于编辑、保存和发送。但HTML文档本身上还是一棵树,DOM是最接近它本质的描述。可以DOM只能在内存中呈现,不是一种很好的持久化形式。

HTML源代码是一个大字符串,这就意味着每个字符在这个源代码中有一个编号。如果给事物编号是一门学问,在于其一,你得知道如何表示事物;其二才轮到如何编号。编号之后事物就有了次序之分,然后就有了产生信息的可能,有了信息才能被计算机处理。

ProseMirror以一种独到的眼光来观察HTML源代码,并且发明了一套独特的编号规则。首先,和DOM的眼光一致,HTML被看成一颗由节点组成的树,这里的节点对应着HTML的一个元素。节点可以容纳其他节点,节点可以为空。把这些节点摊平了,也可以给他们编号。假设把节点看成一种组织的结构,只可进入和退出,从编号的角度,进入节点编号加一,推出节点编号加二。那么这棵节点树上的任意位置都可以用同一编号表示,同时这个节点的任意位置还具有深度(进入了多少节点,或者说有多少节点没有推出)。

但是HTML源代码中不止有节点,还有文字内容。如何将文字内容整合到节点树中呢?一个简单的做法,是虚构出一种专门的文字节点,用来盛放文字。和其他节点有所不同,文字节点像一块布,可以随意被切开,形成新的文字节点。就像是布上可以印花一样,每个文字节点还可以附带标记(Mark),更准确地说,附带的是一个标记集合,将这个文字节点与其他文字节点分别开来。有了文字节点以后,那些个非文字节点可以分为两类,一类可以文字节点为子节点的,另一类则不行。另外,文字节点不是一种用来表示层级的节点,没有进入和退出一说,它的深度,等同于它父节点的深度。

(更新完)