ProseMirror学习

ProseMirror

  • prosemirror-model
  • prosemirror-state
  • prosemirror-view
  • prosemirror-transform

Schema

Schema 描述的是:这篇文档允许出现哪些结构——能有哪些节点(Node)、哪些标记(Mark),以及它们之间如何嵌套、谁里能装谁

创建 Schema 大致是

1
2
3
4
new Schema({
nodes: { doc, paragraph, text, /* ... */ },
marks: { strong, link, /* ... */ },
})
  • nodes:名字 → NodeSpec 的映射。其中必须有一个 根节点(通常叫 doc),且 Schema 会把它当作 schema.topNode(整篇文档的根类型)。

    • content:
    • group:inline、block、tile分组(其概念与 html 节点的 inline 节点与 block 节点一致)
    • marks:
  • marks:名字 → MarkSpec 的映射(加粗、链接等)。
    编译后会得到 schema.nodes / schema.marks:从名字查到 NodeType / MarkType(带上了已解析的 content 规则、默认属性等)。

prosemirror 不允许有空文本节点,我们看到在 p 标签中, 文本内容为空时,会填入一个 br 标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { Schema } from 'prosemirror-model';

export const schema = new Schema({
nodes: {
// 整个文档
doc: {
// 文档内容规定必须是 block 类型的节点(block 与 HTML 中的 block 概念差不多) `+` 号代表可以有一个或多个(规则类似正则)
content: 'block+'
},
// 文档段落
paragraph: {
// 段落内容规定必须是 inline 类型的节点(inline 与 HTML 中 inline 概念差不多), `*` 号代表可以有 0 个或多个(规则类似正则)
content: 'inline*',
// 分组:当前节点所在的分组为 block,意味着它是个 block 节点
group: 'block',
// 渲染为 html 时候,使用 p 标签渲染,第二个参数 0 念做 “洞”,类似 vue 中 slot 插槽的概念,
// 证明它有子节点,以后子节点就填充在 p 标签中
toDOM: () => {
return ['p', 0]
},
// 从别处复制过来的富文本,如果包含 p 标签,将 p 标签序列化为当前的 p 节点后进行展示
parseDOM: [{
tag: 'p'
}]
},
// 段落中的文本
text: {
// 当前处于 inline 分株,意味着它是个 inline 节点。代表输入的文本
group: 'inline'
},
// 1-6 级标题
heading: {
// attrs 与 vue/react 组件中 props 的概念类似,代表定义当前节点有哪些属性,这里定义了 level 属性,默认值 1
attrs: {
level: {
default: 1
}
},
// 当前节点内容可以是 0 个或多个 inline 节点
content: 'inline*',
// 当前节点分组为 block 分组
group: 'block',
// defining: 特殊属性,为 true 代表如果在当前标签内(以 h1 为例),全选内容,直接粘贴新的内容后,这些内容还会被 h1 标签包裹
// 如果为 false, 整个 h1 标签(包括内容与标签本身)将会被替换为其他内容,删除亦如此。
// 还有其他的特殊属性,后续细说
defining: true,
// 转为 html 标签时,根据当前的 level 属性,生成对应的 h1 - h6 标签,节点的内容填充在 h 标签中(“洞”在)。
toDOM(node) {
const tag = `h${node.attrs.level}`
return [tag, 0]
},
// 从别处复制进来的富文本内容,根据标签序列化为当前 heading 节点,并填充对应的 level 属性
parseDOM: [
{tag: "h1", attrs: {level: 1}},
{tag: "h2", attrs: {level: 2}},
{tag: "h3", attrs: {level: 3}},
{tag: "h4", attrs: {level: 4}},
{tag: "h5", attrs: {level: 5}},
{tag: "h6", attrs: {level: 6}}
],
}
},
// 除了上面定义 node 节点,一些富文本样式,可以通过 marks 定义
marks: {
// 文本加粗
strong: {
// 对于加粗的部分,使用 strong 标签包裹,加粗的内容位于 strong 标签内(这里定义的 0 与上面一致,也念做 “洞”,也类似 vue 中的 slot)
toDOM() {
return ['strong', 0]
},
// 从别的地方复制过来的富文本,如果有 strong 标签,则被解析为一个 strong mark
parseDOM: [
{ tag: 'strong' },
],
}
}
})

从现象看,prosemirror 会寻找到我们第一个定义的 block 元素来初始化默认的 state。因此,在定义 schema 的时候,需要注意两点:

Schema 中必要的节点要定义
节点的定义顺序可能会影响编辑器初始内容,如果遇到类似问题,可以排查 schema 中 node 的定义顺序

schema中的工具方法

  1. schema.cached 中绑定的 domParser 可以将 dom 节点解析为 node 节点
  2. domSerializer 可以将 node 内容序列化为 dom 节点。

Node

对于 Node 的类型,只有两种,即 block 与 inline

  • NodeSpec 就是之前在 Schema 中填写的 Node 相关的描述
  • NodeType 是 Schema 实例化过程中,根据传入的 nodeSpec 规格说明书创建的 NodeType 实例,可以认为是 node 数据的工厂,后续所有的 node 类型都需要遵循它的定义,它也能像帮我们创造出一个 node 数据
  • 最后生成的 Node 实例则是文档中对应到 dom 的一个具体数据

node

inline 元素与 block 元素不能混合,即 content 里面要么只能是 inline 类型的,要么只能是 block 类型的,不能混合排列,如 paragraph|text* 就会报错。

Mark