@meshagent/meshagent 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +0 -0
- package/babel.config.cjs +3 -0
- package/client.js +422 -0
- package/dist/entrypoint.js +11262 -0
- package/dist/entrypoint.js.map +7 -0
- package/document-server-client.js +246 -0
- package/entrypoint.js +11568 -0
- package/index.js +2 -0
- package/package.json +33 -0
- package/protocol.js +459 -0
- package/server.js +310 -0
- package/test/assert.js +19 -0
- package/test/document-server-client.test.js +33 -0
- package/test/document.test.js +313 -0
- package/test/protocol.test.js +64 -0
package/server.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
|
|
4
|
+
const uuid = v4;
|
|
5
|
+
|
|
6
|
+
export class ServerXmlDocument {
|
|
7
|
+
constructor(doc, notifyChanges) {
|
|
8
|
+
this._y = doc.get("xml", Y.XmlElement);
|
|
9
|
+
this.doc = doc;
|
|
10
|
+
this._y.observeDeep(yxmlEvents=> {
|
|
11
|
+
for(let yxmlEvent of yxmlEvents) {
|
|
12
|
+
const target = yxmlEvent.target;
|
|
13
|
+
|
|
14
|
+
// Observe when child-elements are added or deleted.
|
|
15
|
+
|
|
16
|
+
if(target instanceof Y.XmlElement) {
|
|
17
|
+
const nodeID = target.getAttribute("$id");
|
|
18
|
+
|
|
19
|
+
const message = {
|
|
20
|
+
root: target == this._y,
|
|
21
|
+
target: nodeID,
|
|
22
|
+
elements: [],
|
|
23
|
+
attributes: {
|
|
24
|
+
set: [],
|
|
25
|
+
delete: []
|
|
26
|
+
},
|
|
27
|
+
text: []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// element changes
|
|
31
|
+
for(let delta of yxmlEvent.changes.delta) {
|
|
32
|
+
if(delta.insert) {
|
|
33
|
+
message.elements.push({ retain: delta.retain, insert: delta.insert.map(i => this.serialize(i)) });
|
|
34
|
+
} else if(delta.delete) {
|
|
35
|
+
message.elements.push({ retain: delta.retain, delete: delta.delete });
|
|
36
|
+
} else if(delta.retain) {
|
|
37
|
+
message.elements.push({ retain: delta.retain });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// attribute changes
|
|
42
|
+
yxmlEvent.changes.keys.forEach((change, key) => {
|
|
43
|
+
if (change.action === 'add') {
|
|
44
|
+
message.attributes.set.push({ "name": key, "value": target.getAttribute(key) });
|
|
45
|
+
} else if (change.action === 'update') {
|
|
46
|
+
message.attributes.set.push({ "name": key, "value": target.getAttribute(key) });
|
|
47
|
+
} else if (change.action === 'delete') {
|
|
48
|
+
message.attributes.delete.push(key);
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
notifyChanges(message);
|
|
52
|
+
|
|
53
|
+
} else if(target instanceof Y.XmlText) {
|
|
54
|
+
const nodeID = target.parent.getAttribute("$id");
|
|
55
|
+
|
|
56
|
+
const message = {
|
|
57
|
+
root: target == this._y,
|
|
58
|
+
target: nodeID,
|
|
59
|
+
elements: [],
|
|
60
|
+
attributes: {
|
|
61
|
+
set: [],
|
|
62
|
+
delete: []
|
|
63
|
+
},
|
|
64
|
+
text: []
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// text changes
|
|
68
|
+
for(let delta of yxmlEvent.changes.delta) {
|
|
69
|
+
if(delta.insert) {
|
|
70
|
+
message.text.push({ retain: delta.retain, insert: delta.insert, attributes: delta.attributes });
|
|
71
|
+
} else if(delta.delete) {
|
|
72
|
+
message.text.push({ retain: delta.retain, delete: delta.delete });
|
|
73
|
+
} else if(delta.retain) {
|
|
74
|
+
message.text.push({ retain: delta.retain, attributes: delta.attributes });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
notifyChanges(message);
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error("Unexpected target type")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// changes should be Uint8Array
|
|
87
|
+
applyBackendChanges(changes) {
|
|
88
|
+
// TODO: need to set transaction origin?
|
|
89
|
+
Y.applyUpdate(this.doc, changes);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
serialize(node) {
|
|
93
|
+
|
|
94
|
+
if(node instanceof Y.XmlElement) {
|
|
95
|
+
const children = [];
|
|
96
|
+
for(let n = node.firstChild; n != null; n = n.nextSibling) {
|
|
97
|
+
children.push(this.serialize(n));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
element: {
|
|
101
|
+
tagName: node.nodeName,
|
|
102
|
+
attributes: node.getAttributes(),
|
|
103
|
+
children: children,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if(node instanceof Y.XmlText) {
|
|
107
|
+
return {
|
|
108
|
+
text: {
|
|
109
|
+
delta: node.toDelta()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error("Unexpected node type "+node);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
doDelete(element) {
|
|
119
|
+
let i = 0;
|
|
120
|
+
if (element.parent) {
|
|
121
|
+
for(let e = element.parent.firstChild; e != null; e = e.nextSibling) {
|
|
122
|
+
if(e == element) {
|
|
123
|
+
element.parent.delete(i, 1)
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
throw new Error("element was not deleted" + element);
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error("Cannot delete top level element");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
createNodes(children) {
|
|
135
|
+
const nodes = [];
|
|
136
|
+
for(let child of children) {
|
|
137
|
+
if(child.text) {
|
|
138
|
+
const text = new Y.XmlText();
|
|
139
|
+
if(text.delta) {
|
|
140
|
+
text.applyDelta(text.delta);
|
|
141
|
+
}
|
|
142
|
+
nodes.push(text);
|
|
143
|
+
} else if(child.element) {
|
|
144
|
+
const element = new Y.XmlElement(child.element.name);
|
|
145
|
+
// assign a default node id
|
|
146
|
+
element.setAttribute("$id", child.element.attributes?.id ?? uuid());
|
|
147
|
+
if(child.element.attributes) {
|
|
148
|
+
for(let k of Object.keys(child.element.attributes)) {
|
|
149
|
+
element.setAttribute(k, child.element.attributes[k]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if(child.element.children) {
|
|
153
|
+
element.insert(0, this.createNodes(child.element.children));
|
|
154
|
+
}
|
|
155
|
+
nodes.push(element);
|
|
156
|
+
} else {
|
|
157
|
+
throw new Error("Unexpected xml data encountered, item is not text or an element");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return nodes;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
doInsertChildren(element, { after, index, children }) {
|
|
164
|
+
let afterElement;
|
|
165
|
+
if(after) {
|
|
166
|
+
for(let e = element.firstChild; e != null; e = e.nextSibling) {
|
|
167
|
+
if(e instanceof Y.XmlElement ) {
|
|
168
|
+
if(e.getAttribute("$id") == after) {
|
|
169
|
+
afterElement = e;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if(afterElement == null) {
|
|
175
|
+
throw new Error("Unable to find child element to insert after: "+after);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if(afterElement) {
|
|
180
|
+
element.insertAfter(afterElement, this.createNodes(children));
|
|
181
|
+
} else {
|
|
182
|
+
if(index !== undefined && index != null) {
|
|
183
|
+
element.insert(index, this.createNodes(children));
|
|
184
|
+
} else {
|
|
185
|
+
element.push(this.createNodes(children));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
doDeleteChildren(element, { after, index, length }) {
|
|
191
|
+
let afterElement;
|
|
192
|
+
if(after) {
|
|
193
|
+
let i = 0;
|
|
194
|
+
for(let e = element.firstChild; e != null; e = e.nextSibling) {
|
|
195
|
+
if(e instanceof Y.XmlElement ) {
|
|
196
|
+
if(e.getAttribute("$id") == after) {
|
|
197
|
+
afterElement = e;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
if(afterElement == null) {
|
|
204
|
+
throw new Error("Unable to find child element to insert after: "+after);
|
|
205
|
+
}
|
|
206
|
+
element.delete(i, length);
|
|
207
|
+
} else {
|
|
208
|
+
element.delete(index, length);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
doRemoveAttributes(element, attributes) {
|
|
214
|
+
for(let k of attributes) {
|
|
215
|
+
element.removeAttribute(k);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
doSetAttributes(element, attributes) {
|
|
220
|
+
for(let k of Object.keys(attributes)) {
|
|
221
|
+
element.setAttribute(k, attributes[k]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
findNode(nodeID, root = this._y) {
|
|
226
|
+
if(!nodeID) {
|
|
227
|
+
return root;
|
|
228
|
+
}
|
|
229
|
+
if(root instanceof Y.XmlElement) {
|
|
230
|
+
if(root.getAttribute("$id") == nodeID) {
|
|
231
|
+
return root;
|
|
232
|
+
}
|
|
233
|
+
for(let e = root.firstChild; e != null; e = e.nextSibling) {
|
|
234
|
+
let child = this.findNode(nodeID, e);
|
|
235
|
+
if(child) {
|
|
236
|
+
return child;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
doInsertText(element, {index, text, attributes}) {
|
|
244
|
+
if(typeof text != "string") {
|
|
245
|
+
throw new Error("Inserted text must be a string");
|
|
246
|
+
}
|
|
247
|
+
if(element.nodeName != "text") {
|
|
248
|
+
throw new Error("text can only be inserted in a text element");
|
|
249
|
+
}
|
|
250
|
+
const xmlText = element.firstChild;
|
|
251
|
+
xmlText.insert(index, text, attributes);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
doFormatText(element, {from, length, attributes}) {
|
|
255
|
+
if(element.nodeName != "text") {
|
|
256
|
+
throw new Error("text can only be inserted in a text element");
|
|
257
|
+
}
|
|
258
|
+
const xmlText = element.firstChild;
|
|
259
|
+
xmlText.format(from, length, attributes);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
doDeleteText(element, {index, length}) {
|
|
263
|
+
if(element.nodeName != "text") {
|
|
264
|
+
throw new Error("text can only be inserted in a text element");
|
|
265
|
+
}
|
|
266
|
+
const xmlText = element.firstChild;
|
|
267
|
+
xmlText.delete(index, length);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
applyChanges(changes) {
|
|
271
|
+
for(let change of changes) {
|
|
272
|
+
const element = change.nodeID ? this.findNode(change.nodeID) : this._y;
|
|
273
|
+
if(element == null) {
|
|
274
|
+
throw new Error("Element was not found "+change.nodeID);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if(change.delete) {
|
|
278
|
+
this.doDelete(element);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if(change.insertChildren) {
|
|
282
|
+
this.doInsertChildren(element, change.insertChildren);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if(change.deleteChildren) {
|
|
286
|
+
this.doDeleteChildren(element, change.deleteChildren);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if(change.removeAttributes) {
|
|
290
|
+
this.doRemoveAttributes(element, change.removeAttributes);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if(change.setAttributes) {
|
|
294
|
+
this.doSetAttributes(element, change.setAttributes);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if(change.insertText) {
|
|
298
|
+
this.doInsertText(element, change.insertText);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if(change.formatText) {
|
|
302
|
+
this.doFormatText(element, change.formatText);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if(change.deleteText) {
|
|
306
|
+
this.doDeleteText(element, change.deleteText);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
package/test/assert.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function assertEqual(v1,v2, message) {
|
|
2
|
+
if(v1 != v2) {
|
|
3
|
+
throw new Error(v1 + "!="+v2 +": expected "+ message);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function assertDeltaEqual(d1, d2) {
|
|
8
|
+
|
|
9
|
+
// make sure our deltas match
|
|
10
|
+
|
|
11
|
+
assertEqual(d1.length, d2.length, "same length delta");
|
|
12
|
+
for(let i = 0; i < d1.length; i++) {
|
|
13
|
+
assertEqual(d1[i].insert, d2[i].insert, "delta insert is equal");
|
|
14
|
+
assertEqual(Object.keys(d1[i].attributes ?? {}).length, Object.keys(d2[i].attributes ?? {}).length, "same attribute count");
|
|
15
|
+
for(let k of Object.keys(d1[i].attributes ?? {})) {
|
|
16
|
+
assertEqual(d1[i].attributes[k], d2[i].attributes[k], "deltas attributes are equal");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Protocol, WebSocketProtocolChannel, Completer } from "../protocol";
|
|
2
|
+
import { RoomClient } from "../document-server-client";
|
|
3
|
+
import WebSocket from "ws";
|
|
4
|
+
import * as runtime from "../entrypoint";
|
|
5
|
+
|
|
6
|
+
globalThis.WebSocket = WebSocket;
|
|
7
|
+
|
|
8
|
+
test('can sync changes',async () => {
|
|
9
|
+
|
|
10
|
+
const client = new RoomClient({protocol: new Protocol({channel: new WebSocketProtocolChannel({url: "ws://localhost:8080/rooms/room_name"})}), runtime: runtime});
|
|
11
|
+
const client2 = new RoomClient({protocol: new Protocol({channel: new WebSocketProtocolChannel({url: "ws://localhost:8080/rooms/room_name"})}), runtime: runtime});
|
|
12
|
+
|
|
13
|
+
client.start();
|
|
14
|
+
client2.start();
|
|
15
|
+
|
|
16
|
+
const doc = await client.sync.open("/rooms/room_name");
|
|
17
|
+
const doc2 = await client2.connect("/rooms/room_name");
|
|
18
|
+
|
|
19
|
+
console.log("CONNECTED");
|
|
20
|
+
doc.root.createChildElement("test", {"hello":"world"});
|
|
21
|
+
|
|
22
|
+
const completer = new Completer();
|
|
23
|
+
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
completer.resolve();
|
|
26
|
+
}, 1000*1);
|
|
27
|
+
|
|
28
|
+
await completer.fut;
|
|
29
|
+
|
|
30
|
+
if((doc2.root.getChildren()[0]).getAttribute("hello")!="world") {
|
|
31
|
+
throw new Error("attribute was not set")
|
|
32
|
+
}
|
|
33
|
+
})
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { ServerXmlDocument } from "../server.js";
|
|
3
|
+
import { ClientXmlDocument, XmlElement, XmlText } from "../client.js";
|
|
4
|
+
import { assertEqual, assertDeltaEqual } from "./assert.js";
|
|
5
|
+
import { v4 } from "uuid";
|
|
6
|
+
|
|
7
|
+
function makeDoc() {
|
|
8
|
+
const doc = new Y.Doc();
|
|
9
|
+
const root = doc.get("xml", Y.XmlElement);
|
|
10
|
+
|
|
11
|
+
const server = new ServerXmlDocument(doc, (message) => {
|
|
12
|
+
client.receiveChanges(message);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const client = new ClientXmlDocument({ id: v4(), sendChanges: (changes) => {
|
|
16
|
+
server.applyChanges(changes.changes);
|
|
17
|
+
}});
|
|
18
|
+
return [ root, client, server ];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('set attribute', () => {
|
|
22
|
+
const [ root, client, server] = makeDoc();
|
|
23
|
+
|
|
24
|
+
client.root.setAttribute("test", "v1");
|
|
25
|
+
|
|
26
|
+
assertEqual(client.root.getAttribute("test"), "v1", "root attributes are equal");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
test('insert and delete element', () => {
|
|
31
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
32
|
+
const xmlElement = client.root.createChildElement("child", { "hello" : "world" });
|
|
33
|
+
|
|
34
|
+
// Can insert
|
|
35
|
+
let child = client.root.getChildren()[0];
|
|
36
|
+
assertEqual(child instanceof XmlElement, true, "inserted an element");
|
|
37
|
+
assertEqual(xmlElement instanceof XmlElement, true, "inserted an element");
|
|
38
|
+
assertEqual(xmlElement.id, child.id, "child id matches returned id");
|
|
39
|
+
assertEqual(child.tagName, "child", "inserted child tag names are equal");
|
|
40
|
+
assertEqual(child.getAttribute("hello"), "world", "inserted child attributes are equal");
|
|
41
|
+
|
|
42
|
+
// Can delete node
|
|
43
|
+
child.delete();
|
|
44
|
+
assertEqual(client.root.getChildren().length, 0, "can delete element");
|
|
45
|
+
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
test('update attribute', () => {
|
|
50
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
51
|
+
|
|
52
|
+
const child = client.root.createChildElement("child", { "hello" : "world" });
|
|
53
|
+
|
|
54
|
+
// Can update attribute
|
|
55
|
+
child.setAttribute("hello","mod");
|
|
56
|
+
assertEqual(child.getAttribute("hello"), "mod", "child attributes are equal");
|
|
57
|
+
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('remove attribute', () => {
|
|
61
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
62
|
+
|
|
63
|
+
const child = client.root.createChildElement("child", { "hello" : "world" });
|
|
64
|
+
child.setAttribute("hello","mod");
|
|
65
|
+
|
|
66
|
+
// Can remove attribute
|
|
67
|
+
child.removeAttribute("hello");
|
|
68
|
+
assertEqual(child.getAttribute("hello") == null, true, "child attribute was removed");
|
|
69
|
+
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('insert, extend, and shrink text delta', () => {
|
|
73
|
+
|
|
74
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
75
|
+
|
|
76
|
+
// Can insert text
|
|
77
|
+
const child = client.root.createChildElement("text", { "hello" : "world" });
|
|
78
|
+
assertEqual(child instanceof XmlElement, true, "inserted an element");
|
|
79
|
+
|
|
80
|
+
let text = child.getChildren()[0];
|
|
81
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
82
|
+
assertEqual(text instanceof XmlText, true, "added a text element inside the element");
|
|
83
|
+
assertEqual(child.tagName, "text", "inserted text tag names are equal");
|
|
84
|
+
assertEqual(child.getAttribute("hello"), "world", "inserted child attributes are equal");
|
|
85
|
+
|
|
86
|
+
assertEqual(text.delta.length, 0, "deltas are empty");
|
|
87
|
+
text.insert(0, "hello world");
|
|
88
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
89
|
+
assertEqual(text.delta.length, 1, "inserted a delta");
|
|
90
|
+
assertEqual(text.delta[0].insert, "hello world", "text matches");
|
|
91
|
+
|
|
92
|
+
text.insert(0, "hello world");
|
|
93
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
94
|
+
assertEqual(text.delta.length, 1, "extended delta");
|
|
95
|
+
assertEqual(text.delta[0].insert, "hello worldhello world", "text was extended");
|
|
96
|
+
|
|
97
|
+
text.delete("hello world".length, "hello world".length);
|
|
98
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
99
|
+
|
|
100
|
+
assertEqual(text.delta.length, 1, "trimmed a delta");
|
|
101
|
+
assertEqual(text.delta[0].insert, "hello world", "text matches");
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
test('format text deltas', () => {
|
|
107
|
+
|
|
108
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
109
|
+
|
|
110
|
+
// Can insert text
|
|
111
|
+
const child = client.root.createChildElement("text", { "hello" : "world" });
|
|
112
|
+
assertEqual(child instanceof XmlElement, true, "inserted an element");
|
|
113
|
+
|
|
114
|
+
let text = child.getChildren()[0];
|
|
115
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
116
|
+
assertEqual(text instanceof XmlText, true, "added a text element inside the element");
|
|
117
|
+
assertEqual(child.tagName, "text", "inserted text tag names are equal");
|
|
118
|
+
assertEqual(child.getAttribute("hello"), "world", "inserted child attributes are equal");
|
|
119
|
+
|
|
120
|
+
assertEqual(text.delta.length, 0, "deltas are empty");
|
|
121
|
+
text.insert(0, "hello world");
|
|
122
|
+
// format whole item
|
|
123
|
+
text.format(0, "hello world".length, {
|
|
124
|
+
"bold" : true
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
128
|
+
|
|
129
|
+
assertEqual(text.delta.length, 1, "kept delta");
|
|
130
|
+
assertEqual(text.delta[0].insert, "hello world", "text matches");
|
|
131
|
+
assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
|
|
132
|
+
|
|
133
|
+
// format start
|
|
134
|
+
text.format(0, "hello".length, {
|
|
135
|
+
"italic" : true
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
139
|
+
|
|
140
|
+
assertEqual(text.delta.length, 2, "split delta");
|
|
141
|
+
assertEqual(text.delta[0].insert, "hello", "first word matches");
|
|
142
|
+
assertEqual(text.delta[1].insert, " world", "second word matches");
|
|
143
|
+
assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
|
|
144
|
+
assertEqual(text.delta[0].attributes["italic"], true, "attribute matches");
|
|
145
|
+
assertEqual(text.delta[1].attributes["bold"], true, "attribute matches");
|
|
146
|
+
assertEqual(text.delta[1].attributes["italic"], null, "attribute matches");
|
|
147
|
+
|
|
148
|
+
// format end
|
|
149
|
+
text.format(3, 2, {
|
|
150
|
+
"underline" : true
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
154
|
+
|
|
155
|
+
assertEqual(text.delta.length, 3, "split delta");
|
|
156
|
+
assertEqual(text.delta[0].insert, "hel", "first word matches");
|
|
157
|
+
assertEqual(text.delta[1].insert, "lo", "second word matches");
|
|
158
|
+
assertEqual(text.delta[2].insert, " world", "third word matches");
|
|
159
|
+
|
|
160
|
+
assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
|
|
161
|
+
assertEqual(text.delta[0].attributes["italic"], true, "attribute matches");
|
|
162
|
+
assertEqual(text.delta[0].attributes["underline"], null, "attribute matches");
|
|
163
|
+
|
|
164
|
+
assertEqual(text.delta[1].attributes["bold"], true, "attribute matches");
|
|
165
|
+
assertEqual(text.delta[1].attributes["italic"], true, "attribute matches");
|
|
166
|
+
assertEqual(text.delta[1].attributes["underline"], true, "attribute matches");
|
|
167
|
+
|
|
168
|
+
assertEqual(text.delta[2].attributes["bold"], true, "attribute matches");
|
|
169
|
+
assertEqual(text.delta[2].attributes["italic"], null, "attribute matches");
|
|
170
|
+
assertEqual(text.delta[2].attributes["underline"], null, "attribute matches");
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// format across items
|
|
174
|
+
text.format(0, "hello world".length, {
|
|
175
|
+
"strikethrough" : true
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
179
|
+
assertEqual(text.delta.length, 3, "retain delta");
|
|
180
|
+
assertEqual(text.delta[0].insert, "hel", "first word matches");
|
|
181
|
+
assertEqual(text.delta[1].insert, "lo", "second word matches");
|
|
182
|
+
assertEqual(text.delta[2].insert, " world", "third word matches");
|
|
183
|
+
|
|
184
|
+
assertEqual(text.delta[0].attributes["bold"], true, "attribute matches");
|
|
185
|
+
assertEqual(text.delta[0].attributes["italic"], true, "attribute matches");
|
|
186
|
+
assertEqual(text.delta[0].attributes["underline"], null, "attribute matches");
|
|
187
|
+
assertEqual(text.delta[0].attributes["strikethrough"], true, "attribute matches");
|
|
188
|
+
|
|
189
|
+
assertEqual(text.delta[1].attributes["bold"], true, "attribute matches");
|
|
190
|
+
assertEqual(text.delta[1].attributes["italic"], true, "attribute matches");
|
|
191
|
+
assertEqual(text.delta[1].attributes["underline"], true, "attribute matches");
|
|
192
|
+
assertEqual(text.delta[1].attributes["strikethrough"], true, "attribute matches");
|
|
193
|
+
|
|
194
|
+
assertEqual(text.delta[2].attributes["bold"], true, "attribute matches");
|
|
195
|
+
assertEqual(text.delta[2].attributes["italic"], null, "attribute matches");
|
|
196
|
+
assertEqual(text.delta[2].attributes["underline"], null, "attribute matches");
|
|
197
|
+
assertEqual(text.delta[2].attributes["strikethrough"], true, "attribute matches");
|
|
198
|
+
|
|
199
|
+
// format across items
|
|
200
|
+
text.format(1, 1, {
|
|
201
|
+
"dot" : true
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
205
|
+
assertEqual(text.delta.length, 5, "split delta");
|
|
206
|
+
assertEqual(text.delta[0].insert, "h", "first word matches");
|
|
207
|
+
assertEqual(text.delta[1].insert, "e", "second word matches");
|
|
208
|
+
assertEqual(text.delta[2].insert, "l", "third word matches");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('delete start of delta text', () => {
|
|
212
|
+
|
|
213
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
214
|
+
|
|
215
|
+
// Text Delete
|
|
216
|
+
client.root.createChildElement("text", { "hello" : "world" });
|
|
217
|
+
assertEqual(client.root.getChildren().length, 1, "inserted text element");
|
|
218
|
+
let child = client.root.getChildren()[0];
|
|
219
|
+
|
|
220
|
+
// Delete start
|
|
221
|
+
assertEqual(child instanceof XmlElement, true, "inserted an element");
|
|
222
|
+
let text = child.getChildren()[0];
|
|
223
|
+
assertEqual(text instanceof XmlText, true, "inserted a text element");
|
|
224
|
+
text.insert(0, "hello world");
|
|
225
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
226
|
+
assertEqual(text.delta.length, 1, "inserted a delta");
|
|
227
|
+
assertEqual(text.delta[0].insert, "hello world", "text matches");
|
|
228
|
+
text.delete(0, "hello ".length);
|
|
229
|
+
assertEqual(text.delta.length, 1, "kept delta");
|
|
230
|
+
assertEqual(text.delta[0].insert, "world", "text matches");
|
|
231
|
+
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('delete end of delta text', () => {
|
|
235
|
+
|
|
236
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
237
|
+
|
|
238
|
+
// Text Delete
|
|
239
|
+
client.root.createChildElement("text", { "hello" : "world" });
|
|
240
|
+
assertEqual(client.root.getChildren().length, 1, "inserted text element");
|
|
241
|
+
let child = client.root.getChildren()[0];
|
|
242
|
+
let text = child.getChildren()[0];
|
|
243
|
+
text.insert(0, "world");
|
|
244
|
+
|
|
245
|
+
// Delete end
|
|
246
|
+
text.delete("world".length - 1, 1);
|
|
247
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
248
|
+
assertEqual(text.delta.length, 1, "kept delta");
|
|
249
|
+
assertEqual(text.delta[0].insert, "worl", "text matches");
|
|
250
|
+
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('delete center of delta text', () => {
|
|
254
|
+
|
|
255
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
256
|
+
|
|
257
|
+
client.root.createChildElement("text", { "hello" : "world" });
|
|
258
|
+
assertEqual(client.root.getChildren().length, 1, "inserted text element");
|
|
259
|
+
let child = client.root.getChildren()[0];
|
|
260
|
+
let text = child.getChildren()[0];
|
|
261
|
+
text.insert(0, "worl");
|
|
262
|
+
|
|
263
|
+
// Delete center
|
|
264
|
+
text.delete(2, 1);
|
|
265
|
+
assertDeltaEqual(serverRoot.firstChild.firstChild.toDelta(), text.delta);
|
|
266
|
+
assertEqual(text.delta.length, 1, "kept delta");
|
|
267
|
+
assertEqual(text.delta[0].insert, "wol", "text matches");
|
|
268
|
+
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('insert elements at positions', () => {
|
|
272
|
+
|
|
273
|
+
const [ serverRoot, client, server] = makeDoc();
|
|
274
|
+
|
|
275
|
+
// Inserts at end
|
|
276
|
+
client.root.createChildElement("child2", { "hello2" : "world2" });
|
|
277
|
+
|
|
278
|
+
let child = client.root.getChildren()[0];
|
|
279
|
+
assertEqual(child.tagName, "child2", "inserted child tag names are equal");
|
|
280
|
+
assertEqual(child.getAttribute("hello2"), "world2", "inserted child attributes are equal");
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
// Inserts deep
|
|
284
|
+
child.createChildElement("child3", { "hello3" : "world3" });
|
|
285
|
+
|
|
286
|
+
const deepChild = child;
|
|
287
|
+
|
|
288
|
+
child = child.getChildren()[0];
|
|
289
|
+
assertEqual(child.tagName, "child3", "inserted child tag names are equal");
|
|
290
|
+
assertEqual(child.getAttribute("hello3"), "world3", "inserted child attributes are equal");
|
|
291
|
+
|
|
292
|
+
// Inserts after deep
|
|
293
|
+
child.parent.createChildElement("child4", { "hello4" : "world4" });
|
|
294
|
+
|
|
295
|
+
child = deepChild.getChildren()[1];
|
|
296
|
+
assertEqual(child.tagName, "child4", "inserted child tag names are equal");
|
|
297
|
+
assertEqual(child.getAttribute("hello4"), "world4", "inserted child attributes are equal");
|
|
298
|
+
|
|
299
|
+
// Inserts after element deep
|
|
300
|
+
deepChild.createChildElementAfter(deepChild.getChildren()[0], "child5", { "hello5" : "world5" });
|
|
301
|
+
|
|
302
|
+
child = deepChild.getChildren()[1];
|
|
303
|
+
assertEqual(child.tagName, "child5", "inserted child tag names are equal");
|
|
304
|
+
assertEqual(child.getAttribute("hello5"), "world5", "inserted child attributes are equal");
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
// Inserts after element deep
|
|
308
|
+
deepChild.createChildElementAt(2, "child6", { "hello6" : "world6" });
|
|
309
|
+
|
|
310
|
+
child = deepChild.getChildren()[2];
|
|
311
|
+
assertEqual(child.tagName, "child6", "inserted child tag names are equal");
|
|
312
|
+
assertEqual(child.getAttribute("hello6"), "world6", "inserted child attributes are equal");
|
|
313
|
+
});
|