Skip to content

Commit

Permalink
Handle Tree.toXML to return proper XML string (#805)
Browse files Browse the repository at this point in the history
  • Loading branch information
raararaara committed May 10, 2024
1 parent a1c7f31 commit 555d5ad
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 37 deletions.
16 changes: 0 additions & 16 deletions src/document/crdt/rht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,22 +199,6 @@ export class RHT {
return `{${items.join(',')}}`;
}

/**
* `toXML` converts the given RHT to XML string.
*/
public toXML(): string {
if (!this.size()) {
return '';
}

const attrs = [...this.nodeMapByKey]
.filter(([, v]) => v instanceof RHTNode && !v.isRemoved())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([k, v]) => `${k}="${JSON.parse(v.getValue())}"`);

return ` ${attrs.join(' ')}`;
}

/**
* `size` returns the size of RHT
*/
Expand Down
69 changes: 50 additions & 19 deletions src/document/crdt/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
MaxTimeTicket,
} from '@yorkie-js-sdk/src/document/time/ticket';
import { CRDTGCElement } from '@yorkie-js-sdk/src/document/crdt/element';

import {
IndexTree,
TreePos,
Expand All @@ -40,6 +41,7 @@ import type {
} from '@yorkie-js-sdk/src/util/index_tree';
import { Indexable } from '@yorkie-js-sdk/src/document/document';
import type * as Devtools from '@yorkie-js-sdk/src/devtools/types';
import { escapeString } from '@yorkie-js-sdk/src/document/json/strings';

/**
* `TreeNode` represents a node in the tree.
Expand Down Expand Up @@ -588,11 +590,36 @@ export class CRDTTreeNode extends IndexTreeNode<CRDTTreeNode> {
* `canStyle` checks if node is able to style.
*/
public canStyle(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
if (this.isText) {
return false;
}

return (
!this.getCreatedAt().after(maxCreatedAt) &&
(!this.removedAt || editedAt.after(this.removedAt))
);
}

/**
* `setAttrs` sets the attributes of the node.
*/
public setAttrs(
attrs: { [key: string]: string },
editedAt: TimeTicket,
): Set<string> {
if (!this.attrs) {
this.attrs = new RHT();
}

const affectedKeys = new Set<string>();
for (const [key, value] of Object.entries(attrs)) {
if (this.attrs.set(key, value, editedAt)) {
affectedKeys.add(key);
}
}

return affectedKeys;
}
}

/**
Expand Down Expand Up @@ -625,7 +652,24 @@ export function toXML(node: CRDTTreeNode): string {
return currentNode.value;
}

return `<${node.type}${node.attrs?.toXML() || ''}>${node.children
let attrs = '';
if (node.attrs && node.attrs.size()) {
attrs =
' ' +
Array.from(node.attrs)
.filter((n) => !n.isRemoved())
.sort((a, b) => a.getKey().localeCompare(b.getKey()))
.map((n) => {
const obj = JSON.parse(n.getValue());
if (typeof obj === 'string') {
return `${n.getKey()}="${obj}"`;
}
return `${n.getKey()}="${escapeString(n.getValue())}"`;
})
.join(' ');
}

return `<${node.type}${attrs}>${node.children
.map((child) => toXML(child))
.join('')}</${node.type}>`;
}
Expand Down Expand Up @@ -757,7 +801,7 @@ export class CRDTTree extends CRDTGCElement {
range: [CRDTTreePos, CRDTTreePos],
attributes: { [key: string]: string } | undefined,
editedAt: TimeTicket,
maxCreatedAtMapByActor: Map<string, TimeTicket> | undefined,
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
): [Map<string, TimeTicket>, Array<TreeChange>] {
const [fromParent, fromLeft] = this.findNodesAndSplitText(
range[0],
Expand All @@ -777,33 +821,20 @@ export class CRDTTree extends CRDTGCElement {
toLeft,
([node]) => {
const actorID = node.getCreatedAt().getActorID();
let maxCreatedAt: TimeTicket | undefined = maxCreatedAtMapByActor
const maxCreatedAt = maxCreatedAtMapByActor
? maxCreatedAtMapByActor!.has(actorID)
? maxCreatedAtMapByActor!.get(actorID)!
: InitialTimeTicket
: MaxTimeTicket;

if (
node.canStyle(editedAt, maxCreatedAt) &&
!node.isText &&
attributes
) {
maxCreatedAt = createdAtMapByActor!.get(actorID);
if (node.canStyle(editedAt, maxCreatedAt) && attributes) {
const maxCreatedAt = createdAtMapByActor!.get(actorID);
const createdAt = node.getCreatedAt();
if (!maxCreatedAt || createdAt.after(maxCreatedAt)) {
createdAtMapByActor.set(actorID, createdAt);
}
if (!node.attrs) {
node.attrs = new RHT();
}

const affectedKeys = new Set<string>();
for (const [key, value] of Object.entries(attributes)) {
if (node.attrs.set(key, value, editedAt)) {
affectedKeys.add(key);
}
}

const affectedKeys = node.setAttrs(attributes, editedAt);
if (affectedKeys.size > 0) {
const affectedAttrs = Array.from(affectedKeys).reduce(
(acc: { [key: string]: any }, key) => {
Expand Down
2 changes: 0 additions & 2 deletions src/document/json/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@ export class Tree {
[fromPos, toPos],
attrs,
ticket,
undefined,
);

this.context.push(
Expand Down Expand Up @@ -323,7 +322,6 @@ export class Tree {
[fromPos, toPos],
attrs,
ticket,
undefined,
);

this.context.push(
Expand Down
21 changes: 21 additions & 0 deletions test/integration/tree_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,27 @@ describe('Tree.style', function () {
});
});

it('Can style nested object', function ({ task }) {
const key = toDocKey(`${task.name}-${new Date().getTime()}`);
const doc = new yorkie.Document<{ t: Tree }>(key);

doc.update((root) => {
root.t = new Tree({
type: 'doc',
children: [{ type: 'p', children: [{ type: 'text', value: 'hello' }] }],
});
assert.equal(root.t.toXML(), /*html*/ `<doc><p>hello</p></doc>`);
});

doc.update((root) =>
root.t.style(0, 1, { img: { src: 'yorkie.png' }, rep: 'false' }),
);
assert.equal(
doc.getRoot().t.toXML(),
/*html*/ `<doc><p img="{\\"src\\":\\"yorkie.png\\"}" rep="false">hello</p></doc>`,
);
});

it('Can sync its content containing attributes with other replicas', async function ({
task,
}) {
Expand Down
26 changes: 26 additions & 0 deletions test/unit/document/crdt/tree_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ElementRHT } from '@yorkie-js-sdk/src/document/crdt/element_rht';
import { CRDTObject } from '@yorkie-js-sdk/src/document/crdt/object';
import {
InitialTimeTicket as ITT,
MaxTimeTicket as MTT,
TimeTicket,
} from '@yorkie-js-sdk/src/document/time/ticket';
import { CRDTRoot } from '@yorkie-js-sdk/src/document/crdt/root';
Expand All @@ -32,6 +33,7 @@ import {
toXML,
TreeNodeForTest,
} from '@yorkie-js-sdk/src/document/crdt/tree';
import { stringifyObjectValues } from '@yorkie-js-sdk/src/util/object';

/**
* `idT` is a dummy CRDTTreeNodeID for testing.
Expand Down Expand Up @@ -89,6 +91,30 @@ describe('CRDTTreeNode', function () {
assert.deepEqual(left.id, CRDTTreeNodeID.of(ITT, 0));
assert.deepEqual(right!.id, CRDTTreeNodeID.of(ITT, 5));
});

it('Can convert to XML', function () {
const text = new CRDTTreeNode(idT, 'text', 'hello');
assert.equal(toXML(text), 'hello');

const elem = new CRDTTreeNode(idT, 'p', []);
elem.append(text);
assert.equal(toXML(elem), /*html*/ `<p>hello</p>`);

const elemWithAttrs = new CRDTTreeNode(idT, 'p', []);
elemWithAttrs.append(text);
elemWithAttrs.setAttrs({ b: '"t"', i: 'true' }, MTT);
assert.equal(toXML(elemWithAttrs), /*html*/ `<p b="t" i="true">hello</p>`);

elemWithAttrs.setAttrs(
stringifyObjectValues({ img: { src: 'yorkie.png' } }),
MTT,
);

assert.equal(
toXML(elemWithAttrs),
/*html*/ `<p b="t" i="true" img="{\\"src\\":\\"yorkie.png\\"}">hello</p>`,
);
});
});

// NOTE: To see the XML string as highlighted, install es6-string-html plugin in VSCode.
Expand Down

0 comments on commit 555d5ad

Please sign in to comment.