diff --git a/public/prosemirror.html b/public/prosemirror.html
index 33250b371..aca1d553d 100644
--- a/public/prosemirror.html
+++ b/public/prosemirror.html
@@ -323,32 +323,34 @@
Yorkie.Tree
slice: { content, openStart, openEnd },
} = step;
- // 02-1. Backspace: Level delete
+ // 01. Move: level up/down, move left/right.
// TODO(hackerwins): replaceAround replaces the given range with given gap.
if (stepType === 'replaceAround') {
// root.tree.move(from, to, gapFrom, gapTo);
continue;
}
- // 02-2. Enter Key: Insert Paragraph
+ // 02. Split: split the given range.
if (
stepType === 'replace' &&
openStart &&
openEnd &&
structure
) {
- // TODO(hackerwins): Figure out how many depth to split.
- // root.tree.split(from, 2);
+ root.tree.edit(from, to, docToTreeNode(node.toJSON()), [
+ openStart,
+ openEnd,
+ ]);
continue;
}
- // 02-1. Delete the given range.
+ // 03. Edit: Delete the given range.
if (!content.content.length) {
root.tree.edit(from, to);
continue;
}
- // 03-4. Edit: Insert the given content.
+ // 04. Edit: Replace the given range with the given content.
for (const node of content.content) {
root.tree.edit(from, to, docToTreeNode(node.toJSON()));
}
diff --git a/src/document/crdt/tree.ts b/src/document/crdt/tree.ts
index ad8e7eb56..cd110d098 100644
--- a/src/document/crdt/tree.ts
+++ b/src/document/crdt/tree.ts
@@ -550,9 +550,8 @@ export class CRDTTree extends CRDTGCElement {
/**
* `findFloorNode` finds node of given id.
*/
- private findFloorNode(id: CRDTTreeNodeID) {
+ private findFloorNode(id: CRDTTreeNodeID): CRDTTreeNode | undefined {
const entry = this.nodeMapByID.floorEntry(id);
-
if (!entry || !entry.key.getCreatedAt().equals(id.getCreatedAt())) {
return;
}
@@ -561,66 +560,60 @@ export class CRDTTree extends CRDTGCElement {
}
/**
- * `findNodesAndSplitText` finds `TreePos` of the given `CRDTTreeNodeID` and
- * splits the text node if necessary.
+ * `findNodesAndSplit` finds `TreePos` of the given `CRDTTreeNodeID` and
+ * splits nodes for the given split level.
*
- * `CRDTTreeNodeID` is a position in the CRDT perspective. This is
- * different from `TreePos` which is a position of the tree in the local
- * perspective.
+ * The ids of the given `pos` are the ids of the node in the CRDT perspective.
+ * This is different from `TreePos` which is a position of the tree in the
+ * physical perspective.
*/
- public findNodesAndSplitText(
+ public findNodesAndSplit(
pos: CRDTTreePos,
editedAt: TimeTicket,
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ splitLevel: number,
): [CRDTTreeNode, CRDTTreeNode] {
- const treeNodes = this.toTreeNodes(pos);
-
- if (!treeNodes) {
- throw new Error(`cannot find node at ${pos}`);
- }
- const [parentNode] = treeNodes;
- let [, leftSiblingNode] = treeNodes;
-
- // Find the appropriate position. This logic is similar to the logical to
- // handle the same position insertion of RGA.
-
- if (leftSiblingNode.isText) {
- const absOffset = leftSiblingNode.id.getOffset();
- const split = leftSiblingNode.split(
+ // 01. Find the parent and left sibling node of the given position.
+ const [parent, leftSibling] = this.toTreeNodes(pos);
+ let leftNode = leftSibling;
+
+ // 02. Split nodes for the given split level.
+ if (leftNode.isText) {
+ const absOffset = leftNode.id.getOffset();
+ const split = leftNode.split(
pos.getLeftSiblingID().getOffset() - absOffset,
absOffset,
);
if (split) {
- split.insPrevID = leftSiblingNode.id;
- this.nodeMapByID.put(split.id, split);
-
- if (leftSiblingNode.insNextID) {
- const insNext = this.findFloorNode(leftSiblingNode.insNextID)!;
-
+ split.insPrevID = leftNode.id;
+ if (leftNode.insNextID) {
+ const insNext = this.findFloorNode(leftNode.insNextID)!;
insNext.insPrevID = split.id;
- split.insNextID = leftSiblingNode.insNextID;
+ split.insNextID = leftNode.insNextID;
}
- leftSiblingNode.insNextID = split.id;
+ leftNode.insNextID = split.id;
+
+ this.nodeMapByID.put(split.id, split);
}
}
- const allChildren = parentNode.allChildren;
- const index =
- parentNode === leftSiblingNode
- ? 0
- : allChildren.indexOf(leftSiblingNode) + 1;
+ // 03. Find the appropriate left node. If some nodes are inserted at the
+ // same position concurrently, then we need to find the appropriate left
+ // node. This is similar to RGA.
+ const allChildren = parent.allChildren;
+ const index = parent === leftNode ? 0 : allChildren.indexOf(leftNode) + 1;
- for (let i = index; i < parentNode.allChildren.length; i++) {
+ for (let i = index; i < parent.allChildren.length; i++) {
const next = allChildren[i];
-
- if (next.id.getCreatedAt().after(editedAt)) {
- leftSiblingNode = next;
- } else {
+ if (!next.id.getCreatedAt().after(editedAt)) {
break;
}
+
+ leftNode = next;
}
- return [parentNode, leftSiblingNode];
+ return [parent, leftNode];
}
/**
@@ -631,13 +624,13 @@ export class CRDTTree extends CRDTGCElement {
attributes: { [key: string]: string } | undefined,
editedAt: TimeTicket,
) {
- const [fromParent, fromLeft] = this.findNodesAndSplitText(
+ const [fromParent, fromLeft] = this.findNodesAndSplit(
range[0],
editedAt,
+ 0,
);
- const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt);
+ const [toParent, toLeft] = this.findNodesAndSplit(range[1], editedAt, 0);
const changes: Array = [];
-
changes.push({
type: TreeChangeType.Style,
from: this.toIndex(fromParent, fromLeft),
@@ -670,15 +663,21 @@ export class CRDTTree extends CRDTGCElement {
public edit(
range: [CRDTTreePos, CRDTTreePos],
contents: Array | undefined,
+ splitLevels: [number, number],
editedAt: TimeTicket,
latestCreatedAtMapByActor?: Map,
): [Array, Map] {
// 01. split text nodes at the given range if needed.
- const [fromParent, fromLeft] = this.findNodesAndSplitText(
+ const [fromParent, fromLeft] = this.findNodesAndSplit(
range[0],
editedAt,
+ splitLevels[0],
+ );
+ const [toParent, toLeft] = this.findNodesAndSplit(
+ range[1],
+ editedAt,
+ splitLevels[1],
);
- const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt);
// TODO(hackerwins): If concurrent deletion happens, we need to seperate the
// range(from, to) into multiple ranges.
@@ -803,7 +802,6 @@ export class CRDTTree extends CRDTGCElement {
): void {
const fromIdx = this.toIndex(fromParent, fromLeft);
const toIdx = this.toIndex(toParent, toLeft);
-
return this.indexTree.nodesBetween(fromIdx, toIdx, callback);
}
@@ -814,11 +812,12 @@ export class CRDTTree extends CRDTGCElement {
public editT(
range: [number, number],
contents: Array | undefined,
+ splitLevels: [number, number],
editedAt: TimeTicket,
): void {
const fromPos = this.findPos(range[0]);
const toPos = this.findPos(range[1]);
- this.edit([fromPos, toPos], contents, editedAt);
+ this.edit([fromPos, toPos], contents, splitLevels, editedAt);
}
/**
@@ -926,17 +925,9 @@ export class CRDTTree extends CRDTGCElement {
*/
public pathToPosRange(path: Array): [CRDTTreePos, CRDTTreePos] {
const fromIdx = this.pathToIndex(path);
-
return [this.findPos(fromIdx), this.findPos(fromIdx + 1)];
}
- /**
- * `pathToTreePos` finds the tree position path.
- */
- public pathToTreePos(path: Array): TreePos {
- return this.indexTree.pathToTreePos(path);
- }
-
/**
* `pathToPos` finds the position of the given index in the tree by path.
*/
@@ -1007,8 +998,7 @@ export class CRDTTree extends CRDTGCElement {
*/
public deepcopy(): CRDTTree {
const root = this.getRoot();
- const tree = new CRDTTree(root.deepcopy(), this.getCreatedAt());
- return tree;
+ return new CRDTTree(root.deepcopy(), this.getCreatedAt());
}
/**
@@ -1019,7 +1009,6 @@ export class CRDTTree extends CRDTGCElement {
leftSiblingNode: CRDTTreeNode,
): Array {
const treePos = this.toTreePos(parentNode, leftSiblingNode);
-
if (!treePos) {
return [];
}
@@ -1035,7 +1024,6 @@ export class CRDTTree extends CRDTGCElement {
leftSiblingNode: CRDTTreeNode,
): number {
const treePos = this.toTreePos(parentNode, leftSiblingNode);
-
if (!treePos) {
return -1;
}
@@ -1043,26 +1031,27 @@ export class CRDTTree extends CRDTGCElement {
return this.indexTree.indexOf(treePos);
}
- private toTreeNodes(pos: CRDTTreePos) {
+ /**
+ * `toTreeNodes` converts the given pos to parent and left sibling nodes.
+ */
+ private toTreeNodes(pos: CRDTTreePos): [CRDTTreeNode, CRDTTreeNode] {
const parentID = pos.getParentID();
const leftSiblingID = pos.getLeftSiblingID();
const parentNode = this.findFloorNode(parentID);
- let leftSiblingNode = this.findFloorNode(leftSiblingID);
-
- if (!parentNode || !leftSiblingNode) {
- return [];
+ let leftNode = this.findFloorNode(leftSiblingID);
+ if (!parentNode || !leftNode) {
+ throw new Error(`cannot find node at ${pos}`);
}
if (
leftSiblingID.getOffset() > 0 &&
- leftSiblingID.getOffset() === leftSiblingNode.id.getOffset() &&
- leftSiblingNode.insPrevID
+ leftSiblingID.getOffset() === leftNode.id.getOffset() &&
+ leftNode.insPrevID
) {
- leftSiblingNode =
- this.findFloorNode(leftSiblingNode.insPrevID) || leftSiblingNode;
+ leftNode = this.findFloorNode(leftNode.insPrevID) || leftNode;
}
- return [parentNode, leftSiblingNode!];
+ return [parentNode, leftNode!];
}
/**
@@ -1076,8 +1065,6 @@ export class CRDTTree extends CRDTGCElement {
return;
}
- let treePos;
-
if (parentNode.isRemoved) {
let childNode: CRDTTreeNode;
while (parentNode.isRemoved) {
@@ -1085,40 +1072,36 @@ export class CRDTTree extends CRDTGCElement {
parentNode = childNode.parent!;
}
- const childOffset = parentNode.findOffset(childNode!);
+ const offset = parentNode.findOffset(childNode!);
+ return {
+ node: parentNode,
+ offset,
+ };
+ }
- treePos = {
+ if (parentNode === leftSiblingNode) {
+ return {
node: parentNode,
- offset: childOffset,
+ offset: 0,
};
- } else {
- if (parentNode === leftSiblingNode) {
- treePos = {
- node: parentNode,
- offset: 0,
- };
- } else {
- let offset = parentNode.findOffset(leftSiblingNode);
-
- if (!leftSiblingNode.isRemoved) {
- if (leftSiblingNode.isText) {
- return {
- node: leftSiblingNode,
- offset: leftSiblingNode.paddedSize,
- };
- } else {
- offset++;
- }
- }
+ }
- treePos = {
- node: parentNode,
- offset,
+ let offset = parentNode.findOffset(leftSiblingNode);
+ if (!leftSiblingNode.isRemoved) {
+ if (leftSiblingNode.isText) {
+ return {
+ node: leftSiblingNode,
+ offset: leftSiblingNode.paddedSize,
};
}
+
+ offset++;
}
- return treePos;
+ return {
+ node: parentNode,
+ offset,
+ };
}
/**
@@ -1168,11 +1151,12 @@ export class CRDTTree extends CRDTGCElement {
range: TreePosRange,
timeTicket: TimeTicket,
): [Array, Array] {
- const [fromParent, fromLeft] = this.findNodesAndSplitText(
+ const [fromParent, fromLeft] = this.findNodesAndSplit(
range[0],
timeTicket,
+ 0,
);
- const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket);
+ const [toParent, toLeft] = this.findNodesAndSplit(range[1], timeTicket, 0);
return [this.toPath(fromParent, fromLeft), this.toPath(toParent, toLeft)];
}
@@ -1184,11 +1168,12 @@ export class CRDTTree extends CRDTGCElement {
range: TreePosRange,
timeTicket: TimeTicket,
): [number, number] {
- const [fromParent, fromLeft] = this.findNodesAndSplitText(
+ const [fromParent, fromLeft] = this.findNodesAndSplit(
range[0],
timeTicket,
+ 0,
);
- const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket);
+ const [toParent, toLeft] = this.findNodesAndSplit(range[1], timeTicket, 0);
return [this.toIndex(fromParent, fromLeft), this.toIndex(toParent, toLeft)];
}
diff --git a/src/document/json/tree.ts b/src/document/json/tree.ts
index 2c7204f44..195111570 100644
--- a/src/document/json/tree.ts
+++ b/src/document/json/tree.ts
@@ -153,38 +153,37 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) {
function validateTextNode(textNode: TextNode): boolean {
if (!textNode.value.length) {
throw new Error('text node cannot have empty value');
- } else {
- return true;
}
+
+ return true;
}
/**
* `validateTreeNodes` ensures that treeNodes consists of only one type.
*/
function validateTreeNodes(treeNodes: Array): boolean {
- if (treeNodes.length) {
- const firstTreeNodeType = treeNodes[0].type;
- if (firstTreeNodeType === DefaultTextType) {
- for (const treeNode of treeNodes) {
- const { type } = treeNode;
- if (type !== DefaultTextType) {
- throw new Error(
- 'element node and text node cannot be passed together',
- );
- }
- validateTextNode(treeNode as TextNode);
+ if (!treeNodes.length) {
+ return true;
+ }
+
+ const firstTreeNodeType = treeNodes[0].type;
+ if (firstTreeNodeType === DefaultTextType) {
+ for (const treeNode of treeNodes) {
+ const { type } = treeNode;
+ if (type !== DefaultTextType) {
+ throw new Error('element node and text node cannot be passed together');
}
- } else {
- for (const treeNode of treeNodes) {
- const { type } = treeNode;
- if (type === DefaultTextType) {
- throw new Error(
- 'element node and text node cannot be passed together',
- );
- }
+ validateTextNode(treeNode as TextNode);
+ }
+ } else {
+ for (const treeNode of treeNodes) {
+ const { type } = treeNode;
+ if (type === DefaultTextType) {
+ throw new Error('element node and text node cannot be passed together');
}
}
}
+
return true;
}
@@ -363,11 +362,13 @@ export class Tree {
.filter((a) => a) as Array;
}
+ // TODO(hackerwins): Implement splitLevels.
const [, maxCreatedAtMapByActor] = this.tree!.edit(
[fromPos, toPos],
crdtNodes.length
? crdtNodes.map((crdtNode) => crdtNode?.deepcopy())
: undefined,
+ [0, 0],
ticket,
);
diff --git a/src/document/operation/tree_edit_operation.ts b/src/document/operation/tree_edit_operation.ts
index 086faa955..c9e21f19c 100644
--- a/src/document/operation/tree_edit_operation.ts
+++ b/src/document/operation/tree_edit_operation.ts
@@ -85,9 +85,11 @@ export class TreeEditOperation extends Operation {
logger.fatal(`fail to execute, only Tree can execute edit`);
}
const tree = parentObject as CRDTTree;
+ // TODO(hackerwins): Implement splitLevels.
const [changes] = tree.edit(
[this.fromPos, this.toPos],
this.contents?.map((content) => content.deepcopy()),
+ [0, 0],
this.getExecutedAt(),
this.maxCreatedAtMapByActor,
);
diff --git a/test/integration/tree_test.ts b/test/integration/tree_test.ts
index caa2119be..e34db0f50 100644
--- a/test/integration/tree_test.ts
+++ b/test/integration/tree_test.ts
@@ -263,12 +263,12 @@ describe('Tree', () => {
);
});
- const actualOperations: Array = [];
+ const actualOps: Array = [];
doc.subscribe('$.t', (event) => {
if (event.type === 'local-change') {
const { operations } = event.value;
- actualOperations.push(
+ actualOps.push(
...(operations.filter(
(op) => op.type === 'tree-edit',
) as Array),
@@ -289,7 +289,7 @@ describe('Tree', () => {
});
assert.deepEqual(
- actualOperations.map((it) => {
+ actualOps.map((it) => {
return {
type: it.type,
fromPath: it.fromPath,
diff --git a/test/unit/document/crdt/tree_test.ts b/test/unit/document/crdt/tree_test.ts
index cd10c5535..8bd1a390c 100644
--- a/test/unit/document/crdt/tree_test.ts
+++ b/test/unit/document/crdt/tree_test.ts
@@ -30,6 +30,7 @@ import {
CRDTTreeNodeID,
CRDTTreePos,
toXML,
+ TreeNodeForTest,
} from '@yorkie-js-sdk/src/document/crdt/tree';
/**
@@ -101,13 +102,18 @@ describe('CRDTTree.Edit', function () {
// 1
//
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
assert.equal(t.toXML(), /*html*/ ``);
assert.equal(t.getRoot().size, 2);
// 1
// h e l l o
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'hello')], timeT());
+ t.editT(
+ [1, 1],
+ [new CRDTTreeNode(posT(), 'text', 'hello')],
+ [0, 0],
+ timeT(),
+ );
assert.equal(t.toXML(), /*html*/ `hello
`);
assert.equal(t.getRoot().size, 7);
@@ -115,46 +121,43 @@ describe('CRDTTree.Edit', function () {
// h e l l o
w o r l d
const p = new CRDTTreeNode(posT(), 'p', []);
p.insertAt(new CRDTTreeNode(posT(), 'text', 'world'), 0);
- t.editT([7, 7], [p], timeT());
+ t.editT([7, 7], [p], [0, 0], timeT());
assert.equal(t.toXML(), /*html*/ `hello
world
`);
assert.equal(t.getRoot().size, 14);
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// h e l l o !
w o r l d
- t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '!')], timeT());
+ t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '!')], [0, 0], timeT());
assert.equal(t.toXML(), /*html*/ `hello!
world
`);
- assert.deepEqual(
- JSON.stringify(t.toTestTreeNode()),
- JSON.stringify({
- type: 'r',
- children: [
- {
- type: 'p',
- children: [
- { type: 'text', value: 'hello', size: 5, isRemoved: false },
- { type: 'text', value: '!', size: 1, isRemoved: false },
- ],
- size: 6,
- isRemoved: false,
- },
- {
- type: 'p',
- children: [
- { type: 'text', value: 'world', size: 5, isRemoved: false },
- ],
- size: 5,
- isRemoved: false,
- },
- ],
- size: 15,
- isRemoved: false,
- }),
- );
+ assert.deepEqual(t.toTestTreeNode(), {
+ type: 'r',
+ children: [
+ {
+ type: 'p',
+ children: [
+ { type: 'text', value: 'hello', size: 5, isRemoved: false },
+ { type: 'text', value: '!', size: 1, isRemoved: false },
+ ],
+ size: 6,
+ isRemoved: false,
+ } as TreeNodeForTest,
+ {
+ type: 'p',
+ children: [
+ { type: 'text', value: 'world', size: 5, isRemoved: false },
+ ],
+ size: 5,
+ isRemoved: false,
+ } as TreeNodeForTest,
+ ],
+ size: 15,
+ isRemoved: false,
+ });
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// h e l l o ~ !
w o r l d
- t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '~')], timeT());
+ t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '~')], [0, 0], timeT());
assert.equal(t.toXML(), /*html*/ `hello~!
world
`);
});
@@ -163,10 +166,20 @@ describe('CRDTTree.Edit', function () {
// 0 1 2 3 4 5 6 7 8
// a b
c d
const tree = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- tree.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- tree.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
- tree.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], timeT());
- tree.editT([5, 5], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT());
+ tree.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ tree.editT(
+ [1, 1],
+ [new CRDTTreeNode(posT(), 'text', 'ab')],
+ [0, 0],
+ timeT(),
+ );
+ tree.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ tree.editT(
+ [5, 5],
+ [new CRDTTreeNode(posT(), 'text', 'cd')],
+ [0, 0],
+ timeT(),
+ );
assert.deepEqual(tree.toXML(), /*html*/ `ab
cd
`);
let treeNode = tree.toTestTreeNode();
@@ -177,7 +190,7 @@ describe('CRDTTree.Edit', function () {
// 02. delete b from first paragraph
// 0 1 2 3 4 5 6 7
// a
c d
- tree.editT([2, 3], undefined, timeT());
+ tree.editT([2, 3], undefined, [0, 0], timeT());
assert.deepEqual(tree.toXML(), /*html*/ `a
cd
`);
treeNode = tree.toTestTreeNode();
@@ -194,142 +207,178 @@ describe('CRDTTree.Edit', function () {
// 0 1 2 3 4
// a b
- t.editT([0, 0], [pNode], timeT());
- t.editT([1, 1], [textNode], timeT());
+ t.editT([0, 0], [pNode], [0, 0], timeT());
+ t.editT([1, 1], [textNode], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
// Find the closest index.TreePos when leftSiblingNode in crdt.TreePos is removed.
// 0 1 2
//
- t.editT([1, 3], undefined, timeT());
+ t.editT([1, 3], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ ``);
- let [parent, left] = t.findNodesAndSplitText(
+ let [parent, left] = t.findNodesAndSplit(
new CRDTTreePos(pNode.id, textNode.id),
timeT(),
+ 0,
);
assert.equal(t.toIndex(parent, left), 1);
// Find the closest index.TreePos when parentNode in crdt.TreePos is removed.
// 0
//
- t.editT([0, 2], undefined, timeT());
+ t.editT([0, 2], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ ``);
- [parent, left] = t.findNodesAndSplitText(
+ [parent, left] = t.findNodesAndSplit(
new CRDTTreePos(pNode.id, textNode.id),
timeT(),
+ 0,
);
assert.equal(t.toIndex(parent, left), 0);
});
});
-describe.skip('CRDTTree.Split', function () {
+describe('CRDTTree.Split', function () {
it('Can split text nodes', function () {
// 00. Create a tree with 2 paragraphs.
// 0 1 6 11
// hello world
const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'helloworld')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT(
+ [1, 1],
+ [new CRDTTreeNode(posT(), 'text', 'helloworld')],
+ [0, 0],
+ timeT(),
+ );
+ const expectedIntial = {
+ type: 'root',
+ children: [
+ {
+ type: 'p',
+ children: [
+ { type: 'text', value: 'helloworld', size: 10, isRemoved: false },
+ ],
+ size: 10,
+ isRemoved: false,
+ } as TreeNodeForTest,
+ ],
+ size: 12,
+ isRemoved: false,
+ };
+ assert.deepEqual(t.toTestTreeNode(), expectedIntial);
// 01. Split left side of 'helloworld'.
- // tree.split(1, 1);
- // TODO(JOOHOJANG): make new helper function when implement Tree.split
- //betweenEqual(tree, 1, 11, ['text.helloworld']);
+ t.editT([1, 1], undefined, [0, 0], timeT());
+ assert.deepEqual(t.toTestTreeNode(), expectedIntial);
// 02. Split right side of 'helloworld'.
- // tree.split(11, 1);
- // TODO(JOOHOJANG): make new helper function when implement Tree.split
- //betweenEqual(tree, 1, 11, ['text.helloworld']);
+ t.editT([11, 11], undefined, [0, 0], timeT());
+ assert.deepEqual(t.toTestTreeNode(), expectedIntial);
// 03. Split 'helloworld' into 'hello' and 'world'.
- // tree.split(6, 1);
- // TODO(JOOHOJANG): make new helper function when implement Tree.split
- //betweenEqual(tree, 1, 11, ['text.hello', 'text.world']);
+ t.editT([6, 6], undefined, [0, 0], timeT());
+ assert.deepEqual(t.toTestTreeNode(), {
+ type: 'root',
+ children: [
+ {
+ type: 'p',
+ children: [
+ { type: 'text', value: 'hello', size: 5, isRemoved: false },
+ { type: 'text', value: 'world', size: 5, isRemoved: false },
+ ],
+ size: 10,
+ isRemoved: false,
+ } as TreeNodeForTest,
+ ],
+ size: 12,
+ isRemoved: false,
+ });
});
- it('Can split element nodes', function () {
+ it.skip('Can split element nodes level 1', function () {
+ // 0 1 2 3 4
+ // a b
+
// 01. Split position 1.
let t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- // tree.split(1, 2);
+ t.editT([1, 1], undefined, [1, 1], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
assert.equal(t.getSize(), 6);
// 02. Split position 2.
- // 0 1 2 3 4
- // a b
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- // tree.split(2, 2);
+ t.editT([2, 2], undefined, [1, 1], timeT());
assert.deepEqual(t.toXML(), /*html*/ `a
b
`);
assert.equal(t.getSize(), 6);
// 03. Split position 3.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- // tree.split(3, 2);
+ t.editT([3, 3], undefined, [1, 1], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
assert.equal(t.getSize(), 6);
+ });
- // 04. Split position 3.
- t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT());
- assert.deepEqual(t.toXML(), /*html*/ `abcd
`);
- // tree.split(3, 2);
- assert.deepEqual(t.toXML(), /*html*/ `ab
cd
`);
- assert.equal(t.getSize(), 8);
+ it.skip('Can split element nodes multi-level', function () {
+ // 0 1 2 3 4 5 6
+ // a b
- // 05. Split multiple nodes level 1.
- t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
- assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- // tree.split(3, 1);
+ // 01. Split nodes level 1.
+ let t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- assert.equal(t.getSize(), 6);
+ t.editT([3, 3], undefined, [1, 1], timeT());
+ assert.deepEqual(
+ t.toXML(),
+ /*html*/ `ab
`,
+ );
- // Split multiple nodes level 2.
+ // 02. Split nodes level 2.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- // tree.split(3, 2);
+ t.editT([3, 3], undefined, [2, 2], timeT());
assert.deepEqual(
t.toXML(),
- /*html*/ `ab
`,
+ /*html*/ `a
b
`,
);
- assert.equal(t.getSize(), 8);
- // Split multiple nodes level 3.
+ // Split multiple nodes level 3. But, it is allowed to split only level 2.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
- // tree.split(3, 3);
+ t.editT([3, 3], undefined, [3, 3], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `a
b
`,
);
- assert.equal(t.getSize(), 10);
});
- it('Can split and merge element nodes by edit', function () {
+ it.skip('Can split and merge element nodes by edit', function () {
const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'abcd')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT(
+ [1, 1],
+ [new CRDTTreeNode(posT(), 'text', 'abcd')],
+ [0, 0],
+ timeT(),
+ );
assert.deepEqual(t.toXML(), /*html*/ `abcd
`);
assert.equal(t.getSize(), 6);
@@ -339,7 +388,7 @@ describe.skip('CRDTTree.Split', function () {
assert.deepEqual(t.toXML(), /*html*/ `ab
cd
`);
assert.equal(t.getSize(), 8);
- t.editT([3, 5], undefined, timeT());
+ t.editT([3, 5], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `abcd
`);
assert.equal(t.getSize(), 6);
});
@@ -351,16 +400,16 @@ describe('CRDTTree.Merge', function () {
// 0 1 2 3 4 5 6 7 8
// a b
c d
const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
- t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([5, 5], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
+ t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([5, 5], [new CRDTTreeNode(posT(), 'text', 'cd')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
cd
`);
// 02. delete b, c and the second paragraph.
// 0 1 2 3 4
// a d
- t.editT([2, 6], undefined, timeT());
+ t.editT([2, 6], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ad
`);
const node = t.toTestTreeNode();
@@ -370,7 +419,7 @@ describe('CRDTTree.Merge', function () {
assert.equal(node.children![0].children![1].size, 1); // d
// 03. insert a new text node at the start of the first paragraph.
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', '@')], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', '@')], [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `@ad
`);
});
@@ -379,11 +428,11 @@ describe('CRDTTree.Merge', function () {
// 0 1 2 3 4 5 6 7 8 9 10
// a b
c d
const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
- t.editT([6, 6], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([7, 7], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
+ t.editT([6, 6], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([7, 7], [new CRDTTreeNode(posT(), 'text', 'cd')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
cd
`,
@@ -392,105 +441,111 @@ describe('CRDTTree.Merge', function () {
// 02. delete b, c and second paragraph.
// 0 1 2 3 4 5
// a d
- t.editT([3, 8], undefined, timeT());
+ t.editT([3, 8], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ad
`);
});
it.skip('Can merge different levels with edit', function () {
+ // TODO(hackerwins): Fix this test.
// 01. edit between two element nodes in the same hierarchy.
// 0 1 2 3 4 5 6 7 8
// a b
let t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT());
+ t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
`,
);
- t.editT([5, 6], undefined, timeT());
+ t.editT([5, 6], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
// 02. edit between two element nodes in same hierarchy.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT());
+ t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
`,
);
- t.editT([6, 7], undefined, timeT());
+ t.editT([6, 7], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
// 03. edit between text and element node in same hierarchy.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT());
+ t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
`,
);
- t.editT([4, 6], undefined, timeT());
+ t.editT([4, 6], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `a
`);
// 04. edit between text and element node in same hierarchy.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT());
+ t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
`,
);
- t.editT([5, 7], undefined, timeT());
+ t.editT([5, 7], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `ab
`);
// 05. edit between text and element node in same hierarchy.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT());
+ t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
`,
);
- t.editT([4, 7], undefined, timeT());
+ t.editT([4, 7], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ `a
`);
// 06. edit between text and element node in same hierarchy.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT());
- t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT());
+ t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
`,
);
- t.editT([3, 7], undefined, timeT());
+ t.editT([3, 7], undefined, [0, 0], timeT());
assert.deepEqual(t.toXML(), /*html*/ ``);
// 07. edit between text and element node in same hierarchy.
t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT());
- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT());
- t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([5, 5], [new CRDTTreeNode(posT(), 'b')], timeT());
- t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT());
- t.editT([10, 10], [new CRDTTreeNode(posT(), 'p')], timeT());
- t.editT([11, 11], [new CRDTTreeNode(posT(), 'text', 'ef')], timeT());
+ t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT());
+ t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT([5, 5], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT());
+ t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', 'cd')], [0, 0], timeT());
+ t.editT([10, 10], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT());
+ t.editT(
+ [11, 11],
+ [new CRDTTreeNode(posT(), 'text', 'ef')],
+ [0, 0],
+ timeT(),
+ );
assert.deepEqual(
t.toXML(),
/*html*/ `ab
cd
ef
`,
);
- t.editT([9, 10], undefined, timeT());
+ t.editT([9, 10], undefined, [0, 0], timeT());
assert.deepEqual(
t.toXML(),
/*html*/ `ab
cdef
`,