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)

    Install

    npm i richdoc

    DownloadsWeekly Downloads

    12

    Version

    2.0.0

    License

    ISC

    Last publish

    Collaborators

    • luin
    • pychen