阅读 263

ProseMirror学习笔记 2——document对象

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

document 基本结构

ProseMirror document 是树形结构。

一个 Porsemirror 的 document 是一个 node 类型, 它含有一个fragment 对象, fragment 对象又包含了 0 个或更多子 node. 如图 都是一层层的嵌套 在这里插入图片描述

在这里插入图片描述

      

Prosemirror 跟 DOM 一样是递归的树状结构. 不过, Prosemirror 在存储内联元素(比如 、 文本、strong、em等)的方式上跟 DOM 有点不同.

在 HTML 中, 一个 paragraph 及其中包含的标记, 表现形式就像一个树, 比如有以下 HTML 结构:

<p>This is <strong>strong text with <em>emphasis</em></strong></p>
复制代码

在这里插入图片描述 然而在 Prosemirror 中, 内联元素被表示成一个扁平的模型, 他们的节点标记被作为 metadata 信息附加到相应 node 上。

比如这里的文本text、strong标签、em标签都被挂载到最近的父块级元素p上,从而形成扁平化的结构 在这里插入图片描述

 

我们可以使用 字符的偏移量 而不是一个树节点的路径来表示其所处段落中的位置, 并且使一些诸如 splitting 内容或者改变内容style 的操作变得很容易

这也意味着, 每个 document 只有一种数据结构表示方式.。文本节点中相邻且相同的 marks 被合并在一起, 而且不允许空文本节点. marks 的顺序在 schema 中指定.

因为内联元素会以扁平的方式挂载到块级元素上,所以 一个 Prosemirror document 就是一颗 block nodes 的树, 它的大多数 叶子结点 是 textblock 类型, 该节点是包含 text 的块级节。.你也可以有一些内容为空的简单的 leaf nodes, 比如一个水平分隔线 hr 元素, 或者一个 video 元素.

       

document特性 —— 分裂

暂时这么说吧,因为我也不知道该叫他啥。有点类似于react的state

DOM 树与 ProseMirror document 的另一个不同是他们对 nodes 对象的表示方式. 在 DOM 中, nodes是带有 idmutable 对象(可以修改的对象), 这意味着一个 node 只能出现在它的父级 node 下(如果它出现在别处, 那它在此处就没了, 因为有 唯一id, 所以唯一), 当这个 node 更新的时候, 它就 mutated 了(node 更新是在原来的 node上更新, 此谓之 mutated 即突变.表示在原有基础上修改, 修改前后始终是一个对象).

Prosemirror 中 nodes 仅仅是 values(区别于 DOM 的 mutable, values 是 unmutable 的), 表示一个节点就像表示一个字符x一样. x可以同时出现在不同的数据结构中, 它不跟当前的数据结构绑定, 如果你对它增加y, 你将会得到一个新的 value: xy 而不用对原始的 x做任何修改.

这就是 Prosemirror document 的机制. 它的值不会改变, 而且可以被当做一个原始值去计算一个新的 document. (就好像分裂一样,以之前的为基础,不断分类出新的node)这些 document 的 nodes 们不知道它所处的数据结构是什么, 因为它们可以存在于多个结构中, 甚至可以在一个结构中重复多次. 它们是 values, 不是拥有状态的对象

这意味着每次你更新 document, 你就会得到一个新的 document. 这个新的 document 共享旧的 document 的所有没有在这次更新中改变的子 nodes 的 value, 这让新建一个 document 变得很廉价.

这种机制有很多优点. 它让当 state 更新的时候编辑器始终可用, 因为新的 state 就代表了新的 document(如果更新未完成, 则 state 不会出现, 因此 document 也没有, 编辑器仍然是之前的 state + document——译者注), 新旧状态可以瞬间切换(而没有中间状态). 这种状态切换更可以以一种简单的数学推理的方式完成——而如果你的值在背后不断变化(指像 DOM 的节点一样突变——译者注), 这种推理将非常困难. Prosemirror 的这种机制使得协同编辑成为可能, 而且能够通过比较之前绘制在屏幕上的 document 和当前的 document 算法来非常高效的 update DOM.

因为 nodes 都被表示为正常的 JavaScript 对象, 而明确 freezing 他们的属性(防止 mutate)非常影响性能, 因此事实上虽然 Prosemirror 的 document 以一种非突变的机制运行, 但是你还是能够手动修改他们. 只是 Prosemirror 不支持这么做, 如果你强行 mutate 这些数据结构的话, 编辑器可能会崩溃, 因为这些数据结构总是在多处共享使用(修改一处, 影响其他你不知道的地方). 因此, 务必小心!!! 同时记住, 这个道理对一些 node 对象上存储的数组和对象同样适用, 比如 node attributes 对象, 或者存在 fragments 上的子 nodes..

因为 nodes 和 fragment 是一种持久化的数据结构(意即 immutable ——译者注), 你绝对不应该直接修改他们. 如果你需要操作 document, 那么它就应该一直不变(操作后产生新的 document, 旧的 document 一直不变).

大多数情况下, 你需要使用 transformations 去更新 document 而不用直接修改 nodes. 这也方便留下一个变化的记录, 变化的记录对作为编辑器 state 一部分的 document 是必要的.

如果你非要去手动更新 document, Prosemirror 在 Node 和 Fragment 上提供了一些有用的辅助函数去新建一个 document 的全新版本. 你可能会常常用到 Node.replace 方法, 该方法用一个含有新的 content 的 slice 替换指定 document 的 range 内的内容. 如果想要浅更新一个 node, 你可以使用 copy 方法, 该方法新建了一个相同的 node, 不过为这个相同的新 node 可以指定新的 content. Fragments 也有一些更新 document 的方法, 比如 replaceChildappend.

       

Node类型

这个类表示组成ProseMirror文档树的节点。因此,document是Node的实例,其子节点也是Node的实例。

node 是持久的数据结构。你不能改变它们,而是用你想要的内容创建一个新的node。旧的一直指向旧的文档形状。通过尽可能多地在新旧数据之间共享结构,可以降低成本,这样的树形结构(没有反向指针)使之更容易实现。

整个 document 都是一个 node. document 的 content 作为顶级 node 的子 nodes. 通常上来说, 这些顶级 node 的子 node 是一系列的 block nodes, 这些 block nodes 中有些可能包含 textblocks, 这些 textblocks 有包含 inline content. 不过, 顶级 node 也可以只是一个 textblock, 这样的话整个 document 就只包含 inline content.

哪些 node 被允许出现在哪些位置是由 document 的 schema 决定的. 为了用编程的方式(而不是直接对编辑器输入内容的方式)创建 nodes, 你必须遍历 schema, 比如下面的使用 node 和 text 方法.

import {schema} from "prosemirror-schema-basic"

// null 参数的位置是用来在必要的情况下指定属性的
let doc = schema.node("doc", null, [
  schema.node("paragraph", null, [schema.text("One.")]),
  schema.node("horizontal_rule"),
  schema.node("paragraph", null, [schema.text("Two!")])
])
复制代码

属性介绍

  • type: NodeType 节点类型。通过 type 属性可以知道 node 的名字, 它可以使用的 attributes, 诸如此类的信息. Node types(和 mark types) 只会被每个 schema 创建一次, 它们知道自己是属于哪个 schema。

  • attrs: Object

·允许和需要的属性类型。比如:一个 image node 可能使用 attrs存储 alt 文本信息和 URL 信息.

  • content: Fragment node的所有子节点。node 的 content 被存储在一个指向 Fragment 实例的字段上, 它的内容是一个 nodes 数组. 即使那些没有 content 或者不允许有 content 的 nodes 也是如此, 这些不许或没有 content 的节点被共享的 empty fragment 替代。

    和node一样,Fragment是unmutable的数据结构,不应该对它们或其内容进行mutate。而是在需要时创建新实例。

  • marks: [Mark]

    比如像 emphasis 或者 link 的一些标记。

  • text: ?⁠string 对于文本节点,这个字段存放文本内容

  • nodeSize: number 节点的大小。对于文本节点,该字段表示字符数;对于叶子节点,大小1;对于非叶子结点,大小为content大小加2(开始和结束标记)

  • inlineContent :?boolean

为true表示该 node 只接受 inline 元素作为 content,可以通过判断此节点来决定下一步是否往里面加 inline node

  • isTextBlock :?boolean

为true表示这个 node 是个含有 inline content 的 block nodes.

因此, 一个典型的 “paragraph” node 是一个 textblock 类型的节点, 然后一个 blockquote(引用元素)则是一个可能由其他 block 元素构成其内容的 block 元素. Text 节点, 回车, 和 inline 的 images 都是 inline leaf nodes, 而水平分隔线(hr 元素)节点是一个典型的 block leaf nodes.(leaf nodes 翻译成 叶节点, 表示其不能再含有子节点; leaf nodes 如上所说, 可能是 inline 的, 也可能是 block 的——译者注).
复制代码
  • isLeaf :?boolea 为true 表示该 node 不允许含有任何 content.

方法介绍

在这里插入图片描述

  • child(index: number) → Node

通过index查找节点。比如view.state.doc.child(0) 找到的就是1

  • descendants(f: fn(node: Node, pos: number, parent: Node) → ?⁠bool)

    遍历所有后代节点

  • copy(content: ?⁠Fragment = null) → Node 使用与此节点相同的mark创建一个新节点,其中包含给定的content(如果未给定内容,则为空)。

  • slice(from: number, to: ?⁠number = this.content.size) → Slice 给定开始和结束position ,截取一个片段,返回一个Slice对象

  • replace(from: number, to: number, slice: Slice) → Node 用给定的slice替换给定位置(from,to)之间的文档部分。切片必须“fit”,这意味着它的'open'面必须能够连接到周围的内容,并且它的content节点必须是它们所在节点的有效子节点。如果违反了其中任何一条,则抛出ReplaceError类型的错误。

  • resolve(pos: number) → ResolvedPos

-解析给定position在文档中的内容,并返回一个包含position信息的对象

     

查找node

Prosemirror nodes 支持两种类型的 索引——它们既可以被当成树类型, 因为它们使用 offsets 来区别每个 nodes; 也可以被当成一个具有一系列 token 的扁平的结构(token 可以理解为一个计数单位).

  1. 第一种,index 允许你像在 DOM 中那样, 与单个 nodes 进行交互, 使用 child method 和 childCount 直接访问 child nodes, 写递归函数去遍历 document(如果你想遍历所有的 nodes, 使用 descendants 和 nodesBetween).

  2. 第二种,index 当在文档定位一个指定的 position 的时候更有用. 它可以以一个整数表示文档中的任意位置——这个整数是 token 的顺序. 这些 token 对象在内存中其实并不存在——它们只是用来计数方便——不过 document 的树状结构以及每个 node 都知道它们自己的大小尺寸使得按位置访问它们变得廉价.

    Document 的起始位置, 在所有 content 的开头, 位置是 0.

进入或者离开不是 leaf node 的节点(比如能够包含内容的节点, 都算是非叶子结点)计为 1 个 token. 所以如果 document 以一个 paragraph(标签是 p) 开头, 在段落开头的 position 是 1 (即 < p > 之后的位置)Text nodes 的每个字符记为 1 个 token. 所以如果在 document 的开头的 paragraph 包含单词 “hi”, 那么 position 2 在 “h” 之后, position 3 在 “i” 之后, position 4 在整个段落之后(即 < /p> 之后)叶子结点 如果不允许 content 的(比如图片节点), 计做 1 个 token.因此, 如果你有一个 document, 表示成 HTML 就像下面这样:

<p>One</p>
<blockquote><p>Two<img src="..."></p></blockquote>
复制代码

Token 顺序和 position 则看起来像下面这样: 在这里插入图片描述

每个 node 都有一个 nodeSize 属性表示整个 node 的尺寸大小, 你还可以通过 .contentSize 获得 node 的 content 的尺寸大小. 需要注意的是对于 document 的外层节点(即 DOM 中 contenteditable 属性所处的节点, 是整个 document 的根节点)来说, 开始和关闭 token 不被认为是 document 的一部分(因为你无法将光标放到 document 的外面), 因此 document 的尺寸是 doc.contentSize, 而不是 doc.nodeSize(虽然 document 的开关标签不被认为是 document 的一部分, 但是仍然计数. 后者始终比前者大2).

如果手动计算这些位置涉及到相当数量的计算工作. (因此)你可以通过调用 Node.resolve 来获得一个 position 的更多数据结构的描述. 这个数据结构将会告诉你当前 position 的父级 node 是什么, 它在父级 node 中的偏移量是多少, 它的父级 node 的祖先 nodes 有哪些, 和其他一些信息.

一定要注意区分子 node 的 index(比如每个 childCount), document 范围的 position, 和 node 的偏移(有时候这个偏移会用在一个递归函数表示当前处理的 node 的位置, 此时就涉及到 node 的偏移)之间的区别.

     

复制粘贴和拖拽——slice

对于用户的复制粘贴和拖拽之类的操作, 涉及到一个叫做 slice of document 的概念, 例如在两个 position 之间的 content 就是一个 slice. 这种 slice 与一个完整的 node 或者 fragment 不同, slice 可能是 “open”(意思即一个 slice 包含的标签可能没有关闭, 比如 < p>123< /p>< p>456< /p> 中, 一个 slice 可能是 23< /p>< p>45).

例如, 如果你用光标选择从一个段落的中间到另一个段落的中间, 那么你选择的 slice 就是含有两个段落, 第一个在开始的地方 open, 第二个在结束的地方 open, 然后如果你使用接口(而不是通过与 view 交互)选择了一个段落 node, 那你就选择了一个 close 的 node. 如果对待 slice 像普通的 node content 一样的话, 它的 content 可能不符合 schema 的约束, 因为某些所需要的 nodes(如使 slice content 是一个完整的 node 的标签, 如上例中的开始部分的

和结束部分的

) 落在了 slice 之外.

Slice 数据结构就是被用来表示这种的数据的. 它存储了一个含有两侧 open depth (意思就是相对于根节点的层级深度)信息的 fragment. 你可以在 nodes 上使用 slice method 来从 document 上 “切” 出去一片 “slice”.

//假设文档有两个 p 标签, 第一个 p 标签包含 a, 另一个 p 标签包含 b, 即:
// <p>a</p><p>b</p>
let slice1 = doc.slice(0, 3) // The first paragraph
console.log(slice1.openStart, slice1.openEnd) // → 0 0
let slice2 = doc.slice(1, 5) // From start of first paragraph
                            // to end of second
console.log(slice2.openStart, slice2.openEnd) // → 1 1
复制代码
文章分类
前端
文章标签