🎁 100 free AI Toolkit licenses – apply by August 15.Learn more

比较文档版本

Available in Team plan

Snapshot Compare 允许你并排查看两个文档版本并突出显示所有更改。使用它来跟踪编辑,审查贡献,并恢复早期状态。

Snapshot Compare 扩展为 快照 添加了额外的功能,允许你可视化比较两个版本之间的更改,以便跟踪添加、删除或修改的内容。这些比较称为 diffs

访问私有注册表

Snapshot Compare 扩展已发布在 Tiptap 的私有 npm 注册表中。通过遵循 私有注册表指南 集成该扩展。如果你已经验证了你的 Tiptap 账号,可以直接进入 #安装

安装

从我们的私有注册表安装扩展:

npm install @tiptap-pro/extension-snapshot-compare

设置

你可以使用以下选项配置 SnapshotCompare 扩展:

设置类型默认值描述
providerTiptapCollabProvidernull协作提供者实例
mapDiffToDecorationsfunction() => {}控制将 diff 映射到装饰以显示其内容

注意,你需要为 TiptapCollabProvider 提供 user 标识符,因为该信息用于优化 diffs。

const provider = new TiptapCollabProvider({
  // ...
  user: 'your user identifier' // 必填!我们使用用户标识符来优化 diffs,因此提供它非常重要。
})

const editor = new Editor({
  // ...
  extensions: [
    // ...
    SnapshotCompare.configure({
      provider,
    }),
  ],
})

使用 mapDiffToDecorations 进行 diff 装饰

该扩展具有默认映射 (defaultMapDiffToDecorations) 来表示 diffs 作为 ProseMirror 装饰。
对于更复杂的集成和控制,你可以使用 mapDiffToDecorations 选项自定义此映射。

示例: 对内联插入应用自定义预定义背景颜色

SnapshotCompare.configure({
  mapDiffToDecorations: ({ diff, tr, editor, defaultMapDiffToDecorations }) => {
    if (diff.type === 'inline-insert') {
      // 返回 ProseMirror 装饰或 null
      return Decoration.inline(
        diff.from,
        diff.to,
        {
          class: 'diff',
          style: {
            backgroundColor: diff.attribution.color.backgroundColor,
          },
        },
        // 将 diff 作为装饰的规格传递,这对于 `extractAttributeChanges` 是必需的
        { diff },
      )
    }

    // 回退到默认映射
    return defaultMapDiffToDecorations({
      diff,
      tr,
      editor,
      attributes: {
        // 为装饰添加自定义属性
        'data-tiptap-user-id': myUserIdMapping[diff.attribution.userId],
      },
    })
  },
})

存储

SnapshotCompare 存储对象包含以下属性:

类型描述
isPreviewingboolean指示 diff 视图是否处于激活状态
diffsDiff[]在 diff 视图中显示的 diffs
previousContentJSONContent | null应用 diff 视图之前的内容

使用 isPreviewing 属性检查 diff 视图是否当前处于激活状态:

if (editor.storage.snapshotCompare.isPreviewing) {
  // diff 视图当前处于激活状态
}

使用 diffs 属性访问在 diff 视图中显示的 diffs:

editor.storage.snapshotCompare.diffs

属性 previousContent 由扩展内部使用,用于在退出 diff 视图时恢复内容。通常,你不需要直接与之交互。

命令

命令描述
compareVersions比较两个版本并在编辑器中渲染 diff
showDiff给定更改跟踪变换,在编辑器中显示 diff
hideDiff隐藏 diff 并恢复之前的内容

compareVersions

使用 compareVersions 命令计算并显示两个文档版本之间的差异。

editor.chain().compareVersions({
  fromVersion: 1,
})

选项

你可以传入额外的选项以更好地控制 diff 过程:

描述
fromVersion比较的起始版本。fromVersiontoVersion 之间的顺序是灵活的
toVersion要比较的结束版本(默认:最新版本)
hydrateUserData向每个用户的更改添加上下文数据
onCompare手动处理 diff 结果
enableDebugging启用详细日志记录以进行故障排查

使用 hydrateUserData 添加元数据

每个 diff 具有 attribution 字段,允许你使用 hydrateUserData 回调函数添加附加元数据。

注意

请注意,userIdTiptapCollabProvider 填充,应该用于识别进行更改的用户。如果提供者未提供 user 字段,则 userId 将为 null有关更多信息,请参见 TiptapCollabProvider 文档

示例: 基于用户为 diffs 着色

const colorMapping = new Map([
  ['user-1', '#ff0000'],
  ['user-2', '#00ff00'],
  ['user-3', '#0000ff'],
])

editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  hydrateUserData: ({ userId }) => {
    return {
      color: {
        backgroundColor: colorMapping.get(userId),
      },
    }
  },
})

editor.storage.snapshotCompare.diffs[0].attribution.color.backgroundColor // '#ff0000'

使用 onCompare 自定义 diff 过程

如果你需要更好地控制 diff 过程,可以使用 onCompare 选项接收结果并自行处理。

示例: 按用户过滤 diffs

editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  onCompare: (ctx) => {
    if (ctx.error) {
      // 处理 diff 过程中的错误
      console.error(ctx.error)
      return
    }

    // 过滤 diffs 以仅显示特定用户的更改
    const diffsToDisplay = ctx.diffSet.filter((diff) => diff.attribution.userId === 'user-1')

    editor.commands.showDiff(ctx.tr, { diffs: diffsToDisplay })
  },
})

showDiff

使用 showDiff 命令在编辑器中显示 diff,使用更改跟踪变换 (tr)。这代表自上一个快照以来对文档所做的所有更改。你可以使用此变换在编辑器中显示 diff。

通常,在使用 compareVersionsonCompare 自定义或过滤 diffs 后使用此命令。

showDiff 命令临时替换当前编辑器内容为 diff 视图,显示版本之间的差异。它还会缓存当前在编辑器中显示的内容,以便你稍后可以恢复它。

// 这将显示在编辑器中记录的更改跟踪变换的变更
editor.commands.showDiff(tr)

选项

你可以传入额外的选项以控制如何显示 diffs:

描述
diffs一个要可视化的 diffs 数组

示例: 显示特定的 diffs

// 这将仅显示由 ID 为 'user-1' 的用户所做的 diffs
const diffsToDisplay = tr.toDiff().filter((diff) => diff.attribution.userId === 'user-1')

editor.commands.showDiff(tr, { diffs: diffsToDisplay })

hideDiff

使用 hideDiff 命令隐藏 diff 并恢复之前的内容。

// 这将隐藏 diff 视图并恢复之前的内容
editor.commands.hideDiff()

添加样式

Snapshot Compare 扩展为 diff 视图的元素应用了类,以帮助你为插入及删除的文本设置样式。请参见本页面上方的代码示例中的完整示例,了解如何为 diff 视图设置样式。

基础 diff 样式

扩展为 diff 元素添加了以下属性:

  • data-diff-type="inline-insert" - 插入的文本
  • data-diff-type="inline-delete" - 删除的文本
  • data-diff-type="inline-update" - 更新的文本
  • data-diff-type="block-insert" - 插入的块
  • data-diff-type="block-delete" - 删除的块

你可以通过 CSS 选择器针对这些带有数据属性的元素进行样式设置。

下面是插入和删除文本的基础样式示例:

/* 当没有用户信息时,退回到红色和绿色的颜色 */

/* 样式插入的文本 */
[data-diff-type='inline-insert'],
[data-diff-type='inline-update'],
[data-diff-type='block-insert'] {
  background-color: green;
}

/* 样式删除的文本 */
[data-diff-type='inline-delete'],
[data-diff-type='block-delete'] {
  background-color: red;
  text-decoration: line-through;
}

重置删除文本的标记样式

当应用了样式标记(比如 粗体斜体代码)时,删除的文本(即应用格式前的文本)会继承这些格式。这是因为删除的文本出现在相应HTML标签内部。为避免此问题,请在删除内容中重置这些标记样式。

首先,在删除的内容内部重置外层标记样式。

/* 为删除内容重置粗体/strong 样式 */
strong {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-weight: normal;
  }
}

/* 为删除内容重置斜体/em 样式 */
em {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-style: normal;
  }
}

/* 为删除内容重置代码/code 样式 */
code {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-family: sans-serif;
  }
}

然后,如果删除内容内部有标记标签,则重新应用这些标记样式,确保格式正确应用。

/* 确保删除内容内的标记样式正常工作 */
[data-diff-type='inline-delete'],
[data-diff-type='block-delete'] {
  strong {
    font-weight: bold;
  }
  em {
    font-style: italic;
  }
  code {
    font-family: monospace;
  }
}

用户归属样式

当有用户归属信息时,你可以根据进行更改的用户来为 diffs 设置样式:

/* 带用户归属的 diffs 样式 */
[data-diff-user-id] {
  position: relative;
}

/* 用于显示用户名的提示信息 */
[data-diff-user-id]::before {
  content: attr(data-diff-user-id);
  position: absolute;
  visibility: hidden;
}

[data-diff-user-id]:hover::before {
  visibility: visible;
}

使用 NodeView (高级)

在使用 自定义节点视图 时,默认的 diff 映射可能无法按预期工作。你可以自定义映射并直接在自定义节点视图中渲染 diffs。

使用 extractAttributeChanges 助手提取节点中的属性更改。这使你能够访问节点的先前和当前属性,从而可能突出显示自定义节点视图中的属性更改。

注意

当将 diffs 映射到装饰时,你需要将 diff 作为装饰的 spec 传递。这对于 extractAttributeChanges 的正常工作是必需的。

示例: 自定义标题节点视图以显示更改

import { extractAttributeChanges } from '@tiptap-pro/extension-snapshot-compare'

const Heading = BaseHeading.extend({
  addNodeView() {
    return ReactNodeViewRenderer(({ node, decorations }) => {
      const { before, after, isDiffing } = extractAttributeChanges(decorations)

      return (
        <NodeViewWrapper style={{ position: 'relative' }}>
          {isDiffing && before.level !== after.level && (
            <span
              style={{
                position: 'absolute',
                right: '100%',
                fontSize: '14px',
                color: '#999',
                backgroundColor: '#f0f0f070',
              }}
              // 显示级别属性的变化
            >
              #<s>{before.level}</s>
              {after.level}
            </span>
          )}
          <NodeViewContent as={`h${node.attrs.level ?? 1}`} />
        </NodeViewWrapper>
      )
    })
  },
})

技术细节

Diff

Diff 是一个表示文档中所做更改的对象。它包含以下属性:

属性类型描述
type'inline-insert' | 'inline-delete' | 'block-insert' | 'block-delete' | 'inline-update' | 'block-update'所做更改的类型
fromnumber更改的起始位置
tonumber更改的结束位置
contentFragment添加或删除的内容
attributionAttribution有关更改的元数据,例如执行更改的用户
attributes?Record<string, any>更改 的属性;仅对 'inline-update''block-update' diffs 可用
previousAttributes?Record<string, any>更改 的属性;仅对 'inline-update''block-update' diffs 可用

DiffSet

DiffSet 是一个 Diff 对象数组,每个对象对应于特定的更改,例如插入、删除或更新。你可以迭代数组以检查单个更改或基于 diff 类型应用自定义逻辑。

const diffsToDisplay = diffSet.filter((diff) => diff.attribution.userId === 'user-1')

// 在编辑器中显示过滤后的 diffs
editor.commands.showDiff(tr, { diffs: diffsToDisplay })

Attribution

Attribution 对象包含有关更改的元数据。它包括以下属性:

属性类型描述
type'added' | 'removed'指示所做更改的类型
userIdstring | undefined执行更改的用户的 ID
idY.ID | undefined执行更改的用户的 Y.js 客户端 ID

你可以扩展 Attribution 接口以包括其他属性:

declare module '@tiptap-pro/extension-snapshot-compare' {
  interface Attribution {
    userName: string
  }
}

ChangeTrackingTransform

ChangeTrackingTransform 是一个记录对文档所做更改的类(基于 ProseMirror 的 Transform)。
它表示一个变换,其步骤描述了从文档的一个版本到另一个版本所做的所有更改。它具有以下属性:

属性类型描述
stepsChangeTrackingStep[]表示对文档所做更改的步骤数组
docNode应用更改后文档的状态
beforeNode应用更改前文档的状态

ChangeTrackingStep

ChangeTrackingStep 是一个基于 ProseMirror 的 ReplaceStep 类表示的类,代表对文档所做的单更改。它具有以下属性:

属性类型描述
attributionAttribution有关更改的元数据,例如执行更改的用户

类型

以下是 SnapshotCompare 扩展的完整 TypeScript 定义:

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    snapshotCompare: {
      /**
       * 给定更改跟踪变换,在编辑器内显示 diff
       */
      showDiff: (tr: ChangeTrackingTransform, options?: { diffs?: DiffSet }) => ReturnType
      /**
       * 隐藏 diff 并恢复之前的内容
       */
      hideDiff: () => ReturnType
      /**
       * 比较两个版本并将 diff 渲染到编辑器中
       */
      compareVersions: <
        T extends Pick<Attribution, 'color'> & Record<string, any> = Pick<Attribution, 'color'> &
          Record<string, any>,
      >(options: {
        /**
         * 开始 diff 的版本
         */
        fromVersion: number
        /**
         * 结束 diff 的版本
         * 如果未提供,将使用最新快照
         */
        toVersion?: number
        /**
         * 允许为每个用户的更改添加上下文数据
         */
        hydrateUserData?: (context: {
          /**
           * 事件的类型
           */
          type: 'added' | 'removed'
          /**
           * 操作此内容的 userId
           */
          userId: string | undefined
          /**
           * 内容的 yjs 标识符
           */
          id?: y.ID
        }) => T
        /**
         * 如果提供,允许自定义渲染 diffs 到编辑器的行为。
         * @note 默认行为是立即显示 diff。
         */
        onCompare?: (
          context:
            | {
                error?: undefined
                /**
                 * 编辑器实例
                 */
                editor: Editor
                /**
                 * 带有归属元数据的变换所有更改
                 */
                tr: ChangeTrackingTransform<Attribution & T>
                /**
                 * 作为 diffs 数组表示的更改
                 */
                diffSet: DiffSet<Attribution & T>
              }
            | {
                error: Error
              },
        ) => void
        /**
         * 详细记录 diffing 过程以帮助追踪错误
         */
        enableDebugging?: boolean
      }) => ReturnType
    }
  }
}

export type SnapshotCompareOptions = {
  /**
   * tiptap 提供者实例。这对于扩展示计算 diffs 是必需的,但不是必需的以显示它们。
   * 也可以传递 TiptapCollabProvider 实例。
   */
  provider: TiptapCollabProvider | null
  /**
   * 这允许你控制将 diff 映射到装饰以显示该 diff 内容的方式
   */
  mapDiffToDecorations?: (options: {
    /**
     * 要映射到装饰的 diff
     */
    diff: Diff
    /**
     * 编辑器实例
     */
    editor: Editor
    /**
     * 更改跟踪变换
     */
    tr: ChangeTrackingTransform
    /**
     * 将 diff 映射到装饰的默认实现
     */
    defaultMapDiffToDecorations: typeof defaultMapDiffToDecorations
  }) => ReturnType<typeof defaultMapDiffToDecorations>
}

export type SnapshotCompareStorageInactive = {
  /**
   * diff 视图当前是否处于激活状态
   */
  isPreviewing: false
  /**
   * diff 视图应用之前的内容
   */
  previousContent: null
  /**
   * 已应用的更改跟踪变换
   * 因为 diff 视图没有激活,它当前是空的
   */
  diffs: DiffSet
  /**
   * 已应用的更改跟踪变换
   */
  tr: null
}

export type SnapshotCompareStorageActive = {
  /**
   * diff 视图当前是否处于激活状态
   */
  isPreviewing: true
  /**
   * diff 视图应用之前的内容
   */
  previousContent: JSONContent
  /**
   * 已应用的更改跟踪变换
   */
  diffs: DiffSet
  /**
   * 已应用的更改跟踪变换
   */
  tr: ChangeTrackingTransform
}

export type SnapshotCompareStorage = SnapshotCompareStorageInactive | SnapshotCompareStorageActive