每个 Prosemirror document 都有一个与之相关的 schema. 这个 schema 描述了 document 中的的nodes 类型, 和 nodes 们的嵌套关系. 例如, schema 可以规定, 顶级节点可以包含一个或者更多的 blocks, 同时段落 paragraph nodes 可以包含含有任意数量的 inline nodes, 这些 inline nodes 可以含有任意数量的 marks.
一个document schema,有nodes和marks对象,之后nodes和marks都会遵从这个对象的约束。并提供创建和反序列化此类文档的功能
nodes: Object<NodeSpec> | OrderedMap<NodeSpec>
将名称映射到NodeSpec对象。它们的顺序很重要,它决定了默认情况下哪些解析规则优先,以及给定组中哪些节点排在第一位。
marks: ?Object<MarkSpec> | OrderedMap<MarkSpec>
它们的顺序决定了marks的排序顺序和尝试解析规则的顺序。
new Schema(spec: SchemaSpec)
可以通过给定schemaSpec自定义schema.
注意:spec 是 specification缩写 ,是规格、说明书的意思;
在 document 中的每个节点都有一个 type, 它代表了一个 node 的语义化上意思和 node 的属性, 这些属性包括在编辑器中的渲染方式.
当你定义一个 schema 的时候, 你需要列举每一个用到的 node types, 用一个 nodespec描述它们:
const trivialSchema = new Schema({
nodes: {
doc: {content: "paragraph+"},
paragraph: {content: "text*"},
text: {inline: true},
/* ... and so on */
}
})
上述代码定义了一个允许 document 包含一个或更多 paragraphs 的 schema, 每个 paragraph 又能包含任意数量的 text.
每个 schema 至少得定义顶级 node 的 type(顶级 node 的名字默认是 “doc”, 不过你可以配置它), 和规定 text content 的 “text” type.
作为 inline 类型来计算 index 等的 nodes 必须声明它的 inline 属性( text 类型, 就被定义成 inline 了)
上面 schema 示例代码中的 content 字段的字符串值被叫做 ‘content expressions’. 他们控制着对于当前 type 的 node 来说, 它的children可以有哪些 nodes 类型.
比如说, (content 字段的内容是)”paragraph” 意思就是 “一个 paragraph”, “paragraph+” 意思就是 “一个或者更多 paragraph”.与此相似, “paragraph*” 意思就是 “0 个或者更多 paragraph”, “caption?” 意思就是 “0 个或者 1 个 caption node”. 你也可以在 node 名字之后使用类似于正则表达式中表示范围含义的表达式, 比如 {2}(正好 2 个), {1, 5}(1 个到 5 个), 或者{2, }(两个或更多).
这种表达式可以被联合起来创建一个系列, 例如 “heading paragraph+” 表示 “开头一个 heading, 之后一个或更多 paragraphs”. 你也可以使用管道符号 “|” 操作符来表示在两个表达式中选择一个, 比如 “(paragraph | blockquote)+”.
一些元素 type 的 group 可能在你的 schema 会出现多次, 比如你有一个 “block” 概念的 nodes, 他们可以出现在顶级元素之下, 也可以嵌套进 blockquote 类型的 node 内. 你可以通过指定 schema 的 group 属性来创建一个 node group, 然后在你的其他表达式中填 group 的名字即可:
const groupSchema = new Schema({
nodes: {
doc: {content: "block+"},
paragraph: {group: "block", content: "text*"},
blockquote: {group: "block", content: "block+"},
text: {}
}
})
上面示例中, “block+” 等价于 “(paragraph | blockquote)+”.
建议在允许 block content 的 nodes(在示例中就是 doc 和 blockquote)中设置为至少有一个 child node, 因为如果 node 为空的话浏览器将折叠它, 使它无法编辑(这句话的意思是, 如果上述 doc 或者 blockquote 的 content 设置为 block* 而不是 block+ 就表示允许不存在 child nodes 存在的情况(它沿用了通用的正则符号: * 表示0个或更多, + 表示1个或更多), 那么此时编辑的话浏览器输入的是 text node, 是 inline 节点, 导致无法输入, 读者可以试试).
在 schema 中, nodes 的书写顺序很重要. 当对一个必选的 node 新建一个默认实例的时候, 比如在应用了一个 replace step 之后, 为了保持当前文档仍然符合 schema 的约束, 会使用能满足 schema 约束的第一个 node 的 expression. 如果 node 的 expression 是一个 group, 则这个 group 的第一个 node type(决定于当前 group 的成员 node 出现在 schema 的顺序)将被使用. 如果我在上述的 schema 示例中调换了 “paragraph” 和 “blockquote” 的顺序, 当编辑器试图新建一个 block node 的时候将会报 stack overflow——因为编辑器会首先尝试新建一个 “blockquote” node, 但是这个 node 需要至少一个 block node, 于是它就首先又需要创建一个 “blockquote” node 作为内容, 以此往复.
不是每个 Prosemirror 库中的 node 操作函数都会检查它当前处理 content 的可用性——高级概念例如 transforms 会检查, 但是底层的 node 新建方法通常不会, 这些底层方法通常将可用性检查交给它们的调用者. 它们(即使当前操作的 content 不可用, 但是这些底层方法也)完全可能可用, 比如, NodeType.create, 它会创建一个含有不可用 content 的节点. 对于在一个 slices 的 “open” 一边的 node 而言, 这甚至是情有可原的(因为 slice 不是一个可用的节点, 但是又需要直接操作 slice ——总不能让用户手动补全吧?——译者注). 有一个 createChecked 方法可以检查给定 content 是否符合 schema, 也有一个 check 方法来 assert 给定的 content 是否可用.
Marks 通常被用来对 inline content 增加额外的样式和其他信息. schema 必须声明当前 document 允许的所有 schema(就像声明 nodes 那样).
Mark types 是一个有点像 node types 的对象, 它用来给不同的 mark 分类和提供额外的信息.
默认情况下, 有 inline content 的 nodes 允许所有的定义在 schema 的 marks 应用于它的 child nodes.
下面是一个简单的 schema 示例, 支持在 paragraphs 中设置文本的 strong 和 emphasis marks, 不过 heading 则不允许设置这两种 marks.
const markSchema = new Schema({
nodes: {
doc: {content: "block+"},
paragraph: {group: "block", content: "text*", marks: "_"},
heading: {group: "block", content: "text*", marks: ""},
text: {inline: true}
},
marks: {
strong: {},
em: {}
}
})
marks 字段的值可以写成用逗号分隔开的 marks 名字, 或者 mark groups——
”_”, 它是通配符的意思, 允许所有的 marks.;
空字符串“” 表示不允许任何 marks.
Document 的 schema 也定义了 node 和 mark 允许有哪些 attributes. 如果你的 node type 需要额外的 node 专属的信息, 比如 heading node 的 level 信息(H1, H2等等), 此时适合使用 attribute.
Attribute 是一个普通的纯对象, 它有一些预先定义好的(在每个 node 或 mark 上)属性, 指向可以被 JSON 序列化的值. 为了指定哪些 attributes 被允许出现, 可以在 node spec 和 mark 的 spec 中使用可选的 attr 属性:
heading: {
content: "text*",
attrs: {level: {default: 1}}
}
在上面这个 schema 中, 每个 heading node 实例都有一个 level 属性通过 .attrs.level 访问. 如果在新建 heading 的时候没有指定, level 默认是 1.
如果你在定义 node 的时候没有给一个 attribute 默认值的话, 当新建这个 node 的时候, 如果没有显式传入 attribute 就会报错. 这也让 Prosemirror 在调用一些接口如 createAndFill 来生成满足 schema 约束的 node 的时候变得不可能.
为了能在浏览器中编辑元素, 就必须使 document nodes 以 DOM 的形式展示出来. 最简单的方式就是在 schema 中对每个 node 注明如何在 DOM 中显示. 这可以在 schema 的每个 node spec 中指定 toDOM 字段来实现.
这个字段应该指向一个函数, 这个函数将当前 node 作为参数, 返回 node 的 DOM 结构描述. 这可以直接是一个 DOM node, 或者一个 array 来描述, 例如:
const schema = new Schema({
nodes: {
doc: {content: "paragraph+"},
paragraph: {
content: "text*",
toDOM(node) { return ["p", 0] }
},
text: {}
}
})
上面示例中, [“p”, 0] 的含义是 paragraph 节点在 HTML 中被渲染成
标签. 0 代表一个 “hole”, 表示该 node 的内容应该被渲染的地方(意思就是如果这个节点预期是有内容的, 就应该在数组最后写上 0). 你也可以在标签后面加上一个对象表示 HTML 的 attributes, 例如 [“div”, {class: “c”}, 0]. leaf nodes 不需要 “hole” 在它们的 DOM 中, 因为他们没有内容.
- 上面示例中, [“p”, 0] 的含义是 paragraph 节点在 HTML 中被渲染成 < p> 标签.
- 0 代表一个 “hole”, 表示该 node 的内容应该被渲染的地方(意思就是如果这个节点预期是有内容的, 就应该在数组最后写上 0). 你也可以在标签后面加上一个对象表示 HTML 的 attributes, 例如 [“div”, {class: “c”}, 0]…
- 叶子结点 不需要 “hole” 在它们的 DOM 中, 因为他们没有内容.
Mark 的 specs 有一个跟 nodes 相似的 toDOM 方法, 不同的是他们需要渲染成单独的标签去直接包裹着 content, 所以这些 content 直接在返回的 node 中, 上面的 “hole” 就不用专门指定了.
当用户粘贴或者拖拽东西到编辑器中的时候. Prosemirror-model 模块的函数可以来处理这些事情, 不过你也可以在 schema 中的 parseDOM 属性中直接描述如何格式化信息.
这里列出了一组格式化的规则, 描述了 DOM 如何映射成 node 或者 mark. 例如, 基本的 schema 对于 emphasis mark 写成下面这样:
parseDOM: [
{tag: "em"}, // Match <em> nodes
{tag: "i"}, // and <i> nodes
{style: "font-style=italic"} // and inline 'font-style: italic'
]
上面中的 tag 字段也可以是一个 CSS 选择器, 比如 “div.myclass” 这种字符串.。与此相似, style 字段也可以匹配行内 CSS 样式.
当一个 schema 包含 parseDOM 字段时, 你可以使用 DOMParser.fromSchema 创建一个 DOMParser 对象. 编辑器在新建默认的剪切板内容 parser 的时候就是这么干的, 不过你可以覆盖它.