# Rich Doc
Format for representing rich text documents and changes.

[![Circle CI](https://circleci.com/gh/shimohq/richdoc.svg?style=svg&circle-token=c3f471a1394539d85dec103e87c5404c6a56ea92)](https://circleci.com/gh/shimohq/richdoc)

## 说明
Operator 表示一个修改。每个修改可能是三种类型：`insert`, `remove` 和 `retain`。其中 `insert` 表示插入，`remove` 表示删除，`retain` 表示保留（用来跳过或者修改属性）。

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

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

其中 Delta 包含两个方法：`compose` 和 `transform`。其中 `compose` 用来合并两个 Delta：

```javascript
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))`。举例而言，还是上面的文档：

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

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

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

同时 B 进行了操作，把 World 换成 Tom：

```javascript
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)` 的结果为：

```javascript
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)` 的结果为：

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

而后，`A.compose(AB)` 和 `B.compose(BA)` 都为：

```javascript
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 优先。

## 安装

```javascript
npm install richdoc
```

## 用法

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

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

```javascript
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 可以序列化成字符串方便传输：

```javascript
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。
  
2. 客户端将修改提交给服务端。

  当客户端要将本地修改 Y 发给服务端时，必须保证 X 为空（见下条情况）。此时客户端需要进行下列操作：
  
  1. 将 Y 发送给服务器
  2. 令 X <- Y
  3. 设 Y 为空 Delta
  
3. 客户端收到服务端的确认。

  当服务端收到客户端的修改时（即 Y），服务端会向客户端发送 ACK 响应来确认。此时客户端需要进行下列操作：
  
  1. A <- A + X
  2. 设 X 为空 Delta
  
  之后每 500ms 客户端再次将本地修改 Y 提交给服务端，从而形成循环。
  
4. 收到其他客户端的修改。

  当客户端收到服务端发送来的其他客户端的修改 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)`
