ProseMirror Model 模块

prosemirror

ProseMirror Model 模块

prosemirror-model 模块主要与文档内容相关,包括文档结构(节点 node、片段 fragment、标记 mark、切片 slice),节点的数据结构(schema 即编辑器可以容纳哪些类型的内容),解析文档的位置(resolve position),所对应的页面元素(与 DOM 之间的转换)。

ProseMirror 的每一个文档都需要对应于一个 schema 以对数据结构进行约束,它描述了文档中允许哪些 nodes 以及它们可以包含哪些类型的节点作为子节点。

ProseMirror 文档的数据结构和 DOM 的树形数据结构类似,也是由一系列不同层级的节点构成,但也有所不同。

例如在页面的 editable DOM 节点是 <p>This is <strong>strong text with <em>emphasis</em></strong></p> 其 DOM 树型数据结构如下:

development-structure
development-structure

ProseMirror 对于节点的解析会有所不同,对于一些「装饰性」的 inline HTML 行内标签,如 <strong> 等,ProseMirror 会将它们解析为样式标记 mark 以作为节点的属性,简化了树形结构。以上 DOM 结构示例所对应的 ProseMirror 树型数据结构如下:

prosemirror-structure
prosemirror-structure

每一个节点 node 都有指定 schema 类型,以下是一个节点 node 对象(schema 数据结构的实例化形式)示意图:

node-structure
node-structure

Schema 类

Schema 类的实例化对象包含了可以在文档中使用的 nodes 和 marks 的类型

通过方法 new Schema(spec) 进行实例化(其中参数 spec 是符合一种 TypeScript interface SchemaSpec),得到一个 schema 对象,以下称作「数据结构的约束对象」

SchemaSpec

TypeScript interface SchemaSpec 描述了数据结构的配置对象

  • nodes 属性:列明可以在编辑器中使用的所有类型的节点。它可以是一个纯 JS 对象,以键值对的形式描述不同类型的节点,键名是节点的名称,值则是一个对象以描述节点可接受的配置参数(它需要符合一种 TypeScript interface NodeSpec,具体介绍可查看 node.type 属性的相关介绍);它也可以是一个 OrderMap 映射,它和纯 JS 对象类似,但是提供了更多实用的方法以便进行有序的键值映射
  • marks(可选)属性:列明可以在编辑器中使用的所有类型的样式标记。它可以是一个纯 JS 对象,以键值对的形式描述不同类型的样式标记,键名是样式标记的名称,值则是一个对象以描述样式标记可接受的配置参数(它需要符合一种 TypeScript interface MarkSpec,具体介绍可以查看 mark.type 属性的相关介绍);它也可以是一个 OrderMap 映射,它和纯 JS 对象类似,但是提供了更多实用的方法以便进行有序的键值映射
  • topNode(可选)属性:一个字符串,用于设置顶级节点的名称,默认值为 doc

schema 数据结构的约束对象包含一些属性和方法:

  • spec 属性:一个对象,与该数据结构约束对象的配置参数类似(但是有所不同)
    提示

    它包含三个属性:

    • nodes 属性:是一个 OrderMap 映射,表示该文档所支持的节点类型。
    • marks 属性:是一个 OrderMap 映射,表示该文档所支持的样式标记类型。
    • topNode(可选)属性:字符串,顶级节点的名称

    在实例化 schema 对象时在所传递的参数中 nodemarks 可以是纯 JS 对象,而在 schema 对象的 spec 属性中它们都以一个 OrderMap 映射来表示,虽然和纯 JS 对象类似但是它更强调顺序,由于 nodesmarks 涉及对 DOM 的匹配和转换,而优先级别对此有重要影响

  • nodes 属性:一个对象,以键值对的形式列出所有类型的节点,键名是各种节点的名称,对应的值为一个 nodeType 对象(具体介绍可查看 node.type 属性的相关介绍)
  • marks 属性:一个对象,以键值对的形式列出所有类型的样式标记,键名是各种样式标记的名称,对应的值为一个 markType 对象(具体介绍可查看 mark.type 属性的相关介绍)
  • topNodeType 属性:一个 nodeType 对象(具体介绍可查看 node.type 属性的相关介绍),表示文档顶级节点的类型
  • cached 属性:一个对象,用于存储数据(开发者可以将所需的数据存储到该属性上,作为缓存,避免重复计算),但需要注意为该对象添加数据(设置新的属性)时避免属性名的冲突
  • node(type, attrs?, content?, marks?) 方法:基于该 schema 创建一个指定类型 type 的节点
    各个参数的具体说明:
    • 第一个参数 type 设置节点的类型,可以是表示节点名称的字符串,也可以是表示节点类型的 nodeType 对象
    • 第二个(可选)参数 attrs 设置节点所具有的属性 attributes 的值,默认值为 null 表示 attributes 采用预设值
    • 第三个(可选)参数 content 设置节点内容(子节点),可以是 fragment 片段,也可以是一个 node 节点实例,也可以是 [nodes] 一系列节点构成的数组
    • 第四个(可选)参数 marks 设置节点的样式标记,是 [marks] 一系列样式标记对象构成的数组
  • text(textContent, marks?) 方法:根据给定的文本内容 textContent(字符串)创建一个文本节点,可以通过第二个(可选)参数 marks(它是一系列样式标记对象构成的数组)为文本添加样式标记
  • mark(type, attrs?) 方法:基于该 schema 创建一个指定类型 type 的样式标记,可以通过第二个(可选)参数 attrs 为样式标记设置 attributes 属性值
  • nodeFromJSON(jsonObj) 方法:将传入的 JSON 对象 jsonObj 反序列化为一个 node 节点。
    提示

    该方法 this 已经重绑定为当前对象(即当前的 schema 数据结构约束对象)

    传入的 JSON 对象 jsonObj 是由相应的 Node.toJSON() 方法所生成

  • markFromJSON(jsonObj) 方法:将传入的 JSON 对象 jsonObj 反序列化为一个 mark 样式标记。
    提示

    该方法 this 已经重绑定为当前对象(即当前的 schema 数据结构约束对象)

    传入的 JSON 对象 jsonObj 是由相应的 Mark.toJSON() 方法所生成

Node 类

该类的实例化对象在 ProseMirror 不同模块中都需要使用,由于它是编辑器内容的基础。

可以调用 Node 类静态方法 Node.fromJSON(schema, json) 来解析 JSON 对象获取一个 node 对象。该方法的各参数的说明如下:

  • 第一个参数 schema 是一个对象,表示该节点的数据结构(约束规则)
  • 第二个参数 json 是 JSON 对象,它包含了节点的内容
其他实例化 Node 的方法

可以根据给定的 schema 规则创建一个 node 实例

说明

这里的 schema 是数据约束 Schema 类的实例化对象,具体可参考官方文档的关于 class Schema 的部分

  • 方法 schema.node(type | NodeType, attrs, content | Node | [Node], marks) 创建一个 node 实例对象
  • 方法 schema.nodeFromJSON(json) 解析 JSON 对象获取一个 node 对象

也可以调用 nodeType 的一系列相关方法创建一个 node 实例对象

说明

nodeType 是指 NodeType 类的实例对象,具体可参考官方文档的关于 class NodeType 的部分

  • nodeType.create(attrs, content | Node | [Node], marks)
  • nodeType.createChecked(attrs, content | Node | [Node], marks)
  • nodeType.createAndFill(attrs, content | Node | [Node], marks)

另外还可以调用 domParser.parse(dom, options) 方法来解析 DOM 节点获取一个 node 对象

DOMParser

上面所说的 domParser 对象是 class DomParser 类的实例化,以下称为「DOM 解析器」

可以使用该类的静态方法 static DOMParse.fromSchema(schema) 基于 schema 数据结构的约束对象创建一个 DOM 解析器

也可以通过方法 new DOMParser(schema, rules) 进行实例化,其中第一个参数 schema数据结构的约束对象,第二个参数 rules 是一个数组(其元素是对象,需要符合 TypeScript interface ParseRule)表示一系列的 DOM 解析规则

domParser DOM 解析器包含一些属性和方法:

  • schema 属性:一个 schema 数据结构的约束对象,表示该 DOM 解释器所基于的哪个 schema 的规则对 DOM 进行解析的
  • rule 属性:一个数组,其元素是对象,需要符合 TypeScript interface ParseRule,表示一系列的 DOM 解析规则
  • parse(dom, options?) 方法:将给定的 dom DOMNode 解析为 node 节点对象,第二个(可选)参数 options 是一个对象(需要符合 TypeScript interface ParseOptions)用于添加一些通用的解析规则(例如空格的处理方式等)
    ParseOptions

    TypeScript interface ParseOptions 描述了一些通用的解析规则,符合该约束的对象可以作为方法 domParser.parse 或方法 domParser.parseSlice 的参数

    • preserveWhitespace(可选)属性:一个布尔值或字符串 full,以决定如何解析 DOM 元素中的空格
      该属性值可以是以下三个值之一,表示对空格的不同解析处理方式:
      • true 表示完全保留空格键,但是换行符 \n 等会替换为空格键
      • false(默认值)表示可以对空格键进行简化,即多个相连的空格键会「坍缩」为一个
      • "full" 表示完全保留空格键和换行符
    • findPosition(可选)属性:一个数组,其元素是对象 {node: DOMNode, offset: number, pos⁠?: number} 以表示(将 DOM 解析为编辑器文档内容时)需要寻找哪些 DOMNode 在编辑器中的对应位置。
      该数组元素(对象)的各个属性的具体说明如下:
      • node 属性:表示需要寻找位于哪个 DOMNode 中的位置
      • offset 属性:是一个数字,用于表示所需要寻找的位置相对于 DOMNode 开头位置偏移量多少个字符
      • pos(可选)属性:表示在编辑器中的对应位置(这是 ProseMirror 在解析 DOM 时自动生成的)
      说明

      在调用 domParser.parse(dom, options) 方法对 dom 元素进行解析时,不仅要获得解析后的内容,还可能需要得到原本在 DOM 元素上的某个位置对应映射到编辑器文档的什么位置

      这时候就可对属性 options.findPosition 进行配置,列出一系列所需寻找的位置,例如 positionList = [{dom1, offset1}, {dom2, offset2}](第一个要查找的位置在 dom 的子元素 dom1 中,偏移量为 offset1;第二个要查找的位置在 dom 的子元素 dom2 中,偏移量为 offset2

      然后 ProseMirror 在执行 dom 解析的同时,会进行寻找查找,然后将映射位置添加到原数组里,即为各元素添加上 pos 属性,表示该 DOM 位置对应映射到编辑器文档的位置(符合 index schema 规则),然后可以通过访问数组各元素(对象)的属性 pos 获取相应的 DOM 位置对应映射到编辑器文档中的什么位置

      js
      const positionList = [{dom1, offset1}, {dom2, offset2}];
      
      domParser.parse(dom, {findPosition: positionList});
      
      console.log(`firstPos: ${positionList[0].pos}`);
      console.log(`secondPos: ${positionList[1].pos}`);
      

      ⚠️ 如果所需查找的位置不在解析范围内,则 ProseMirror 不会为相应的位置添加上 pos 属性

      该属性这对于解析转换 DOM 的同时要获取位置信息的场景很实用,例如要加载一段 HTML 作为编辑器初始化文档内容,如果 HTML 原本具有选区,则可以基于以上方法在编辑器文档中进行选区的恢复

    • from(可选)属性:一个数字(索引值),表示解析是从给定的 DOM 的第几个子元素开始的
    • to(可选)属性:一个数字(索引值),表示解析直到给定的 DOM 的第几个子元素就停止
    • topNode(可选)属性:一个 node 节点对象,表示解析的内容置于哪个节点里,即该属性用于设置容器节点。默认为根节点(编辑器的 schema 数据结构的约束对象所定义的 topNodeType 所对应的节点)
    • topMatch(可选)属性:一个 contentMatch 对象 用于约束在上述的 topNode 容器节点里可以容纳哪些内容/子节点
    • context(可选)属性:一个 resolvedPos 对象(表示一个文档的位置,它一般是指 topNode 容器节点之上的位置,通过将一系列的祖先节点纳入考虑)可以提供更丰富的上下文 context 信息,有助于更准确地解析 DOM
  • parseSlice(dom, options?) 方法:与前一个方法 domParser.parse 类似,不过该方法将给定的 dom DOMNode 解析为 slice 切片对象,第二个(可选)参数 options 与前一个方法一样
提示

在 ProseMirror 的官方文档中,数据类型 node 是指 ProseMirror 节点,而 dom.Node 是指 DOM 节点

注意

node 实例对象属于持久化的数据,不应该直接修改它。所以在(应用事务 apply transaction)更新文档时,更改的部分是基于给定的内容来创建新的 node 实例对象,而旧的 node 实例依然保留指向旧文档的相应部分。

后面的章节列出了 node 节点对象所包含的一些属性和方法

节点信息

  • type 属性:表示该节点的类型,它是一个 nodeType 对象
    nodeType

    上面所说的 nodeType 对象是 class NodeType 的实例化,它是编辑器在使用 new Schema() 实例化数据约束对象时,根据传入的参数 nodes(该参数值是一个对象,每一个属性的属性名称是节点名称,相应的属性值是该节点的配置对象),创建一系列相应的 nodeType 实例对象,它们包含了不同节点的信息,例如名称。

    这些 nodeType 对象可以用于标记/表示相应的节点 node 的类型。

    nodeType 对象包含一些属性和方法:

    • name 属性:一个字符串,表示该类型的节点的名称
    • schema 属性:一个 schema 对象,表示该类型的节点在哪个 schema 对象中进行定义的
    • spec 属性:一个对象(它需要符合一种 TypeScript interface NodeSpec),描述该类型的节点可接受的配置参数
      NodeSpec

      TypeScript interface NodeSpec 描述该类型的节点可接受哪些配置参数

      实际在哪里定义

      其实该 interface 所约束的是在实例化 schema 时,在配置对象中,针对该类型的节点可以设置哪些属性

      • content(可选)属性:该节点允许包含哪些内容(即子节点),如果未设定该属性值,则该节点不允许容纳其他子节点/内容。
        content expression 内容表达式

        其属性值是 content expression 内容表达式,是一个字符串,它包含一些特殊符号的(类似于正则表达式),以描述该节点可以容纳哪些内容/子节点

        例如以下是一个 schema 实例对象,其中属性 nodes 为该编辑器的不同节点进行配置,而每个节点的属性 content 用于约束它的内容/子节点

        js
        const groupSchema = new Schema({
          nodes: {
            doc: {
              content: "paragraph+" // 允许 doc 节点的内容中包含 paragraph 节点(作为子节点)
            },
            paragraph: {
              content: "text*" // 允许 paragraph 节点的内容中仅包含文本节点
            },
            text: {} // 不允许其他子节点作为该节点的内容
          }
        })
        

        以上示例中,各节点的配置参数中 content 属性就是用于设置该节点可以容纳的内容/子节点:

        例如 paragraph+paragraph 表示该节点(doc 整个文档)的内容中包含「段落节点」作为子节点,+ 符号是正则表达式中用于表示数量的特殊符号,表示节点的数量是一个或多个

        text*text 表示内容中包含「文本节点」作为子节点,* 符号是正则表达式另一个表示数量的特殊符号,表示节点的数量是零个或多个。

        除了前面说到的 +*,表示数量的特殊符号还有 ? 表示零个或一个;还可以指定具体的数量,例如 paragraph{2} 表示内容正好包含两个「段落节点」;也可以指定一个范围,例如 paragraph{1, 5} 表示内容可以包含一到五个「段落节点」,或者 paragraph{2,} 表示内容可以包含两个或更多的「段落节点」。

        此外还可以表示不同的节点类型的组合,例如 heading paragraph+ 表示该节点的内容是先有一个标题节点作为子节点,然后有一个或多个段落节点作为子节点,这两种类型的子节点有明显的先后要求;如果使用 | 符号,例如 (paragraph | blockquote)+ 则表示子节点的类型在「段落节点」和「引文节点」之间二选一,而数量是一个或多个。

        注意

        如果 ProseMirror 节点所对应的 DOM 节点是块级节点,则在实例化 schema 时,该节点的 content 属性应该允许至少有一个子节点,这样在页面渲染出来的 DOM 节点才不会因为内容为空而「坍缩」导致不易于编辑输入。

      • marks(可选)属性:该节点内允许使用哪些标记。
        标记

        其属性值是一个字符串,不同标记之间以空格间隔,用以描述哪些样式标记可以应用于节点内。

        其中 "_" 下划线符号表示在该节点内允许使用所有类型的标记,而 "" 空字符串表示不允许使用任何样式标记。

        js
        const markSchema = new Schema({
          nodes: {
            doc: {
              content: "block+"
            },
            paragraph: {
              group: "block",
              content: "text*",
              marks: "_" // 该节点内允许使用所有标记,例如可以对「文本节点」使用 strong 和 em 标记
            },
            heading: {
              group: "block",
              content: "text*",
              marks: "" // 该节点内不允许使用任何标记
            },
            text: {
              inline: true
            }
          },
          // 在 schema 中注册两种标记
          marks: {
            strong: {},
            em: {}
          }
        })
        

        如果节点是可以包含内容(inline 行内类型的子节点,例如 paragraph 节点),则默认允许使用所有样式标记,以便为子节点添加样式;而其他类型的节点,则默认不允许任何样式标记。

      • group(可选)属性:该节点所属的组别。
        组别

        其属性值是一个字符串,用于指定该节点属于哪些组别,可以指定多个组别,组别名称之间用空格分隔。

        由于编辑器的文档是树形结构,节点之间存在嵌套关系,ProseMirror 需要精准控制各个节点内允许包含哪些内容,即哪些类型的节点作为当前节点的子节点,通过在 schema 对象中各节点的 content 属性进行设置,该属性值是一个字符串,如果该节点内允许多种类型的节点,则需要将这些节点的名称用空格间隔拼接为一个冗长的字符串。

        为了方便,可以先给「基础」节点(一般这些节点的内容只包含「文本节点」)指定组别,然后在更上一级节点的 content 属性中,使用组别名称来表示这一类的节点

        js
        const markSchema = new Schema({
          nodes: {
            doc: {
              // 相当于 (heading | paragraph | blockquote)+
              content: "block+"
            },
            paragraph: {
              group: "block", // 将「段落节点」指定到 block 组
              content: "text*"
            },
            heading: {
              group: "block", // 将「标题节点」指定到 block 组
              content: "text*"
            },
            blockquote: {
              group: "block", // 将「引文节点」指定到 block 组
              // 这里类似「循环引用」
              // 虽然自身指定为 block 组,其内容也可以有 block 组的节点
              // 即编辑器内引文中可以嵌套引文(以及其他属于 block 组的节点)
              content: "block+"
            },
            text: {
              inline: true
            }
          }
        })
        
      • inline(可选)属性:一个布尔值,表示该节点是否为行内类型。对于 text 「文本节点」该属性会默认设置为 true
      • atom(可选)属性:一个布尔值,表示该节点是否需要以一个整体(原子化)对待
        原子化

        节点作为一个整体对待,就是不能编辑该节点里面的内容,在编辑器的视图中只能整体加入或整体移除

        虽然它不是叶子节点,但是这样的节点在 ProseMirror 的 view 视图系统的位置计数中也只算 1 个单位。

      • attrs(可选)属性:一个对象,设置该节点所拥有的 attributes 以添加额外的信息
        添加额外信息

        在实例化 schema 时,可以在各节点的 attrs 属性中设置额外的信息

        其属性值需要是一个「纯对象」 plain object(可以进行 JSON 反序列化的对象),其中该对象的每一个选项都可以通过(可选)属性 default 设置默认值

        js
        const markSchema = new Schema({
          nodes: {
            doc: {
              content: "block+"
            },
            heading: {
              content: "text*",
              // 通过 attrs 属性为「标题节点」添加额外的信息
              attrs: {
                // 为「标题节点」添加 level 属性
                // 默认值是 1,也可以在创建「标题节点」时指定其他值
                level: {
                  default: 1
                }
              }
            }
            text: {inline: true}
          }
        })
        
      • selectable(可选)属性:一个布尔值,表示该节点是否允许选中,以创建一个 nodeSelection 节点选区。对于非文本的节点(例如图片节点)其默认值是 true(即可以被选中)
      • draggable(可选)属性:一个布尔值,默认为 false。表示该节点(在非选中的情况下)是否允许拖拽移动。
      • code(可选)属性:一个布尔值,表示该节点内是否包含代码文字。
        代码

        由于代码文字的处理方式不同(格式化的方式不同,例如需要保留空格和缩进),所以对于包含代码的节点,需要设置该属性。

      • whitespace(可选)属性:属性值可以是 "pre""normal",用于设置该节点如何解析空格符号。
        解析空格

        如果属性值是 "normal"(默认值)则 domParser 对象在解析节点内的空格,会用空格替代换行符 \n 等类似的符号,而多个连续的空格会「压缩」为一个空格。

        如果属性值是 "pre" 则保留空格等格式符号(对于设置了 code 属性为 true 的节点,whitespace 属性默认会设置为 "pre" 值)

      • definingAsContext(可选)属性:一个布尔值,表示该节点(对于其子节点而言)是否作为具有特殊语义的上下文 well-defining context。该属性会影响因为粘贴导致其内容被完全替换时的行为。
        具有语义对粘贴替换的影响

        例如元素 <ul>(对于列表项而言)就是具有特殊语义的上下文,表示在它里面的内容是一个无序的列表。

        如果该属性设置为 true 则表示该节点具有重要的语义,那么对于通过粘贴完全替换了该节点的全部内容的操作,该节点(作为父节点)依然保留,它还是会作为新插入内容的容器。

        如果设置为 false,则当它的内容完全替换时,该节点也会移除(然后默认新建一个 paragraph 段落节点作为容器,包裹新插入的内容)

      • definingForContent(可选)属性:一个布尔值,表示该节点(对于其子节点而言)是否作为具有特殊语义的上下文 well-defining context。该属性会影响复制其部分内容时的行为。
        具有语义对复制的影响

        例如元素 <ul> 对于其子元素 <li> 就是具有特殊语义的,表示该列表项是在无序列表中,而不是在有序列表中。

        如果该属性设置为 true 则表示该节点具有重要的语义,那么对于复制剪切该节点的部分内容的操作,例如复制某个 <li> 元素(而不是整个列表时)的内容时,一般会「带上」 <ul> 元素,以确保复制内容的正确语义。

        如果设置为 false,则复制部分内容时不会包含它

      • defining(可选)属性:一个布尔值,用于控制与该节点相关的复制粘贴行为。如果设置为 true 时会同时将该节点的 definingAsContext 属性和 definingForContent 属性都设置为 true
      • isolating(可选)属性:一个布尔值,默认值为 false,表示该节点是否为独立(隔离)的节点。
        独立隔离节点

        如果为 true,则相当于在节点四周建起了一个隔离区,则在该节点(内部)的边缘进行的操作会受到约束。

        例如在该节点的开头按下删除键是无法实现预期的效果(删除该节点),因为该节点是光标的边界,光标是无法「穿透」该节点跳到前一个节点。

        另外对于 lifting 「提升」操作也有影响(通过组合键 Shift + Tab 实现缩进文本的提升)

        常见的一个示例是表格的单元格 table cell,在单元格的开头按下删除键,不应该像段落的行为一样,所以该单元格并不会删除掉而跳转到上一个单元格。

      • toDom(可选)属性:一个函数,用于设置如何将该节点转换为相应的 DOM 元素。
        节点序列化为 HTML

        节点的这个方法会在 DOM 生成器/序列化器的静态方法 DOMSerializer.fromSchema(schema) 中被调用,将文档中的这种类型的节点序列化为 HTML(DOM 元素)

        DOMSerializer

        上面所说的 class DOMSerializer 类用于将编辑器的 node 节点对象和 mark 样式标记对象序列化为 DOM 元素,其实例化称为「DOM 生成器」或「DOM 序列化器」

        可以使用该类的静态方法 static DOMSerializer.fromSchema(schema) 基于 schema 数据结构的约束对象创建一个 DOM 序列化器

        也可以通过方法 new DOMSerializer(nodes, marks) 进行实例化,各参数的具体说明如下:

        • 第一个参数 nodes 是一个对象 {nodeName: fn(node)} 以键值对的形式为不同类型的节点设置序列化函数,其中键是节点的名称,值是对应的序列化函数
          序列化函数 fn(node) 会将所需序列化的节点对象 node 作为参数传入,它的返回值有多种形式(要符合 TypeScript type DOMOutputSpec 以表示/描述所序列化而成的 DOM 结构)
        • 第二个参数 marks 是一个对象 {markName: fn(mark, inline)} 也是以键值对的形式为不同类型的样式标记设置序列化函数,其中键是样式标记的名称,值是对应的序列化函数
          序列化函数 fn(mark, inline) 第一个参数 mark 是需要序列化的样式标记对象,第二个参数 inline 是一个布尔值,以表示该样式标记的内容 content 是否为 inline 类型(一般为 true)。也可用 null 来替代序列化函数,以表示该类型的样式标记在序列化时应该被忽略

        domSerializer DOM 序列化器包含一些属性和方法:

        • nodes 属性:一个对象 {nodeName: fn(node)} 以键值对的形式列出了一系列不同类型的节点的序列化函数
        • marks 属性:一个对象 {markName: fn(mark, inline)} 以键值对的形式列出一系列不同类型的样式标记的序列化函数
        • serializeFragment(fragment, options?, target?) 方法:将给定的 fragment 片段序列化为 HTML,返回值可以是 HTMLElementDocumentFragment
          第二个(可选)参数 options 是一个对象,用于设置 Document 对象,以便调用浏览器相关的 API 创建 DOM 元素。(默认使用 window.document 来获取 Document 对象)如果执行环境不在浏览器中,则需要配置该参数(通过该对象的属性 document 来给定一个 Document 对象)
          第三个(可选)参数 target 可以是 HTMLElementDocumentFragment,用于设置初始的 DOM 片段(可以将其理解为容器 ❓ 序列化生成的 DOM 元素会插入到它里面)
        • serializeNode(node, options) 方法:将给定的 node 节点对象序列化为 DOM 元素,返回值是 DOMNode。第二个(可选)参数 options 的作用与前一个方法 serializeFragment 的参数一样

        另外该类还提供了一些静态方法

        • static renderSpec(doc, structure, xmlNS⁠?) 静态方法:根据给定的 DOM 结构 structure(符合 TypeScript type DOMOutputSpec),使用参数 doc (一个 Document 对象,以调用浏览器相关的 API 创建 DOM 元素)创建 DOM 节点。
          返回一个对象 {dom: DOMNode, contentDOM?: HTMLElement}(其实该结构也符合 TypeScript type DOMOutputSpec),其中属性 dom 表示(节点对象)序列化生成的 DOM 元素,属性 contentDOM 表示一个容器元素(装载节点对象的内容序列化生成的 HTML 内容)
          由于 TypeScript type DOMOutputSpec 有多种形式,可以把该静态方法理解为将参数 structure(虽然它本来也符合 DOMOutputSpec 类型,但可能是 string 或数组形式)统一转换为 {dom, contentDOM} 的形式(这种形式可以调用 DOM 相关的 API 进行更高级的定制)
        • static nodesFromSchema(schema) 静态方法:从 schema 数据结构的约束对象中获取各种类型的节点的序列化函数,返回一个对象 {nodeName: fn(node)} 以键值对的形式列出了一系列不同类型的节点的序列化函数(其中序列化函数 fn(node) 的返回值有多种形式,需要符合 TypeScript type DOMOutputSpec 以表示/描述所序列化而成的 DOM 结构)
        • static marksFromSchema(schema) 静态方法:从 schema 数据结构的约束对象中获取各种类型的样式标记的序列化函数,返回一个对象 {markName: fn(mark, inline)} 以键值对的形式列出了一系列不同类型的节点的序列化函数(其中序列化函数 fn(mark, inline) 的返回值有多种形式,需要符合 TypeScript type DOMOutputSpec 以表示/描述所序列化而成的 DOM 结构)

        该属性值是一个函数 fn(node) 参数 node 是该节点实例(一个对象)

        该函数的返回值需要符合 TypeScript type DOMOutputSpec,可以是一个字符串、一个 DOM 元素、一个对象,或一个数组(用于描述 DOM 元素)

        DOMOutputSpec

        DOMOutputSpec 类型可以有多种形式来描述一个 DOM 元素

        ts
        type DOMOutputSpec = string |
          DOMNode |
          {dom: DOMNode, contentDOM?: HTMLElement} |
          [string, any]
        
        • 可以是一个字符串,则该节点序列化为 HTML 时会变成该文本字符串。这种情况通常适用于简单的节点,例如文本节点或者叶子节点(没有子节点的节点)
        • 可以是一个 DOM 元素,则该节点序列化为 HTML 时就转换为该 DOM 元素
        • 可以是一个对象,具有属性 dom(该属性值是一个 DOM 元素),和(可选)属性 contentDOM(该属性值也是一个 DOM 元素,以指定该节点的内容/子节点渲染的位置,可以将这个 DOM 元素理解为一个容器)
        • 可以是一个数组(甚至是嵌套数组),而这个数组中各个元素都有不同的规定
          1. 第一个元素是 DOM 元素的名称(可以带有命名空间 URL 的前缀,并以空格分隔 ❓ 以便创建 SVG 元素)
          2. 第二个元素有多种类型
            • 如果是一个「纯对象」 plain object 则表示添加到 DOM 元素上的 attribute
            • 如果不是一个「纯对象」,则它表示该 DOM 元素的子元素,那么它需要满足 DOMOutputSpec 类型(也就是说可以是一个数组,构成嵌套关系,用于描述子元素)
            • 也可以是数字 0 称为 hole 占位符,以表示子节点/内容插入的位置
          3. 后面的元素都表示该 DOM 元素的子元素,那么它需要满足 DOMOutputSpec 类型
          注意

          如果使用 0 占位符来表示其内容的插入位置,则该描述 DOM 元素的数组中就不能再设置子元素(如果同时设置了子元素的插入位置,又设置了子元素,会产生冲突)

          js
          const schema = new Schema({
            nodes: {
              doc: {
                content: "paragraph+"
              },
              paragraph: {
                content: "text*",
                toDOM(node) {
                  // 表示「段落节点」转换为 <p> 元素
                  // 而节点的内容直接插入到 <p> 元素内
                  return ["p", 0]
                }
              },
              article: {
                content: "paragraph+",
                toDom(node) {
                  // 表示「文章节点」转换为 <article> 元素
                  // 并添加 target 类名
                  // 而节点的内容直接插入到 <article> 元素内
                  return ["article", {class: "target"}, 0]
                }
              },
              div: {
                content: "paragraph+",
                toDOm(node) {
                  // 表示「div 节点」转换为 <div> 元素,并添加样式
                  // 其中还有一个子元素 <p>,而节点的内容是在这个 <p> 元素内
                  return ['div', {style:'color:red'}, ['p', 0]]
                  // 🚫 如果返回的数组是 ['div', {style:'color:red'}, ['p'], 0] 则这是错误的写法
                  // 因为 0 是表示放置节点内容(子元素)的位置,这和 <p> 作为子元素的设定冲突了
                }
              }
              text: {}
            }
          })
          
        注意

        对于 text 「文本节点」应该配置该属性,因为 ProseMirror 会将「文本节点」自动生成为页面的文本内容。

      • parseDOM(可选)属性:一个数组,包含了将 DOM 元素解析为节点的一些规则。
        HTML 反序列化为节点

        该属性值是一个数组,其中每个元素都是一个对象以表示一种解析规则。ProseMirror 会利用这些规则,通过静态方法 DOMParser.fromSchema(schema) 创建一个 DOM 解析器,以将不同的 DOM 元素解析为相应的节点。

        解析规则(对象)需要符合一种 TypeScript interface TagParseRule

        ❓ ❓ ❓ 其实这里 TypeScript 类型是否应该为 type ParseRule(它包含 interface TagParseRule 和 interface StyleParseRule,分别可以基于 HTML 标签名称或行内样式 inline style 将 DOM 元素解析为节点 node 或样式标记 mark)

        TagParseRule

        TypeScript interface TagParseRule(它继承了另一个 TypeScript interface GenericParseRule)描述了基于 HTML 标签名将 DOM 元素解析为相应类型的节点的规则

        GenericParseRule

        TypeScript interface GenericParseRule 会被 interface TagParseRule(基于 HTML 标签名称将 DOM 元素解析为节点或样式标记)和 interface StyleParseRule(基于行内样式 inline style 将 DOM 元素解析为样式标记 ❓ 感觉应该也支持解析为节点)所继承

        即 TypeScript interface GenericParseRule 包含了一些配置 DOM 解析规则的通用的字段

        • priority(可选)属性:一个数值(默认值为 50),设置解析规则的优先级顺序,数值越大优先级越高,匹配时该规则会先执行。
          注意

          优先级是针对 schema 内部所创建的解析器

          如果自己手动创建的 DOM 解析器,则解析规则的优先级是按照它们在 parseDOM 数组中的顺序

        • consuming(可选)属性:一个布尔值(默认为 true) 表示一个 DOM 元素匹配到该规则匹配时,是否消耗「机会」,如果为 true停止去尝试余下的规则;如果设置为 false 则依然尝试去执行余下的匹配规则(可能造成一个 DOM 元素解析为多个 node 节点 ❓ )
        • context(可选)属性:一个字符串,用以设置该规则要满足的上下文,可以理解为限制所匹配 DOM 元素需要在哪些父元素内。
          限制上下文

          因为 ParseRule 解析规则中的属性 tagnamespacestyle 等都只能约束要匹配的 DOM 元素的,而 context 属性则可以用于限制其父节点类型(相当于限制所匹配 DOM 元素需要在哪些父元素内)

          该属性值是字符串,可以由一个或多个节点(或 group 名称)构成,结尾有一个 / 斜线或两个 // 斜线,可以将它理解为路径 path 的分隔符,以表示父子节点的层级,以下是一些示例

          • paragraph/ 表示其他属性匹配到的 DOM 元素所对应的节点的父节点需要是 paragraph 「段落节点」类型
          • blockquote/paragraph/ 表示其他属性匹配到的 DOM 元素所对应的父节点需要是 paragraph 「段落节点」类型,而且该「段落节点」需要在 blockquote 「引文节点」中
          • section// 表示其他属性匹配到的 DOM 元素所对应的祖先节点需要是 section 「章节节点」类型。双斜线表示可以嵌套多级,因此该规则表示只要满足祖先节点为 section 即可
          • blockquote/|list_item/ 表示其他属性匹配到的 DOM 元素所对应的父节点可以是 blockquote 「引文节点」或 list_item 「列表项」节点之一
        • mark(可选)属性:一个字符串(样式标记的名称),表示将匹配的 DOM 元素解析为哪一种样式标记(包裹住 DOM 元素的内容 ❓ )
        • ignore(可选)属性:是否忽略所匹配到的 DOM 元素,不将它转换为任何类型的 node 或 mark。这对于在解析 HTML 时排除特定类型的内容非常有用,例如空白字符或注释。
        • skip(可选)属性:一个布尔值,表示是否忽略所匹配到的 DOM 元素,不将该 DOM 元素本身转换为任何类型的 node 或 mark,但依然解析它的内容/子元素
          忽略 DOM 元素的不同方法对比

          属性 skip 该属性只是忽略该 DOM 元素本身,它的内容/子元素依然会被解析,可以用来舍弃一些只是作为容器的 DOM 元素,例如 <div><span>

          属性 ignore 则适合忽略所匹配的整个 DOM 元素

          属性 contentElement 可以手动设置当前所匹配的 DOM 元素的哪些后代元素进入解释器(解析的结果作为节点的 content),实现「跳过」解析某些后代元素

        • closeParent(可选)属性:一个布尔值,表示是否将所匹配到的 DOM 元素的父元素「关闭」,则该父节点会被「分割」成两部分
          关闭父元素

          在当前所匹配到的 DOM 元素的前面对父节点进行「关闭」

          例如 HTML 是 <p>a<br/>c</p>,该解析规则匹配的是 <br/> 元素转换为 hard_break 换行节点,如果该属性设置为 true,则会在 <br/> 元素前后添加 </p><p> 标签将父元素「关闭」,相当于将父元素所对应的节点分割成两个节点

          具体的例子可以查看论坛的这个讨论以及这个 patch

        • attrs(可选)属性:一个对象,为所匹配到的 DOM 元素解析后生成的节点(或样式标记)设置所拥有的 attribute(以添加额外信息)
          注意

          如果解析规则中同时设置属性 getAttrs,则优先采用,即该属性会被忽略掉(实际上 ProseMirror 将属性 getAttrs(一个函数)的返回值覆盖掉属性 attrs

          可以将属性 attrs 理解为一种「静态」的方式来设置 node 或 mark 的 attribute,而属性 getAttrs 则提供了一种「动态」的方式(根据所匹配的 DOM 元素)来设置 attribute

        • tag(可选)属性:一个字符串,表示一个 CSS 选择器,以描述需要匹配哪个 DOM 元素。
          注意

          每个解析规则都应该具有 tag 属性或 style 属性,以将特定的 DOM 元素解析为相应的 node 或 mark

          js
          const schema = new Schema({
            nodes: {
              paragraph: {
                content: "inline*",
                // 为「段落节点」设置一个 DOM 解析规则
                // 将 <p> 元素解析为段落节点
                parseDOM: [{tag: "p"}],
              },
            },
            marks: {
                em: {
                  // 为「强调样式」设置 2 个 DOM 解析规则
                  parseDOM: [
                    {tag: "i"}, // 将 <i> 元素转换为该类型的样式标记
                    {tag: "em"}, // 将 <em> 元素转换为该类型的样式标记
                  ],
                },
            }
          })
          
        • namespace(可选)属性:一个字符串或 null(没有命名空间),表示所需匹配的 DOM 元素还需要符合的命名空间(前缀,用于匹配 SVG 元素 ❓ )。
          注意

          在解析规则需要同时设置了属性 tag 时,该属性才起作用。

          在解析 DOM 时,会先进行命名空间的匹配,如果符合再使用 tag 属性所设置的 CSS 选择器进行匹配。

        • node(可选)属性:一个字符串(节点类型的名称),表示将匹配的 DOM 元素解析为哪一种节点
          注意

          在解析规则需要同时设置了属性 tag 时,该属性才起作用。

          而在一个解析规则中,属性 nodemarkignore 这三个仅且只能设置其中一个(如果该 ParseRule 解析规则是设置在符合 TypeScript interface NodeSpecMarkSpec 的对象中,也可以省略设置这三个属性,因为匹配的 DOM 元素要转换为哪一种 node 或 mark 可以由 ProseMirror 自动推断出来)

        • getAttrs(可选)属性:一个函数 fn(node: HTMLElement),它的返回值有多种类型,对应于有不同的作用。
          更精细地设置 attributes

          属性 getAttrs 是一个函数,用于构建更精细、更复杂的条件,可以基于 attributes 来匹配 DOM 元素,或者为转换而得的 node(或 mark)设置 attribute

          该函数的入参是 node 即当前所匹配的 DOM 元素

          该函数的返回值可以有多用类型:

          • 可以返回一个对象,表示转换生成的 node 或 mark 所拥有的 attribute
          • 可以返回 nullundefined,表示转换生成的 node 或 mark 没有 attribute
          • 可以返回 false,表示该 DOM 元素无法满足规则,即无法匹配
        • contentElement(可选)属性:可以采用多种类型的值,以指定在所匹配到的 DOM 元素里的哪个子元素作为内容(进入解释器)
          指定作为内容的子元素

          如果该解析规则将所匹配的 DOM 元素转换为非叶子 node 或 mark(即这个 node 或 mark 具有 content 内容/子节点),则默认情况下将所匹配的 DOM 元素的子元素/内容进行解析,作为 node 的 content

          但是如果希望将 DOM 元素的某个后代元素(而不是直接子元素)作为 content,则可以配置该属性

          该属性值可以采用多种类型的值:

          • 可以是一个字符串,表示 CSS 选择器,ProseMirror 会在寻找符合条件的子孙元素
          • 可以是一个函数 fn(dom.Node) 入参是所匹配的 DOM 元素。返回值也是一个 DOM 元素(子孙元素)
          • 可以直接指定一个 DOM 元素

          指定的 DOM 元素会传递给 DOM 解析器,转换生成的 nodes 或 marks 作为当前节点的 content

          忽略解析元素的不同方法对比

          属性 contentElement 可以手动设置当前所匹配的 DOM 元素的哪些后代元素进入解释器(解析的结果作为节点的 content),实现「跳过」解析某些后代元素

          属性 ignore 则适合忽略所匹配的整个 DOM 元素

          属性 skip 则适合忽略所匹配的 DOM 元素本身,而依然解析其后代元素

        • getContent(可选)属性:一个函数 fn(dom.Node, schema),返回值是一个 Fragment 对象(它表示一些节点的集合)作为转换生成的节点的 content
          指定节点的内容

          属性 contentElement 指定所匹配的 DOM 元素里的哪个子元素进入 DOM 解析器,再将转换生成的 nodes 或 marks 作为当前节点的 content

          而该属性则直接返回一些节点的集合(而不是 DOM 元素,不需要再调用 DOM 解析器),作为当前节点 content。

          该属性值是一个函数 fn(dom.Node, schema) 第一个参数是当前所匹配的 DOM 元素,第二个参数 schema 是表示数据结构的对象

          该函数的返回值是一个 fragment 对象,它表示一些节点的集合

          默认情况下会将所匹配的 DOM 元素的子元素传入到 DOM 解释器中,再将转换生成的 nodes 或 marks 作为当前节点的 content。但是如果设置了该属性,就会调用该属性所配置的函数,并以该函数的返回值作为节点的 content

          可以将该属性理解为为所匹配的 DOM 元素自定义子节点/内容的 DOM parser,而不使用 ProseMirror 的默认 DOM 解释器

        • preserveWhitespace(可选)属性:一个布尔值或字符串 full,以决定如何解析 DOM 元素中的空格
          设置解析空格的方式

          该属性值可以是以下三个值之一:

          • true 表示完全保留空格键,但是换行符 \n 等会替换为空格键
          • false 表示可以对空格键进行简化,即多个相连的空格键会「坍缩」为一个
          • "full" 表示完全保留空格键和换行符
      • toDebugString(可选)属性:一个函数 fn(node)(入参是该节点),返回值是一个字符串。以设置该节点序列化为字符串的默认行为,一般用于调试(例如在错误信息中用字符串来表示该节点)。
      • leafText(可选)属性:一个函数 fn(node)(入参是该节点),返回值是一个字符串。当该类型的节点是一个叶子节点时,设置该节点序列化为字符串的默认行为(会在方法 node.textBetween()方法 node.textContent() 里使用)
      • 其他属性,还可以为节点添加任意属性(属性的名称是字符串,属性值是任意类型)
        添加额外信息

        还可以为节点添加任意属性,以便为节点添加额外的信息。

        然后就可以通过 node.type.spec.xxx 来获取该信息

    • inlineContent 属性:一个布尔值,表示该类型的节点的 content 内容 expect 预期由 inline 行内类型的节点构成
    • isBlock 属性:一个布尔值,以表示该节点是否为 block 块级类型
      block 块级类型的节点

      在 ProseMirror 的内部判断节点是否为 block 块级类型的依据如下

      ts
      this.isBlock = !(spec.inline || name == "text")
      

      即在 schema 中没有为该节点设置 inline 属性,并且该节点的名称不是 text 文本节点

    • isInline 属性:一个布尔值,以表示该节点是否为 inline 行内类型
      inline 行内类型的节点

      节点可以分成两类,一种是 inline 行内类型,另一种是 block 块级类型

      所以在 ProseMirror 的内部是基于该节点是否为 block 块级类型,来判断它是否为 inline 行内类型(互斥关系)

      ts
      get isInline() { return !this.isBlock }
      
    • isText 属性:一个布尔值,以表示该类型的节点是否为 text 文本节点(根据节点的 name 名称是否为 text 来判断)
    • isTextblock 属性:一个布尔值,以表示该节点是否为 block 块级类型,且它的 content 内容由 inline 行内类型的节点构成(相当于 isBlockinlineContent 均为 true
    • isLeaf 属性:一个布尔值,以表示该类型的节点是否为叶子节点,即它不包含子节点(没有 content 属性)
    • isAtom 属性:一个布尔值,以表示该类型的节点是否作为一个整体(原子化)对待
      原子化

      节点作为一个整体对待,就是不能编辑该节点里面的内容,在编辑器的视图中只能整体加入或整体移除

    • contentMatch 属性:一个 contentMatch 对象,可以将它理解为该节点的 content expression 内容表达式(类似于正则表达式,以约束节点可以容纳哪些子节点)的具象化
      contentMatch 对象

      ContentMatch的实例(一个对象)描述了节点可以容纳哪些内容/子节点,可以将其理解为是 content expression 内容表达式(一个字符串,类似于正则表达式,以约束节点可以容纳哪些子节点)的具象化

      例如它具有一些方法可以判断其他类型的节点是否可以作为它的子节点

      该方法一般在 ProseMirror 核心模块里使用,开发者一般通过 schema 中通过 content expression 内容表达式进行配置

    • markSet 属性:可以是一个数组(它的每个元素都是 class MarkType 的实例对象),表示该节点允许添加哪些 mark 样式标记。也可以是 null 标识所有类型的 mark 都支持
    • whitespace 属性:可以是 "pre""normal",表示该类型的节点如何处理空格
      解析空格

      如果属性值是 "normal"(默认值)则会用空格替代换行符 \n 等类似的符号,而多个连续的空格会「压缩」为一个空格。

      如果属性值是 "pre" 则保留空格等格式符号(对于设置了 code 属性为 true 的节点,whitespace 属性默认会设置为 "pre" 值)

    • hasRequiredAttrs() 方法:返回一个布尔值,以表示该类型的节点是否具有必须指派的 attribute 属性(如果该方法返回值为 true 但是在创建该节点时没有设置默认值的 attribute,则会报错)
    • compatibleContent(otherNodeType) 方法:返回一个布尔值,以表示给定的节点类型 otherNodeType 其内容是否与当前节点类型的内容兼容
      如果给定的其他节点类型 otherNodeType 和当前节点类型的内容兼容,则 otherNodeType 类型的节点的内容/子节点可以插入到当前节点里
    • create(attrs?, content?, marks?) 方法:创建一个该类型的节点实例
      创建节点

      该方法的各参数的具体说明如下:

      • 第一个(可选)参数 attrs:这是一个对象,用于设置节点的 attributes。如果该类型的节点没有必须要指派的 attribute 属性,则可以传递 null 表示使用默认值来作为该节点的各种 attribute 的值。
      • 第二个(可选)参数 content:可以是一个 fragment 对象(它表示一些节点的集合),或是一个 node 节点对象,或是一个由一系列的节点对象构成的数组,也可以是 null。作为当前节点的子节点
      • 第三个(可选)参数 marks:可以是一个由一系列的 mark 样式标记对象构成的数组,为节点添加样式标记;也可以是 null,表示不使用样式标记。
    • createChecked(attrs?, content?, marks?) 方法:类似于 create() 方法,也是用于创建一个该类型的节点。不过该方法会先对入参 content 进行检查,看这些节点是否满足 schema 的约束,如果满足则创建并返回新建的节点;如果不满足就抛出一个错误。
    • createAndFill(attrs?, content?, marks?) 方法:类似于 create() 方法,也是用于创建一个该类型的节点。不过该方法会对入参 content 进行检查,看看是否需要在前面或后面增添一些节点,以满足 schema 的约束。如果可以创建一个满足条件的节点,则返回该节点;如果无法创建节点,则返回 null
      创建节点并自动填充内容

      通过该方法一般都可以创建节点,即使我们为参数 content 传入的是 nullFragment.empty 空的内容,因为 ProseMirror 会自动创建子节点进行补充,以便拼凑出满足 schema 约束的内容

    • validContent(fragment) 方法:返回一个布尔值,查看给定的 fragment 对象是否满足该类型的节点的 schema 约束,以判断 fragment 对象所包含的这些节点可否作为该节点的 content 内容(子节点)
    • allowMarkType(markType) 方法:返回一个布尔值,查看给定的 markType 样式标记类型是否为该类型的节点所允许
    • allowsMarks(marks) 方法:返回一个布尔值,查看给定的 marks 一系列样式标记(数组)是否为该类型的节点所允许
    • allowedMarks(marks) 方法:该方法的作用是从给定的 marks 一系列样式标记(数组)中清除掉不被该类型的节点所允许的样式标记,然后将允许的样式标记构成一个数组返回
  • attrs 属性:一个对象,表示该节点拥有的 attributes
  • marks 属性:一个数组(其元素是 mark 样式标记对象),表示应用到该节点的一系列样式标记
  • content 属性:一个 fragment 对象,表示该节点所包含的内容/子节点
    内容为空

    对于不允许有内容的节点的叶子节点,其实也具有属性 content,其值为一个空的 fragment 对象

    而空的 fragment 对象并非通过 class Fragment 实例化得到,而是该类的属性 Fragment.empty 所存储的,所以内容为空的节点都共享着同一个空的 fragment 对象,只用于表示该节点的内容为空

  • text(可选)属性:一个字符串,这是 text 文本节点才具有的属性,返回的字符串是该文本节点的内容
  • textContent 属性:获取将该节点内的所有文本内容,将它们合并为一个字符串返回
  • textBetween(from, to, blockSeparator, leafText) 方法:获取该节点内容的特定范围内的文本
    该方法各参数的具体说明如下:
    • 第一、二个参数 fromto 都是一个数字,遵循基于 index schema 规则,表示文档的位置,所获取的文本需要在节点内容 content 的这两个范围内
    • 第三个(可选)参数 blockSeparator 是一个字符串,作为块级节点的分隔符。如果抽取的文本来自两个块级节点,则文本之间会插入一个分隔符
    • 第四个(可选)参数 leafText 是一个字符串,或一个函数 fn(node)(入参是当前所需转换的叶子节点,该函数返回一个字符串或 null)。使用该参数可以将叶子节点(但不是 text 文本节点)转换为字符串
  • toString() 方法:返回一个字符串,以表示该节点,一般作为 debug 用
  • nodeSize 属性:一个数字,表示该节点的大小
    index schema 索引规则

    index schema 是指 ProseMirror 自定义的内容索引定位规则,通过数值来定位文档节点位置,这里的 nodeSize 节点大小就是采用这套系统。

    • 对于 text 文本节点,该数值是字符的数量
    • 对于叶子节点该数值是 1
    • 对于非叶子节点,该数值是 其内容(子节点)的 nodeSize 之和 + 2(因为进入和离开一个非叶子节点索引值都会增加 1
  • hasMarkup(type, attrs, marks) 方法:返回一个布尔值,判断该节点是否含有指定「外观」
    说明

    node 节点的 markup 「外观」其实就是指它的 nodeType 节点类型,该节点还可能需要具有特定的 mark 样式标记和 attribute 属性


    该方法的各参数的具体说明如下:
    • 第一个参数 type:一个 nodeType 对象,表示该节点需要是特定的节点类型
    • 第二个(可选)参数 attrs:是一个对象,描述该节点应该具有的 attributes
    • 第三个(可选)参数 marks:一个数值,它的元素是由一系列 mark 对象构成,表示该节点应该具有这些样式标记
  • rangeHasMark(from, to, type) 方法:返回一个布尔值,判断该节点的内容在给定范围(fromto 都是一个数字,遵循基于 index schema 规则,表示文档的位置)中的节点(包括后代节点)是否具有特定类型的样式标记(type 可以是一个 mark 实例或一个 markType 实例)。只要在该范围内的其中一个后代节点具有特定类型的样式标记就可以返回 true
  • isBlock 属性:一个布尔值,判断该节点是否为 block 块级类型
  • isInline 属性:一个布尔值,判断该节点是否为 inline 行内类型
节点的类型

节点可以分成两类,一种是 inline 行内类型,另一种是 block 块级类型

所以在 ProseMirror 的内部是基于该节点是否为 block 块级类型,来判断它是否为 inline 行内类型(互斥关系)

  • isTextblock 属性:一个布尔值,判断该节点是否为 block 块级类型,且它的 content 内容由 inline 行内类型的节点构成(相当于 isBlockinlineContent 均为 true
  • inlineContent 属性:一个布尔值,判断该类型的节点的 content 内容是否 expect 预期由 inline 行内类型的节点构成
  • isText 属性:一个布尔值,判断该类型的节点是否为 text 文本节点(根据节点的 name 名称是否为 text 来判断)
  • isLeaf 属性:一个布尔值,判断该类型的节点是否为叶子节点,即它不包含子节点(没有 content 属性)
  • isAtom 属性:一个布尔值,判断该类型的节点是否作为一个整体(原子化)对待
    原子化

    节点作为一个整体对待,就是不能编辑该节点里面的内容,在编辑器的视图中只能整体加入或整体移除

  • contentMatchAt(index) 方法:获取该节点的部分内容(索引值从 0index 的子节点)的 content match 匹配情况,返回的是一个 contentMatch 对象(可以将它理解为该节点的 content expression 内容表达式的具象化)
  • check() 方法:检查该节点及其内容(后代节点)是否符合 schema 数据结构的约束,如果不满足才会抛出错误

子节点相关

  • childCount 属性:一个数字,表示该节点拥有的(直接)子节点的数量
  • child(index) 方法:(在直接子节点所构成的数组中)根据索引值 index 获取子节点。如果入参 index 的位置超过(直接)子节点的数量范围,则抛出一个错误
  • maybeChild(index) 方法:也是(在直接子节点所构成的数组中)根据索引值 index 获取子节点,如果存在则返回该子节点,但是如果不存在并不会抛出错误
  • forEach(fn) 方法:遍历该节点的(直接)子节点,并分别执行给定的回调函数 fn
    回调函数 fn(node, offset, index) 会依次传递三个参数:
    • 当前所遍历的子节点 node
    • 该子节点相对于父节点(当前节点)的偏移大小 offset(一个数字,基于 index schema 计算而得,表示文档的位置)
    • 当前所遍历的子节点(在子节点数组中)的索引值 index
  • nodesBetween(from, to, fn, startPos) 方法:遍历在该节点内,触及到特定范围的子节点(包括它们的后代节点),并分别执行给定的回调函数 fn
    在遍历触及给定范围 fromto 的子节点(后代节点)时,其顺序是从外层到里层,依次调用 fn 函数
    该方法的具体参数说明如下:
    • 第一、二个参数 fromto 都是一个数字,遵循基于 index schema 规则,表示文档的位置,所遍历的节点需要在这两个范围内(不包括末尾 to
    • 第三个参数是回调函数 fn(node, pos, parent, index) 如果该函数返回 false 则当前所遍历的子节点的所有后代节点都不执行该回调函数,否则内嵌的后代节点递归继续执行该函数
      回调函数的参数说明

      回调函数依次传递四个参数:

      • 参数 node 是当前所遍历的后代节点
      • 参数 pos 是相对于祖先节点(当前节点)的位置(一个数字,基于 index schema 计算而得的数字)
      • 参数 parent 是当前所遍历的后代节点的父节点
      • 参数 index 是当前所遍历的节点在父节点中的索引值(节点数组中的索引值)
    • 第四个(可选)参数 startPos 用于设定起始位置的数值(用数字表示片段中的位置,其默认值是 0),该参数会影响传递给回调函数 fn 的第二个参数 pos
  • descendants(fn) 方法:遍历该节点的所有后代节点,分别执行回调函数 fn(node, pos, parent, index)(各参数的具体作用可以参考上一个方法的说明),同样地如果回调函数 fn 返回 false 则该子节点的所有后代节点都不执行该回调函数
  • firstChild 属性:获取第一个子节点。如果没有则返回 null
  • lastChild 属性:获取最后一个子节点。如果没有则返回 null
  • nodeAt(pos) 方法:根据给定的位置(一个数字,遵循 index schema 规则,表示节点的内容中的位置),在该节点的内容中查找该位置正好位于哪个后代节点里。返回一个后代节点,如果不存在则返回 null
    提示

    由于节点是树状结构,而遵循 index schema 的位置定位是扁平结构,所以根据 pos 位置反过来查找节点时,ProseMirror 会通过迭代在树结构中寻找的后代节点

    一般返回嵌套的叶子节点(根据 node.isText 进行判断),只有当 pos 位置正好是位于一个非叶子节点的末尾时(根据 offset == pos 进行判断,一般是在当前节点的内容的最后 ❓ ),才返回该非叶子节点

  • childAfter(pos) 方法:根据给定的位置(一个数字,遵循 index schema 规则,表示节点的内容中的位置),在该节点的内容中查找该位置之后是哪个(直接)子节点
    返回一个对象 {node: Node, index: number, offset: number} 该对象的属性 node 表示子节点(也可能是 null),属性 index 是(该子节点在直接子节点所构成的数组中)的索引值,属性 offset 是(该子节点开头相对于父节点内容/片段开头的)偏移量
  • childBefore(pos) 方法:根据给定的位置(一个数字,遵循 index schema 规则,表示节点的内容中的位置),在该节点的内容中查找该位置之前是哪个(直接)子节点
    返回一个对象 {node: Node, index: number, offset: number} 该对象的属性 node 表示子节点(也可能是 null),属性 index 是(该子节点在直接子节点所构成的数组中)的索引值,属性 offset 是(该子节点开头相对于父节点内容/片段开头的)偏移量

比较节点

  • eq(otherNode) 方法:返回一个布尔值,比较给定的节点 otherNode 和当前节点是否相同
  • sameMarkup(otherNode) 方法:比较两个节点是否具有相同的「外观」(包括 nodeType 节点类型、mark 样式标记和 attribute 属性)
    节点的外观

    在 ProseMirror 中节点的 markup 「外观」是指 nodeType 节点类型、attributes 属性,以及一系列 mark 样式标记

解析位置

  • resolve(pos) 方法:解析给定的位置 pos(一个数字,遵循 index schema 规则,表示节点的内容中的位置)的上下文情况,返回一个 resolvePos 对象

操作节点

  • copy(content?) 方法:拷贝节点(实际上是创建一个与当前的节点具有相同的「外观」的即具有相同的节点,即 nodeType 节点类型、attributes 属性,以及 mark 样式标记都相同)
    (可选)参数 content 是一个 fragment 对象,作为新建节点的内容,也可以是 null(默认值)则创建一个内容为空的节点(即新建的节点不包含子节点)
  • mark(marks) 方法:拷贝节点(实际上是创建一个与当前的节点具有相同的内容的节点,而且「外观」类似,即 nodeType 节点类型、attributes 属性),但是参数 marks(它是一个数组,其元素是一系列 mark 样式标记对象)来设置节点的样式标记
  • cut(from, to) 方法:拷贝节点(实际上是创建一个具有相同的「外观」的节点),而节点的内容 content 是从当前节点(从 fromto 范围)截取而得
    说明

    参数 fromto 都是一个数字,表示当前节点里的位置(遵循基于 index schema 规则)

    参数 to 是可选的,如果省略则采用默认值 this.content.size,即一直截取到当前节点的末尾

  • slice(from, to?, includeParents⁠?) 方法:对该节点的内容(从 fromto 范围)进行裁剪,生成一个 slice 对象
    第一、二个参数 fromto 都是一个数字,表示当前节点里的位置(遵循基于 index schema 规则)
    第二个(可选)参数 to 如果省略则采用默认值 this.content.size 即一直截取到当前节点内容的末尾
    第三个(可选)参数 includeParents⁠ 是一个布尔值,表示切片是否以当前节点作为父节点,这会影响切片两端开放节点的嵌套「深度」值(默认值为 false 即不以当前节点作为父节点,而是以切片两端的节点的最近的共同祖先作为父节点)
  • canReplace(from, to, replacementFragment⁠?, start⁠?, end⁠?) 方法:返回一个布尔值,以表示是否能用 replacementFragment⁠ 片段(或截取片段中索引值从 startend,但不包含 end,部分的节点)替换该节点特定范围的内容(参数 fromto 是数字,表示直接子节点的索引值,但不包含 to),即替换后的节点是否还能够保证节点满足 schema 数据结构的约束
  • canReplaceWith(from, to, nodeType, marks?) 方法:返回一个布尔值,以表示是否能够用特定类型的 nodeType 节点替换该节点特定范围的内容(参数 fromto 是数字,表示直接子节点的索引值),即替换后的节点是否还能够保证节点满足 schema 数据结构的约束。如果设置了第四个(可选)参数 marks(一个数组,包含一系列的样式标记对象)则还需要考虑给定的样式标记是否可以应用到该节点(内容)上
    提示

    该方法一般用于检查是否可以用给定类型 nodeType 替换选区(选中的范围),而实际执行替换操作需要调用事务对象的方法 tr.replaceSelectionWith()

    而通过事务对象的方法 tr.replaceSelectionWith() 对选中内容进行替换时,ProseMirror 可能会自动调整替换范围(例如当选区的 from 和 to 相同且都位于父级节点的起始或结尾位置,而给定的 node 并不适合此位置时,则可能将给定的 node 放置到合适的祖先节点里,以满足 schema 的约束)

    相应地,使用以上方法 canReplaceWith 进行可替换性检查时,不能单单判断选区所在的父节点是否适合,而需要遍历其祖先节点,考虑其中是否存在任何可插入给定类型节点的位置,以下是示例代码

    ts
    // refer to https://github.com/ProseMirror/prosemirror-example-setup/blob/master/src/menu.ts#L11-L18
    function canInsert(state: EditorState, nodeType: NodeType) {
      let $from = state.selection.$from; // 获取当前选区的 $from
      // 从 $from 所在的父节点开始向上遍历
      // 寻找父节点/祖先节点中,是否存在可以接受给定类型的节点 nodeType
      for (let d = $from.depth; d >= 0; d--) {
        let index = $from.index(d);
        if ($from.node(d).canReplaceWith(index, index, nodeType)) return true;
      }
      return false;
    }
    
  • replace(from, to, slice) 方法:用给定的切片 slice 替换节点的内容指定的(从 fromto)范围,返回替换后的节点(实际上是创建一个节点)
    注意

    参数 fromto 都是一个数字,遵循基于 index schema 规则,表示文档的位置,所替换的内容在这两个范围内

    由于切片 slice 两端的节点可能是开放的,所以将其插入到文档中时,需要保证这个切片与插入位置的前后节点是「相容」的(通过两端节点的开放深度来判断),否则会抛出 replaceError 错误。而且需要满足节点的 schema 数据结构的约束

  • canAppend(otherNode) 方法:返回一个布尔值,以表示将给定的节点 otherNode内容(子节点) append 追加到当前节点里是否合适,即增添内容以后是否还能够保证节点满足 schema 数据结构的约束,以避免不合适的节点 merge 融合
    如果给定的节点 otherNode 的内容为空(没有子节点),则考察两个节点所允许的子节点类型,只有至少存在一种节点类型可以同时作为两者的子节点时才返回 true
  • toJSON() 方法:将该节点序列化为一个 JSON 对象

Fragment 类

Fragment 类的实例化对象用于表示一些节点的集合(数组形式),它们一般作为某个目标节点的属性 content 的值,即用来描述目标节点包含哪些内容/子节点。

该类提供了一些静态方法和属性进行实例化:

  • static Fragment.fromJSON(schema, value) 静态方法:基于传入的 JSON 对象进行反序列化,获取一个 fragment 对象。第一个参数 schema 是数据约束对象,第二个(可选)参数 value 是 JSON 对象(一般是通过 fragment.toJSON() 生成的)
  • static Fragment.fromArray(array) 静态方法:基于传入的一组节点 array(其元素是一系列节点对象 node),创建一个 fragment 对象。
    相邻的文本节点可自动整合

    如果入参的数组 array 中相邻的元素是 text 文本节点,而且具有相同的样式标记,那么它们会被连接整合为一个 text 文本节点

  • static Fragment.from(nodes) 静态方法:这是一个更通用的方法,入参 nodes 可以多种类型(可以是一个 fragment 对象,也可以是一个 node 节点,也可以是一个由一系列 node 节点构成的数组),根据参数创建一个相应的 fragment 对象。如果入参是 null 则返回一个空的 fragment 对象
  • static Fragment.empty 静态属性:获取一个空的 fragment 对象,表示没有包含任何节点
    空片段

    不是一个方法,而是 class Fragment 类的一个属性,以避免创建一大堆空的片段,每次通过该属性访问的都是同一个 fragment 对象(它指向同一个内存地址),以便复用

class Fragment 实例化得到 fragment 对象,以下称作「片段」

后面的章节列出了 fragment 片段对象包含的一些属性和方法

注意

fragment 对象和 node 节点对象一样,也属于持久化的数据,不能直接修改,只能通过创建新的 fragment 对象来实现更新,所以以下关于修改片段的方法,实际上是通过创建并返回一个新的片段来实现的

片段信息

  • size 属性:一个数值,表示该片段的大小
    index schema 索引规则

    片段大小的计算方式采用的是 index schema 规则

    它是 ProseMirror 自定义的内容索引定位规则,通过数值来定位文档的位置,先从单个节点角度来理解,节点的属性 nodeSize(一个数值)表示其大小,遵循以下规则

    • 对于 text 文本节点,该数值是字符的数量
    • 对于叶子节点该数值是 1
    • 对于非叶子节点,该数值是 其内容(子节点)的 nodeSize 之和 + 2(因为进入和离开一个非叶子节点索引值都会增加 1

    而对于 fragment 片段大小 size 的值就等于它包含的所有节点的 nodeSize 之和

  • childCount 属性:获取片段中节点的数量(不包含嵌套的后代节点)
  • textBetween(from, to, blockSeparator, leafText) 方法:获取该片段的特定范围内的文本,返回一个字符串
    该方法各参数的具体说明如下:
    • 第一、二个参数 fromto 都是一个数字,遵循 index schema 规则,表示片段中的位置,所获取的文本需要在片段的这两个范围内
    • 第三个(可选)参数 blockSeparator 是一个字符串,作为块级节点的分隔符。如果抽取的文本来自两个块级节点,则文本之间会插入一个分隔符
    • 第四个(可选)参数 leafText 是一个字符串,或一个函数 fn(node)(入参是当前所需转换的叶子节点,该函数返回一个字符串或 null)。使用该参数可以将叶子节点(但不是 text 文本节点)转换为字符串

节点相关

  • firstChild 属性:获取当前片段中的第一个节点,如果为空则返回 null
  • lastChild 属性:获取当前片段中的最后一个节点,如果为空则返回 null
  • child(index) 方法:获取位于当前片段给定(在节点数组中)的索引值 index 的节点。⚠️ 如果给定索引值超出了片段的节点数量就会抛出错误
  • maybeChild(index) 方法:也是用于获取位于当前片段的给定位置(在节点数组中的索引值)index 的节点,如果存在则返回该节点,都是如果不存在不会抛出错误
  • findIndex(pos, round?) 方法:根据给定的位置 pos(一个数字,遵循 index schema 规则,表示片段中的位置)查找对应的节点,返回一个对象 {index: number, offset: number} 该对象的属性 index 表示该位置所落入的节点(在节点数组中)的索引值,属性 offset 表示该节点的开头相对于片段开头的偏移量
    参数 round 控制返回当前节点还是下一个节点

    假如位置 pos 正好位于某个节点的中间,那么通过第二个(可选)参数 round(它是一个数字)的正负值来控制返回哪一个节点(返回所在的节点还是下一个节点)

    该参数的作用就像是四舍五入的「修约」行为,所以该参数取名为 round

    参数 round默认值-1 表示「向下修约」,即返回的是所在的节点(对应的索引值),以及该节点的开头位置相对于片段开头的偏移量。

    如果 round 的值大于 0 表示「向上修约」,即返回下一个节点(对应的索引值),以及当前节点的末尾相对于片段开头的偏移量

    但是如果位置 pos 刚好位于节点的末尾,无论 round 正负值如何,都是返回下一个节点(对应的索引值),以及当前节点的末尾相对于片段开头的偏移量

  • replaceChild(index, node) 方法:用给定的节点 node 替换当前片段指定位置(在节点数组中的索引值) index 的节点(最后返回的是一个新的片段)
  • addToStart(node) 方法:将给定的节点的 node 添加 prepend 到当前片段的开头(最后返回的是一个新的片段)
  • addToEnd(node) 方法:将给定的节点的 node 添加 append 到当前片段结尾(最后返回的是一个新的片段)
  • forEach(fn) 方法:遍历片段中的节点(并不包括嵌套的后代节点),并分别执行给定的回调函数 fn
    回调函数 fn(node, offset, index) 会依次传递三个参数:
    • 当前所遍历的节点 node
    • 该节点相对于片段开头位置的偏移大小 offset(一个数字,基于 index schema 计算而得,表示文档的位置)
    • 当前所遍历的子节点(在节点数组中)的索引值 index
  • descendants(fn) 方法:遍历该片段中的节点(及其后代节点),分别执行回调函数 fn(node, pos, parent, index)(各参数的具体作用可以参考上一个方法的说明),同样地如果回调函数 fn 返回 false 则该节点的所有后代节点都不执行该回调函数
  • nodesBetween(from, to, fn, nodeStart, parent) 方法:遍历该片段的特定范围内的节点(包括它们的后代节点),并分别执行给定的回调函数 fn
    • 第一、二个参数 fromto 都是一个数字,遵循基于 index schema 规则,表示文档的位置,所遍历的节点需要在这两个范围内(不包括末尾 to
    • 第三个参数是回调函数 fn(node, start, parent, index) 如果该函数返回 false 则当前所遍历的节点的所有后代节点都不执行该回调函数,否则内嵌的后代节点递归继续执行该函数
      回调函数的参数说明

      回调函数依次传递四个参数:

      • 参数 node 是当前所遍历的节点
      • 参数 start 是该节点相对于片段的的位置(一个数字,基于 index schema 计算而得的数字,表示文档的位置)
      • 参数 parent 是当前所遍历的节点的父节点
      • 参数 index 是当前所遍历的节点在父节点中的索引值(节点数组中的索引值)
    • 第四个(可选)参数 nodeStart 设定起始位置的数值(用数字表示片段中的位置,其默认值是 0),该参数会影响传递给回调函数 fn 的第二个参数 start
    • 第五个(可选)参数 parent 设置整个 fragment 片段的父节点

操作片段

  • append(otherFragment) 方法:将给定的其他片段 otherFragment 添加到后面,与当前片段整合为一个片段(最后返回的是一个新的片段)
  • cut(from, to) 方法:根据给定的范围(从 fromto)裁剪当前片段(最后返回的是一个新的片段)。如果不指定第二个(可选)参数 to,则范围的默认结束点是一直到当前片段的末尾

比较片段

  • eq(otherFragment) 方法:返回一个布尔值,以表示给定的片段 otherFragment 与当前的片段是否相等
  • findDiffStart(otherFragment, pos?) 方法:从左往右/前往后查找当前片段与给定片段 otherFragment 之间的差异的起始位置,返回一个数字(遵循 index schema 规则),表示差异起始点在文档的位置。如果两个片段完全相同则返回 null
    第二个(可选)参数用于设置片段开头位置的起始值(默认值为 0
  • findDiffEnd(otherFragment) 方法:从右往左/后往前查找当前片段与给定片段 otherFragment 之间的差异的结束位置,返回一个对象 {a: number, b: number}(该对象的属性值都是一个数字,遵循 index schema 规则),分别表示差异的结束点在当前片段的位置 a,以及在给定片段的位置 b。如果两个片段完全相同则返回 null
注意

为了提高查找的效率,寻找差异时起始位置 start 是从左往右查找,而结束位置 end 是从右往左查找

但是在特定的场景下,可能存在倒置的情况,即 startend 更大

例如需要对比差异的两个 fragment 片段是 frag1=abcfrag2=abbc,(假设a 前面的位置是 0)则从左往右寻找差异的起始位置 start=2(表示在 ab 后面的位置),从右往左寻找差异的结束位置 end1=1(表示在第一个片段 a 后面的位置),end2=2(表示在第二个片段 ab 后面的位置),其中 startend1 还有大

实际上结束位置应该是 end1=2(在第一个片段 ab 后面的位置),end2=3(在第二个片段 abb 后面的位置)

如果存在倒置(重叠)的情况则可以通过以下代码进行校正

js
// refer to https://github.com/ProseMirror/website/blob/master/example/footnote/index.js#L129-L133
// fragment.findDiffStart(otherFragment) 从左往右找出两个片段之间的差异的起始位置(遵循 index schema 规则)
let start = node.content.findDiffStart(state.doc.content);

// 如果两个片段存在差异,则继续执行后续操作
if (start != null) {
  // fragment.findDiffEnd(otherFragment) 从左往右找出两个片段之间的差异的结束位置(遵循 index schema 规则)
  const endResult = node.content.findDiffEnd(state.doc.content)
  if (endResult) {
    let {a: endA, b: endB} = endResult
    // 为了提高查找的效率,寻找差异时起始位置是从左往右查找,而结束位置是从右往左查找
    // 但是在特定的场景下,可能存在倒置的情况,即 start 比 end 更大,则需要进行校正
    // 具体可以参考 https://discuss.prosemirror.net/t/overlap-handling-of-finddiffstart-and-finddiffend/2856/4
    let overlap = start - Math.min(endA, endB)
    if (overlap > 0) { endA += overlap; endB += overlap }
  }
}

序列化

  • toString() 方法:返回一个描述片段的字符串,一般用于调试
  • toJSON() 方法:将该片段序列化为一个 JSON 对象,它包含片段的详细信息

Slice 类

Slice 类的实例化对象用于表示文档中的一个片段,它与 fragment 对象类似,区别在于它所包含的前后两端的节点可能是不完整的(只包含部分内容)

class Slice 实例化得到 slice 对象,以下称作「切片」,它使用一个 fragment 对象和两个数字openStartopenEnd 来表示

开放深度

可以将 slice 片段理解为 fragment 片段的「进阶」版本,除了可以描述完整的片段对象,还可以描述不完整的片段。它使用两个数字来表示片段两端的开放深度 Open depth ,即片段两端的不完整节点(相对于片段)的深度(不完整的节点位于该 fragment 片段中的嵌套的最深的值)

假如 ProseMirror 文档所在页面渲染出的 HTML 结构如下,由一个标题节点内嵌有一个段落节点构成,内容为 abc

html-structure
html-structure

如果按照以下示意图对文档进行裁剪

slice-demo-1
slice-demo-1

则可以得到 <p>ab,则该切片(所操作)的目标片段 fragment 是 heading 标题节点的内容,由于在切片的开头包含完整的段落节点,所以 openStart=0 并无开放节点;而在切片的末端段落节点并不完整/存在开放节点,(段落节点在该 fragment 片段中是第一层节点)所以 openEnd=1

如果按照以下示意图对文档进行裁剪

slice-demo-2
slice-demo-2

则可以得到 <h1><p>ab,则该切片(所操作)的目标片段 fragment 是整个文档的内容,由于在切片的开头包含完整的节点,所以 openStart=0 并无开放节点;而在切片的末端段落节点并不完整/存在开放节点,(段落节点在该 fragment 片段中是嵌套在第二层的节点)所以 openEnd=2

如果按照以下示意图对文档进行裁剪

slice-demo-3
slice-demo-3

则可以得到 <p>ab</p>,则该切片(所操作)的目标片段 fragment 是 heading 标题节点的内容,由于在切片的开头和结尾都包含完整的节点(段落节点),所以两个数值为 openStart=0openEnd=0,表示两端并无开放节点

提示

还可以通过方法 selection.content() 基于选区构建一个 slice

一般以编程方式 programmatic 操作文档都是生成片段(具有完整的节点),而用户通过视图 view 操作文档则一般生成切片(具有不完整的节点),例如框选文档内容(选中节点的部分)进行复制粘贴时

注意

由于切片的两端的节点可能并不完整,所以需要进行必要的处理,例如补全节点令其满足 schema 数据结构的约束,才可以插入到编辑器中

通过 new Slice(content, openStart, openEnd) 实例化得到一个 slice 切片对象

相关参数的具体说明如下:

  • 第一个参数 content 是一个 fragment 对象,可以将切片理解为对哪一个节点的内容 content(即 fragment 对象)进行操作/裁剪,也可以理解为通过自动补全开始或结束的标签来构建出完整的 fragment 对象
  • 第二个参数 openStart 是切片左侧开放节点(相对于片段)的嵌套深度
  • 第三个参数 openEnd 是切片右侧开放节点(相对于片段)的嵌套深度
注意

如果设置了非零的开放深度,则需要确保片段里有相应嵌套深度的节点,例如当 fragment 是一个空片段时,则 openStartopenEnd 都只能是 0

此外 class Slice 还提供了一些静态方法和属性可以进行实例化:

  • static Slice.fromJSON(schema, json) 静态方法:基于传入的 JSON 对象进行反序列化,获取一个 slice 对象。第一个参数 schema 是数据约束对象,第二个(可选)参数 json 是 JSON 对象(一般是通过 slice.toJSON() 生成的)
  • static Slice.maxOpen(fragment, openIsolating?) 静态方法:基于给定的 fragment 片段创建一个 slice 对象,而且让两端开放节点的「深度」达到最大。第二个(可选)参数 openIsolating 是一个布尔值,用以表示是否让 isolating 独立隔离的节点也可以开放(默认为 true,即可以对该节点进行部分裁剪)
    最大开放深度

    该方法可以理解为从 fragment 片段的第一个节点和最后一个节点里面嵌套最深的后代节点截取内容

  • static Slice.empty 静态属性:获取一个空的 slice 切片对象,表示没有包含任何节点
    空切片

    不是一个方法,而是 class Slice 类的一个属性,以避免创建一大堆空的切片,每次通过该属性访问的都是同一个 slice 对象(它指向同一个内存地址),以便复用

还可以使用节点的方法 node.slice() 对该节点的内容进行裁剪,生成一个 slice 对象

slice 切片对象包含一些属性和方法:

  • content 属性:一个 fragment 对象,该切片是基于哪个片段裁剪而得的
    说明

    基于 slice 构建的 fragment 是自包含的,即 fragment 的内容和 slice 的内容相同,只是 slice 可能表示该 fragment 的左(和/或)右的标签不完整

    slice-fragment
    slice-fragment

    例如上图是基于选区构建的 slice,然后再通过方法 slice.content 获取对应的 fragment 并将其打印出来,可以看到该 fragment 包含两个 paragraph 节点,但是其内容都分别只有一个字母(因为每个段落都只选中的一个字母),这和文档里的完整段落节点不同

  • openStart 属性:一个数字,表示该片段开始/左侧的开放节点(相对于片段)的嵌套深度
  • openEnd 属性:一个数字,表示该片段结束/右侧的开放节点(相对于片段)的嵌套深度
  • size 属性:一个数字,表示切片的大小
    切片大小

    切片大小是指如果将该它插入文档后,文档增加多少个 token 值

    该值遵循 index schema 规则

    实际上是通过 fragment 片段大小和开放深度计算出来的 this.content.size - this.openStart - this.openEnd

  • eq(otherSlice) 方法:返回一个布尔值,以表示给定的切片 otherSlice 与当前切片是否相同
  • toJSON() 方法:将该切片序列化为一个 JSON 对象,它包含切片的详细信息

ResolvedPos 类

文档的每个位置可以用一个数字(整数)来表示,这样就可以将文档描述为扁平化的线性结构。该数字是遵循 ProseMirror 自定义的 index schema 规则计算出来的,这样在 ProseMirror 核心模块的代码中可以通过简单的一个数字就对文档进行精确定位。

但是数字所包含的信息太有限了,所以 ProseMirror 提供了 ResolvedPos 类,它的实例化对象表示对一个特定的 position 位置的解析结果,包含更多的描述性信息,例如该 position 所在的节点(以及祖先节点)等,以及提供了一些实用的方法

class ResolvedPos 实例化得到 resolvedPos 对象,以下称为「位置解析结果」

可以通过调用节点的方法 node.resolve(pos) 对给定的位置进行解析,得到一个 resolvedPos 对象

参数 pos 是一个数字,遵循 index schema 规则,表示文档的位置,即相对于节点 node 片段/内容的开头位置的偏移量

根节点

一般使用 state.doc.resolve(pos) 基于文档的根节点 doc 得到 resolvedPos 对象

如果使用其他节点调用方法 node.resolve(pos) 获得的得到 resolvedPos 对象,则位置解析结果很多属性都是相对于该节点而言的(而不是相对文档的根节点),即该节点作为解析所得的 resolvedPos 的根节点

也可以直接解构选区对象,例如 let {$from, $to} = state.selection 直接得到 resolvedPos 对象

resolvedPos 位置解析结果包含一些属性和方法:

  • doc 属性:该位置的根节点
    根节点

    对于使用节点的方法 node.resolve(pos) 获取的位置解析结果对象,则该属性是指节点 node

    对于通过 state.doc.resolve(pos) 获取的位置解析结果对象,则该属性就是指文档的根节点 doc

    如果通过解构选区(例如 selection.$from)获取的位置解析结果对象,则该属性也是指文档的根节点 doc

  • parent 属性:该位置的父节点
    文本节点

    需要注意如果所解析的位置在文本内容里,但是文本节点并不能作为父节点

    因为文本节点在 ProseMirror 模型中是扁平的,它们是没有 content 属性,它不能视作一个具有深度的层级

    所以当所解析的位置在文本内容中,则它的父节点是指包含该文本节点的节点(例如段落节点)

  • depth 属性:一个数字,表示该位置的父节点相对于「根节点」的深度
    文本节点

    需要注意如果所解析的位置在文本内容里,但是文本节点并不能作为父节点来考虑(因为文本节点在 ProseMirror 模型中是扁平的,它们是没有 content 属性,它不能视作一个具有深度的层级)

    所以当所解析的位置在文本内容中,则深度信息所属的节点是包含该文本内容的节点(例如段落节点)


    该属性相当于把扁平的线性位置 pos 信息转换为树形的层级 depth 信息
    例如在顶级段落节点的文本内容上的位置,基于文档根节点 doc 进行解析,则该位置的属性 depth1(表示该段落节点相对于根节点 doc 的层级为 1
    resolvedPos depth for text
    resolvedPos depth for text

    resolvedPos depth for node
    resolvedPos depth for node
  • pos 属性:一个数字,表示所解析的位置相对于「根节点」的偏移量
    提示

    如果该位置解析结果是通过方法 node.resolve(pos) 获得的,则该属性就和参数 pos 一致

    它们都是表示解析位置相对于节点 node 片段/内容的开头位置的偏移量,遵循 index schema 规则

    开头位置

    对于以 HTML 展示文档数据的情况而言,节点的开头位置就是起始标签前的位置

    而节点的片段/内容的开头位置即起始标签后的位置(根据 index schema 规则,标签的大小是 1 个 token)

  • parentOffset 属性:一个数值,表示该位置相对于父节点的偏移量
    偏移量

    相对于直接父节点偏移量是指解析位置相对于其所在的父节点节点的片段/内容的开头的偏移量,遵循 index schema 规则(根据该规则,如果所解析的位置在文本内容里,则按照字符数量计算偏移量)

    开头位置

    对于以 HTML 展示文档数据的情况而言,节点的开头位置就是起始标签前的位置

    而节点的片段/内容的开头位置即起始标签后的位置(根据 index schema 规则,标签的大小是 1 个 token)

  • textOffset 属性:一个数值,如果所解析的位置在文本内容中,就表示该位置距离该文本节点开头的偏移量(字符数量)
    节点交界处

    如果所解析的位置在两个节点的交界处(即使前后两个都是文本节点),则会返回 0

    text offset between node
    text offset between node

    也就是说在文本节点的末尾,该属性值为 0(因为这种情况也算是节点的交界处)

    text offset at the node end
    text offset at the node end

  • node(depth?) 方法:获取指定层级深度 depth 的祖先节点
    层级深度

    在 ProseMirror 中节点通过嵌套形成文档树,所以节点之间存在层级关系

    该方法的参数 depth 就是表示相对于「根节点」而言的内嵌层级

    根节点

    对于使用节点的方法 node.resolve(pos) 获取的位置解析结果对象,则「根节点」是该节点 node

    对于通过 state.doc.resolve(pos) 或通过解构选区(例如 selection.$from)获取的位置解析结果对象,则「根节点」就是指文档的根节点 doc


    例如 depth=0 就表示根节点
    root node
    root node

    参数 depth 是可选的
    • 如果省略(即传入 undefined)或传入 null,则在内部会采用默认值 this.depth(即该位置解析结果的父节点相对于「根节点」的深度),那么该方法获取到的就是父节点(与属性 resolvedPos.parent 的值一样)
      with default depth
      with default depth
    • 如果传入的是负数,则内部会采用 this.depth+depth(即该位置解析结果的父节点相对于「根节点」的深度值与参数 depth 相加),相当于基于当前位置的深度信息往上回溯 depth 层,获取内嵌层级更小的祖先节点
      with negative value
      with negative value
    • 如果传入的值超过了该位置(父节点)所在的深度,则返回 undefined(对于参数为负值的情况也类似)
  • index(depth?) 方法:以层级深度 depth 的祖先节点作为容器,它包含一系列子节点,则该方法返回的值表示该位置所属的那个子节点(在同级节点所构成的数组中)的索引值
    索引值

    索引值计数是从 0 开始的


    例如 depth=0 就表示以根节点为容器,它所包含的一系列子节点中,该位置所属的子节点的索引值
    参数 depth 是可选的
    • 如果省略(即传入 undefined)或传入 null,则在内部会采用默认值 this.depth(即该位置解析结果的父节点相对于「根节点」的深度),那么该方法以父节点作为容器
      with default depth
      with default depth
    • 如果传入的是负数,则内部会采用 this.depth+depth(即该位置解析结果的父节点相对于「根节点」的深度值与参数 depth 相加),相当于基于当前位置的深度值获取内嵌层级更小的祖先节点作为容器
    • 如果传入的值超过了该位置(父节点)所在的深度,则返回 undefined(对于参数为负值的情况也类似)
    节点交界处

    如果所解析的位置在两个节点的交界处,则所返回索引值是用于表示后一个节点

    index between text node
    index between text node

    也就是说在文本节点的末尾,该方法的返回值依然表示下一个文本节点(假设后方存在一个虚拟的文本节点)

    text offset at the end of text node
    text offset at the end of text node

    需要注意 depth 的值决定了所选择的容器是哪一个,即使光标的位置在文本节点的尾部,但是对于段落节点而言所解析的位置依然在其内部

    index inside text node for parent
    index inside text node for parent

  • indexAfter(depth⁠?) 方法:它和方法 index() 类似,都是以层级深度 depth 的祖先节点作为容器,它包含一系列子节点,则该方法返回的值表示该位置所属的那个子节点的后一个相邻节点(在同级节点所构成的数组中)的索引值
    节点交界处

    如果所解析的位置在两个节点的交界处,则方法 resolvedPos.index() 所返回索引值就是用于表示后一个节点,所以它和方法 resolvedPos.indexAfter() 返回的值一样

    indexAfter between text node
    indexAfter between text node


    参数 depth 是可选的,更多介绍参照前一个方法 index()
  • start(depth?) 方法:获取指定层级深度 depth 的祖先节点的内容的开头相对于「根节点」的偏移量
    节点内容的开头位置

    偏移量遵循根据 index schema 规则

    对于以 HTML 展示文档数据的情况而言,节点的片段/内容的开头位置即起始标签的位置,根据 index schema 规则,标签的大小是 1 个 token


    参数 depth 是可选的
    • 如果省略(即传入 undefined)或传入 null,则在内部会采用默认值 this.depth (即该位置解析结果的父节点相对于「根节点」的深度),那么该方法获取到就是父节点的内容的开头位置相对于根节点的偏移量
    • 如果传入的是负数,则内部会采用 this.depth+depth(即该位置解析结果的父节点相对于「根节点」的深度值与参数 depth 相加),相当于基于当前位置的深度值信息往上回溯 depth 层,获取内嵌层级更小的祖先节点的内容的开头的偏移量
  • end(depth?) 方法:它和方法 start() 类似,获取指定层级深度 depth 的祖先节点的内容的结尾相对于「根节点」的偏移量
    节点内容的结尾位置

    偏移量遵循根据 index schema 规则

    对于以 HTML 展示文档数据的情况而言,节点的片段/内容的开头位置即结束标签的位置


    参数 depth 是可选的,更多介绍参照前一个方法 start()
    resolvedPos start and end
    resolvedPos start and end
  • before(depth⁠?) 方法:获取指定层级深度 depth 的祖先节点的开头相对于「根节点」的偏移量
    节点的开头位置

    偏移量遵循根据 index schema 规则

    对于以 HTML 展示文档数据的情况而言,节点的开头位置即起始标签的位置


    参数 depth 是可选的
    • 如果省略(即传入 undefined)或传入 null,则在内部会采用默认值 this.depth (即该位置解析结果的父节点相对于「根节点」的深度),那么该方法获取到就是父节点的开头位置相对于根节点的偏移量
    • 如果传入的是负数,则内部会采用 this.depth+depth(即该位置解析结果的父节点相对于「根节点」的深度值与参数 depth 相加),相当于基于当前位置的深度值信息往上回溯 depth 层,获取内嵌层级更小的祖先节点的开头的偏移量
    特殊的深度值

    如果参数 depth 为当前解析位置的深度值加上一(即 this.depth+1),则该方法返回的是当前位置相对于「根节点」的偏移量,和属性 resolvedPos.pos 值一样

  • after(depth?) 方法:获取指定层级深度 depth 的祖先节点的结尾相对于「根节点」的偏移量
    节点的结尾位置

    偏移量遵循根据 index schema 规则

    对于以 HTML 展示文档数据的情况而言,节点的结尾位置即结束标签的位置


    参数 depth 是可选的,更多介绍参照前一个方法 before()
    特殊的深度值

    如果参数 depth 为当前解析位置的深度值加上一(即 this.depth+1),则该方法返回的是当前位置相对于「根节点」的偏移量,和属性 resolvedPos.pos 值一样


    resolvedPos before and end
    resolvedPos before and end
  • nodeAfter 属性:所解析位置的后面的节点。如果后面没有节点,则返回 null。如果所解析的位置在文本节点里,则返回该位置之后的文本内容
  • nodeBefore 属性:所解析位置的前面的节点。如果前面没有节点,则返回 null。如果所解析的位置在文本节点里,则返回该位置之前的文本内容
  • posAtIndex(index, depth?) 方法:以层级深度 depth 的祖先节点作为容器,它包含一系列子节点,该方法获取索引值为 index 的子节点的开头位置(相对于「根节点」)的偏移量
    偏移量

    偏移量遵循 index schema 规则

    开头位置

    对于文本节点,则是第一个字符前面的位置

    而对于其他节点,如果以 HTML 展示文档数据的情况而言,节点的开头位置就是起始标签前的位置


    参数 depth 是可选的
    • 如果省略(即传入 undefined)或传入 null,则在内部会采用默认值 this.depth(即该位置解析结果的父节点相对于「根节点」的深度),则以该位置的父节点作为容器,获取指定索引值 index 的子节点的开头的偏移量
      posAtIndex by default
      posAtIndex by default
    • 如果传入的是负数,则内部会采用 this.depth+depth(即该位置解析结果所在的节点相对于「根节点」的深度值与参数 depth 相加),相当于基于当前位置的深度值获取内嵌层级更小的祖先节点作为容器
  • marks() 方法:获取该位置所具有的样式标记,返回一个数组(其中元素是一系列的 mark 对象)
    如果所解析的位置是在一个内容非空的节点的开始位置,则返回该节点所具有的样式标记
    如果所解析的位置在一个节点的末尾,则会根据样式标记的配置属性 inclusive 来判断是否将其包含到所返回的数组中
  • marksAcross($end) 方法:获取当前位置之后应该使用的样式标记,返回一个数组(其中元素是一系列的 mark 对象)
    这个方法与上一个方法 marks() 类似,也是返回样式标记,但适用场景不同,该方法是用于获取执行完删除操作后(光标所在)位置的样式标记情况
    提示

    通过鼠标点击或键盘方向键移动光标,所对应位置的样式标记(更准确而言应该是如果继续键入内容,所需应用的样式标记)是由该位置前方内容所具有的样式标记决定的

    而执行完删除操作后,光标所对应位置的样式标记是由删除内容所具有的样式标记决定,以确保文本格式的连续性,让用户体验更符合直觉

    关于该方法所针对场景的具体介绍可以参考这个 Issue


    该方法会排除配置属性 inclusivefalse 的样式标记,以及没有出现在 $end 位置(一般是选区的 upper boundary 上界 $to)样式标记
    如果该位置是在它的父级节点的结束的位置,或者它的父级节点不是一个 textblock,则会返回 null(不会有任何样式标记被保留)
  • sharedDepth(anotherPos) 方法:返回的是一个数值,表示当前位置与另一个给定位置 anotherPos(一个数值,表示文档中的位置)所具有的最近的共同祖先节点的层级深度
  • blockRange(anotherResolvedPos⁠?, pred⁠?) 方法:基于当前位置和给定位置创建一个 nodeRange 节点范围对象,它包含这里两个位置;如果无法创建,则返回 null
    注意

    正如名称 blockRange 通过该方法创建的 nodeRange 所包含节点是 block 块级节点

    如果需要创建一个可以包含 inline 内联节点的 nodeRange,可以通过实例化 class NodeRange 来实现

    有一个与之相关的帖子

    NodeRange 节点范围对象

    上面所说的 nodeRange 对象(以下称为节点内容范围)是 class NodeRange 的实例化,用于表示一系列同级的相邻节点

    除了使用 resolvedPos.blockRange() 方法构建一个节点范围对象,还可以通过方法 new NodeRange($from, $to, depth) 进行实例化

    各参数的具体说明如下:

    • 参数 $from$to 都是 ResolvedPos 位置解析结果对象(它们一般基于同一个根节点获得),分别表示该节点内容范围的开始和结束位置
    • 参数 depth 是一个数值,表示该节点范围对象所指向的节点(相对于根节点)的层级深度
    提示

    官方文档中的一段描述

    $from and $to should point into the same node until at least the given depth, since a node range denotes an adjacent set of nodes in a single parent node.

    其意思是分别从 $from$to 的「根节点」(两者一般基于同一个根节点获得)向下 depth 层寻找该范围所指向的节点的过程中,需要确保至少从 0depth 每一个层级的节点都是包含这两个位置的,这样才可以保证通过 $from$to 所定义的范围是指向一个节点

    例如文档结构如下

    html
    <ul>
      <li>
        <p>abc</p>
      </li>
      <li>
        <p>123</p>
        <p>456</p>
      </li>
    </ul>
    

    如果 $from1 后面位置, $to4 后面位置,在构造一个 NodeRange 的时候,参数 depth 最大只能是第二个 li 节点所对应的深度(或者更浅),因为如果再深的话 $from$to 就没有共同的父级节点,就无法构建一个 NodeRange

    💡 而 $from$to 两个位置本身的深度可能不同,且一般都大于 NodeRange 构建函数的参数 depth,这样才能保证可以从「根节点」向下 depth 层(有足够的「下探」深度)能寻找到共享的节点

    nodeRange 对象包含一些属性:

    • $from$to 属性:都是 ResolvedPos 位置解析结果对象,表示该范围的开始和结束位置
    • parent 属性:该范围所指向的节点,即以该节点作为容器,包含 $from$to 两个位置
    • depth 属性:一个数值,表示该范围所指向的节点(相对于根节点)的层级深度
    • start 属性:以该范围所指向的节点作为容器,它包含一系列的子节点,则该属性表示开头位置 $from 所在子节点的开头(相对于根节点)的偏移量
    • end 属性:以该范围所指向的节点作为容器,它包含一系列的子节点,则该属性表示结尾位置 $to 所在子节点的结尾(相对于根节点)的偏移量
      节点的开头与结尾

      偏移量遵循根据 index schema 规则

      对于以 HTML 展示文档数据的情况而言,节点的开头位置即起始标签的位置;节点的结尾位置即结束标签的位置

      而对于文本节点,则节点的开头位置就是第一个字符前面的位置;结尾位置就是最后一个字符后面的位置


      nodeRange start and end
      nodeRange start and end
    • startIndex 属性:它是属性 start 所指的节点(在同级节点构成的数组中)的索引值
    • endIndex 属性:它是属性 end 所指的节点(在同级节点构成的数组中)的索引值( ⚠️ 注意所指的节点是 $end 位置后面的那个子节点)
      注意

      以范围所指向的节点作为容器,它包含一系列的子节点,则属性 startIndex 索引值所指的子节点,它一般就是包含 $from 位置的那个子节点

      但是属性 endIndex 所指的子节点,一般并不是包含 $end 位置的那个子节点

      这是由于属性 end 表示子节点的结尾的偏移量,对于以 HTML 展示文档数据的情况而言,节点的结尾位置即结束标签的位置,该位置已经属于后一个兄弟节点

      所以属性 endIndex 所指的子节点,一般是指 $end 位置后面的那个子节点

      nodeRange startIndex and endIndex
      nodeRange startIndex and endIndex


    第二个(可选)参数 pred 是你可以传递一个指示函数,来决定该祖先节点是否可接受(如果较近的祖先节点不可接受,则继续沿着文档树向上回溯,返回所找到的最近的符合条件的祖先节点,直至文档的根节点为止)
    提示

    如果可以通过调用位置解析结果的方法 resolvedPos.blockRange(otherResolvedPos) 生成该对象,只需要提供两个 ResolvedPos 对象即可,不需要指定共享的节点(相对于根节点)的深度

    ProseMirror 会自动沿着文档树向上回溯,寻找最靠近这两个位置的共享祖先节点

    该方法的源码如下

    ts
    blockRange(other: ResolvedPos = this, pred?: (node: Node) => boolean): NodeRange | null {
      if (other.pos < this.pos) return other.blockRange(this)
      for (let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0); d >= 0; d--)
        if (other.pos <= this.end(d) && (!pred || pred(this.node(d))))
          return new NodeRange(this, other, d)
      return null
    }
    

    可知该方法选择位于前方的位置(即偏移量较小的 ResolvedPos 对象)的深度作为基础,往上回溯寻找共享的祖先节点

    在初始化循环参数时,深度值 let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0) 会基于前方位置的父节点的内容类型是否为 inlineContent 来决定是否从父节点还是祖父节点开始迭代

    当父节点所包含的子节点为 inline 内联类型,则跳过父节点(即使它可能就是最近两个位置的共享节点),从祖父节点开始寻找两个位置共享的节点

    blockRange
    blockRange

  • sameParent(anotherResolvedPos⁠) 方法:返回一个布尔值,以表示当前位置与给定的其他位置 anotherResolvedPos⁠(它也是位置解析结果对象)是否位于相同的父节点
    注意

    根据该方法的源码

    ts
    sameParent(other: ResolvedPos): boolean {
      return this.pos - this.parentOffset == other.pos - other.parentOffset
    }
    

    判断的依据是 this.pos-this.parentOffset 差值是否相等

    所以隐含的前提是这两个位置解析结果对象的根节点需要是相同的

    一般使用 state.doc.resolve(pos) 基于文档的根节点 doc 得到 resolvedPos 对象;或直接解构选区对象,例如 let {$from, $to} = state.selection 直接得到 resolvedPos 对象,其根节点也是文档的根节点 doc,所以一般都满足上述隐含条件

  • max(anotherResolvedPos⁠) 方法:比较当前位置与给定的另一个位置 anotherResolvedPos⁠ 的(相对于根节点)偏移量,返回较大的那一个 ResolvedPos 对象
    注意

    隐含的前提是这两个位置解析结果对象的根节点需要是相同的

  • min(anotherResolvedPos⁠) 方法:比较当前位置与给定的另一个位置 anotherResolvedPos⁠ 的(相对于根节点)偏移量,返回较小的那一个 ResolvedPos 对象
    注意

    隐含的前提是这两个位置解析结果对象的根节点需要是相同的

Mark 类

该类的实例化对象用于为节点对象添加样式或额外信息的,例如为节点标记上 emphasized 强调。

ProseMirror 在创建 schema 数据结构约束对象时(根据配置参数)实例化生成一系列的 mark 对象,以下称作「样式标记」

注意

任何在文档中所需要使用的 mark 样式标记,都需要在 schema 数据结构的约束对象的属性 marks 中声明/注册

js
const markSchema = new Schema({
  nodes: {
    doc: {content: "block+"},
    // 段落节点支持所有 mark 样式标记添加到它的内容/后代节点上
    paragraph: {group: "block", content: "text*", marks: "_"},
    // 段落节点在它的内容/后代节点上不允许使用任何样式标记
    heading: {group: "block", content: "text*", marks: ""},
    text: {inline: true}
  },
  // 声明在文档中需要使用的一系列 mark 样式标记
  marks: {
    strong: {},
    em: {}
  }
})
提示

节点的内容如果是 inline 内联类型的,则默认支持所有类型的 mark 样式标记添加到它的后代节点上;而其他类型的节点,则默认不允许任何样式标记。

也可以在 schema 数据结构的约束对象中通过节点的 marks 属性进行设置,如果要为节点设置多个 mark 样式标记,它们之间用空格分隔,另外可以使用 "_" 下划线符号表示在该节点内允许使用所有类型的标记,而使用 "" 空字符串表示不允许使用任何样式标记。

mark 样式标记对象包含一些属性和方法:

  • type 属性:表示该样式标记类型,它是一个 markType 对象
    markType

    上面所说的 markType 对象是 class MarkType 的实例化,它是编辑器在使用 new Schema() 实例化数据约束对象时,根据传入的参数 marks(该参数值是一个对象,每一个属性的属性名称是样式标记名称,相应的属性值是该样式标记的配置对象),创建一系列相应的 markType 实例对象,它们包含了不同样式标记的信息,例如名称。

    这些 markType 对象可以用于标记/表示相应的样式标记 mark 的类型。

    markType 对象包含一些属性和方法:

    • name 属性:一个字符串,表示该类型的样式标记的名称
    • schema 属性:一个 schema 对象,表示该类型的样式表季在哪个 schema 对象中进行定义的
    • spec 属性:一个对象(它需要符合一种 TypeScript interface MarkSpec),描述该类型的样式标记可接受的配置参数
      MarkSpec

      TypeScript interface MarkSpec 描述该类型的样式标记可接受哪些配置参数

      实际在哪里定义

      其实该 interface 所约束的是在实例化 schema 时,在配置对象中,针对该类型的样式标记可以设置哪些属性

      • attrs(可选)属性:一个对象,设置该样式标记所拥有的 attributes 以添加额外的信息
        添加额外信息

        在实例化 schema 时,可以在各样式标记的 attrs 属性中设置额外的信息

        其属性值需要是一个「纯对象」 plain object(可以进行 JSON 反序列化的对象),其中该对象的每一个选项都可以通过(可选)属性 default 设置默认值

      • inclusive(可选)属性:布尔值(默认值为 true),表示光标位于该样式标记结尾处,该样式标记是否需要 active 激活
        激活样式标记

        样式标记 active 激活是指对未来输入的内容也应用上该样式标记

        例如对于文本节点设置了该样式标记,如果光标位于文本节点最后,继续输入的文字是否依然采用该样式标记

        如果光标置于样式标记的开头时,而该样式标记所应用节点同时位于其父节点的开头,则是否激活也同样受该属性控制

      • excludes(可选)属性:字符串,用于设置当前样式标记不能与哪些样式标记共存。可以是样式标记类型名称或组别名称,如果要设置多个名称则用空格分隔。
        不可共存的样式标记

        默认排除的是同类型的样式标记,即该样式标记应用到节点上时,如果节点已经有同类型的样式标记(可能所具有的 attrs 是不同的),则会被覆盖替换

        可以使用 _ 下划线来表示不能与其他样式标记共存;可以使用空字符,或任何字符串(非样式标记类型名称或组别名称)来表示可以与任何其他样式标记共存(对于同类型的样式标记也可以共存,只要 attrs 是不同的即可)

        另外该属性会影响方法 mark.addToSet(set) 的执行结果,该方法将当前样式标记对象 mark 添加指定集合 set(该集合包含一系列的样式标记对象)中。如果在当前样式标记的属性 excludes 所指明的不能共存的类型,在集合中出现(但反过来,集合中的样式标记对象的 excludes 属性并没有指明当前样式标记类型),则表示可以将当前样式标记添加到集合中,但是要先将这些不相容的样式标记对象清除,再将当前样式标记对象添加到集合中;如果集合所包含的样式标记中,它们的属性 excludes 对当前样式标记类型进行了排除(但反过来,当前样式标记没有指明不能与之共存),则当前样式标记对象并不能添加到集合中

      • group(可选)属性:字符串,以设置该样式标记所属的组别。可以指定多个组别,组别名称之间用空格分隔。
      • spanning(可选)属性:布尔值(默认值为 true),以设置该样式标记序列化为 HTML 时是否可以跨越多个相邻的节点,将该样式标记应用到这些节点上。
      • toDOM(可选)属性:一个函数 fn(mark, inline) 以定义该样式标记序列化为 HTML 的默认方式
        函数 fn(mark, inline) 第一个参数 mark 是该样式标记实例化对象,第二个参数 inline 是一个布尔值以表示该样式标记的内容(节点)是否为 inline 内联类型(与之相对应的是 block 块级类型)
        该函数的返回值需要符合一种 TypeScript type DOMOutputSpec,具体介绍可查看 node.toDOM() 方法的相关介绍
        如果返回值是数组形式,而且在它的(包括内嵌数组)元素中有数字 0(称为 hole 占位符),则意味着该样式标记所对应的 DOM 元素可以插入内容/子节点(在 hole 的位置);否则会将该样式标记的内容/子节点追加 append 到(该样式标记所对应/修饰的)Node 节点里面 ❓
      • parseDOM(可选)属性:一个数组,包含了将 DOM 元素解析为样式标记的一些规则。
        HTML 反序列化为样式标记

        该属性值是一个数组,其中每个元素都是一个对象(需要符合一种 TypeScript type ParseRule)以表示一种解析规则。

        ParseRule

        TypeScript type ParseRule=TagParseRule | StyleParseRule 它可以是两种接口类型之一,分别表示基于 HTML 标签名称或行内样式 inline style 将 DOM 解析为相应类型的样式标记的规则

        💡 关于 TypeScript interface TagParseRule 的具体介绍可查看 node.parseDOM() 方法的相关部分

        StyleParseRule

        TypeScript interface StyleParseRule(它继承了另一个 TypeScript interface GenericParseRule,具体介绍可查看 node.parseDOM() 方法的相关部分)描述了基于行内样式 inline style 将 DOM 元素解析为相应类型的样式标记的规则

        • style(可选)属性:一个字符串,它是一个 style property 样式属性名称,则所匹配 DOM 元素需要具有该 inline style property 行内样式( 💡 字符串也可以采用 "property=value" 的形式,以指定行内样式属性需要具有特定的值, ⚠️ 注意属性名称和值之间用 = 等号来连接)。所匹配成功的 DOM 元素会解析为样式标记 Mark(而不是节点 node,所以该属性只能在 marks 的 parseDOM 中设置)。
          js
          const schema = new Schema({
            marks: {
                em: {
                  // 为「强调样式」设置 3 个 DOM 解析规则
                  parseDOM: [
                    // 将含有内联样式 style="font-style:italic"(注意以下使用 = 等号而不是 : 冒号来连接样式属性和值)的 DOM 元素转换为该类型的样式标记
                    {style: "font-style=italic"}
                  ],
                },
            }
          })
          
          提示

          如果需要基于 DOM 元素的 property 或 attribute 设置构建更复杂的匹配条件,可以通过 getAttrs 属性来设置

        • clearMark(可选)属性:一个函数 fn(mark: Mark)(参数 mark 是在 pending marks 待添加的样式标记实例),返回值是一个布尔值,表示是否清除 active mark 激活的/待添加的样式标记
          解释

          该属性设置为 false 可以清除该样式标记,可用于标准化一些原本语义混乱的 HTML,以保证解析所得到的文档的语义一致性

          例如一段 HTML <i style="font-style=normal">abc</i>,如果基于 prosemirror-schema-basic 模块的 schema 构建编辑器,则这一段 HTML 可以匹配多个解析规则,首先基于标签名称匹配到的是 em 强调样式标记的规则,则 pending marks 待添加的样式标记中就包含了 em mark;然后又基于内联样式匹配到另一条规则

          如果希望在包含 font-style=normal 内联样式标记的文本上清除 em 强调样式标记,则可以为 em mark 添加一条解析规则(基于内联样式来匹配),并在其中设置 clearMark 属性(将其函数 fn(mark)的返回值)设置为 true

        • getAttrs(可选)属性:一个函数 fn(style: string),它的返回值有多种类型,对应于有不同的作用。
          更精细地设置 attributes

          属性 getAttrs 是一个函数,用于构建更精细、更复杂的条件,可以基于 attributes 来匹配 DOM 元素,或者为转换而得的 node(或 mark)设置 attribute。

          该函数的入参是 string 即当前所匹配的 DOM 元素所拥有对应内联样式的值(例如在 style 属性所设置的样式 property 是 font-weight,而解析的 HTML 元素的相关内联样式是 "font-weight: 900",则该函数的的入参就是 900

          该函数的返回值可以有多用类型:

          • 可以返回一个对象,表示转换生成的 node 或 mark 所拥有的 attribute
          • 可以返回 nullundefined,表示转换生成的 node 或 mark 没有 attribute
          • 可以返回 false,表示该 DOM 元素无法满足规则,即无法匹配
      • 其他属性,还可以为节点添加任意属性(属性的名称是字符串,属性值是任意类型)
        添加额外信息

        还可以为节点添加任意属性,以便为节点添加额外的信息。

        然后就可以通过 mark.type.spec.xxx 来获取该信息

    • create(attrs?) 方法:创建一个该类型的样式标记实例
      (可选)参数 attrs(默认值是 null)是一个对象,用于设置样式标记的 attributes。如果该类型的样式标记没有必须要指派的 attribute 属性,则可以传递 null 表示使用默认值来作为该节点的各种 attribute 的值。
    • removeFromSet(set) 方法:从集合 set(一个数组,包含一系列的 mark 样式标记对象)中移除该类型的样式标记实例。如果 set 原本不是一个空数组,则返回一个新数组(它剔除了该类型的样式标记);如果 set 本来是一个空数组,则直接返回该数组
    • isInSet(set) 方法:返回一个布尔值,以表示集合 set (一个数组,包含一系列的 mark 样式标记对象)中是否包含该类型的样式标记实例
    • excludes(otherMark) 方法:返回一个布尔值,以表示该类型的样式标记是否不能与给定的 otherMark(一个 mark 对象)共存(即该类型的样式标记的 markType.spec.excludes 字符串中包含了 otherMark 名称)
  • attrs 属性:一个对象,表示该样式标记拥有的 attributes
  • addToSet(set) 方法:将当前的样式标记添加到给定的集合 set (一个数组,包含一系列的 mark 样式标记对象)中。返回一个新的集合(包含了该样式标记)。
    如果在当前样式标记的属性 markType.spec.excludes 所指明的不能共存的类型,在集合中出现(但反过来,集合中的样式标记对象的 excludes 属性并没有指明当前样式标记类型),则表示可以将当前样式标记添加到集合中,但是要先将这些不相容的样式标记对象清除,再将当前样式标记对象添加到集合中;如果集合所包含的样式标记中,它们的属性 markType.spec.excludes 对当前样式标记类型进行了排除(但反过来,当前样式标记没有指明不能与之共存),则当前样式标记对象并不能添加到集合中
  • removeFromSet(set) 方法:将当前的样式标记从给定的集合 set(一个数组,包含一系列的 mark 样式标记对象)中移除。返回一个新的集合(移除了该样式标记)。
  • isInset(set) 方法:返回一个布尔值,以表示集合 set (一个数组,包含一系列的 mark 样式标记对象)中是否包含该样式标记
  • eq(otherMark) 方法:返回一个布尔值,以表示当前样式标记是否与给定的 otherMark 另一个样式标记对象具有相同的类型和 attributes
  • toJSON() 方法:将该样式标记对象序列化为 JSON 对象

此外该类还提供了一些静态方法:

  • static Mark.fromJSON(schema, json) 静态方法:基于 schema 数据结构约束对象将 json 对象反序列化为一个样式标记实例
  • static Mark.sameSet(markArrA, markArrB) 静态方法:返回一个布尔值,以表示两个数组(它们的元素都是一系列的样式标记对象)是否为同一个样式标记的集合
  • static Mark.setFrom(markArr) 静态方法:基于给定的参数 markArr(可以是 null、单独一个 标记对象,或者一个未排序的 marks 数组),新建一个样式标记的集合
  • static Mark.none 静态属性:返回一个空的样式标记的集合(空数组)

Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes