richdoc
v2.0.0
Published
Format for representing rich text documents and changes
Downloads
100
Readme
Rich Doc
Format for representing rich text documents and changes.
说明
Operator 表示一个修改。每个修改可能是三种类型:insert
, remove
和 retain
。其中 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 包含两个方法:compose
和 transform
。其中 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 会发生变化:
- 用户在客户端进行了修改操作。
用户对文档进行了修改,产生 Delta E(根据定义,显然 E 是基于 Y 的修改)。客户端此时需要更新 Y,使得 Y <- Y + E。
- 客户端将修改提交给服务端。
当客户端要将本地修改 Y 发给服务端时,必须保证 X 为空(见下条情况)。此时客户端需要进行下列操作:
将 Y 发送给服务器
令 X <- Y
设 Y 为空 Delta
客户端收到服务端的确认。
当服务端收到客户端的修改时(即 Y),服务端会向客户端发送 ACK 响应来确认。此时客户端需要进行下列操作:
- A <- A + X
- 设 X 为空 Delta
之后每 500ms 客户端再次将本地修改 Y 提交给服务端,从而形成循环。
- 收到其他客户端的修改。
当客户端收到服务端发送来的其他客户端的修改 B 时(显然这些修改是基于 A 的),客户端执行如下操作:
- A' <- A + B
- X' <- T(B, X)
- Y' <- T(T(X, B), Y)
- D <- T(Y, T(X, B))
- A <- A'
- X <- X'
- Y <- Y'
- 将 D 应用于当前文档上(并对应修改用户界面)
演化测试
除了单元测试外,可以通过执行 npm run evolution 启动演化测试。程序会自动生成随机文档并不断演化文档,通过如下三个公式测试代码的正确性:
a === a.compose(b).compose(a.invert(b))
a.compose(a.transform(b)) === b.compose(b.transform(a))
pack(a) === unpack(a)