本文较为复杂本人理解不深,因此一些代码中的注释可能翻译的不准确,需要的话可以去 查看原文

追踪修改

「修改」是 ProseMirror 中的一等公民(这句翻译使用了一些书介绍 JavaScript 中函数时的说法:「函数」是 JavaScript 中的一等公民)。 你可以持有它的引用然后用它做一些事情。比如 rebasing(中文意译成变基)、反转修改或者检查它看它做了什么。

这个示例使用了上述的这些特性,以允许你「提交」你的修改,或者反转某个独立的提交,亦或者发现某个文本变化源自何处:

提交信息:

Remix on Glitch

鼠标悬浮在某个提交上,以高亮该提交的所做的文本修改。

当前页面不会列出来 所有的源码,而只会说明一些大家最感兴趣的部分。

首先,我们需要做的事情是实现提交历史追踪。用编辑器插件比较适合做这个事情,因为它可以接收到任何一个修改。这个插件的 state 看起来应该是这个样子:

class TrackState {
  constructor(blameMap, commits, uncommittedSteps, uncommittedMaps) {
    // blameMap 是一种数组数据结构,它含有一系列的文档的范围,以及与其相关的提交。
    // 这可以用来做一些很有用的事情,比如高亮某个提交的修改范围等
    this.blameMap = blameMap
    // 提交的历史,以一个对象的数组存储
    this.commits = commits
    // 通过自上次提交以来发生的 steps 来反转新的 step 和他们相应的 map
    this.uncommittedSteps = uncommittedSteps
    this.uncommittedMaps = uncommittedMaps
  }

  // 对当前 state 应用一个 transform
  applyTransform(transform) {
    // 在当前 transaction 中反转它的 step,以在下一次的提交中保存它们(被用来 undo)
    let inverted =
      transform.steps.map((step, i) => step.invert(transform.docs[i]))
    let newBlame = updateBlameMap(this.blameMap, transform, this.commits.length)
    // 创建一个新的 state,因为编辑器的 state 以及它的任意一个部分,都是一个不可突变的存储结构,任何修改都会产生一个新的 state
    return new TrackState(newBlame, this.commits,
                          this.uncommittedSteps.concat(inverted),
                          this.uncommittedMaps.concat(transform.mapping.maps))
  }
  // 当一个 transaction 被标记为一个 commit 的时候,下面这个函数用来将所有那些暂未提交的 step 放到下一个提交中去。
  applyCommit(message, time) {
    if (this.uncommittedSteps.length == 0) return this
    let commit = new Commit(message, time, this.uncommittedSteps,
                            this.uncommittedMaps)
    return new TrackState(this.blameMap, this.commits.concat(commit), [], [])
  }
}

插件本身仅仅只是接收所有的 transactions 然后更新自身的 state。当该插件设置了一个 meta 信息到 transaction 的时候,就表示该 transaction 是一个提交 transaction, meta 的属性值即为提交信息:

import {Plugin} from "prosemirror-state"

const trackPlugin = new Plugin({
  state: {
    init(_, instance) {
      return new TrackState([new Span(0, instance.doc.content.size, null)], [], [], [])
    },
    apply(tr, tracked) {
      if (tr.docChanged) tracked = tracked.applyTransform(tr)
      let commitMessage = tr.getMeta(this)
      if (commitMessage) tracked = tracked.applyCommit(commitMessage, new Date(tr.time))
      return tracked
    }
  }
})

像这样一样追踪历史允许各种有用的事情,比如可以搞清楚是谁何时添加了一段内容,或者反转一个独立的提交。

反转一个旧的 setps 需要 rebasing 该 step 到最新 step 之间的所有的 step,这也是下面这个函数所做的工作:

import {Mapping} from "prosemirror-transform"

function revertCommit(commit) {
  let trackState = trackPlugin.getState(state)
  let index = trackState.commits.indexOf(commit)
  // 如果一个提交不在历史操作中,我们就不能反转它
  if (index == -1) return

  // 提交完所有未提交的修改,反转才会被执行
  if (trackState.uncommittedSteps.length)
    return alert("先提交你的修改!")

  // 这是从当前文档初始提交到现在文档的 mapping
  let remap = new Mapping(trackState.commits.slice(index)
                          .reduce((maps, c) => maps.concat(c.maps), []))
  let tr = state.tr
  // 以当前文档为基础,在这个 commit 中构建一个包含所有反转 steps 的 transaction。
  // 这些 step 需要以相反的顺序被应用
  for (let i = commit.steps.length - 1; i >= 0; i--) {
    // mapping 被分隔成不包括当前和之前的 step 的 mapping
    let remapped = commit.steps[i].map(remap.slice(i + 1))
    if (!remapped) continue
    let result = tr.maybeStep(remapped)
    // 如果一个 step 可以被应用,那么添加该 step 的 map 到我们的 mapping 流,这样后续的 step 可以以当前 step 继续 mapping。
    if (result.doc) remap.appendMap(remapped.getMap(), i)
  }
  // 添加一个提交信息,然后 dispatch
  if (tr.docChanged)
    dispatch(tr.setMeta(trackPlugin, `Revert '${commit.message}'`))
}

因为当合并一些改变的时候会导致一些隐藏的冲突,因此复杂的反转步骤可能导致一些不甚直观的结果(尤其是对相同内容先后做不同更改的时候)。 在一个生产环境的应用中,也许应该在检测到这种情况的时候提供给用户一个交互界面,以让用户手动解决冲突。