richdoc

2.0.0 • Public • Published

Rich Doc

Format for representing rich text documents and changes.

Circle CI

说明

Operator 表示一个修改。每个修改可能是三种类型:insert, removeretain。其中 insert 表示插入,remove 表示删除,retain 表示保留(用来跳过或者修改属性)。

Delta 是一组修改的集合,用来表示对一篇文档的修改。当集合中所有的修改都为 insert 时,此 Delta 即可表示文档本身。如下所示的 Delta 表示一篇内容为“Hello World”的文档:

const doc = new Delta([
  new TextOperator({ action: 'insert', data: 'Hello ' }),
  new TextOperator({ action: 'insert', data: 'World', attributes: { bold: true } })
]);

其中 Delta 包含两个方法:composetransform。其中 compose 用来合并两个 Delta:

const doc = new Delta([
  new TextOperator({ action: 'insert', data: 'Hello ' }),
  new TextOperator({ action: 'insert', data: 'World', attributes: { bold: true } })
]);
 
const change = new Delta([
  new TextOperator({ action: 'retain', data: 6 }),
  new TextOperator({ action: 'insert', data: 'Tom' }),
  new TextOperator({ action: 'remove', data: 5 })
]);
 
const updatedDoc = doc.compose(change);
 
// updatedDoc 的结果为:
new Delta([
  new TextOperator({ action: 'insert', data: 'Hello Tom' })
]);

transform 则用于操作变基。如对一篇文档,A 先做了修改并提交到服务器,而 B 也在同一时刻对文档做了修改并提交到服务器。此时服务器先收到 A,后收到 B,且 A 和 B 都是对同一版本做的修改。为了合并这两个操作,需要变换 B 为 B' 使得 A.compose(B') === B.compose(A'),这个变换过程就是通过 transform 实现的,即 A.compose(A.transform(B)) === B.compose(B.transform(A))。举例而言,还是上面的文档:

const doc = new Delta([
  new TextOperator({ action: 'insert', data: 'Hello ' }),
  new TextOperator({ action: 'insert', data: 'World', attributes: { bold: true } })
]);

此时 A 进行了操作,在 Hello 后面加个逗号:

const A = new Delta([
  new TextOperator({ action: 'retain', data: 5 }),
  new TextOperator({ action: 'insert', data: ',' })
]);

同时 B 进行了操作,把 World 换成 Tom:

const B = new Delta([
  new TextOperator({ action: 'retain', data: 6 }),
  new TextOperator({ action: 'insert', data: 'Tom' }),
  new TextOperator({ action: 'remove', data: 5 })
]);

A.transform(B) 的结果为:

const AB = new Delta([
  new TextOperator({ action: 'retain', data: 7 }),
  new TextOperator({ action: 'insert', data: 'Tom' }),
  new TextOperator({ action: 'remove', data: 5 })
]);

B.transform(A) 的结果为:

const BA = new Delta([
  new TextOperator({ action: 'retain', data: 5 }),
  new TextOperator({ action: 'insert', data: ',' })
]);

而后,A.compose(AB)B.compose(BA) 都为:

new Delta([
  new TextOperator({ action: 'retain', data: 5 }),
  new TextOperator({ action: 'insert', data: ',' }),
  new TextOperator({ action: 'retain', data: 1 }),
  new TextOperator({ action: 'insert', data: 'Tom' }),
  new TextOperator({ action: 'remove', data: 5 })
]);

有种特殊情况是,A 和 B 同时在同一位置插入了内容,这时就要确定谁的内容放在前面,为此 transform 接受第二个参数,表示操作优先级。公式为 A.compose(A.transform(B, true)) === B.compose(B.transform(A, false))。为了统一起见,服务端先收到的 Delta 优先。

安装

npm install richdoc

用法

import { Delta, TableOperator, Operator, Operator, TextOperator } from 'richdoc';

举例来说,对于一篇只有一个 3x3 表格的文档,其中单元格 A1 有“Hello World”几个字,可以表示为:

const doc = new Delta([
  new TableOperator({
    action: 'insert',
    data: {
      rows: new Delta([
        new Operator({ action: 'insert', data: 3 })
      ]),
      cols: new Delta([
        new Operator({ action: 'insert', data: 1 }),
        new Operator({ action: 'insert', data: 1, attributes: { width: 50 } }),
        new Operator({ action: 'insert', data: 1 })
      ]),
      cells: {
        A1: new CellOperator({
          action: 'insert',
          data: new Delta([
            new TextOperator({ action: 'insert', data: 'Hello ' }),
            new TextOperator({ action: 'insert', data: 'World', attributes: { bold: true } })
          ])
        })
      }
    }
  })
]);

序列化

Delta 可以序列化成字符串方便传输:

import { pack, unpack } from 'richdoc';
 
const packed = pack(delta);
const unpacked = unpack(packed);

变化流程

为了方便叙述,这里定义两个伪函数:T(A, B) = A.transform(B), A + B = A.compose(B)。可以得出:A + T(A, B) = B + T(B, A)

客户端

客户端保存有三个 Delta,A, X 和 Y,其中:

  • A 表示当前客户端已知的服务端的最新版本的文档;
  • X 表示当前客户端已经提交给服务端,但是没有收到服务端确认的修改;
  • Y 表示当前客户端本地的修改,还没有提交到服务端。

当发生下列情况时,此三个 Delta 会发生变化:

  1. 用户在客户端进行了修改操作。

用户对文档进行了修改,产生 Delta E(根据定义,显然 E 是基于 Y 的修改)。客户端此时需要更新 Y,使得 Y <- Y + E。

  1. 客户端将修改提交给服务端。

当客户端要将本地修改 Y 发给服务端时,必须保证 X 为空(见下条情况)。此时客户端需要进行下列操作:

  1. 将 Y 发送给服务器

  2. 令 X <- Y

  3. 设 Y 为空 Delta

  4. 客户端收到服务端的确认。

当服务端收到客户端的修改时(即 Y),服务端会向客户端发送 ACK 响应来确认。此时客户端需要进行下列操作:

  1. A <- A + X
  2. 设 X 为空 Delta

之后每 500ms 客户端再次将本地修改 Y 提交给服务端,从而形成循环。

  1. 收到其他客户端的修改。

当客户端收到服务端发送来的其他客户端的修改 B 时(显然这些修改是基于 A 的),客户端执行如下操作:

  1. A' <- A + B
  2. X' <- T(B, X)
  3. Y' <- T(T(X, B), Y)
  4. D <- T(Y, T(X, B))
  5. A <- A'
  6. X <- X'
  7. Y <- Y'
  8. 将 D 应用于当前文档上(并对应修改用户界面)

演化测试

除了单元测试外,可以通过执行 npm run evolution 启动演化测试。程序会自动生成随机文档并不断演化文档,通过如下三个公式测试代码的正确性:

  1. a === a.compose(b).compose(a.invert(b))
  2. a.compose(a.transform(b)) === b.compose(b.transform(a))
  3. pack(a) === unpack(a)

Package Sidebar

Install

npm i richdoc

Weekly Downloads

9

Version

2.0.0

License

ISC

Last publish

Collaborators

  • luin
  • pychen