From 0fbf4f231bc80bcd866c7b0102b78bd1fa989985 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Thu, 31 Aug 2023 18:08:23 +0900 Subject: [PATCH 01/21] Add quill example for two clients test --- public/quill-two-clients.html | 323 ++++++++++++++++++++++++++++++++++ public/style.css | 23 ++- 2 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 public/quill-two-clients.html diff --git a/public/quill-two-clients.html b/public/quill-two-clients.html new file mode 100644 index 000000000..fd972495d --- /dev/null +++ b/public/quill-two-clients.html @@ -0,0 +1,323 @@ + + + + + + Yorkie + Quill Two Clients Example + + + + + + + + +
+
+
+ Client A : + +
+
+
+
+
+ Client B : + +
+
+
+
+
+ + + + + + diff --git a/public/style.css b/public/style.css index eb1dfcd29..24c705218 100644 --- a/public/style.css +++ b/public/style.css @@ -66,13 +66,15 @@ button { font-size: 1rem; } -#document:before { +#document:before, +.document:before { display: block; content: 'document: '; font-size: 1rem; } -#document-text:before { +#document-text:before, +.document-text:before { display: block; content: 'text: '; font-size: 1rem; @@ -81,9 +83,12 @@ button { #network-status, #peers, #document, -#document-text { +.document, +#document-text, +.document-text { margin: 1rem 0; font-family: monospace; + overflow: scroll; } .ql-editor { @@ -162,6 +167,18 @@ button { border: none; outline: none; } +.client-container { + display: flex; + width: 95%; +} +.client-container .ql-container { + height: auto; +} +#client-a, +#client-b { + width: 50%; + margin: 15px; +} #editor { width: 80%; margin: 30px 10px; From b48f364a8150b9a310c1ed53fc4053e729586b1c Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 8 Sep 2023 18:52:00 +0900 Subject: [PATCH 02/21] Add testcases related bold mark --- test/integration/text_test.ts | 52 +++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/test/integration/text_test.ts b/test/integration/text_test.ts index eee70b8d2..e1df6a17d 100644 --- a/test/integration/text_test.ts +++ b/test/integration/text_test.ts @@ -444,7 +444,8 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - it('ex2. concurrent formatting and insertion', async function () { + // TODO(MoonGyu1): remove skip after applying mark operation + it.skip('ex2. concurrent formatting and insertion', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -474,15 +475,9 @@ describe('peri-text example: text concurrent edit', function () { await c1.sync(); assert.equal( d1.toSortedJSON(), - '{"k1":[{"attrs":{"bold":true},"val":"The "},{"val":"brown "},{"attrs":{"bold":true},"val":"fox jumped."}]}', + '{"k1":[{"attrs":{"bold":true},"val":"The "},{"attrs":{"bold":true},"val":"brown "},{"attrs":{"bold":true},"val":"fox jumped."}]}', 'd1', ); - // TODO(MoonGyu1): d1 and d2 should have the result below after applying mark operation - // assert.equal( - // d1.toSortedJSON(), - // '{"k1":[{"attrs":{"bold":true},"val":"The "},{"attrs":{"bold":true},"val":"brown "},{"attrs":{"bold":true},"val":"fox jumped."}]}', - // 'd1', - // ); assert.equal(d2.toSortedJSON(), d1.toSortedJSON()); }, this.test!.title); }); @@ -598,7 +593,8 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - it('ex6. conflicting overlaps(bold) - 1', async function () { + // TODO(MoonGyu1): remove skip after applying mark operation + it.skip('ex6. conflicting overlaps(bold) - 1', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -617,7 +613,7 @@ describe('peri-text example: text concurrent edit', function () { `{"k1":[{"attrs":{"bold":true},"val":"The fox jumped."}]}`, ); d1.update((root) => { - root.k1.setStyle(4, 15, { bold: false }); + root.k1.removeStyle(4, 15, { bold: false }); }, `non-bolds text by c1`); assert.equal( d1.toSortedJSON(), @@ -642,7 +638,8 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - it('ex6. conflicting overlaps(bold) - 2', async function () { + // TODO(MoonGyu1): remove skip after applying mark operation + it.skip('ex6. conflicting overlaps(bold) - 2', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -661,7 +658,7 @@ describe('peri-text example: text concurrent edit', function () { `{"k1":[{"attrs":{"bold":true},"val":"The fox jumped."}]}`, ); d1.update((root) => { - root.k1.setStyle(4, 15, { bold: false }); + root.k1.removeStyle(4, 15, { bold: false }); }, `non-bolds text by c1`); assert.equal( d1.toSortedJSON(), @@ -724,7 +721,8 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - it('ex8. text insertion at span boundaries(bold)', async function () { + // TODO(MoonGyu1): remove skip after applying mark operation + it.skip('ex8. text insertion at span boundaries(bold)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -761,7 +759,7 @@ describe('peri-text example: text concurrent edit', function () { // That is, the text inserted before the bold span becomes non-bold, and the text inserted after the bold span becomes bold. assert.equal( d1.toSortedJSON(), - '{"k1":[{"val":"The "},{"val":"quick "},{"attrs":{"bold":true},"val":"fox jumped"},{"val":" over the dog"},{"val":"."}]}', + '{"k1":[{"val":"The "},{"val":"quick "},{"attrs":{"bold":true},"val":"fox jumped"},{"attrs":{"bold":true},"val":" over the dog"},{"val":"."}]}', 'd1', ); assert.equal(d2.toSortedJSON(), d1.toSortedJSON(), 'd2'); @@ -811,3 +809,29 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); }); + +describe('Style', function () { + // TODO(MoonGyu1): remove skip after applying mark operation + it.skip('should handle style operations', function () { + const doc = new Document<{ k1: Text }>('test-doc'); + assert.equal('{}', doc.toSortedJSON()); + + doc.update((root) => { + root.k1 = new Text(); + root.k1.edit(0, 0, 'ABCD'); + root.k1.removeStyle(0, 4, { bold: true }); + }); + assert.equal( + doc.toSortedJSON(), + `{"k1":[{"attrs":{"bold":"true"},"val":"ABCD"}]}`, + ); + + doc.update((root) => { + root.k1.removeStyle(1, 3, { bold: false }); + }); + assert.equal( + doc.toSortedJSON(), + `{"k1":[{"attrs":{"bold":"true"},"val":"A"},{"attrs":{"bold":"false"},"val":"BC"},{"attrs":{"bold":"true"},"val":"D"}]}`, + ); + }); +}); From 67a23ea1ee11d544e9eec2242a95dfa750c0afad Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Mon, 11 Sep 2023 18:30:21 +0900 Subject: [PATCH 03/21] Add to-do list --- src/api/converter.ts | 8 ++++++-- src/document/crdt/rga_tree_split.ts | 13 +++++++++++++ src/document/crdt/text.ts | 9 +++++++++ src/document/json/text.ts | 12 ++++++++++++ src/document/operation/operation.ts | 4 ++++ .../operation/remove_style_operation.ts | 17 +++++++++++++++++ src/document/operation/style_operation.ts | 4 ++++ src/yorkie.ts | 1 + 8 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/document/operation/remove_style_operation.ts diff --git a/src/api/converter.ts b/src/api/converter.ts index 689c81fd9..4476ec071 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -369,7 +369,9 @@ function toOperation(operation: Operation): PbOperation { toTimeTicket(styleOperation.getExecutedAt()), ); pbOperation.setStyle(pbStyleOperation); - } else if (operation instanceof IncreaseOperation) { + } + // TODO(MoonGyu1): Peritext 1. Add RemoveStyleOperation + else if (operation instanceof IncreaseOperation) { const increaseOperation = operation as IncreaseOperation; const pbIncreaseOperation = new PbOperation.Increase(); pbIncreaseOperation.setParentCreatedAt( @@ -1093,7 +1095,9 @@ function fromOperations(pbOperations: Array): Array { attributes, fromTimeTicket(pbStyleOperation!.getExecutedAt())!, ); - } else if (pbOperation.hasSelect()) { + } + // TODO(MoonGyu1): Peritext 1. Add RemoveStyleOperation + else if (pbOperation.hasSelect()) { // TODO(hackerwins): Select is deprecated. continue; } else if (pbOperation.hasIncrease()) { diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 9a94e8290..044841323 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -238,8 +238,12 @@ export class RGATreeSplitPos { } } +// TODO(MoonGyu1): Peritext 1. Add RGATreeSplitBoundary class + export type RGATreeSplitPosRange = [RGATreeSplitPos, RGATreeSplitPos]; +// TODO(MoonGyu1): Peritext 1. Add RGATreeSplitBoundaryRange + /** * `RGATreeSplitNode` is a node of RGATreeSplit. */ @@ -254,6 +258,8 @@ export class RGATreeSplitNode< private insPrev?: RGATreeSplitNode; private insNext?: RGATreeSplitNode; + // TODO(MoonGyu1): Peritext 1. add `styleOpsBefore` and `styleOpsAfter` + constructor(id: RGATreeSplitNodeID, value?: T, removedAt?: TimeTicket) { super(value!); this.id = id; @@ -355,6 +361,8 @@ export class RGATreeSplitNode< return this.insPrev!.getID(); } + // TODO(MoonGyu1): Peritext 2. Add getter of styleOpsBefore/styleOpsAfter + /** * `setPrev` sets previous node of this node. */ @@ -395,6 +403,8 @@ export class RGATreeSplitNode< } } + // TODO(MoonGyu1): Peritext 2. Add setter of styleOpsBefore/styleOpsAfter + /** * `hasNext` checks if next node exists. */ @@ -761,6 +771,9 @@ export class RGATreeSplit { return [node, node.getNext()!]; } + // TODO(MoonGyu1): Peritext 1. Add `splitNodeByBoundaryPos` method + // TODO(MoonGyu1): It can be optimized later + private findFloorNodePreferToLeft( id: RGATreeSplitNodeID, ): RGATreeSplitNode { diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index b998aef1b..766772424 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -235,12 +235,16 @@ export class CRDTText extends CRDTGCElement { * @internal */ public setStyle( + // TODO(MoonGyu1): Peritext 1. Use RGATreeSplitBoundaryRange range: RGATreeSplitPosRange, attributes: Record, editedAt: TimeTicket, latestCreatedAtMapByActor?: Map, ): [Map, Array>] { // 01. split nodes with from and to + + // TODO(MoonGyu1): Peritext 1. Split node by NodeID of RGATreeSplitBoundaryRange if it is remote operation + // TODO(MoonGyu1): Peritext 1. Use splitNodeByBoundaryPos method const [, toRight] = this.rgaTreeSplit.findNodeWithSplit(range[1], editedAt); const [, fromRight] = this.rgaTreeSplit.findNodeWithSplit( range[0], @@ -250,6 +254,10 @@ export class CRDTText extends CRDTGCElement { // 02. style nodes between from and to const changes: Array> = []; const nodes = this.rgaTreeSplit.findBetween(fromRight, toRight); + + // TODO(MoonGyu1): Peritext 2. Update styleOpsBefore/styleOpsAfter of fromRight/toRight nodes + // if markType is `bold` else keep existing logic below + const createdAtMapByActor = new Map(); const toBeStyleds: Array> = []; @@ -335,6 +343,7 @@ export class CRDTText extends CRDTGCElement { const json = []; for (const node of this.rgaTreeSplit) { + // TODO(MoonGyu1): Peritext 3. Convert operations to attrs and pass as argument of toJSON if (!node.isRemoved()) { json.push(node.getValue().toJSON()); } diff --git a/src/document/json/text.ts b/src/document/json/text.ts index da653ad69..24d26a29a 100644 --- a/src/document/json/text.ts +++ b/src/document/json/text.ts @@ -51,10 +51,12 @@ export type TextPosStructRange = [TextPosStruct, TextPosStruct]; export class Text { private context?: ChangeContext; private text?: CRDTText; + // TODO(MoonGyu1): Peritext 1. Add markType for `bold` constructor(context?: ChangeContext, text?: CRDTText) { this.context = context; this.text = text; + // TODO(MoonGyu1): Peritext 1. initialize markType for `bold` } /** @@ -64,6 +66,7 @@ export class Text { public initialize(context: ChangeContext, text: CRDTText): void { this.context = context; this.text = text; + // TODO(MoonGyu1): Peritext 1. initialize markType for `bold` } /** @@ -163,13 +166,19 @@ export class Text { ); } + // TODO(MoonGyu1): Peritext 1. Split node and get start/end node considering markType by PosRange + // TODO(MoonGyu1): Peritext 1. Change from node to RGATreeSplitBoundaryRange + const attrs = stringifyObjectValues(attributes); const ticket = this.context.issueTimeTicket(); + + // TODO(MoonGyu1): Peritext 1. Use RGATreeSplitBoundaryRange const [maxCreatedAtMapByActor] = this.text.setStyle(range, attrs, ticket); this.context.push( new StyleOperation( this.text.getCreatedAt(), + // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` range[0], range[1], maxCreatedAtMapByActor, @@ -181,6 +190,9 @@ export class Text { return true; } + // TODO(MoonGyu1): Peritext 1. Add removeStyle method + removeStyle(fromIdx: number, toIdx: number, attributes: A) {} + /** * `indexRangeToPosRange` returns TextRangeStruct of the given index range. */ diff --git a/src/document/operation/operation.ts b/src/document/operation/operation.ts index c77b6ad86..2d4cdd0e5 100644 --- a/src/document/operation/operation.ts +++ b/src/document/operation/operation.ts @@ -34,6 +34,8 @@ export type OperationInfo = /** * `TextOperationInfo` represents the OperationInfo for the yorkie.Text. */ + +// TODO(MoonGyu1): Peritext 1. Add RemoveStyleOpInfo export type TextOperationInfo = EditOpInfo | StyleOpInfo; /** @@ -130,6 +132,8 @@ export type StyleOpInfo = { }; }; +// TODO(MoonGyu1): Peritext 1. Add RemoveStyleOpInfo + /** * `TreeEditOpInfo` represents the information of the tree edit operation. */ diff --git a/src/document/operation/remove_style_operation.ts b/src/document/operation/remove_style_operation.ts new file mode 100644 index 000000000..6af4154cf --- /dev/null +++ b/src/document/operation/remove_style_operation.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO(MoonGyu1): Peritext 1. Add RemoveStyleOperation diff --git a/src/document/operation/style_operation.ts b/src/document/operation/style_operation.ts index 675a0d36e..5073d10b2 100644 --- a/src/document/operation/style_operation.ts +++ b/src/document/operation/style_operation.ts @@ -29,6 +29,7 @@ import { Indexable } from '../document'; * `StyleOperation` is an operation applies the style of the given range to Text. */ export class StyleOperation extends Operation { + // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` private fromPos: RGATreeSplitPos; private toPos: RGATreeSplitPos; private maxCreatedAtMapByActor: Map; @@ -36,6 +37,7 @@ export class StyleOperation extends Operation { constructor( parentCreatedAt: TimeTicket, + // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` fromPos: RGATreeSplitPos, toPos: RGATreeSplitPos, maxCreatedAtMapByActor: Map, @@ -54,6 +56,7 @@ export class StyleOperation extends Operation { */ public static create( parentCreatedAt: TimeTicket, + // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` fromPos: RGATreeSplitPos, toPos: RGATreeSplitPos, maxCreatedAtMapByActor: Map, @@ -83,6 +86,7 @@ export class StyleOperation extends Operation { } const text = parentObject as CRDTText; const [, changes] = text.setStyle( + // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` [this.fromPos, this.toPos], this.attributes ? Object.fromEntries(this.attributes) : {}, this.getExecutedAt(), diff --git a/src/yorkie.ts b/src/yorkie.ts index a6627905d..09767b6ce 100644 --- a/src/yorkie.ts +++ b/src/yorkie.ts @@ -72,6 +72,7 @@ export type { MoveOpInfo, EditOpInfo, StyleOpInfo, + // TODO(MoonGyu1): Peritext 1. Add RemoveStyleOpInfo } from '@yorkie-js-sdk/src/document/operation/operation'; export { From 79c4f4112cedc47d4ab445cce8772fde1a3bbe5a Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 15 Sep 2023 10:43:38 +0900 Subject: [PATCH 04/21] Update protocol --- src/api/converter.ts | 77 +++++- src/api/yorkie/v1/resources.proto | 17 +- src/api/yorkie/v1/resources_pb.d.ts | 46 +++- src/api/yorkie/v1/resources_pb.js | 280 ++++++++++++++++++++-- src/document/operation/style_operation.ts | 44 ++-- 5 files changed, 404 insertions(+), 60 deletions(-) diff --git a/src/api/converter.ts b/src/api/converter.ts index 4476ec071..648f08a59 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -47,6 +47,8 @@ import { CRDTArray } from '@yorkie-js-sdk/src/document/crdt/array'; import { CRDTTreePos } from './../document/crdt/tree'; import { RGATreeSplit, + RGATreeSplitBoundary, + BoundaryType, RGATreeSplitNode, RGATreeSplitNodeID, RGATreeSplitPos, @@ -73,6 +75,8 @@ import { TextNode as PbTextNode, TextNodeID as PbTextNodeID, TextNodePos as PbTextNodePos, + TextNodeBoundary as PbTextNodeBoundary, + BoundaryType as PbBoundaryType, TimeTicket as PbTimeTicket, TreeNode as PbTreeNode, TreeNodes as PbTreeNodes, @@ -257,6 +261,32 @@ function toTextNodePos(pos: RGATreeSplitPos): PbTextNodePos { return pbTextNodePos; } +/** + * `toTextNodeBoundary` converts the given model to Protobuf format. + */ +function toTextNodeBoundary( + boundary: RGATreeSplitBoundary | undefined, +): PbTextNodeBoundary { + const pbTextNodeBoundary = new PbTextNodeBoundary(); + + pbTextNodeBoundary.setCreatedAt( + toTimeTicket(boundary?.getID()?.getCreatedAt()), + ); + pbTextNodeBoundary.setOffset(boundary?.getID()?.getOffset() ?? 0); + + switch (boundary?.getType()) { + case BoundaryType.Before: + pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_BEFORE); + case BoundaryType.After: + pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_AFTER); + case BoundaryType.Start: + pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_START); + case BoundaryType.End: + pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_END); + } + return pbTextNodeBoundary; +} + /** * `toTreePos` converts the given model to Protobuf format. */ @@ -355,8 +385,10 @@ function toOperation(operation: Operation): PbOperation { pbStyleOperation.setParentCreatedAt( toTimeTicket(styleOperation.getParentCreatedAt()), ); - pbStyleOperation.setFrom(toTextNodePos(styleOperation.getFromPos())); - pbStyleOperation.setTo(toTextNodePos(styleOperation.getToPos())); + pbStyleOperation.setFrom( + toTextNodeBoundary(styleOperation.getFromBoundary()), + ); + pbStyleOperation.setTo(toTextNodeBoundary(styleOperation.getToBoundary())); const pbCreatedAtMapByActor = pbStyleOperation.getCreatedAtMapByActorMap(); for (const [key, value] of styleOperation.getMaxCreatedAtMapByActor()) { pbCreatedAtMapByActor.set(key, toTimeTicket(value)!); @@ -369,9 +401,7 @@ function toOperation(operation: Operation): PbOperation { toTimeTicket(styleOperation.getExecutedAt()), ); pbOperation.setStyle(pbStyleOperation); - } - // TODO(MoonGyu1): Peritext 1. Add RemoveStyleOperation - else if (operation instanceof IncreaseOperation) { + } else if (operation instanceof IncreaseOperation) { const increaseOperation = operation as IncreaseOperation; const pbIncreaseOperation = new PbOperation.Increase(); pbIncreaseOperation.setParentCreatedAt( @@ -888,6 +918,35 @@ function fromTextNodePos(pbTextNodePos: PbTextNodePos): RGATreeSplitPos { ); } +/** + * `fromTextNodeBoundary` converts the given Protobuf format to model format. + */ +function fromTextNodeBoundary( + pbTextNodeBoundary: PbTextNodeBoundary, +): RGATreeSplitBoundary { + let boundaryType: BoundaryType | undefined; + + switch (pbTextNodeBoundary.getType()) { + case PbBoundaryType.BOUNDARY_TYPE_BEFORE: + boundaryType = BoundaryType.Before; + case PbBoundaryType.BOUNDARY_TYPE_AFTER: + boundaryType = BoundaryType.After; + case PbBoundaryType.BOUNDARY_TYPE_START: + boundaryType = BoundaryType.Start; + case PbBoundaryType.BOUNDARY_TYPE_END: + boundaryType = BoundaryType.End; + default: + boundaryType = undefined; + } + return RGATreeSplitBoundary.of( + RGATreeSplitNodeID.of( + fromTimeTicket(pbTextNodeBoundary.getCreatedAt())!, + pbTextNodeBoundary.getOffset(), + ), + boundaryType, + ); +} + /** * `fromTextNodeID` converts the given Protobuf format to model format. */ @@ -1089,15 +1148,13 @@ function fromOperations(pbOperations: Array): Array { }); operation = StyleOperation.create( fromTimeTicket(pbStyleOperation!.getParentCreatedAt())!, - fromTextNodePos(pbStyleOperation!.getFrom()!), - fromTextNodePos(pbStyleOperation!.getTo()!), + fromTextNodeBoundary(pbStyleOperation!.getFrom()!), + fromTextNodeBoundary(pbStyleOperation!.getTo()!), createdAtMapByActor, attributes, fromTimeTicket(pbStyleOperation!.getExecutedAt())!, ); - } - // TODO(MoonGyu1): Peritext 1. Add RemoveStyleOperation - else if (pbOperation.hasSelect()) { + } else if (pbOperation.hasSelect()) { // TODO(hackerwins): Select is deprecated. continue; } else if (pbOperation.hasIncrease()) { diff --git a/src/api/yorkie/v1/resources.proto b/src/api/yorkie/v1/resources.proto index 75947b1df..7e5bec2e3 100644 --- a/src/api/yorkie/v1/resources.proto +++ b/src/api/yorkie/v1/resources.proto @@ -107,8 +107,8 @@ message Operation { } message Style { TimeTicket parent_created_at = 1; - TextNodePos from = 2; - TextNodePos to = 3; + TextNodeBoundary from = 2; + TextNodeBoundary to = 3; map attributes = 4; TimeTicket executed_at = 5; map created_at_map_by_actor = 6; @@ -331,6 +331,19 @@ message TextNodePos { int32 relative_offset = 3; } +enum BoundaryType { + BOUNDARY_TYPE_BEFORE = 0; + BOUNDARY_TYPE_AFTER = 1; + BOUNDARY_TYPE_START = 2; + BOUNDARY_TYPE_END = 3; +} + +message TextNodeBoundary { + TimeTicket created_at = 1; + int32 offset = 2; + BoundaryType type = 3; +} + message TimeTicket { int64 lamport = 1 [jstype = JS_STRING]; uint32 delimiter = 2; diff --git a/src/api/yorkie/v1/resources_pb.d.ts b/src/api/yorkie/v1/resources_pb.d.ts index 3f1c314c7..0cf39e6a3 100644 --- a/src/api/yorkie/v1/resources_pb.d.ts +++ b/src/api/yorkie/v1/resources_pb.d.ts @@ -461,13 +461,13 @@ export namespace Operation { hasParentCreatedAt(): boolean; clearParentCreatedAt(): Style; - getFrom(): TextNodePos | undefined; - setFrom(value?: TextNodePos): Style; + getFrom(): TextNodeBoundary | undefined; + setFrom(value?: TextNodeBoundary): Style; hasFrom(): boolean; clearFrom(): Style; - getTo(): TextNodePos | undefined; - setTo(value?: TextNodePos): Style; + getTo(): TextNodeBoundary | undefined; + setTo(value?: TextNodeBoundary): Style; hasTo(): boolean; clearTo(): Style; @@ -493,8 +493,8 @@ export namespace Operation { export namespace Style { export type AsObject = { parentCreatedAt?: TimeTicket.AsObject, - from?: TextNodePos.AsObject, - to?: TextNodePos.AsObject, + from?: TextNodeBoundary.AsObject, + to?: TextNodeBoundary.AsObject, attributesMap: Array<[string, string]>, executedAt?: TimeTicket.AsObject, createdAtMapByActorMap: Array<[string, TimeTicket.AsObject]>, @@ -1536,6 +1536,34 @@ export namespace TextNodePos { } } +export class TextNodeBoundary extends jspb.Message { + getCreatedAt(): TimeTicket | undefined; + setCreatedAt(value?: TimeTicket): TextNodeBoundary; + hasCreatedAt(): boolean; + clearCreatedAt(): TextNodeBoundary; + + getOffset(): number; + setOffset(value: number): TextNodeBoundary; + + getType(): BoundaryType; + setType(value: BoundaryType): TextNodeBoundary; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): TextNodeBoundary.AsObject; + static toObject(includeInstance: boolean, msg: TextNodeBoundary): TextNodeBoundary.AsObject; + static serializeBinaryToWriter(message: TextNodeBoundary, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): TextNodeBoundary; + static deserializeBinaryFromReader(message: TextNodeBoundary, reader: jspb.BinaryReader): TextNodeBoundary; +} + +export namespace TextNodeBoundary { + export type AsObject = { + createdAt?: TimeTicket.AsObject, + offset: number, + type: BoundaryType, + } +} + export class TimeTicket extends jspb.Message { getLamport(): string; setLamport(value: string): TimeTicket; @@ -1586,6 +1614,12 @@ export namespace DocEvent { } } +export enum BoundaryType { + BOUNDARY_TYPE_BEFORE = 0, + BOUNDARY_TYPE_AFTER = 1, + BOUNDARY_TYPE_START = 2, + BOUNDARY_TYPE_END = 3, +} export enum ValueType { VALUE_TYPE_NULL = 0, VALUE_TYPE_BOOLEAN = 1, diff --git a/src/api/yorkie/v1/resources_pb.js b/src/api/yorkie/v1/resources_pb.js index 841faa4fa..412fe75c9 100644 --- a/src/api/yorkie/v1/resources_pb.js +++ b/src/api/yorkie/v1/resources_pb.js @@ -19,6 +19,7 @@ var google_protobuf_timestamp_pb = require('google-protobuf/google/protobuf/time goog.object.extend(proto, google_protobuf_timestamp_pb); var google_protobuf_wrappers_pb = require('google-protobuf/google/protobuf/wrappers_pb.js'); goog.object.extend(proto, google_protobuf_wrappers_pb); +goog.exportSymbol('proto.yorkie.v1.BoundaryType', null, global); goog.exportSymbol('proto.yorkie.v1.Change', null, global); goog.exportSymbol('proto.yorkie.v1.ChangeID', null, global); goog.exportSymbol('proto.yorkie.v1.ChangePack', null, global); @@ -56,6 +57,7 @@ goog.exportSymbol('proto.yorkie.v1.RGANode', null, global); goog.exportSymbol('proto.yorkie.v1.RHTNode', null, global); goog.exportSymbol('proto.yorkie.v1.Snapshot', null, global); goog.exportSymbol('proto.yorkie.v1.TextNode', null, global); +goog.exportSymbol('proto.yorkie.v1.TextNodeBoundary', null, global); goog.exportSymbol('proto.yorkie.v1.TextNodeID', null, global); goog.exportSymbol('proto.yorkie.v1.TextNodePos', null, global); goog.exportSymbol('proto.yorkie.v1.TimeTicket', null, global); @@ -928,6 +930,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.yorkie.v1.TextNodePos.displayName = 'proto.yorkie.v1.TextNodePos'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.yorkie.v1.TextNodeBoundary = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.yorkie.v1.TextNodeBoundary, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.yorkie.v1.TextNodeBoundary.displayName = 'proto.yorkie.v1.TextNodeBoundary'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -4221,8 +4244,8 @@ proto.yorkie.v1.Operation.Style.prototype.toObject = function(opt_includeInstanc proto.yorkie.v1.Operation.Style.toObject = function(includeInstance, msg) { var f, obj = { parentCreatedAt: (f = msg.getParentCreatedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), - from: (f = msg.getFrom()) && proto.yorkie.v1.TextNodePos.toObject(includeInstance, f), - to: (f = msg.getTo()) && proto.yorkie.v1.TextNodePos.toObject(includeInstance, f), + from: (f = msg.getFrom()) && proto.yorkie.v1.TextNodeBoundary.toObject(includeInstance, f), + to: (f = msg.getTo()) && proto.yorkie.v1.TextNodeBoundary.toObject(includeInstance, f), attributesMap: (f = msg.getAttributesMap()) ? f.toObject(includeInstance, undefined) : [], executedAt: (f = msg.getExecutedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), createdAtMapByActorMap: (f = msg.getCreatedAtMapByActorMap()) ? f.toObject(includeInstance, proto.yorkie.v1.TimeTicket.toObject) : [] @@ -4268,13 +4291,13 @@ proto.yorkie.v1.Operation.Style.deserializeBinaryFromReader = function(msg, read msg.setParentCreatedAt(value); break; case 2: - var value = new proto.yorkie.v1.TextNodePos; - reader.readMessage(value,proto.yorkie.v1.TextNodePos.deserializeBinaryFromReader); + var value = new proto.yorkie.v1.TextNodeBoundary; + reader.readMessage(value,proto.yorkie.v1.TextNodeBoundary.deserializeBinaryFromReader); msg.setFrom(value); break; case 3: - var value = new proto.yorkie.v1.TextNodePos; - reader.readMessage(value,proto.yorkie.v1.TextNodePos.deserializeBinaryFromReader); + var value = new proto.yorkie.v1.TextNodeBoundary; + reader.readMessage(value,proto.yorkie.v1.TextNodeBoundary.deserializeBinaryFromReader); msg.setTo(value); break; case 4: @@ -4336,7 +4359,7 @@ proto.yorkie.v1.Operation.Style.serializeBinaryToWriter = function(message, writ writer.writeMessage( 2, f, - proto.yorkie.v1.TextNodePos.serializeBinaryToWriter + proto.yorkie.v1.TextNodeBoundary.serializeBinaryToWriter ); } f = message.getTo(); @@ -4344,7 +4367,7 @@ proto.yorkie.v1.Operation.Style.serializeBinaryToWriter = function(message, writ writer.writeMessage( 3, f, - proto.yorkie.v1.TextNodePos.serializeBinaryToWriter + proto.yorkie.v1.TextNodeBoundary.serializeBinaryToWriter ); } f = message.getAttributesMap(true); @@ -4404,17 +4427,17 @@ proto.yorkie.v1.Operation.Style.prototype.hasParentCreatedAt = function() { /** - * optional TextNodePos from = 2; - * @return {?proto.yorkie.v1.TextNodePos} + * optional TextNodeBoundary from = 2; + * @return {?proto.yorkie.v1.TextNodeBoundary} */ proto.yorkie.v1.Operation.Style.prototype.getFrom = function() { - return /** @type{?proto.yorkie.v1.TextNodePos} */ ( - jspb.Message.getWrapperField(this, proto.yorkie.v1.TextNodePos, 2)); + return /** @type{?proto.yorkie.v1.TextNodeBoundary} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TextNodeBoundary, 2)); }; /** - * @param {?proto.yorkie.v1.TextNodePos|undefined} value + * @param {?proto.yorkie.v1.TextNodeBoundary|undefined} value * @return {!proto.yorkie.v1.Operation.Style} returns this */ proto.yorkie.v1.Operation.Style.prototype.setFrom = function(value) { @@ -4441,17 +4464,17 @@ proto.yorkie.v1.Operation.Style.prototype.hasFrom = function() { /** - * optional TextNodePos to = 3; - * @return {?proto.yorkie.v1.TextNodePos} + * optional TextNodeBoundary to = 3; + * @return {?proto.yorkie.v1.TextNodeBoundary} */ proto.yorkie.v1.Operation.Style.prototype.getTo = function() { - return /** @type{?proto.yorkie.v1.TextNodePos} */ ( - jspb.Message.getWrapperField(this, proto.yorkie.v1.TextNodePos, 3)); + return /** @type{?proto.yorkie.v1.TextNodeBoundary} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TextNodeBoundary, 3)); }; /** - * @param {?proto.yorkie.v1.TextNodePos|undefined} value + * @param {?proto.yorkie.v1.TextNodeBoundary|undefined} value * @return {!proto.yorkie.v1.Operation.Style} returns this */ proto.yorkie.v1.Operation.Style.prototype.setTo = function(value) { @@ -12795,6 +12818,217 @@ proto.yorkie.v1.TextNodePos.prototype.setRelativeOffset = function(value) { +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.yorkie.v1.TextNodeBoundary.prototype.toObject = function(opt_includeInstance) { + return proto.yorkie.v1.TextNodeBoundary.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.yorkie.v1.TextNodeBoundary} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.TextNodeBoundary.toObject = function(includeInstance, msg) { + var f, obj = { + createdAt: (f = msg.getCreatedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), + offset: jspb.Message.getFieldWithDefault(msg, 2, 0), + type: jspb.Message.getFieldWithDefault(msg, 3, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.yorkie.v1.TextNodeBoundary} + */ +proto.yorkie.v1.TextNodeBoundary.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.yorkie.v1.TextNodeBoundary; + return proto.yorkie.v1.TextNodeBoundary.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.yorkie.v1.TextNodeBoundary} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.yorkie.v1.TextNodeBoundary} + */ +proto.yorkie.v1.TextNodeBoundary.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.yorkie.v1.TimeTicket; + reader.readMessage(value,proto.yorkie.v1.TimeTicket.deserializeBinaryFromReader); + msg.setCreatedAt(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt32()); + msg.setOffset(value); + break; + case 3: + var value = /** @type {!proto.yorkie.v1.BoundaryType} */ (reader.readEnum()); + msg.setType(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.yorkie.v1.TextNodeBoundary.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.yorkie.v1.TextNodeBoundary.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.yorkie.v1.TextNodeBoundary} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.TextNodeBoundary.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getCreatedAt(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.yorkie.v1.TimeTicket.serializeBinaryToWriter + ); + } + f = message.getOffset(); + if (f !== 0) { + writer.writeInt32( + 2, + f + ); + } + f = message.getType(); + if (f !== 0.0) { + writer.writeEnum( + 3, + f + ); + } +}; + + +/** + * optional TimeTicket created_at = 1; + * @return {?proto.yorkie.v1.TimeTicket} + */ +proto.yorkie.v1.TextNodeBoundary.prototype.getCreatedAt = function() { + return /** @type{?proto.yorkie.v1.TimeTicket} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TimeTicket, 1)); +}; + + +/** + * @param {?proto.yorkie.v1.TimeTicket|undefined} value + * @return {!proto.yorkie.v1.TextNodeBoundary} returns this +*/ +proto.yorkie.v1.TextNodeBoundary.prototype.setCreatedAt = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.TextNodeBoundary} returns this + */ +proto.yorkie.v1.TextNodeBoundary.prototype.clearCreatedAt = function() { + return this.setCreatedAt(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.TextNodeBoundary.prototype.hasCreatedAt = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional int32 offset = 2; + * @return {number} + */ +proto.yorkie.v1.TextNodeBoundary.prototype.getOffset = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.yorkie.v1.TextNodeBoundary} returns this + */ +proto.yorkie.v1.TextNodeBoundary.prototype.setOffset = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional BoundaryType type = 3; + * @return {!proto.yorkie.v1.BoundaryType} + */ +proto.yorkie.v1.TextNodeBoundary.prototype.getType = function() { + return /** @type {!proto.yorkie.v1.BoundaryType} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {!proto.yorkie.v1.BoundaryType} value + * @return {!proto.yorkie.v1.TextNodeBoundary} returns this + */ +proto.yorkie.v1.TextNodeBoundary.prototype.setType = function(value) { + return jspb.Message.setProto3EnumField(this, 3, value); +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. @@ -13166,6 +13400,16 @@ proto.yorkie.v1.DocEvent.prototype.setPublisher = function(value) { }; +/** + * @enum {number} + */ +proto.yorkie.v1.BoundaryType = { + BOUNDARY_TYPE_BEFORE: 0, + BOUNDARY_TYPE_AFTER: 1, + BOUNDARY_TYPE_START: 2, + BOUNDARY_TYPE_END: 3 +}; + /** * @enum {number} */ diff --git a/src/document/operation/style_operation.ts b/src/document/operation/style_operation.ts index 5073d10b2..4be199aa6 100644 --- a/src/document/operation/style_operation.ts +++ b/src/document/operation/style_operation.ts @@ -17,7 +17,7 @@ import { logger } from '@yorkie-js-sdk/src/util/logger'; import { TimeTicket } from '@yorkie-js-sdk/src/document/time/ticket'; import { CRDTRoot } from '@yorkie-js-sdk/src/document/crdt/root'; -import { RGATreeSplitPos } from '@yorkie-js-sdk/src/document/crdt/rga_tree_split'; +import { RGATreeSplitBoundary } from '@yorkie-js-sdk/src/document/crdt/rga_tree_split'; import { CRDTText } from '@yorkie-js-sdk/src/document/crdt/text'; import { Operation, @@ -29,24 +29,22 @@ import { Indexable } from '../document'; * `StyleOperation` is an operation applies the style of the given range to Text. */ export class StyleOperation extends Operation { - // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` - private fromPos: RGATreeSplitPos; - private toPos: RGATreeSplitPos; + private fromBoundary: RGATreeSplitBoundary; + private toBoundary: RGATreeSplitBoundary | undefined; private maxCreatedAtMapByActor: Map; private attributes: Map; constructor( parentCreatedAt: TimeTicket, - // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` - fromPos: RGATreeSplitPos, - toPos: RGATreeSplitPos, + fromBoundary: RGATreeSplitBoundary, + toBoundary: RGATreeSplitBoundary | undefined, maxCreatedAtMapByActor: Map, attributes: Map, executedAt: TimeTicket, ) { super(parentCreatedAt, executedAt); - this.fromPos = fromPos; - this.toPos = toPos; + this.fromBoundary = fromBoundary; + this.toBoundary = toBoundary; this.maxCreatedAtMapByActor = maxCreatedAtMapByActor; this.attributes = attributes; } @@ -56,17 +54,16 @@ export class StyleOperation extends Operation { */ public static create( parentCreatedAt: TimeTicket, - // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` - fromPos: RGATreeSplitPos, - toPos: RGATreeSplitPos, + fromBoundary: RGATreeSplitBoundary, + toBoundary: RGATreeSplitBoundary, maxCreatedAtMapByActor: Map, attributes: Map, executedAt: TimeTicket, ): StyleOperation { return new StyleOperation( parentCreatedAt, - fromPos, - toPos, + fromBoundary, + toBoundary, maxCreatedAtMapByActor, attributes, executedAt, @@ -86,8 +83,7 @@ export class StyleOperation extends Operation { } const text = parentObject as CRDTText; const [, changes] = text.setStyle( - // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` - [this.fromPos, this.toPos], + [this.fromBoundary, this.toBoundary], this.attributes ? Object.fromEntries(this.attributes) : {}, this.getExecutedAt(), this.maxCreatedAtMapByActor, @@ -115,24 +111,24 @@ export class StyleOperation extends Operation { */ public toTestString(): string { const parent = this.getParentCreatedAt().toTestString(); - const fromPos = this.fromPos.toTestString(); - const toPos = this.toPos.toTestString(); + const fromPos = this.fromBoundary.toTestString(); + const toPos = this.toBoundary?.toTestString(); const attributes = this.attributes; return `${parent}.STYL(${fromPos},${toPos},${JSON.stringify(attributes)})`; } /** - * `getFromPos` returns the start point of the editing range. + * `getFromBoundary` returns the start point of the editing range. */ - public getFromPos(): RGATreeSplitPos { - return this.fromPos; + public getFromBoundary(): RGATreeSplitBoundary { + return this.fromBoundary; } /** - * `getToPos` returns the end point of the editing range. + * `getToBoundary` returns the end point of the editing range. */ - public getToPos(): RGATreeSplitPos { - return this.toPos; + public getToBoundary(): RGATreeSplitBoundary | undefined { + return this.toBoundary; } /** From 7a9bcfd1fc0204003939127ab2666c6c81c82ad7 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 15 Sep 2023 10:45:45 +0900 Subject: [PATCH 05/21] Generating setStyle operations --- src/document/crdt/rga_tree_split.ts | 93 +++++++++++-- src/document/crdt/text.ts | 199 ++++++++++++++++++++-------- src/document/json/text.ts | 72 ++++++++-- 3 files changed, 287 insertions(+), 77 deletions(-) diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 044841323..bf18cc461 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -25,6 +25,7 @@ import { TimeTicket, TimeTicketStruct, } from '@yorkie-js-sdk/src/document/time/ticket'; +import { StyleOperation } from '../operation/style_operation'; export interface ValueChange { actor: ActorID; @@ -57,6 +58,13 @@ export type RGATreeSplitNodeIDStruct = { offset: number; }; +export enum BoundaryType { + Before = 'before', + After = 'after', + Start = 'start', + End = 'end', +} + /** * `RGATreeSplitNodeID` is an ID of RGATreeSplitNode. */ @@ -238,11 +246,57 @@ export class RGATreeSplitPos { } } -// TODO(MoonGyu1): Peritext 1. Add RGATreeSplitBoundary class +/** + * `RGATreeSplitBoundary` is the boundary of the text node. + */ +export class RGATreeSplitBoundary { + private id?: RGATreeSplitNodeID; + private type?: BoundaryType; + + constructor(id?: RGATreeSplitNodeID, type?: BoundaryType) { + this.id = id; + this.type = type; + } + + /** + * `of` creates a instance of RGATreeSplitBoundary. + */ + public static of( + id?: RGATreeSplitNodeID, + type?: BoundaryType, + ): RGATreeSplitBoundary { + return new RGATreeSplitBoundary(id, type); + } + + /** + * `getID` returns the ID of this RGATreeSplitBoundary. + */ + public getID(): RGATreeSplitNodeID | undefined { + return this.id; + } + + /** + * `getType` returns the type of this RGATreeSplitBoundary. + */ + public getType(): BoundaryType | undefined { + return this.type; + } + + /** + *`toTestString` returns a String containing + * the meta data of the boundary for debugging purpose. + */ + public toTestString(): string { + return `${this.id?.toTestString()}:${this.type}`; + } +} export type RGATreeSplitPosRange = [RGATreeSplitPos, RGATreeSplitPos]; -// TODO(MoonGyu1): Peritext 1. Add RGATreeSplitBoundaryRange +export type RGATreeSplitBoundaryRange = [ + RGATreeSplitBoundary, + RGATreeSplitBoundary | undefined, +]; /** * `RGATreeSplitNode` is a node of RGATreeSplit. @@ -257,8 +311,8 @@ export class RGATreeSplitNode< private next?: RGATreeSplitNode; private insPrev?: RGATreeSplitNode; private insNext?: RGATreeSplitNode; - - // TODO(MoonGyu1): Peritext 1. add `styleOpsBefore` and `styleOpsAfter` + private styleOpsBefore?: Set; + private styleOpsAfter?: Set; constructor(id: RGATreeSplitNodeID, value?: T, removedAt?: TimeTicket) { super(value!); @@ -361,7 +415,13 @@ export class RGATreeSplitNode< return this.insPrev!.getID(); } - // TODO(MoonGyu1): Peritext 2. Add getter of styleOpsBefore/styleOpsAfter + public getStyleOpsBefore(): Set | undefined { + return this.styleOpsBefore; + } + + public getStyleOpsAfter(): Set | undefined { + return this.styleOpsAfter; + } /** * `setPrev` sets previous node of this node. @@ -403,7 +463,13 @@ export class RGATreeSplitNode< } } - // TODO(MoonGyu1): Peritext 2. Add setter of styleOpsBefore/styleOpsAfter + public setStyleOpsBefore(operations: Set): void { + this.styleOpsBefore = operations; + } + + public setStyleOpsAfter(operations: Set): void { + this.styleOpsAfter = operations; + } /** * `hasNext` checks if next node exists. @@ -771,8 +837,19 @@ export class RGATreeSplit { return [node, node.getNext()!]; } - // TODO(MoonGyu1): Peritext 1. Add `splitNodeByBoundaryPos` method // TODO(MoonGyu1): It can be optimized later + /** + * `findNodeWithSplit` splits and return nodes of the given position. + */ + public splitNodeByBoundary(boundary: RGATreeSplitBoundary): void { + const absoluteID = boundary.getID(); + if (absoluteID?.getCreatedAt()) { + let node = this.findFloorNodePreferToLeft(absoluteID); + const relativeOffset = absoluteID.getOffset() - node.getID().getOffset(); + + this.splitNode(node, relativeOffset); + } + } private findFloorNodePreferToLeft( id: RGATreeSplitNodeID, @@ -815,7 +892,7 @@ export class RGATreeSplit { */ public findBetween( fromNode: RGATreeSplitNode, - toNode: RGATreeSplitNode, + toNode: RGATreeSplitNode | undefined, ): Array> { const nodes = []; diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index 766772424..01cbb60ba 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -23,8 +23,12 @@ import { Indexable } from '@yorkie-js-sdk/src/document/document'; import { RHT } from '@yorkie-js-sdk/src/document/crdt/rht'; import { CRDTGCElement } from '@yorkie-js-sdk/src/document/crdt/element'; import { + BoundaryType, RGATreeSplit, + RGATreeSplitBoundary, + RGATreeSplitBoundaryRange, RGATreeSplitNode, + RGATreeSplitPos, RGATreeSplitPosRange, ValueChange, } from '@yorkie-js-sdk/src/document/crdt/rga_tree_split'; @@ -57,6 +61,22 @@ interface TextChange extends ValueChange> { type: TextChangeType; } +export type MarkName = string; + +export type AttributeSpec = { + default?: any; + required?: boolean; +}; + +export type MarkSpec = { + expand: 'before' | 'after' | 'both' | 'none'; + allowMultiple: boolean; + excludes?: string[]; + attributes?: { [key: string]: AttributeSpec }; +}; + +export type MarkTypes = Map; + /** * `CRDTTextValue` is a value of Text * which has a attributes that expresses the text style. @@ -235,75 +255,83 @@ export class CRDTText extends CRDTGCElement { * @internal */ public setStyle( - // TODO(MoonGyu1): Peritext 1. Use RGATreeSplitBoundaryRange - range: RGATreeSplitPosRange, + range: RGATreeSplitBoundaryRange, attributes: Record, editedAt: TimeTicket, latestCreatedAtMapByActor?: Map, ): [Map, Array>] { - // 01. split nodes with from and to - - // TODO(MoonGyu1): Peritext 1. Split node by NodeID of RGATreeSplitBoundaryRange if it is remote operation - // TODO(MoonGyu1): Peritext 1. Use splitNodeByBoundaryPos method - const [, toRight] = this.rgaTreeSplit.findNodeWithSplit(range[1], editedAt); - const [, fromRight] = this.rgaTreeSplit.findNodeWithSplit( - range[0], - editedAt, - ); + // 01. Split nodes with boundaryRange if it is a remote operation + const isRemote = !!latestCreatedAtMapByActor; + if (isRemote) { + this.rgaTreeSplit.splitNodeByBoundary(range[1]!); + this.rgaTreeSplit.splitNodeByBoundary(range[0]); + } // 02. style nodes between from and to const changes: Array> = []; - const nodes = this.rgaTreeSplit.findBetween(fromRight, toRight); - - // TODO(MoonGyu1): Peritext 2. Update styleOpsBefore/styleOpsAfter of fromRight/toRight nodes - // if markType is `bold` else keep existing logic below - - const createdAtMapByActor = new Map(); - const toBeStyleds: Array> = []; + const fromBoundary = range[0]; + const toBoundary = range[1]; + // 02-1. Update styleOpsBefore and styleOpsAfter if it is a bold type + if (fromBoundary.getType() && toBoundary?.getType()) { + // TODO(MoonGyu1): Peritext 2. Update styleOpsBefore/styleOpsAfter of fromRight/toRight nodes + + const createdAtMapByActor = new Map(); + return [createdAtMapByActor, changes]; + } + // 02-2. Apply the existing logic to style nodes if they are not of a bold type + else { + const fromNode = this.rgaTreeSplit.findNode(fromBoundary.getID()!); + const toNode = toBoundary?.getID()?.getCreatedAt() + ? this.rgaTreeSplit.findNode(toBoundary.getID()!) + : undefined; + + const nodes = this.rgaTreeSplit.findBetween(fromNode, toNode); + const createdAtMapByActor = new Map(); + const toBeStyleds: Array> = []; + + for (const node of nodes) { + const actorID = node.getCreatedAt().getActorID()!; + const latestCreatedAt = latestCreatedAtMapByActor?.size + ? latestCreatedAtMapByActor!.has(actorID!) + ? latestCreatedAtMapByActor!.get(actorID!)! + : InitialTimeTicket + : MaxTimeTicket; + + if (node.canStyle(editedAt, latestCreatedAt)) { + const latestCreatedAt = createdAtMapByActor.get(actorID); + const createdAt = node.getCreatedAt(); + if (!latestCreatedAt || createdAt.after(latestCreatedAt)) { + createdAtMapByActor.set(actorID, createdAt); + } + toBeStyleds.push(node); + } + } - for (const node of nodes) { - const actorID = node.getCreatedAt().getActorID()!; + for (const node of toBeStyleds) { + if (node.isRemoved()) { + continue; + } - const latestCreatedAt = latestCreatedAtMapByActor?.size - ? latestCreatedAtMapByActor!.has(actorID!) - ? latestCreatedAtMapByActor!.get(actorID!)! - : InitialTimeTicket - : MaxTimeTicket; + const [fromIdx, toIdx] = this.rgaTreeSplit.findIndexesFromRange( + node.createPosRange(), + ); + changes.push({ + type: TextChangeType.Style, + actor: editedAt.getActorID()!, + from: fromIdx, + to: toIdx, + value: { + attributes: parseObjectValues(attributes) as A, + }, + }); - if (node.canStyle(editedAt, latestCreatedAt)) { - const latestCreatedAt = createdAtMapByActor.get(actorID); - const createdAt = node.getCreatedAt(); - if (!latestCreatedAt || createdAt.after(latestCreatedAt)) { - createdAtMapByActor.set(actorID, createdAt); + for (const [key, value] of Object.entries(attributes)) { + node.getValue().setAttr(key, value, editedAt); } - toBeStyleds.push(node); } - } - for (const node of toBeStyleds) { - if (node.isRemoved()) { - continue; - } - - const [fromIdx, toIdx] = this.rgaTreeSplit.findIndexesFromRange( - node.createPosRange(), - ); - changes.push({ - type: TextChangeType.Style, - actor: editedAt.getActorID()!, - from: fromIdx, - to: toIdx, - value: { - attributes: parseObjectValues(attributes) as A, - }, - }); - - for (const [key, value] of Object.entries(attributes)) { - node.getValue().setAttr(key, value, editedAt); - } + return [createdAtMapByActor, changes]; } - - return [createdAtMapByActor, changes]; } /** @@ -321,6 +349,67 @@ export class CRDTText extends CRDTGCElement { return [fromPos, this.rgaTreeSplit.indexToPos(toIdx)]; } + /** + * `posRangeToBoundaryRange` returns the boundary range of the given position range. + */ + public posRangeToBoundaryRange( + fromPos: RGATreeSplitPos, + toPos: RGATreeSplitPos, + editedAt: TimeTicket, + expand?: 'before' | 'after' | 'both' | 'none', + ): RGATreeSplitBoundaryRange { + // Make a RGATreeSplitBoundary if it is a mark type + if (expand) { + const [, toRight] = this.rgaTreeSplit.findNodeWithSplit(toPos, editedAt); + const [, fromRight] = this.rgaTreeSplit.findNodeWithSplit( + fromPos, + editedAt, + ); + + let fromNode: RGATreeSplitNode | undefined = fromRight; + let toNode: RGATreeSplitNode | undefined = toRight; + + if (expand === 'after') { + while (fromNode && fromNode.isRemoved()) { + // NOTE(MoonGyu1): have to check if it is a last node + fromNode = fromNode.getNext(); + } + + while (toNode && toNode.isRemoved()) { + // NOTE(MoonGyu1): have to check if it is a last node + toNode = toNode.getNext(); + } + + const fromNodeID = fromNode?.getID(); + const toNodeID = toNode?.getID(); + + const fromBoundaryType = fromNodeID + ? BoundaryType.Before + : BoundaryType.Start; + const toBoundaryType = toNodeID + ? BoundaryType.Before + : BoundaryType.End; + + return [ + RGATreeSplitBoundary.of(fromNodeID, fromBoundaryType), + RGATreeSplitBoundary.of(toNodeID, toBoundaryType), + ]; + } + } + + // Make a RGATreeSplitBoundary without BoundaryType if it is not a mark type + const [, toRight] = this.rgaTreeSplit.findNodeWithSplit(toPos, editedAt); + const [, fromRight] = this.rgaTreeSplit.findNodeWithSplit( + fromPos, + editedAt, + ); + + return [ + RGATreeSplitBoundary.of(fromRight.getID()), + RGATreeSplitBoundary.of(toRight?.getID()), + ]; + } + /** * `length` returns size of RGATreeList. */ diff --git a/src/document/json/text.ts b/src/document/json/text.ts index 24d26a29a..8d7ae03f6 100644 --- a/src/document/json/text.ts +++ b/src/document/json/text.ts @@ -22,10 +22,15 @@ import { } from '@yorkie-js-sdk/src/document/time/ticket'; import { ChangeContext } from '@yorkie-js-sdk/src/document/change/context'; import { + RGATreeSplitBoundaryRange, RGATreeSplitPos, RGATreeSplitPosRange, } from '@yorkie-js-sdk/src/document/crdt/rga_tree_split'; -import { CRDTText, TextValueType } from '@yorkie-js-sdk/src/document/crdt/text'; +import { + CRDTText, + MarkTypes, + TextValueType, +} from '@yorkie-js-sdk/src/document/crdt/text'; import { EditOperation } from '@yorkie-js-sdk/src/document/operation/edit_operation'; import { StyleOperation } from '@yorkie-js-sdk/src/document/operation/style_operation'; import { stringifyObjectValues } from '@yorkie-js-sdk/src/util/object'; @@ -51,12 +56,14 @@ export type TextPosStructRange = [TextPosStruct, TextPosStruct]; export class Text { private context?: ChangeContext; private text?: CRDTText; - // TODO(MoonGyu1): Peritext 1. Add markType for `bold` + private markTypes?: MarkTypes; constructor(context?: ChangeContext, text?: CRDTText) { this.context = context; this.text = text; - // TODO(MoonGyu1): Peritext 1. initialize markType for `bold` + // NOTE(MoonGyu1): It can be converted to custom mark types later + this.markTypes = new Map(); + this.markTypes.set('bold', { expand: 'after', allowMultiple: false }); } /** @@ -66,7 +73,9 @@ export class Text { public initialize(context: ChangeContext, text: CRDTText): void { this.context = context; this.text = text; - // TODO(MoonGyu1): Peritext 1. initialize markType for `bold` + // NOTE(MoonGyu1): It can be converted to custom mark types later + this.markTypes = new Map(); + this.markTypes.set('bold', { expand: 'after', allowMultiple: false }); } /** @@ -157,30 +166,65 @@ export class Text { return false; } - const range = this.text.indexRangeToPosRange(fromIdx, toIdx); + const posRange = this.text.indexRangeToPosRange(fromIdx, toIdx); if (logger.isEnabled(LogLevel.Debug)) { logger.debug( - `STYL: f:${fromIdx}->${range[0].toTestString()}, t:${toIdx}->${range[1].toTestString()} a:${JSON.stringify( + `STYL: f:${fromIdx}->${posRange[0].toTestString()}, t:${toIdx}->${posRange[1].toTestString()} a:${JSON.stringify( attributes, )}`, ); } - // TODO(MoonGyu1): Peritext 1. Split node and get start/end node considering markType by PosRange - // TODO(MoonGyu1): Peritext 1. Change from node to RGATreeSplitBoundaryRange - const attrs = stringifyObjectValues(attributes); const ticket = this.context.issueTimeTicket(); - // TODO(MoonGyu1): Peritext 1. Use RGATreeSplitBoundaryRange - const [maxCreatedAtMapByActor] = this.text.setStyle(range, attrs, ticket); + let hasMarkType = false; + for (const [key, value] of Object.entries(attrs)) { + if (this.markTypes?.has(key)) { + hasMarkType = true; + } + } + + let boundaryRange: RGATreeSplitBoundaryRange; + + // Find the boundaryRange if the attributes have the mark type (bold). + if (hasMarkType) { + for (const [key, value] of Object.entries(attrs)) { + if (this.markTypes?.has(key)) { + const expand = this.markTypes.get(key)!.expand; + + // delete attrs[key]; + + boundaryRange = this.text.posRangeToBoundaryRange( + posRange[0], + posRange[1], + ticket, + expand, + ); + } + } + } + // Find the boundaryRange if the attributes don't have the mark type (bold) + else { + boundaryRange = this.text.posRangeToBoundaryRange( + posRange[0], + posRange[1], + ticket, + ); + } + + // Execute the existing logic + const [maxCreatedAtMapByActor] = this.text.setStyle( + boundaryRange!, + attrs, + ticket, + ); this.context.push( new StyleOperation( this.text.getCreatedAt(), - // TODO(MoonGyu1): Peritext 1. Change from `fromPos/toPos` to `fromBoundary/toBoundary` - range[0], - range[1], + boundaryRange![0], + boundaryRange![1], maxCreatedAtMapByActor, new Map(Object.entries(attrs)), ticket, From 482e2525b55a2568fab060a65877fe8576946286 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 15 Sep 2023 10:48:36 +0900 Subject: [PATCH 06/21] Skip testcases related to bold type --- src/document/operation/operation.ts | 3 --- src/yorkie.ts | 1 - test/integration/document_test.ts | 3 ++- test/integration/snapshot_test.ts | 3 ++- test/integration/text_test.ts | 16 +++++++++------- test/unit/document/document_test.ts | 9 ++++++--- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/document/operation/operation.ts b/src/document/operation/operation.ts index 2d4cdd0e5..275a68828 100644 --- a/src/document/operation/operation.ts +++ b/src/document/operation/operation.ts @@ -35,7 +35,6 @@ export type OperationInfo = * `TextOperationInfo` represents the OperationInfo for the yorkie.Text. */ -// TODO(MoonGyu1): Peritext 1. Add RemoveStyleOpInfo export type TextOperationInfo = EditOpInfo | StyleOpInfo; /** @@ -132,8 +131,6 @@ export type StyleOpInfo = { }; }; -// TODO(MoonGyu1): Peritext 1. Add RemoveStyleOpInfo - /** * `TreeEditOpInfo` represents the information of the tree edit operation. */ diff --git a/src/yorkie.ts b/src/yorkie.ts index 09767b6ce..a6627905d 100644 --- a/src/yorkie.ts +++ b/src/yorkie.ts @@ -72,7 +72,6 @@ export type { MoveOpInfo, EditOpInfo, StyleOpInfo, - // TODO(MoonGyu1): Peritext 1. Add RemoveStyleOpInfo } from '@yorkie-js-sdk/src/document/operation/operation'; export { diff --git a/test/integration/document_test.ts b/test/integration/document_test.ts index 99485d5e1..79ab3c5e7 100644 --- a/test/integration/document_test.ts +++ b/test/integration/document_test.ts @@ -93,7 +93,8 @@ describe('Document', function () { await c2.deactivate(); }); - it('detects the events from doc.subscribe', async function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('detects the events from doc.subscribe', async function () { const c1 = new yorkie.Client(testRPCAddr); const c2 = new yorkie.Client(testRPCAddr); await c1.activate(); diff --git a/test/integration/snapshot_test.ts b/test/integration/snapshot_test.ts index 01fdd7cde..c5fd3833f 100644 --- a/test/integration/snapshot_test.ts +++ b/test/integration/snapshot_test.ts @@ -58,7 +58,8 @@ describe('Snapshot', function () { }, this.test!.title); }); - it('should handle snapshot for text with attributes', async function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('should handle snapshot for text with attributes', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); diff --git a/test/integration/text_test.ts b/test/integration/text_test.ts index e1df6a17d..8bf8c38b6 100644 --- a/test/integration/text_test.ts +++ b/test/integration/text_test.ts @@ -444,7 +444,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): remove skip after applying mark operation + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type it.skip('ex2. concurrent formatting and insertion', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -482,7 +482,8 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - it('ex3. overlapping formatting(bold)', async function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('ex3. overlapping formatting(bold)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -519,7 +520,8 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - it('ex4. overlapping different formatting(bold and italic)', async function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('ex4. overlapping different formatting(bold and italic)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -593,7 +595,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): remove skip after applying mark operation + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type it.skip('ex6. conflicting overlaps(bold) - 1', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -638,7 +640,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): remove skip after applying mark operation + // TODO(MoonGyu1): Remove skip after implementing removeMark operation of bold type it.skip('ex6. conflicting overlaps(bold) - 2', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -721,7 +723,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): remove skip after applying mark operation + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type it.skip('ex8. text insertion at span boundaries(bold)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -811,7 +813,7 @@ describe('peri-text example: text concurrent edit', function () { }); describe('Style', function () { - // TODO(MoonGyu1): remove skip after applying mark operation + // TODO(MoonGyu1): Remove skip after implementing removeMark operation of bold type it.skip('should handle style operations', function () { const doc = new Document<{ k1: Text }>('test-doc'); assert.equal('{}', doc.toSortedJSON()); diff --git a/test/unit/document/document_test.ts b/test/unit/document/document_test.ts index b90903b71..2db9f5fd6 100644 --- a/test/unit/document/document_test.ts +++ b/test/unit/document/document_test.ts @@ -1042,7 +1042,8 @@ describe('Document', function () { }); }); - it('changeInfo test for text', async function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('changeInfo test for text', async function () { type TestDoc = { text: Text }; const doc = new Document('test-doc'); type EventForTest = Array; @@ -1071,7 +1072,8 @@ describe('Document', function () { unsub(); }); - it('changeInfo test for text with attributes', async function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('changeInfo test for text with attributes', async function () { type TestDoc = { textWithAttr: Text }; const doc = new Document('test-doc'); type EventForTest = Array; @@ -1231,7 +1233,8 @@ describe('Document', function () { assert.equal(155, doc.getRoot().counter.getValue()); }); - it('sets any type of custom attribute values and can returns JSON parsable string', function () { + // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + it.skip('sets any type of custom attribute values and can returns JSON parsable string', function () { type AttrsType = { bold?: boolean; indent?: number; From 5bef0ad921cf44de9ba596f5839fd265a62d61df Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 15 Sep 2023 13:07:04 +0900 Subject: [PATCH 07/21] Add boundary type 'none' and apply lint --- src/api/converter.ts | 14 ++++++++++++-- src/api/yorkie/v1/resources.proto | 1 + src/api/yorkie/v1/resources_pb.d.ts | 1 + src/api/yorkie/v1/resources_pb.js | 3 ++- src/document/crdt/rga_tree_split.ts | 16 +++++++++++++++- src/document/crdt/text.ts | 11 +++++++---- src/document/json/text.ts | 8 ++++++-- 7 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/api/converter.ts b/src/api/converter.ts index 648f08a59..1fee5a158 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -277,12 +277,18 @@ function toTextNodeBoundary( switch (boundary?.getType()) { case BoundaryType.Before: pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_BEFORE); + break; case BoundaryType.After: pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_AFTER); + break; case BoundaryType.Start: pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_START); + break; case BoundaryType.End: pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_END); + break; + case BoundaryType.None: + pbTextNodeBoundary.setType(PbBoundaryType.BOUNDARY_TYPE_NONE); } return pbTextNodeBoundary; } @@ -929,14 +935,18 @@ function fromTextNodeBoundary( switch (pbTextNodeBoundary.getType()) { case PbBoundaryType.BOUNDARY_TYPE_BEFORE: boundaryType = BoundaryType.Before; + break; case PbBoundaryType.BOUNDARY_TYPE_AFTER: boundaryType = BoundaryType.After; + break; case PbBoundaryType.BOUNDARY_TYPE_START: boundaryType = BoundaryType.Start; + break; case PbBoundaryType.BOUNDARY_TYPE_END: boundaryType = BoundaryType.End; - default: - boundaryType = undefined; + break; + case PbBoundaryType.BOUNDARY_TYPE_NONE: + boundaryType = BoundaryType.None; } return RGATreeSplitBoundary.of( RGATreeSplitNodeID.of( diff --git a/src/api/yorkie/v1/resources.proto b/src/api/yorkie/v1/resources.proto index 7e5bec2e3..65a1d2010 100644 --- a/src/api/yorkie/v1/resources.proto +++ b/src/api/yorkie/v1/resources.proto @@ -336,6 +336,7 @@ enum BoundaryType { BOUNDARY_TYPE_AFTER = 1; BOUNDARY_TYPE_START = 2; BOUNDARY_TYPE_END = 3; + BOUNDARY_TYPE_NONE = 4; } message TextNodeBoundary { diff --git a/src/api/yorkie/v1/resources_pb.d.ts b/src/api/yorkie/v1/resources_pb.d.ts index 0cf39e6a3..83f60acb0 100644 --- a/src/api/yorkie/v1/resources_pb.d.ts +++ b/src/api/yorkie/v1/resources_pb.d.ts @@ -1619,6 +1619,7 @@ export enum BoundaryType { BOUNDARY_TYPE_AFTER = 1, BOUNDARY_TYPE_START = 2, BOUNDARY_TYPE_END = 3, + BOUNDARY_TYPE_NONE = 4, } export enum ValueType { VALUE_TYPE_NULL = 0, diff --git a/src/api/yorkie/v1/resources_pb.js b/src/api/yorkie/v1/resources_pb.js index 412fe75c9..01c1823a6 100644 --- a/src/api/yorkie/v1/resources_pb.js +++ b/src/api/yorkie/v1/resources_pb.js @@ -13407,7 +13407,8 @@ proto.yorkie.v1.BoundaryType = { BOUNDARY_TYPE_BEFORE: 0, BOUNDARY_TYPE_AFTER: 1, BOUNDARY_TYPE_START: 2, - BOUNDARY_TYPE_END: 3 + BOUNDARY_TYPE_END: 3, + BOUNDARY_TYPE_NONE: 4 }; /** diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index bf18cc461..2172ae3ea 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -63,6 +63,8 @@ export enum BoundaryType { After = 'after', Start = 'start', End = 'end', + // TODO(MoonGyu1): 'None' type can be deleted after replacing existing logic with mark + None = 'none', } /** @@ -415,10 +417,16 @@ export class RGATreeSplitNode< return this.insPrev!.getID(); } + /** + * `getStyleOpsBefore` returns a styleOpsBefore of this node. + */ public getStyleOpsBefore(): Set | undefined { return this.styleOpsBefore; } + /** + * `getStyleOpsAfter` returns a styleOpsAfter of this node. + */ public getStyleOpsAfter(): Set | undefined { return this.styleOpsAfter; } @@ -463,10 +471,16 @@ export class RGATreeSplitNode< } } + /** + * `setStyleOpsBefore` sets styleOpsBefore of this node. + */ public setStyleOpsBefore(operations: Set): void { this.styleOpsBefore = operations; } + /** + * `setStyleOpsAfter` sets styleOpsAfter of this node. + */ public setStyleOpsAfter(operations: Set): void { this.styleOpsAfter = operations; } @@ -844,7 +858,7 @@ export class RGATreeSplit { public splitNodeByBoundary(boundary: RGATreeSplitBoundary): void { const absoluteID = boundary.getID(); if (absoluteID?.getCreatedAt()) { - let node = this.findFloorNodePreferToLeft(absoluteID); + const node = this.findFloorNodePreferToLeft(absoluteID); const relativeOffset = absoluteID.getOffset() - node.getID().getOffset(); this.splitNode(node, relativeOffset); diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index 01cbb60ba..5a251d799 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -71,7 +71,7 @@ export type AttributeSpec = { export type MarkSpec = { expand: 'before' | 'after' | 'both' | 'none'; allowMultiple: boolean; - excludes?: string[]; + excludes?: Array; attributes?: { [key: string]: AttributeSpec }; }; @@ -272,7 +272,10 @@ export class CRDTText extends CRDTGCElement { const fromBoundary = range[0]; const toBoundary = range[1]; // 02-1. Update styleOpsBefore and styleOpsAfter if it is a bold type - if (fromBoundary.getType() && toBoundary?.getType()) { + if ( + fromBoundary.getType() != BoundaryType.None && + toBoundary?.getType() != BoundaryType.None + ) { // TODO(MoonGyu1): Peritext 2. Update styleOpsBefore/styleOpsAfter of fromRight/toRight nodes const createdAtMapByActor = new Map(); @@ -405,8 +408,8 @@ export class CRDTText extends CRDTGCElement { ); return [ - RGATreeSplitBoundary.of(fromRight.getID()), - RGATreeSplitBoundary.of(toRight?.getID()), + RGATreeSplitBoundary.of(fromRight.getID(), BoundaryType.None), + RGATreeSplitBoundary.of(toRight?.getID(), BoundaryType.None), ]; } diff --git a/src/document/json/text.ts b/src/document/json/text.ts index 8d7ae03f6..40c476f4c 100644 --- a/src/document/json/text.ts +++ b/src/document/json/text.ts @@ -234,8 +234,12 @@ export class Text { return true; } - // TODO(MoonGyu1): Peritext 1. Add removeStyle method - removeStyle(fromIdx: number, toIdx: number, attributes: A) {} + /** + * `removeStyle` remove styles from text with the given attributes. + */ + removeStyle(fromIdx: number, toIdx: number, attributes: A) { + // TODO(MoonGyu1): Peritext 1. Add removeStyle method + } /** * `indexRangeToPosRange` returns TextRangeStruct of the given index range. From 899a21ffc1d108ce2a508d4c1e429ba5b251a642 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 15 Sep 2023 16:35:01 +0900 Subject: [PATCH 08/21] Add new StyleOperation interface --- src/document/crdt/rga_tree_split.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 2172ae3ea..d7341c60a 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -25,7 +25,6 @@ import { TimeTicket, TimeTicketStruct, } from '@yorkie-js-sdk/src/document/time/ticket'; -import { StyleOperation } from '../operation/style_operation'; export interface ValueChange { actor: ActorID; @@ -40,6 +39,12 @@ interface RGATreeSplitValue { substring(indexStart: number, indexEnd?: number): RGATreeSplitValue; } +export interface StyleOperation { + fromBoundary: RGATreeSplitBoundary; + toBoundary?: RGATreeSplitBoundary; + attributes: Record; +} + /** * `RGATreeSplitPosStruct` is a structure represents the meta data of the node pos. * It is used to serialize and deserialize the node pos. From ee7eb19be7dedf15858b2ef480a952dea07a2c38 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Fri, 15 Sep 2023 16:41:16 +0900 Subject: [PATCH 09/21] Applying operations to anchor --- src/document/crdt/rga_tree_split.ts | 32 +++++++++++++++++ src/document/crdt/text.ts | 53 ++++++++++++++++++++++++++++- src/document/json/text.ts | 11 ++---- test/integration/text_test.ts | 32 ++++++++--------- 4 files changed, 103 insertions(+), 25 deletions(-) diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index d7341c60a..bfa5f03a9 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -870,6 +870,38 @@ export class RGATreeSplit { } } + /** + * `findOpsetPreferToLeft` find a closest opSet of the given anchor. + */ + public findOpsetPreferToLeft( + node: RGATreeSplitNode, + type: BoundaryType, + ): Set { + // Find current opSet from given anchor + let opSet: Set | undefined; + if (type === BoundaryType.Before) { + opSet = node.getStyleOpsBefore(); + } else if (type === BoundaryType.After) { + opSet = node.getStyleOpsAfter(); + } + + const currentType = type; + let currentNode: RGATreeSplitNode | undefined = node; + + // Traverse the node's anchor to the left to find the closest opSet + while (!opSet && currentNode) { + if (currentType == BoundaryType.Before) { + currentNode = currentNode.getPrev(); + opSet = currentNode?.getStyleOpsAfter(); + } else if (currentType == BoundaryType.After) { + opSet = currentNode?.getStyleOpsBefore(); + } + } + + // If there is no existing opSet, return an empty opSet + return opSet ? opSet : new Set(); + } + private findFloorNodePreferToLeft( id: RGATreeSplitNodeID, ): RGATreeSplitNode { diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index 5a251d799..dc82c4b66 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -30,6 +30,7 @@ import { RGATreeSplitNode, RGATreeSplitPos, RGATreeSplitPosRange, + StyleOperation, ValueChange, } from '@yorkie-js-sdk/src/document/crdt/rga_tree_split'; import { escapeString } from '@yorkie-js-sdk/src/document/json/strings'; @@ -263,6 +264,7 @@ export class CRDTText extends CRDTGCElement { // 01. Split nodes with boundaryRange if it is a remote operation const isRemote = !!latestCreatedAtMapByActor; if (isRemote) { + // NOTE(MoonGyu1): This logic may be meaningless this.rgaTreeSplit.splitNodeByBoundary(range[1]!); this.rgaTreeSplit.splitNodeByBoundary(range[0]); } @@ -276,7 +278,56 @@ export class CRDTText extends CRDTGCElement { fromBoundary.getType() != BoundaryType.None && toBoundary?.getType() != BoundaryType.None ) { - // TODO(MoonGyu1): Peritext 2. Update styleOpsBefore/styleOpsAfter of fromRight/toRight nodes + // Get fromNode and toNode from boundary + const fromNode = this.rgaTreeSplit.findNode(fromBoundary.getID()!); + const toNode = this.rgaTreeSplit.findNode(toBoundary!.getID()!); + + // Define new StyleOperation + const newOp: StyleOperation = { + fromBoundary, + toBoundary, + attributes, + }; + + // Update styleOpsBefore or styleOpsAfter of fromNode + const fromOpSet = this.rgaTreeSplit.findOpsetPreferToLeft( + fromNode, + fromBoundary.getType()!, + ); + fromOpSet.add(newOp); + + if (fromBoundary.getType() === BoundaryType.Before) { + fromNode.setStyleOpsBefore(fromOpSet); + } else if (fromBoundary.getType() === BoundaryType.After) { + fromNode.setStyleOpsAfter(fromOpSet); + } + + // Add a new StyleOperation to between nodes if it has an opSet + const betweenNode = fromNode.getNext(); + while (betweenNode && betweenNode !== toNode) { + const styleOpsBefore = betweenNode.getStyleOpsBefore(); + const styleOpsAfter = betweenNode.getStyleOpsAfter(); + if (styleOpsBefore) { + styleOpsBefore.add(newOp); + betweenNode.setStyleOpsBefore(styleOpsBefore); + } + if (styleOpsAfter) { + styleOpsAfter.add(newOp); + betweenNode.setStyleOpsAfter(styleOpsAfter); + } + } + + // Update styleOpsBefore or styleOpsAfter of toNode + const toOpSet = this.rgaTreeSplit.findOpsetPreferToLeft( + toNode, + toBoundary!.getType()!, + ); + + if (toBoundary!.getType() === BoundaryType.Before) { + toNode.setStyleOpsBefore(toOpSet); + } else if (toBoundary!.getType() === BoundaryType.After) { + toNode.setStyleOpsAfter(toOpSet); + } const createdAtMapByActor = new Map(); return [createdAtMapByActor, changes]; diff --git a/src/document/json/text.ts b/src/document/json/text.ts index 40c476f4c..73e8b034d 100644 --- a/src/document/json/text.ts +++ b/src/document/json/text.ts @@ -179,7 +179,7 @@ export class Text { const ticket = this.context.issueTimeTicket(); let hasMarkType = false; - for (const [key, value] of Object.entries(attrs)) { + for (const [key] of Object.entries(attrs)) { if (this.markTypes?.has(key)) { hasMarkType = true; } @@ -189,7 +189,7 @@ export class Text { // Find the boundaryRange if the attributes have the mark type (bold). if (hasMarkType) { - for (const [key, value] of Object.entries(attrs)) { + for (const [key] of Object.entries(attrs)) { if (this.markTypes?.has(key)) { const expand = this.markTypes.get(key)!.expand; @@ -234,12 +234,7 @@ export class Text { return true; } - /** - * `removeStyle` remove styles from text with the given attributes. - */ - removeStyle(fromIdx: number, toIdx: number, attributes: A) { - // TODO(MoonGyu1): Peritext 1. Add removeStyle method - } + // TODO(MoonGyu1): Peritext 1. Add removeStyle method /** * `indexRangeToPosRange` returns TextRangeStruct of the given index range. diff --git a/test/integration/text_test.ts b/test/integration/text_test.ts index 8bf8c38b6..4b01a7ff1 100644 --- a/test/integration/text_test.ts +++ b/test/integration/text_test.ts @@ -595,7 +595,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + // TODO(MoonGyu1): Remove skip and annotation after implementing addMark operation of bold type it.skip('ex6. conflicting overlaps(bold) - 1', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -614,9 +614,9 @@ describe('peri-text example: text concurrent edit', function () { d1.toSortedJSON(), `{"k1":[{"attrs":{"bold":true},"val":"The fox jumped."}]}`, ); - d1.update((root) => { - root.k1.removeStyle(4, 15, { bold: false }); - }, `non-bolds text by c1`); + // d1.update((root) => { + // root.k1.removeStyle(4, 15, { bold: false }); + // }, `non-bolds text by c1`); assert.equal( d1.toSortedJSON(), `{"k1":[{"attrs":{"bold":true},"val":"The "},{"attrs":{"bold":false},"val":"fox jumped."}]}`, @@ -659,9 +659,9 @@ describe('peri-text example: text concurrent edit', function () { d1.toSortedJSON(), `{"k1":[{"attrs":{"bold":true},"val":"The fox jumped."}]}`, ); - d1.update((root) => { - root.k1.removeStyle(4, 15, { bold: false }); - }, `non-bolds text by c1`); + // d1.update((root) => { + // root.k1.removeStyle(4, 15, { bold: false }); + // }, `non-bolds text by c1`); assert.equal( d1.toSortedJSON(), `{"k1":[{"attrs":{"bold":true},"val":"The "},{"attrs":{"bold":false},"val":"fox jumped."}]}`, @@ -813,24 +813,24 @@ describe('peri-text example: text concurrent edit', function () { }); describe('Style', function () { - // TODO(MoonGyu1): Remove skip after implementing removeMark operation of bold type + // TODO(MoonGyu1): Remove skip and annotation after implementing removeMark operation of bold type it.skip('should handle style operations', function () { const doc = new Document<{ k1: Text }>('test-doc'); assert.equal('{}', doc.toSortedJSON()); - doc.update((root) => { - root.k1 = new Text(); - root.k1.edit(0, 0, 'ABCD'); - root.k1.removeStyle(0, 4, { bold: true }); - }); + // doc.update((root) => { + // root.k1 = new Text(); + // root.k1.edit(0, 0, 'ABCD'); + // root.k1.removeStyle(0, 4, { bold: true }); + // }); assert.equal( doc.toSortedJSON(), `{"k1":[{"attrs":{"bold":"true"},"val":"ABCD"}]}`, ); - doc.update((root) => { - root.k1.removeStyle(1, 3, { bold: false }); - }); + // doc.update((root) => { + // root.k1.removeStyle(1, 3, { bold: false }); + // }); assert.equal( doc.toSortedJSON(), `{"k1":[{"attrs":{"bold":"true"},"val":"A"},{"attrs":{"bold":"false"},"val":"BC"},{"attrs":{"bold":"true"},"val":"D"}]}`, From a233b0e06bbd6bdcd394590b8479f2d76a610853 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Sat, 16 Sep 2023 01:03:43 +0900 Subject: [PATCH 10/21] Convert operations to attrs of toJSON --- src/document/crdt/rga_tree_split.ts | 4 ++ src/document/crdt/text.ts | 106 ++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index bfa5f03a9..0647d14d1 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -43,6 +43,7 @@ export interface StyleOperation { fromBoundary: RGATreeSplitBoundary; toBoundary?: RGATreeSplitBoundary; attributes: Record; + // NOTE(MoonGyu1): May need to introduce TimeTicket to address concurrent cases } /** @@ -884,6 +885,7 @@ export class RGATreeSplit { } else if (type === BoundaryType.After) { opSet = node.getStyleOpsAfter(); } + // NOTE(MoonGyu1): have to consider Start/End boundary type later const currentType = type; let currentNode: RGATreeSplitNode | undefined = node; @@ -893,6 +895,8 @@ export class RGATreeSplit { if (currentType == BoundaryType.Before) { currentNode = currentNode.getPrev(); opSet = currentNode?.getStyleOpsAfter(); + if (opSet) break; + opSet = currentNode?.getStyleOpsBefore(); } else if (currentType == BoundaryType.After) { opSet = currentNode?.getStyleOpsBefore(); } diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index dc82c4b66..6294cc629 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -143,9 +143,16 @@ export class CRDTTextValue { /** * `toJSON` returns the JSON encoding of this value. */ - public toJSON(): string { + public toJSON(markAttrs?: Map): string { const content = escapeString(this.content); const attrsObj = this.attributes.toObject(); + // Merge existing attrsObj and markAttrs + if (markAttrs) { + for (const [key, value] of markAttrs.entries()) { + attrsObj[key] = value; + } + } + const attrs = []; for (const [key, v] of Object.entries(attrsObj)) { const value = JSON.parse(v); @@ -183,6 +190,8 @@ export class CRDTTextValue { */ export class CRDTText extends CRDTGCElement { private rgaTreeSplit: RGATreeSplit; + // NOTE(MoonGyu1): This anchor can be relocated to better place + private lastAnchor?: Set | undefined; constructor( rgaTreeSplit: RGATreeSplit, @@ -266,7 +275,8 @@ export class CRDTText extends CRDTGCElement { if (isRemote) { // NOTE(MoonGyu1): This logic may be meaningless this.rgaTreeSplit.splitNodeByBoundary(range[1]!); - this.rgaTreeSplit.splitNodeByBoundary(range[0]); + if (range[0].getID()?.getCreatedAt()) + this.rgaTreeSplit.splitNodeByBoundary(range[0]); } // 02. style nodes between from and to @@ -279,8 +289,11 @@ export class CRDTText extends CRDTGCElement { toBoundary?.getType() != BoundaryType.None ) { // Get fromNode and toNode from boundary + const toBeStyleds: Array> = []; const fromNode = this.rgaTreeSplit.findNode(fromBoundary.getID()!); - const toNode = this.rgaTreeSplit.findNode(toBoundary!.getID()!); + const toNode = toBoundary?.getID()?.getCreatedAt() + ? this.rgaTreeSplit.findNode(toBoundary!.getID()!) + : undefined; // Define new StyleOperation const newOp: StyleOperation = { @@ -294,17 +307,27 @@ export class CRDTText extends CRDTGCElement { fromNode, fromBoundary.getType()!, ); + + const toOpSet = toNode + ? this.rgaTreeSplit.findOpsetPreferToLeft( + toNode, + toBoundary!.getType()!, + ) + : this.lastAnchor; + fromOpSet.add(newOp); if (fromBoundary.getType() === BoundaryType.Before) { fromNode.setStyleOpsBefore(fromOpSet); + toBeStyleds.push(fromNode); } else if (fromBoundary.getType() === BoundaryType.After) { fromNode.setStyleOpsAfter(fromOpSet); } // Add a new StyleOperation to between nodes if it has an opSet - const betweenNode = fromNode.getNext(); + let betweenNode = fromNode.getNext(); while (betweenNode && betweenNode !== toNode) { + toBeStyleds.push(betweenNode); const styleOpsBefore = betweenNode.getStyleOpsBefore(); const styleOpsAfter = betweenNode.getStyleOpsAfter(); if (styleOpsBefore) { @@ -315,20 +338,38 @@ export class CRDTText extends CRDTGCElement { styleOpsAfter.add(newOp); betweenNode.setStyleOpsAfter(styleOpsAfter); } + betweenNode = betweenNode.getNext(); } // Update styleOpsBefore or styleOpsAfter of toNode - const toOpSet = this.rgaTreeSplit.findOpsetPreferToLeft( - toNode, - toBoundary!.getType()!, - ); - if (toBoundary!.getType() === BoundaryType.Before) { - toNode.setStyleOpsBefore(toOpSet); + toNode!.setStyleOpsBefore(toOpSet!); } else if (toBoundary!.getType() === BoundaryType.After) { - toNode.setStyleOpsAfter(toOpSet); + toBeStyleds.push(toNode!); + toNode!.setStyleOpsAfter(toOpSet!); + } else if (toBoundary!.getType() === BoundaryType.End) { + // TODO(MoonGyu1): Add last node to toBeStyled + if (!toOpSet) this.lastAnchor = new Set(); } + for (const node of toBeStyleds) { + if (node.isRemoved()) { + continue; + } + + const [fromIdx, toIdx] = this.rgaTreeSplit.findIndexesFromRange( + node.createPosRange(), + ); + changes.push({ + type: TextChangeType.Style, + actor: editedAt.getActorID()!, + from: fromIdx, + to: toIdx, + value: { + attributes: parseObjectValues(attributes) as A, + }, + }); + } const createdAtMapByActor = new Map(); return [createdAtMapByActor, changes]; } @@ -425,12 +466,12 @@ export class CRDTText extends CRDTGCElement { if (expand === 'after') { while (fromNode && fromNode.isRemoved()) { - // NOTE(MoonGyu1): have to check if it is a last node + // NOTE(MoonGyu1): Have to check if it is a last node fromNode = fromNode.getNext(); } while (toNode && toNode.isRemoved()) { - // NOTE(MoonGyu1): have to check if it is a last node + // NOTE(MoonGyu1): Have to check if it is a last node toNode = toNode.getNext(); } @@ -485,10 +526,45 @@ export class CRDTText extends CRDTGCElement { public toJSON(): string { const json = []; + // Keep current attributes info for applying to current node + const currentAttr = new Map(); + + // NOTE(MoonGyu1): This logic can be optimized later for (const node of this.rgaTreeSplit) { - // TODO(MoonGyu1): Peritext 3. Convert operations to attrs and pass as argument of toJSON + // Update currentAttr by node anchors + const anchors = [node.getStyleOpsBefore(), node.getStyleOpsAfter()]; + for (const anchor of anchors) { + if (anchor) { + // Traverse each attribute of anchor + anchor.forEach((op) => { + for (const [key, value] of Object.entries(op.attributes)) { + // Add attribute to currentAttr if currentAttr doesn't have the attribute of anchor + if (!currentAttr.has(key)) { + currentAttr.set(key, value); + } + } + }); + + // Traverse each attribute of currentAttr + let hasAttr = false; + for (const [currentKey] of currentAttr.entries()) { + // Remove attribute from currentAttr if anchor doesn't have the attribute of currentAttr + anchor.forEach((op) => { + for (const [anchorKey] of Object.entries(op.attributes)) { + if (currentKey === anchorKey) { + hasAttr = true; + } + } + }); + if (!hasAttr) { + currentAttr.delete(currentKey); + } + } + } + } + if (!node.isRemoved()) { - json.push(node.getValue().toJSON()); + json.push(node.getValue().toJSON(currentAttr)); } } From 812343e651ce42c59399962c7d5bb4220683d4c2 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Sat, 16 Sep 2023 01:27:56 +0900 Subject: [PATCH 11/21] Remove skip related to addMark of bold type --- src/document/crdt/rga_tree_split.ts | 2 +- test/integration/document_test.ts | 3 +-- test/integration/text_test.ts | 24 +++++++++--------------- test/unit/document/document_test.ts | 9 +++------ 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 0647d14d1..818a4bcb5 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -857,7 +857,7 @@ export class RGATreeSplit { return [node, node.getNext()!]; } - // TODO(MoonGyu1): It can be optimized later + // NOTE(MoonGyu1): This logic can be optimized later /** * `findNodeWithSplit` splits and return nodes of the given position. */ diff --git a/test/integration/document_test.ts b/test/integration/document_test.ts index 79ab3c5e7..99485d5e1 100644 --- a/test/integration/document_test.ts +++ b/test/integration/document_test.ts @@ -93,8 +93,7 @@ describe('Document', function () { await c2.deactivate(); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('detects the events from doc.subscribe', async function () { + it('detects the events from doc.subscribe', async function () { const c1 = new yorkie.Client(testRPCAddr); const c2 = new yorkie.Client(testRPCAddr); await c1.activate(); diff --git a/test/integration/text_test.ts b/test/integration/text_test.ts index 4b01a7ff1..8cc37a4f5 100644 --- a/test/integration/text_test.ts +++ b/test/integration/text_test.ts @@ -444,8 +444,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('ex2. concurrent formatting and insertion', async function () { + it('ex2. concurrent formatting and insertion', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -482,8 +481,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('ex3. overlapping formatting(bold)', async function () { + it('ex3. overlapping formatting(bold)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -520,8 +518,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('ex4. overlapping different formatting(bold and italic)', async function () { + it('ex4. overlapping different formatting(bold and italic)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -595,7 +592,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip and annotation after implementing addMark operation of bold type + // TODO(MoonGyu1): Remove skip and annotation after implementing removeStyle operation of bold type it.skip('ex6. conflicting overlaps(bold) - 1', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -640,7 +637,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing removeMark operation of bold type + // TODO(MoonGyu1): Remove skip and annotation after implementing removeStyle operation of bold type it.skip('ex6. conflicting overlaps(bold) - 2', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { @@ -723,8 +720,7 @@ describe('peri-text example: text concurrent edit', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('ex8. text insertion at span boundaries(bold)', async function () { + it('ex8. text insertion at span boundaries(bold)', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.k1 = new Text(); @@ -751,14 +747,12 @@ describe('peri-text example: text concurrent edit', function () { }, `add text by c2`); assert.equal( d2.toSortedJSON(), - `{"k1":[{"val":"The "},{"attrs":{"bold":true},"val":"fox jumped"},{"val":" over the dog"},{"val":"."}]}`, + `{"k1":[{"val":"The "},{"attrs":{"bold":true},"val":"fox jumped"},{"attrs":{"bold":true},"val":" over the dog"},{"val":"."}]}`, ); + await c1.sync(); await c2.sync(); await c1.sync(); - // NOTE(chacha912): The general rule is that an inserted character inherits the bold/non-bold status - // of the preceding character.(Microsoft Word, Google Docs, Apple Pages) - // That is, the text inserted before the bold span becomes non-bold, and the text inserted after the bold span becomes bold. assert.equal( d1.toSortedJSON(), '{"k1":[{"val":"The "},{"val":"quick "},{"attrs":{"bold":true},"val":"fox jumped"},{"attrs":{"bold":true},"val":" over the dog"},{"val":"."}]}', @@ -813,7 +807,7 @@ describe('peri-text example: text concurrent edit', function () { }); describe('Style', function () { - // TODO(MoonGyu1): Remove skip and annotation after implementing removeMark operation of bold type + // TODO(MoonGyu1): Remove skip and annotation after implementing removeStyle operation of bold type it.skip('should handle style operations', function () { const doc = new Document<{ k1: Text }>('test-doc'); assert.equal('{}', doc.toSortedJSON()); diff --git a/test/unit/document/document_test.ts b/test/unit/document/document_test.ts index 2db9f5fd6..b90903b71 100644 --- a/test/unit/document/document_test.ts +++ b/test/unit/document/document_test.ts @@ -1042,8 +1042,7 @@ describe('Document', function () { }); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('changeInfo test for text', async function () { + it('changeInfo test for text', async function () { type TestDoc = { text: Text }; const doc = new Document('test-doc'); type EventForTest = Array; @@ -1072,8 +1071,7 @@ describe('Document', function () { unsub(); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('changeInfo test for text with attributes', async function () { + it('changeInfo test for text with attributes', async function () { type TestDoc = { textWithAttr: Text }; const doc = new Document('test-doc'); type EventForTest = Array; @@ -1233,8 +1231,7 @@ describe('Document', function () { assert.equal(155, doc.getRoot().counter.getValue()); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type - it.skip('sets any type of custom attribute values and can returns JSON parsable string', function () { + it('sets any type of custom attribute values and can returns JSON parsable string', function () { type AttrsType = { bold?: boolean; indent?: number; From 19a79967d4b973295d3669391665791221a556b3 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Sat, 16 Sep 2023 01:29:24 +0900 Subject: [PATCH 12/21] Add skip related to bug of Text.setStyle logic --- test/integration/snapshot_test.ts | 2 +- test/unit/api/converter_test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/integration/snapshot_test.ts b/test/integration/snapshot_test.ts index c5fd3833f..dbadfe091 100644 --- a/test/integration/snapshot_test.ts +++ b/test/integration/snapshot_test.ts @@ -58,7 +58,7 @@ describe('Snapshot', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after implementing addMark operation of bold type + // TODO(MoonGyu1): Remove skip after addressing logic of Text.setStyle it.skip('should handle snapshot for text with attributes', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { diff --git a/test/unit/api/converter_test.ts b/test/unit/api/converter_test.ts index 491795934..0393847bd 100644 --- a/test/unit/api/converter_test.ts +++ b/test/unit/api/converter_test.ts @@ -21,7 +21,8 @@ import { Counter, Text } from '@yorkie-js-sdk/src/yorkie'; import { CounterType } from '@yorkie-js-sdk/src/document/crdt/counter'; describe('Converter', function () { - it('should encode/decode bytes', function () { + // TODO(MoonGyu1): Remove skip after addressing logic of Text.setStyle + it.skip('should encode/decode bytes', function () { const doc = new Document<{ k1: { ['k1-1']: boolean; From 1d385a5e3ee1afcecc7eec24bbbc8875b91909e7 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Sun, 17 Sep 2023 19:37:06 +0900 Subject: [PATCH 13/21] Optimize the logic of setStyle --- src/api/converter.ts | 2 +- src/document/crdt/rga_tree_split.ts | 22 +-- src/document/crdt/text.ts | 202 ++++++++++------------ src/document/json/text.ts | 91 +++++----- src/document/operation/style_operation.ts | 4 +- 5 files changed, 151 insertions(+), 170 deletions(-) diff --git a/src/api/converter.ts b/src/api/converter.ts index 1fee5a158..82c44213d 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -949,11 +949,11 @@ function fromTextNodeBoundary( boundaryType = BoundaryType.None; } return RGATreeSplitBoundary.of( + boundaryType, RGATreeSplitNodeID.of( fromTimeTicket(pbTextNodeBoundary.getCreatedAt())!, pbTextNodeBoundary.getOffset(), ), - boundaryType, ); } diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 818a4bcb5..28ae68de2 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -258,10 +258,10 @@ export class RGATreeSplitPos { * `RGATreeSplitBoundary` is the boundary of the text node. */ export class RGATreeSplitBoundary { + private type: BoundaryType; private id?: RGATreeSplitNodeID; - private type?: BoundaryType; - constructor(id?: RGATreeSplitNodeID, type?: BoundaryType) { + constructor(type: BoundaryType, id?: RGATreeSplitNodeID) { this.id = id; this.type = type; } @@ -270,24 +270,24 @@ export class RGATreeSplitBoundary { * `of` creates a instance of RGATreeSplitBoundary. */ public static of( + type: BoundaryType, id?: RGATreeSplitNodeID, - type?: BoundaryType, ): RGATreeSplitBoundary { - return new RGATreeSplitBoundary(id, type); + return new RGATreeSplitBoundary(type, id); } /** - * `getID` returns the ID of this RGATreeSplitBoundary. + * `getType` returns the type of this RGATreeSplitBoundary. */ - public getID(): RGATreeSplitNodeID | undefined { - return this.id; + public getType(): BoundaryType { + return this.type; } /** - * `getType` returns the type of this RGATreeSplitBoundary. + * `getID` returns the ID of this RGATreeSplitBoundary. */ - public getType(): BoundaryType | undefined { - return this.type; + public getID(): RGATreeSplitNodeID | undefined { + return this.id; } /** @@ -303,7 +303,7 @@ export type RGATreeSplitPosRange = [RGATreeSplitPos, RGATreeSplitPos]; export type RGATreeSplitBoundaryRange = [ RGATreeSplitBoundary, - RGATreeSplitBoundary | undefined, + RGATreeSplitBoundary, ]; /** diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index 6294cc629..4002214c9 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -146,6 +146,7 @@ export class CRDTTextValue { public toJSON(markAttrs?: Map): string { const content = escapeString(this.content); const attrsObj = this.attributes.toObject(); + // Merge existing attrsObj and markAttrs if (markAttrs) { for (const [key, value] of markAttrs.entries()) { @@ -270,31 +271,35 @@ export class CRDTText extends CRDTGCElement { editedAt: TimeTicket, latestCreatedAtMapByActor?: Map, ): [Map, Array>] { + const fromBoundary = range[0]; + const toBoundary = range[1]; + // 01. Split nodes with boundaryRange if it is a remote operation const isRemote = !!latestCreatedAtMapByActor; if (isRemote) { - // NOTE(MoonGyu1): This logic may be meaningless - this.rgaTreeSplit.splitNodeByBoundary(range[1]!); - if (range[0].getID()?.getCreatedAt()) - this.rgaTreeSplit.splitNodeByBoundary(range[0]); + this.rgaTreeSplit.splitNodeByBoundary(toBoundary); + this.rgaTreeSplit.splitNodeByBoundary(fromBoundary); } - // 02. style nodes between from and to + // Get fromNode and toNode from boundary + const fromNode = this.rgaTreeSplit.findNode(fromBoundary.getID()!); + const toNode = toBoundary?.getID()?.getCreatedAt() + ? this.rgaTreeSplit.findNode(toBoundary.getID()!) + : undefined; + const changes: Array> = []; - const fromBoundary = range[0]; - const toBoundary = range[1]; - // 02-1. Update styleOpsBefore and styleOpsAfter if it is a bold type - if ( - fromBoundary.getType() != BoundaryType.None && - toBoundary?.getType() != BoundaryType.None - ) { - // Get fromNode and toNode from boundary - const toBeStyleds: Array> = []; - const fromNode = this.rgaTreeSplit.findNode(fromBoundary.getID()!); - const toNode = toBoundary?.getID()?.getCreatedAt() - ? this.rgaTreeSplit.findNode(toBoundary!.getID()!) - : undefined; + const toBeStyleds: Array> = []; + const createdAtMapByActor = new Map(); + + // 02. style nodes between from and to + const fromBoundaryType = fromBoundary.getType(); + const toBoundaryType = toBoundary.getType(); + const isMarkType = + fromBoundaryType != BoundaryType.None && + toBoundaryType != BoundaryType.None; + // 02-1. Update styleOpsBefore and styleOpsAfter if it is a bold type + if (isMarkType) { // Define new StyleOperation const newOp: StyleOperation = { fromBoundary, @@ -302,25 +307,23 @@ export class CRDTText extends CRDTGCElement { attributes, }; - // Update styleOpsBefore or styleOpsAfter of fromNode + // Get underlying OpSet of fromBoundary and toBoundary const fromOpSet = this.rgaTreeSplit.findOpsetPreferToLeft( fromNode, - fromBoundary.getType()!, + fromBoundaryType, ); const toOpSet = toNode - ? this.rgaTreeSplit.findOpsetPreferToLeft( - toNode, - toBoundary!.getType()!, - ) + ? this.rgaTreeSplit.findOpsetPreferToLeft(toNode, toBoundaryType) : this.lastAnchor; + // Update styleOpsBefore or styleOpsAfter of fromNode fromOpSet.add(newOp); - if (fromBoundary.getType() === BoundaryType.Before) { + if (fromBoundaryType === BoundaryType.Before) { fromNode.setStyleOpsBefore(fromOpSet); toBeStyleds.push(fromNode); - } else if (fromBoundary.getType() === BoundaryType.After) { + } else if (fromBoundaryType === BoundaryType.After) { fromNode.setStyleOpsAfter(fromOpSet); } @@ -342,47 +345,19 @@ export class CRDTText extends CRDTGCElement { } // Update styleOpsBefore or styleOpsAfter of toNode - if (toBoundary!.getType() === BoundaryType.Before) { + if (toBoundaryType === BoundaryType.Before) { toNode!.setStyleOpsBefore(toOpSet!); - } else if (toBoundary!.getType() === BoundaryType.After) { + } else if (toBoundaryType === BoundaryType.After) { toBeStyleds.push(toNode!); toNode!.setStyleOpsAfter(toOpSet!); - } else if (toBoundary!.getType() === BoundaryType.End) { + } else if (toBoundaryType === BoundaryType.End) { // TODO(MoonGyu1): Add last node to toBeStyled if (!toOpSet) this.lastAnchor = new Set(); } - - for (const node of toBeStyleds) { - if (node.isRemoved()) { - continue; - } - - const [fromIdx, toIdx] = this.rgaTreeSplit.findIndexesFromRange( - node.createPosRange(), - ); - changes.push({ - type: TextChangeType.Style, - actor: editedAt.getActorID()!, - from: fromIdx, - to: toIdx, - value: { - attributes: parseObjectValues(attributes) as A, - }, - }); - } - const createdAtMapByActor = new Map(); - return [createdAtMapByActor, changes]; } // 02-2. Apply the existing logic to style nodes if they are not of a bold type else { - const fromNode = this.rgaTreeSplit.findNode(fromBoundary.getID()!); - const toNode = toBoundary?.getID()?.getCreatedAt() - ? this.rgaTreeSplit.findNode(toBoundary.getID()!) - : undefined; - const nodes = this.rgaTreeSplit.findBetween(fromNode, toNode); - const createdAtMapByActor = new Map(); - const toBeStyleds: Array> = []; for (const node of nodes) { const actorID = node.getCreatedAt().getActorID()!; @@ -401,32 +376,34 @@ export class CRDTText extends CRDTGCElement { toBeStyleds.push(node); } } + } - for (const node of toBeStyleds) { - if (node.isRemoved()) { - continue; - } - - const [fromIdx, toIdx] = this.rgaTreeSplit.findIndexesFromRange( - node.createPosRange(), - ); - changes.push({ - type: TextChangeType.Style, - actor: editedAt.getActorID()!, - from: fromIdx, - to: toIdx, - value: { - attributes: parseObjectValues(attributes) as A, - }, - }); + for (const node of toBeStyleds) { + if (node.isRemoved()) { + continue; + } + const [fromIdx, toIdx] = this.rgaTreeSplit.findIndexesFromRange( + node.createPosRange(), + ); + changes.push({ + type: TextChangeType.Style, + actor: editedAt.getActorID()!, + from: fromIdx, + to: toIdx, + value: { + attributes: parseObjectValues(attributes) as A, + }, + }); + + if (!isMarkType) { for (const [key, value] of Object.entries(attributes)) { node.getValue().setAttr(key, value, editedAt); } } - - return [createdAtMapByActor, changes]; } + + return [createdAtMapByActor, changes]; } /** @@ -465,19 +442,18 @@ export class CRDTText extends CRDTGCElement { let toNode: RGATreeSplitNode | undefined = toRight; if (expand === 'after') { - while (fromNode && fromNode.isRemoved()) { - // NOTE(MoonGyu1): Have to check if it is a last node + while (fromNode && fromNode.isRemoved() && fromNode != toNode) { fromNode = fromNode.getNext(); } while (toNode && toNode.isRemoved()) { - // NOTE(MoonGyu1): Have to check if it is a last node toNode = toNode.getNext(); } const fromNodeID = fromNode?.getID(); const toNodeID = toNode?.getID(); + // NOTE(MoonGyu1): Need to check if fromNode does not exist const fromBoundaryType = fromNodeID ? BoundaryType.Before : BoundaryType.Start; @@ -486,8 +462,8 @@ export class CRDTText extends CRDTGCElement { : BoundaryType.End; return [ - RGATreeSplitBoundary.of(fromNodeID, fromBoundaryType), - RGATreeSplitBoundary.of(toNodeID, toBoundaryType), + RGATreeSplitBoundary.of(fromBoundaryType, fromNodeID), + RGATreeSplitBoundary.of(toBoundaryType, toNodeID), ]; } } @@ -500,8 +476,8 @@ export class CRDTText extends CRDTGCElement { ); return [ - RGATreeSplitBoundary.of(fromRight.getID(), BoundaryType.None), - RGATreeSplitBoundary.of(toRight?.getID(), BoundaryType.None), + RGATreeSplitBoundary.of(BoundaryType.None, fromRight.getID()), + RGATreeSplitBoundary.of(BoundaryType.None, toRight?.getID()), ]; } @@ -527,45 +503,26 @@ export class CRDTText extends CRDTGCElement { const json = []; // Keep current attributes info for applying to current node - const currentAttr = new Map(); + let currentAttr = new Map(); - // NOTE(MoonGyu1): This logic can be optimized later for (const node of this.rgaTreeSplit) { - // Update currentAttr by node anchors - const anchors = [node.getStyleOpsBefore(), node.getStyleOpsAfter()]; - for (const anchor of anchors) { - if (anchor) { - // Traverse each attribute of anchor - anchor.forEach((op) => { - for (const [key, value] of Object.entries(op.attributes)) { - // Add attribute to currentAttr if currentAttr doesn't have the attribute of anchor - if (!currentAttr.has(key)) { - currentAttr.set(key, value); - } - } - }); - - // Traverse each attribute of currentAttr - let hasAttr = false; - for (const [currentKey] of currentAttr.entries()) { - // Remove attribute from currentAttr if anchor doesn't have the attribute of currentAttr - anchor.forEach((op) => { - for (const [anchorKey] of Object.entries(op.attributes)) { - if (currentKey === anchorKey) { - hasAttr = true; - } - } - }); - if (!hasAttr) { - currentAttr.delete(currentKey); - } - } - } + const beforeAnchor = node.getStyleOpsBefore(); + const afterAnchor = node.getStyleOpsAfter(); + + // Update currentAttr by before anchor of node + if (beforeAnchor) { + currentAttr = this.getAttrsFromAnchor(beforeAnchor); } + // Apply currentAttr if node is not removed if (!node.isRemoved()) { json.push(node.getValue().toJSON(currentAttr)); } + + // Update currentAttr by after anchor of node + if (afterAnchor) { + currentAttr = this.getAttrsFromAnchor(afterAnchor); + } } return `[${json.join(',')}]`; @@ -655,4 +612,19 @@ export class CRDTText extends CRDTGCElement { public findIndexesFromRange(range: RGATreeSplitPosRange): [number, number] { return this.rgaTreeSplit.findIndexesFromRange(range); } + + /** + * `getAttrsFromAnchor` returns the attributes of the given anchor. + */ + public getAttrsFromAnchor(anchor: Set): Map { + const attrs = new Map(); + + anchor.forEach((op) => { + for (const [key, value] of Object.entries(op.attributes)) { + attrs.set(key, value); + } + }); + + return attrs; + } } diff --git a/src/document/json/text.ts b/src/document/json/text.ts index 73e8b034d..d9dc52b3a 100644 --- a/src/document/json/text.ts +++ b/src/document/json/text.ts @@ -177,59 +177,68 @@ export class Text { const attrs = stringifyObjectValues(attributes); const ticket = this.context.issueTimeTicket(); - - let hasMarkType = false; - for (const [key] of Object.entries(attrs)) { - if (this.markTypes?.has(key)) { - hasMarkType = true; - } - } - let boundaryRange: RGATreeSplitBoundaryRange; - // Find the boundaryRange if the attributes have the mark type (bold). - if (hasMarkType) { - for (const [key] of Object.entries(attrs)) { - if (this.markTypes?.has(key)) { - const expand = this.markTypes.get(key)!.expand; - - // delete attrs[key]; - - boundaryRange = this.text.posRangeToBoundaryRange( - posRange[0], - posRange[1], + for (const [key, value] of Object.entries(attrs)) { + if (this.markTypes?.has(key)) { + const expand = this.markTypes.get(key)!.expand; + + // Find the boundaryRange if the attributes have the mark type (bold). + boundaryRange = this.text.posRangeToBoundaryRange( + posRange[0], + posRange[1], + ticket, + expand, + ); + + // Execute the existing logic + const [maxCreatedAtMapByActor] = this.text.setStyle( + boundaryRange!, + { [key]: value }, + ticket, + ); + + this.context.push( + new StyleOperation( + this.text.getCreatedAt(), + boundaryRange![0], + boundaryRange![1], + maxCreatedAtMapByActor, + new Map([[key, value]]), ticket, - expand, - ); - } + ), + ); + + delete attrs[key]; } } - // Find the boundaryRange if the attributes don't have the mark type (bold) - else { + + if (Object.entries(attrs).length > 0) { + // Find the boundaryRange if the attributes don't have the mark type (bold) boundaryRange = this.text.posRangeToBoundaryRange( posRange[0], posRange[1], ticket, ); - } - - // Execute the existing logic - const [maxCreatedAtMapByActor] = this.text.setStyle( - boundaryRange!, - attrs, - ticket, - ); - this.context.push( - new StyleOperation( - this.text.getCreatedAt(), - boundaryRange![0], - boundaryRange![1], - maxCreatedAtMapByActor, - new Map(Object.entries(attrs)), + // Execute the existing logic + const [maxCreatedAtMapByActor] = this.text.setStyle( + boundaryRange!, + attrs, ticket, - ), - ); + ); + + this.context.push( + new StyleOperation( + this.text.getCreatedAt(), + boundaryRange![0], + boundaryRange![1], + maxCreatedAtMapByActor, + new Map(Object.entries(attrs)), + ticket, + ), + ); + } return true; } diff --git a/src/document/operation/style_operation.ts b/src/document/operation/style_operation.ts index 4be199aa6..7674dcea3 100644 --- a/src/document/operation/style_operation.ts +++ b/src/document/operation/style_operation.ts @@ -30,14 +30,14 @@ import { Indexable } from '../document'; */ export class StyleOperation extends Operation { private fromBoundary: RGATreeSplitBoundary; - private toBoundary: RGATreeSplitBoundary | undefined; + private toBoundary: RGATreeSplitBoundary; private maxCreatedAtMapByActor: Map; private attributes: Map; constructor( parentCreatedAt: TimeTicket, fromBoundary: RGATreeSplitBoundary, - toBoundary: RGATreeSplitBoundary | undefined, + toBoundary: RGATreeSplitBoundary, maxCreatedAtMapByActor: Map, attributes: Map, executedAt: TimeTicket, From 19597e3f1da418cedae39148e2fc953ad0a803d5 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Sun, 17 Sep 2023 20:39:37 +0900 Subject: [PATCH 14/21] Add last node to toBeStyleds if boundary type is End --- src/document/crdt/text.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index 4002214c9..f9eca1b03 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -351,7 +351,12 @@ export class CRDTText extends CRDTGCElement { toBeStyleds.push(toNode!); toNode!.setStyleOpsAfter(toOpSet!); } else if (toBoundaryType === BoundaryType.End) { - // TODO(MoonGyu1): Add last node to toBeStyled + // Add last node to toBeStyled if boundary type is End + let lastNode = fromNode; + while (lastNode.getNext() && !lastNode.getNext()!.isRemoved()) { + lastNode = lastNode.getNext()!; + } + toBeStyleds.push(lastNode); if (!toOpSet) this.lastAnchor = new Set(); } } From 2020e6cab91d1a1e805696e33dd02bfd0643d7ff Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Sun, 17 Sep 2023 23:08:51 +0900 Subject: [PATCH 15/21] Remove unnecessary logic --- src/document/crdt/text.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index f9eca1b03..bc4f0299a 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -329,7 +329,11 @@ export class CRDTText extends CRDTGCElement { // Add a new StyleOperation to between nodes if it has an opSet let betweenNode = fromNode.getNext(); - while (betweenNode && betweenNode !== toNode) { + while ( + betweenNode && + betweenNode !== toNode && + !betweenNode.isRemoved() + ) { toBeStyleds.push(betweenNode); const styleOpsBefore = betweenNode.getStyleOpsBefore(); const styleOpsAfter = betweenNode.getStyleOpsAfter(); @@ -351,13 +355,7 @@ export class CRDTText extends CRDTGCElement { toBeStyleds.push(toNode!); toNode!.setStyleOpsAfter(toOpSet!); } else if (toBoundaryType === BoundaryType.End) { - // Add last node to toBeStyled if boundary type is End - let lastNode = fromNode; - while (lastNode.getNext() && !lastNode.getNext()!.isRemoved()) { - lastNode = lastNode.getNext()!; - } - toBeStyleds.push(lastNode); - if (!toOpSet) this.lastAnchor = new Set(); + if (!toOpSet) this.lastAnchor = new Set(); } } // 02-2. Apply the existing logic to style nodes if they are not of a bold type From 633e8585b9c98bbb0e4b57b7de2aa4f39bec3222 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Mon, 18 Sep 2023 12:41:15 +0900 Subject: [PATCH 16/21] Update toTextNodes of converter --- src/api/converter.ts | 18 ++++++++++++++++++ src/document/crdt/rga_tree_split.ts | 15 +++++++++++++++ src/document/crdt/text.ts | 19 ++----------------- test/unit/api/converter_test.ts | 3 +-- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/api/converter.ts b/src/api/converter.ts index 82c44213d..ca7a21c8f 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -539,6 +539,7 @@ function toTextNodes( rgaTreeSplit: RGATreeSplit, ): Array { const pbTextNodes = []; + let currentAttr = new Map(); for (const textNode of rgaTreeSplit) { const pbTextNode = new PbTextNode(); @@ -547,6 +548,23 @@ function toTextNodes( pbTextNode.setRemovedAt(toTimeTicket(textNode.getRemovedAt())); const pbNodeAttrsMap = pbTextNode.getAttributesMap(); + + const beforeAnchor = textNode.getStyleOpsBefore(); + const afterAnchor = textNode.getStyleOpsAfter(); + if (beforeAnchor) { + currentAttr = rgaTreeSplit.getAttrsFromAnchor(beforeAnchor); + } + if (!textNode.isRemoved()) { + for (const [key, value] of currentAttr.entries()) { + const pbNodeAttr = new PbNodeAttr(); + pbNodeAttr.setValue(value); + pbNodeAttrsMap.set(key, pbNodeAttr); + } + } + if (afterAnchor) { + currentAttr = rgaTreeSplit.getAttrsFromAnchor(afterAnchor); + } + const attrs = textNode.getValue().getAttrs(); for (const attr of attrs) { const pbNodeAttr = new PbNodeAttr(); diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 28ae68de2..7c4291e21 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -906,6 +906,21 @@ export class RGATreeSplit { return opSet ? opSet : new Set(); } + /** + * `getAttrsFromAnchor` returns the attributes of the given anchor. + */ + public getAttrsFromAnchor(anchor: Set): Map { + const attrs = new Map(); + + anchor.forEach((op) => { + for (const [key, value] of Object.entries(op.attributes)) { + attrs.set(key, value); + } + }); + + return attrs; + } + private findFloorNodePreferToLeft( id: RGATreeSplitNodeID, ): RGATreeSplitNode { diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index bc4f0299a..54ebcc9ad 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -514,7 +514,7 @@ export class CRDTText extends CRDTGCElement { // Update currentAttr by before anchor of node if (beforeAnchor) { - currentAttr = this.getAttrsFromAnchor(beforeAnchor); + currentAttr = this.rgaTreeSplit.getAttrsFromAnchor(beforeAnchor); } // Apply currentAttr if node is not removed @@ -524,7 +524,7 @@ export class CRDTText extends CRDTGCElement { // Update currentAttr by after anchor of node if (afterAnchor) { - currentAttr = this.getAttrsFromAnchor(afterAnchor); + currentAttr = this.rgaTreeSplit.getAttrsFromAnchor(afterAnchor); } } @@ -615,19 +615,4 @@ export class CRDTText extends CRDTGCElement { public findIndexesFromRange(range: RGATreeSplitPosRange): [number, number] { return this.rgaTreeSplit.findIndexesFromRange(range); } - - /** - * `getAttrsFromAnchor` returns the attributes of the given anchor. - */ - public getAttrsFromAnchor(anchor: Set): Map { - const attrs = new Map(); - - anchor.forEach((op) => { - for (const [key, value] of Object.entries(op.attributes)) { - attrs.set(key, value); - } - }); - - return attrs; - } } diff --git a/test/unit/api/converter_test.ts b/test/unit/api/converter_test.ts index 0393847bd..491795934 100644 --- a/test/unit/api/converter_test.ts +++ b/test/unit/api/converter_test.ts @@ -21,8 +21,7 @@ import { Counter, Text } from '@yorkie-js-sdk/src/yorkie'; import { CounterType } from '@yorkie-js-sdk/src/document/crdt/counter'; describe('Converter', function () { - // TODO(MoonGyu1): Remove skip after addressing logic of Text.setStyle - it.skip('should encode/decode bytes', function () { + it('should encode/decode bytes', function () { const doc = new Document<{ k1: { ['k1-1']: boolean; From 68d4d3dea376b90394ec2df36985f91428b7ad47 Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Mon, 18 Sep 2023 16:35:07 +0900 Subject: [PATCH 17/21] Apply ops when get text values --- src/document/crdt/text.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index 54ebcc9ad..f01834102 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -550,15 +550,33 @@ export class CRDTText extends CRDTGCElement { */ public values(): Array> { const values = []; + let currentAttr = new Map(); for (const node of this.rgaTreeSplit) { + const beforeAnchor = node.getStyleOpsBefore(); + const afterAnchor = node.getStyleOpsAfter(); + + if (beforeAnchor) { + currentAttr = this.rgaTreeSplit.getAttrsFromAnchor(beforeAnchor); + } + if (!node.isRemoved()) { const value = node.getValue(); + const attributes = value.getAttributes(); + if (currentAttr) { + for (const [key, value] of currentAttr.entries()) { + attributes[key] = value; + } + } values.push({ - attributes: parseObjectValues(value.getAttributes()), + attributes: parseObjectValues(attributes), content: value.getContent(), }); } + + if (afterAnchor) { + currentAttr = this.rgaTreeSplit.getAttrsFromAnchor(afterAnchor); + } } return values; From 3f4744d1d7f7ce250873e43ddcdd7ad358f3aadd Mon Sep 17 00:00:00 2001 From: MoonGyu1 Date: Mon, 18 Sep 2023 16:35:25 +0900 Subject: [PATCH 18/21] Fix error --- public/quill.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/quill.html b/public/quill.html index e25673fb4..02cee04fd 100644 --- a/public/quill.html +++ b/public/quill.html @@ -77,7 +77,7 @@ }); await client.attach(doc, { - initialPresence: { username: `username-${shortUniqueID()}` }, + initialPresence: { username: `username-${shortUniqueID}` }, }); doc.update((root) => { From ce14cb04fbef8280f121485c5fbfefa06cb0da82 Mon Sep 17 00:00:00 2001 From: Yourim Cha Date: Mon, 18 Sep 2023 18:09:30 +0900 Subject: [PATCH 19/21] Add fields markOpsBefore and markOpsAfter to TextNode in protobuf --- src/api/converter.ts | 100 ++++- src/api/yorkie/v1/resources.proto | 12 + src/api/yorkie/v1/resources_pb.d.ts | 62 +++ src/api/yorkie/v1/resources_pb.js | 543 +++++++++++++++++++++++- src/api/yorkie/v1/yorkie_grpc_web_pb.js | 10 +- src/document/crdt/rga_tree_split.ts | 56 ++- test/integration/snapshot_test.ts | 3 +- 7 files changed, 741 insertions(+), 45 deletions(-) diff --git a/src/api/converter.ts b/src/api/converter.ts index ca7a21c8f..9f273a76d 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -52,6 +52,7 @@ import { RGATreeSplitNode, RGATreeSplitNodeID, RGATreeSplitPos, + StyleOperation as MarkOperation, } from '@yorkie-js-sdk/src/document/crdt/rga_tree_split'; import { CRDTText, CRDTTextValue } from '@yorkie-js-sdk/src/document/crdt/text'; import { @@ -83,6 +84,8 @@ import { TreePos as PbTreePos, TreeNodeID as PbTreeNodeID, ValueType as PbValueType, + MarkOps as PbMarkOps, + MarkOp as PbMarkOp, } from '@yorkie-js-sdk/src/api/yorkie/v1/resources_pb'; import { IncreaseOperation } from '@yorkie-js-sdk/src/document/operation/increase_operation'; import { @@ -128,6 +131,33 @@ function toPresenceChange( return pbPresenceChange; } +/** + * `toMarkOps` converts the given model to Protobuf format. + */ +function toMarkOps(markOps: Set): PbMarkOps { + const pbMarkOps = new PbMarkOps(); + const pbMarkOpsList: Array = []; + for (const markOp of markOps) { + pbMarkOpsList.push(toMarkOp(markOp)); + } + pbMarkOps.setOperationsList(pbMarkOpsList); + return pbMarkOps; +} + +/** + * `toMarkOp` converts the given model to Protobuf format. + */ +function toMarkOp(markOp: MarkOperation): PbMarkOp { + const pbMarkOp = new PbMarkOp(); + pbMarkOp.setFromboundary(toTextNodeBoundary(markOp.fromBoundary)); + pbMarkOp.setToboundary(toTextNodeBoundary(markOp.toBoundary)); + const pbAttributes = pbMarkOp.getAttributesMap(); + for (const [key, value] of Object.entries(markOp.attributes)) { + pbAttributes.set(key, value); + } + return pbMarkOp; +} + /** * `toCheckpoint` converts the given model to Protobuf format. */ @@ -539,7 +569,6 @@ function toTextNodes( rgaTreeSplit: RGATreeSplit, ): Array { const pbTextNodes = []; - let currentAttr = new Map(); for (const textNode of rgaTreeSplit) { const pbTextNode = new PbTextNode(); @@ -548,23 +577,6 @@ function toTextNodes( pbTextNode.setRemovedAt(toTimeTicket(textNode.getRemovedAt())); const pbNodeAttrsMap = pbTextNode.getAttributesMap(); - - const beforeAnchor = textNode.getStyleOpsBefore(); - const afterAnchor = textNode.getStyleOpsAfter(); - if (beforeAnchor) { - currentAttr = rgaTreeSplit.getAttrsFromAnchor(beforeAnchor); - } - if (!textNode.isRemoved()) { - for (const [key, value] of currentAttr.entries()) { - const pbNodeAttr = new PbNodeAttr(); - pbNodeAttr.setValue(value); - pbNodeAttrsMap.set(key, pbNodeAttr); - } - } - if (afterAnchor) { - currentAttr = rgaTreeSplit.getAttrsFromAnchor(afterAnchor); - } - const attrs = textNode.getValue().getAttrs(); for (const attr of attrs) { const pbNodeAttr = new PbNodeAttr(); @@ -573,6 +585,14 @@ function toTextNodes( pbNodeAttrsMap.set(attr.getKey(), pbNodeAttr); } + const styleOpsBefore = textNode.getStyleOpsBefore(); + if (styleOpsBefore) { + pbTextNode.setMarkopsbefore(toMarkOps(styleOpsBefore)); + } + const styleOpsAfter = textNode.getStyleOpsAfter(); + if (styleOpsAfter) { + pbTextNode.setMarkopsafter(toMarkOps(styleOpsAfter)); + } pbTextNodes.push(pbTextNode); } @@ -985,6 +1005,22 @@ function fromTextNodeID(pbTextNodeID: PbTextNodeID): RGATreeSplitNodeID { ); } +/** + * `fromMarkOp` converts the given Protobuf format to model format. + */ +function fromMarkOp(pbMarkOp: PbMarkOp): MarkOperation { + const attributes: Record = {}; + pbMarkOp.getAttributesMap().forEach((value, key) => { + attributes[key as string] = value; + }); + + return { + fromBoundary: fromTextNodeBoundary(pbMarkOp.getFromboundary()!), + toBoundary: fromTextNodeBoundary(pbMarkOp.getToboundary()!), + attributes, + }; +} + /** * `fromTextNode` converts the given Protobuf format to model format. */ @@ -998,10 +1034,30 @@ function fromTextNode(pbTextNode: PbTextNode): RGATreeSplitNode { ); }); - const textNode = RGATreeSplitNode.create( - fromTextNodeID(pbTextNode.getId()!), - textValue, - ); + let styleOpsBefore: Set | undefined; + const markOpsBefore = pbTextNode.getMarkopsbefore()?.getOperationsList(); + if (markOpsBefore) { + styleOpsBefore = new Set(); + for (const op of markOpsBefore) { + styleOpsBefore.add(fromMarkOp(op)); + } + } + + let styleOpsAfter: Set | undefined; + const markOpsAfter = pbTextNode.getMarkopsafter()?.getOperationsList(); + if (markOpsAfter) { + styleOpsAfter = new Set(); + for (const op of markOpsAfter) { + styleOpsAfter.add(fromMarkOp(op)); + } + } + + const textNode = RGATreeSplitNode.create({ + id: fromTextNodeID(pbTextNode.getId()!), + value: textValue, + styleOpsBefore, + styleOpsAfter, + }); textNode.remove(fromTimeTicket(pbTextNode.getRemovedAt())); return textNode; } diff --git a/src/api/yorkie/v1/resources.proto b/src/api/yorkie/v1/resources.proto index 65a1d2010..27fc676f5 100644 --- a/src/api/yorkie/v1/resources.proto +++ b/src/api/yorkie/v1/resources.proto @@ -231,6 +231,8 @@ message TextNode { TimeTicket removed_at = 3; TextNodeID ins_prev_id = 4; map attributes = 5; + MarkOps mark_ops_before = 6; + MarkOps mark_ops_After = 7; } message TextNodeID { @@ -238,6 +240,16 @@ message TextNodeID { int32 offset = 2; } +message MarkOps { + repeated MarkOp operations = 1; +} + +message MarkOp { + TextNodeBoundary from_boundary = 1; + TextNodeBoundary to_boundary = 2; + map attributes = 3; +} + message TreeNode { TreeNodeID id = 1; string type = 2; diff --git a/src/api/yorkie/v1/resources_pb.d.ts b/src/api/yorkie/v1/resources_pb.d.ts index 83f60acb0..7d6a9c838 100644 --- a/src/api/yorkie/v1/resources_pb.d.ts +++ b/src/api/yorkie/v1/resources_pb.d.ts @@ -1084,6 +1084,16 @@ export class TextNode extends jspb.Message { getAttributesMap(): jspb.Map; clearAttributesMap(): TextNode; + getMarkopsbefore(): MarkOps | undefined; + setMarkopsbefore(value?: MarkOps): TextNode; + hasMarkopsbefore(): boolean; + clearMarkopsbefore(): TextNode; + + getMarkopsafter(): MarkOps | undefined; + setMarkopsafter(value?: MarkOps): TextNode; + hasMarkopsafter(): boolean; + clearMarkopsafter(): TextNode; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): TextNode.AsObject; static toObject(includeInstance: boolean, msg: TextNode): TextNode.AsObject; @@ -1099,6 +1109,8 @@ export namespace TextNode { removedAt?: TimeTicket.AsObject, insPrevId?: TextNodeID.AsObject, attributesMap: Array<[string, NodeAttr.AsObject]>, + markopsbefore?: MarkOps.AsObject, + markopsafter?: MarkOps.AsObject, } } @@ -1126,6 +1138,56 @@ export namespace TextNodeID { } } +export class MarkOps extends jspb.Message { + getOperationsList(): Array; + setOperationsList(value: Array): MarkOps; + clearOperationsList(): MarkOps; + addOperations(value?: MarkOp, index?: number): MarkOp; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): MarkOps.AsObject; + static toObject(includeInstance: boolean, msg: MarkOps): MarkOps.AsObject; + static serializeBinaryToWriter(message: MarkOps, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): MarkOps; + static deserializeBinaryFromReader(message: MarkOps, reader: jspb.BinaryReader): MarkOps; +} + +export namespace MarkOps { + export type AsObject = { + operationsList: Array, + } +} + +export class MarkOp extends jspb.Message { + getFromboundary(): TextNodeBoundary | undefined; + setFromboundary(value?: TextNodeBoundary): MarkOp; + hasFromboundary(): boolean; + clearFromboundary(): MarkOp; + + getToboundary(): TextNodeBoundary | undefined; + setToboundary(value?: TextNodeBoundary): MarkOp; + hasToboundary(): boolean; + clearToboundary(): MarkOp; + + getAttributesMap(): jspb.Map; + clearAttributesMap(): MarkOp; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): MarkOp.AsObject; + static toObject(includeInstance: boolean, msg: MarkOp): MarkOp.AsObject; + static serializeBinaryToWriter(message: MarkOp, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): MarkOp; + static deserializeBinaryFromReader(message: MarkOp, reader: jspb.BinaryReader): MarkOp; +} + +export namespace MarkOp { + export type AsObject = { + fromboundary?: TextNodeBoundary.AsObject, + toboundary?: TextNodeBoundary.AsObject, + attributesMap: Array<[string, string]>, + } +} + export class TreeNode extends jspb.Message { getId(): TreeNodeID | undefined; setId(value?: TreeNodeID): TreeNode; diff --git a/src/api/yorkie/v1/resources_pb.js b/src/api/yorkie/v1/resources_pb.js index 01c1823a6..7f4d98917 100644 --- a/src/api/yorkie/v1/resources_pb.js +++ b/src/api/yorkie/v1/resources_pb.js @@ -36,6 +36,8 @@ goog.exportSymbol('proto.yorkie.v1.JSONElement.Primitive', null, global); goog.exportSymbol('proto.yorkie.v1.JSONElement.Text', null, global); goog.exportSymbol('proto.yorkie.v1.JSONElement.Tree', null, global); goog.exportSymbol('proto.yorkie.v1.JSONElementSimple', null, global); +goog.exportSymbol('proto.yorkie.v1.MarkOp', null, global); +goog.exportSymbol('proto.yorkie.v1.MarkOps', null, global); goog.exportSymbol('proto.yorkie.v1.NodeAttr', null, global); goog.exportSymbol('proto.yorkie.v1.Operation', null, global); goog.exportSymbol('proto.yorkie.v1.Operation.Add', null, global); @@ -657,6 +659,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.yorkie.v1.TextNodeID.displayName = 'proto.yorkie.v1.TextNodeID'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.yorkie.v1.MarkOps = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.yorkie.v1.MarkOps.repeatedFields_, null); +}; +goog.inherits(proto.yorkie.v1.MarkOps, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.yorkie.v1.MarkOps.displayName = 'proto.yorkie.v1.MarkOps'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.yorkie.v1.MarkOp = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.yorkie.v1.MarkOp, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.yorkie.v1.MarkOp.displayName = 'proto.yorkie.v1.MarkOp'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -9237,7 +9281,9 @@ proto.yorkie.v1.TextNode.toObject = function(includeInstance, msg) { value: jspb.Message.getFieldWithDefault(msg, 2, ""), removedAt: (f = msg.getRemovedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), insPrevId: (f = msg.getInsPrevId()) && proto.yorkie.v1.TextNodeID.toObject(includeInstance, f), - attributesMap: (f = msg.getAttributesMap()) ? f.toObject(includeInstance, proto.yorkie.v1.NodeAttr.toObject) : [] + attributesMap: (f = msg.getAttributesMap()) ? f.toObject(includeInstance, proto.yorkie.v1.NodeAttr.toObject) : [], + markopsbefore: (f = msg.getMarkopsbefore()) && proto.yorkie.v1.MarkOps.toObject(includeInstance, f), + markopsafter: (f = msg.getMarkopsafter()) && proto.yorkie.v1.MarkOps.toObject(includeInstance, f) }; if (includeInstance) { @@ -9299,6 +9345,16 @@ proto.yorkie.v1.TextNode.deserializeBinaryFromReader = function(msg, reader) { jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.yorkie.v1.NodeAttr.deserializeBinaryFromReader, "", new proto.yorkie.v1.NodeAttr()); }); break; + case 6: + var value = new proto.yorkie.v1.MarkOps; + reader.readMessage(value,proto.yorkie.v1.MarkOps.deserializeBinaryFromReader); + msg.setMarkopsbefore(value); + break; + case 7: + var value = new proto.yorkie.v1.MarkOps; + reader.readMessage(value,proto.yorkie.v1.MarkOps.deserializeBinaryFromReader); + msg.setMarkopsafter(value); + break; default: reader.skipField(); break; @@ -9363,6 +9419,22 @@ proto.yorkie.v1.TextNode.serializeBinaryToWriter = function(message, writer) { if (f && f.getLength() > 0) { f.serializeBinary(5, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.yorkie.v1.NodeAttr.serializeBinaryToWriter); } + f = message.getMarkopsbefore(); + if (f != null) { + writer.writeMessage( + 6, + f, + proto.yorkie.v1.MarkOps.serializeBinaryToWriter + ); + } + f = message.getMarkopsafter(); + if (f != null) { + writer.writeMessage( + 7, + f, + proto.yorkie.v1.MarkOps.serializeBinaryToWriter + ); + } }; @@ -9517,6 +9589,80 @@ proto.yorkie.v1.TextNode.prototype.clearAttributesMap = function() { return this;}; +/** + * optional MarkOps markOpsBefore = 6; + * @return {?proto.yorkie.v1.MarkOps} + */ +proto.yorkie.v1.TextNode.prototype.getMarkopsbefore = function() { + return /** @type{?proto.yorkie.v1.MarkOps} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.MarkOps, 6)); +}; + + +/** + * @param {?proto.yorkie.v1.MarkOps|undefined} value + * @return {!proto.yorkie.v1.TextNode} returns this +*/ +proto.yorkie.v1.TextNode.prototype.setMarkopsbefore = function(value) { + return jspb.Message.setWrapperField(this, 6, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.TextNode} returns this + */ +proto.yorkie.v1.TextNode.prototype.clearMarkopsbefore = function() { + return this.setMarkopsbefore(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.TextNode.prototype.hasMarkopsbefore = function() { + return jspb.Message.getField(this, 6) != null; +}; + + +/** + * optional MarkOps markOpsAfter = 7; + * @return {?proto.yorkie.v1.MarkOps} + */ +proto.yorkie.v1.TextNode.prototype.getMarkopsafter = function() { + return /** @type{?proto.yorkie.v1.MarkOps} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.MarkOps, 7)); +}; + + +/** + * @param {?proto.yorkie.v1.MarkOps|undefined} value + * @return {!proto.yorkie.v1.TextNode} returns this +*/ +proto.yorkie.v1.TextNode.prototype.setMarkopsafter = function(value) { + return jspb.Message.setWrapperField(this, 7, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.TextNode} returns this + */ +proto.yorkie.v1.TextNode.prototype.clearMarkopsafter = function() { + return this.setMarkopsafter(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.TextNode.prototype.hasMarkopsafter = function() { + return jspb.Message.getField(this, 7) != null; +}; + + @@ -9699,6 +9845,401 @@ proto.yorkie.v1.TextNodeID.prototype.setOffset = function(value) { +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.yorkie.v1.MarkOps.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.yorkie.v1.MarkOps.prototype.toObject = function(opt_includeInstance) { + return proto.yorkie.v1.MarkOps.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.yorkie.v1.MarkOps} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.MarkOps.toObject = function(includeInstance, msg) { + var f, obj = { + operationsList: jspb.Message.toObjectList(msg.getOperationsList(), + proto.yorkie.v1.MarkOp.toObject, includeInstance) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.yorkie.v1.MarkOps} + */ +proto.yorkie.v1.MarkOps.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.yorkie.v1.MarkOps; + return proto.yorkie.v1.MarkOps.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.yorkie.v1.MarkOps} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.yorkie.v1.MarkOps} + */ +proto.yorkie.v1.MarkOps.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.yorkie.v1.MarkOp; + reader.readMessage(value,proto.yorkie.v1.MarkOp.deserializeBinaryFromReader); + msg.addOperations(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.yorkie.v1.MarkOps.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.yorkie.v1.MarkOps.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.yorkie.v1.MarkOps} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.MarkOps.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getOperationsList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 1, + f, + proto.yorkie.v1.MarkOp.serializeBinaryToWriter + ); + } +}; + + +/** + * repeated MarkOp operations = 1; + * @return {!Array} + */ +proto.yorkie.v1.MarkOps.prototype.getOperationsList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, proto.yorkie.v1.MarkOp, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.yorkie.v1.MarkOps} returns this +*/ +proto.yorkie.v1.MarkOps.prototype.setOperationsList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 1, value); +}; + + +/** + * @param {!proto.yorkie.v1.MarkOp=} opt_value + * @param {number=} opt_index + * @return {!proto.yorkie.v1.MarkOp} + */ +proto.yorkie.v1.MarkOps.prototype.addOperations = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.yorkie.v1.MarkOp, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.yorkie.v1.MarkOps} returns this + */ +proto.yorkie.v1.MarkOps.prototype.clearOperationsList = function() { + return this.setOperationsList([]); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.yorkie.v1.MarkOp.prototype.toObject = function(opt_includeInstance) { + return proto.yorkie.v1.MarkOp.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.yorkie.v1.MarkOp} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.MarkOp.toObject = function(includeInstance, msg) { + var f, obj = { + fromboundary: (f = msg.getFromboundary()) && proto.yorkie.v1.TextNodeBoundary.toObject(includeInstance, f), + toboundary: (f = msg.getToboundary()) && proto.yorkie.v1.TextNodeBoundary.toObject(includeInstance, f), + attributesMap: (f = msg.getAttributesMap()) ? f.toObject(includeInstance, undefined) : [] + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.yorkie.v1.MarkOp} + */ +proto.yorkie.v1.MarkOp.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.yorkie.v1.MarkOp; + return proto.yorkie.v1.MarkOp.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.yorkie.v1.MarkOp} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.yorkie.v1.MarkOp} + */ +proto.yorkie.v1.MarkOp.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.yorkie.v1.TextNodeBoundary; + reader.readMessage(value,proto.yorkie.v1.TextNodeBoundary.deserializeBinaryFromReader); + msg.setFromboundary(value); + break; + case 2: + var value = new proto.yorkie.v1.TextNodeBoundary; + reader.readMessage(value,proto.yorkie.v1.TextNodeBoundary.deserializeBinaryFromReader); + msg.setToboundary(value); + break; + case 3: + var value = msg.getAttributesMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString, null, "", ""); + }); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.yorkie.v1.MarkOp.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.yorkie.v1.MarkOp.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.yorkie.v1.MarkOp} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.MarkOp.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getFromboundary(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.yorkie.v1.TextNodeBoundary.serializeBinaryToWriter + ); + } + f = message.getToboundary(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.yorkie.v1.TextNodeBoundary.serializeBinaryToWriter + ); + } + f = message.getAttributesMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(3, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); + } +}; + + +/** + * optional TextNodeBoundary fromBoundary = 1; + * @return {?proto.yorkie.v1.TextNodeBoundary} + */ +proto.yorkie.v1.MarkOp.prototype.getFromboundary = function() { + return /** @type{?proto.yorkie.v1.TextNodeBoundary} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TextNodeBoundary, 1)); +}; + + +/** + * @param {?proto.yorkie.v1.TextNodeBoundary|undefined} value + * @return {!proto.yorkie.v1.MarkOp} returns this +*/ +proto.yorkie.v1.MarkOp.prototype.setFromboundary = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.MarkOp} returns this + */ +proto.yorkie.v1.MarkOp.prototype.clearFromboundary = function() { + return this.setFromboundary(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.MarkOp.prototype.hasFromboundary = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional TextNodeBoundary toBoundary = 2; + * @return {?proto.yorkie.v1.TextNodeBoundary} + */ +proto.yorkie.v1.MarkOp.prototype.getToboundary = function() { + return /** @type{?proto.yorkie.v1.TextNodeBoundary} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TextNodeBoundary, 2)); +}; + + +/** + * @param {?proto.yorkie.v1.TextNodeBoundary|undefined} value + * @return {!proto.yorkie.v1.MarkOp} returns this +*/ +proto.yorkie.v1.MarkOp.prototype.setToboundary = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.MarkOp} returns this + */ +proto.yorkie.v1.MarkOp.prototype.clearToboundary = function() { + return this.setToboundary(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.MarkOp.prototype.hasToboundary = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * map attributes = 3; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.yorkie.v1.MarkOp.prototype.getAttributesMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 3, opt_noLazyCreate, + null)); +}; + + +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.yorkie.v1.MarkOp} returns this + */ +proto.yorkie.v1.MarkOp.prototype.clearAttributesMap = function() { + this.getAttributesMap().clear(); + return this;}; + + + if (jspb.Message.GENERATE_TO_OBJECT) { diff --git a/src/api/yorkie/v1/yorkie_grpc_web_pb.js b/src/api/yorkie/v1/yorkie_grpc_web_pb.js index ff8f94ad1..2eef83bd0 100644 --- a/src/api/yorkie/v1/yorkie_grpc_web_pb.js +++ b/src/api/yorkie/v1/yorkie_grpc_web_pb.js @@ -4,11 +4,7 @@ * @public */ -// Code generated by protoc-gen-grpc-web. DO NOT EDIT. -// versions: -// protoc-gen-grpc-web v1.4.2 -// protoc v3.20.3 -// source: yorkie/v1/yorkie.proto +// GENERATED CODE -- DO NOT EDIT! /* eslint-disable */ @@ -46,7 +42,7 @@ proto.yorkie.v1.YorkieServiceClient = /** * @private @const {string} The hostname */ - this.hostname_ = hostname.replace(/\/+$/, ''); + this.hostname_ = hostname; }; @@ -72,7 +68,7 @@ proto.yorkie.v1.YorkieServicePromiseClient = /** * @private @const {string} The hostname */ - this.hostname_ = hostname.replace(/\/+$/, ''); + this.hostname_ = hostname; }; diff --git a/src/document/crdt/rga_tree_split.ts b/src/document/crdt/rga_tree_split.ts index 7c4291e21..120f3880d 100644 --- a/src/document/crdt/rga_tree_split.ts +++ b/src/document/crdt/rga_tree_split.ts @@ -322,20 +322,41 @@ export class RGATreeSplitNode< private styleOpsBefore?: Set; private styleOpsAfter?: Set; - constructor(id: RGATreeSplitNodeID, value?: T, removedAt?: TimeTicket) { + constructor({ + id, + value, + removedAt, + styleOpsBefore, + styleOpsAfter, + }: { + id: RGATreeSplitNodeID; + value?: T; + removedAt?: TimeTicket; + styleOpsBefore?: Set; + styleOpsAfter?: Set; + }) { super(value!); this.id = id; this.removedAt = removedAt; + this.styleOpsBefore = styleOpsBefore; + this.styleOpsAfter = styleOpsAfter; } /** * `create` creates a instance of RGATreeSplitNode. */ - public static create( - id: RGATreeSplitNodeID, - value?: T, - ): RGATreeSplitNode { - return new RGATreeSplitNode(id, value); + public static create({ + id, + value, + styleOpsBefore, + styleOpsAfter, + }: { + id: RGATreeSplitNodeID; + value?: T; + styleOpsBefore?: Set; + styleOpsAfter?: Set; + }): RGATreeSplitNode { + return new RGATreeSplitNode({ id, value, styleOpsBefore, styleOpsAfter }); } /** @@ -523,11 +544,11 @@ export class RGATreeSplitNode< * `split` creates a new split node of the given offset. */ public split(offset: number): RGATreeSplitNode { - return new RGATreeSplitNode( - this.id.split(offset), - this.splitValue(offset), - this.removedAt, - ); + return new RGATreeSplitNode({ + id: this.id.split(offset), + value: this.splitValue(offset), + removedAt: this.removedAt, + }); } /** @@ -571,7 +592,11 @@ export class RGATreeSplitNode< * `deepcopy` returns a new instance of this RGATreeSplitNode without structural info. */ public deepcopy(): RGATreeSplitNode { - return new RGATreeSplitNode(this.id, this.value, this.removedAt); + return new RGATreeSplitNode({ + id: this.id, + value: this.value, + removedAt: this.removedAt, + }); } /** @@ -603,7 +628,7 @@ export class RGATreeSplit { private removedNodeMap: Map>; constructor() { - this.head = RGATreeSplitNode.create(InitialRGATreeSplitNodeID); + this.head = RGATreeSplitNode.create({ id: InitialRGATreeSplitNodeID }); this.treeByIndex = new SplayTree(); this.treeByID = new LLRBTree(RGATreeSplitNode.createComparator()); this.removedNodeMap = new Map(); @@ -655,7 +680,10 @@ export class RGATreeSplit { const inserted = this.insertAfter( fromLeft, - RGATreeSplitNode.create(RGATreeSplitNodeID.of(editedAt, 0), value), + RGATreeSplitNode.create({ + id: RGATreeSplitNodeID.of(editedAt, 0), + value, + }), ); if (changes.length && changes[changes.length - 1].from === idx) { diff --git a/test/integration/snapshot_test.ts b/test/integration/snapshot_test.ts index dbadfe091..09516ef93 100644 --- a/test/integration/snapshot_test.ts +++ b/test/integration/snapshot_test.ts @@ -58,7 +58,8 @@ describe('Snapshot', function () { }, this.test!.title); }); - // TODO(MoonGyu1): Remove skip after addressing logic of Text.setStyle + // TODO(MoonGyu1): Remove skip after applying mark + // when creating a snapshot in the Go SDK. it.skip('should handle snapshot for text with attributes', async function () { await withTwoClientsAndDocuments<{ k1: Text }>(async (c1, d1, c2, d2) => { d1.update((root) => { From adb5d78727815b55dd1ff2532c6dad5530fda945 Mon Sep 17 00:00:00 2001 From: Yourim Cha Date: Tue, 19 Sep 2023 01:45:17 +0900 Subject: [PATCH 20/21] Fix bug where style operationInfo is missing after removed node --- public/quill-two-clients.html | 2 -- public/style.css | 4 ---- src/document/crdt/text.ts | 28 +++++++++++++--------------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/public/quill-two-clients.html b/public/quill-two-clients.html index 713b24ee2..df926b160 100644 --- a/public/quill-two-clients.html +++ b/public/quill-two-clients.html @@ -139,7 +139,6 @@ clientASyncButton.addEventListener('click', async () => { await clientA.sync(); - syncText(docA, quillA); }); const { @@ -150,7 +149,6 @@ clientBSyncButton.addEventListener('click', async () => { await clientB.sync(); - syncText(docB, quillB); }); // 04. bind the document with the Quill. diff --git a/public/style.css b/public/style.css index 7e634bd10..47fd7000d 100644 --- a/public/style.css +++ b/public/style.css @@ -14,10 +14,6 @@ menu, nav, output, section, summary, time, mark, audio, video { margin: 0; padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; box-sizing: border-box; } ol, diff --git a/src/document/crdt/text.ts b/src/document/crdt/text.ts index f01834102..5fe9e4c5b 100644 --- a/src/document/crdt/text.ts +++ b/src/document/crdt/text.ts @@ -329,21 +329,19 @@ export class CRDTText extends CRDTGCElement { // Add a new StyleOperation to between nodes if it has an opSet let betweenNode = fromNode.getNext(); - while ( - betweenNode && - betweenNode !== toNode && - !betweenNode.isRemoved() - ) { - toBeStyleds.push(betweenNode); - const styleOpsBefore = betweenNode.getStyleOpsBefore(); - const styleOpsAfter = betweenNode.getStyleOpsAfter(); - if (styleOpsBefore) { - styleOpsBefore.add(newOp); - betweenNode.setStyleOpsBefore(styleOpsBefore); - } - if (styleOpsAfter) { - styleOpsAfter.add(newOp); - betweenNode.setStyleOpsAfter(styleOpsAfter); + while (betweenNode && betweenNode !== toNode) { + if (!betweenNode.isRemoved()) { + toBeStyleds.push(betweenNode); + const styleOpsBefore = betweenNode.getStyleOpsBefore(); + const styleOpsAfter = betweenNode.getStyleOpsAfter(); + if (styleOpsBefore) { + styleOpsBefore.add(newOp); + betweenNode.setStyleOpsBefore(styleOpsBefore); + } + if (styleOpsAfter) { + styleOpsAfter.add(newOp); + betweenNode.setStyleOpsAfter(styleOpsAfter); + } } betweenNode = betweenNode.getNext(); } From 7f9b76e850f17abac8033b2dac3bc01017b7ffe1 Mon Sep 17 00:00:00 2001 From: Yourim Cha Date: Tue, 17 Oct 2023 15:30:51 +0900 Subject: [PATCH 21/21] Apply anchor style when creating a change during text insertion --- public/quill-two-clients.html | 224 ++++++++++++++++++---------- src/document/crdt/rga_tree_split.ts | 6 + 2 files changed, 148 insertions(+), 82 deletions(-) diff --git a/public/quill-two-clients.html b/public/quill-two-clients.html index df926b160..e0391b9ef 100644 --- a/public/quill-two-clients.html +++ b/public/quill-two-clients.html @@ -12,27 +12,24 @@ - -
Client A : - +
Client B : - +
-