jazz-tools 0.13.10 → 0.13.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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +10 -0
- package/dist/{chunk-NFVKGXSH.js → chunk-I7NYAGLP.js} +23 -398
- package/dist/{chunk-NFVKGXSH.js.map → chunk-I7NYAGLP.js.map} +1 -1
- package/dist/coValues/coPlainText.d.ts +7 -0
- package/dist/coValues/coPlainText.d.ts.map +1 -1
- package/dist/coValues/coRichText.d.ts +2 -257
- package/dist/coValues/coRichText.d.ts.map +1 -1
- package/dist/exports.d.ts +1 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/package.json +2 -2
- package/src/coValues/coPlainText.ts +23 -1
- package/src/coValues/coRichText.ts +2 -660
- package/src/exports.ts +1 -7
- package/src/tests/coPlainText.test.ts +21 -3
- package/dist/tests/coRichText.test.d.ts +0 -2
- package/dist/tests/coRichText.test.d.ts.map +0 -1
- package/src/tests/coRichText.test.ts +0 -937
@@ -1,937 +0,0 @@
|
|
1
|
-
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
2
|
-
import { describe, expect, test } from "vitest";
|
3
|
-
import { splitNode } from "../coValues/coRichText.js";
|
4
|
-
import {
|
5
|
-
Account,
|
6
|
-
CoRichText,
|
7
|
-
Marks,
|
8
|
-
type TextPos,
|
9
|
-
type TreeNode,
|
10
|
-
cojsonInternals,
|
11
|
-
createJazzContextFromExistingCredentials,
|
12
|
-
isControlledAccount,
|
13
|
-
} from "../index.js";
|
14
|
-
import { randomSessionProvider } from "../internal.js";
|
15
|
-
|
16
|
-
const connectedPeers = cojsonInternals.connectedPeers;
|
17
|
-
const Crypto = await WasmCrypto.create();
|
18
|
-
|
19
|
-
describe("CoRichText", async () => {
|
20
|
-
const me = await Account.create({
|
21
|
-
creationProps: { name: "Hermes Puggington" },
|
22
|
-
crypto: Crypto,
|
23
|
-
});
|
24
|
-
|
25
|
-
const text = CoRichText.createFromPlainText("hello world", { owner: me });
|
26
|
-
|
27
|
-
test("Construction", () => {
|
28
|
-
expect(text + "").toEqual("hello world");
|
29
|
-
});
|
30
|
-
|
31
|
-
describe("Mutation", () => {
|
32
|
-
test("insertion", () => {
|
33
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
34
|
-
owner: me,
|
35
|
-
});
|
36
|
-
|
37
|
-
text.insertAfter(5, " cruel");
|
38
|
-
expect(text + "").toEqual("hello cruel world");
|
39
|
-
});
|
40
|
-
|
41
|
-
test("deletion", () => {
|
42
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
43
|
-
owner: me,
|
44
|
-
});
|
45
|
-
|
46
|
-
text.deleteRange({ from: 3, to: 8 });
|
47
|
-
expect(text + "").toEqual("helrld");
|
48
|
-
});
|
49
|
-
|
50
|
-
describe("inserting marks", () => {
|
51
|
-
test("basic mark insertion", () => {
|
52
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
53
|
-
owner: me,
|
54
|
-
});
|
55
|
-
|
56
|
-
// Add mark covering "hello"
|
57
|
-
text.insertMark(0, 4, Marks.Strong, { tag: "strong" });
|
58
|
-
|
59
|
-
const marks = text.resolveMarks();
|
60
|
-
expect(marks).toHaveLength(1);
|
61
|
-
expect(marks[0]).toMatchObject({
|
62
|
-
startAfter: 0,
|
63
|
-
startBefore: 1,
|
64
|
-
endAfter: 3,
|
65
|
-
endBefore: 5,
|
66
|
-
tag: "strong",
|
67
|
-
});
|
68
|
-
});
|
69
|
-
|
70
|
-
test("inserting mark with custom owner", () => {
|
71
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
72
|
-
owner: me,
|
73
|
-
});
|
74
|
-
|
75
|
-
text.insertMark(
|
76
|
-
0,
|
77
|
-
5,
|
78
|
-
Marks.Strong,
|
79
|
-
{ tag: "strong" },
|
80
|
-
{ markOwner: me },
|
81
|
-
);
|
82
|
-
|
83
|
-
const mark = text.marks![0];
|
84
|
-
expect(mark!._owner).toStrictEqual(me);
|
85
|
-
});
|
86
|
-
|
87
|
-
test("inserting multiple non-overlapping marks", () => {
|
88
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
89
|
-
owner: me,
|
90
|
-
});
|
91
|
-
|
92
|
-
text.insertMark(0, 5, Marks.Strong, { tag: "strong" }); // "hello"
|
93
|
-
text.insertMark(6, 11, Marks.Em, { tag: "em" }); // "world"
|
94
|
-
|
95
|
-
const marks = text.resolveMarks();
|
96
|
-
expect(marks).toHaveLength(2);
|
97
|
-
|
98
|
-
// Verify positions and types
|
99
|
-
const [mark1, mark2] = marks;
|
100
|
-
expect(mark1!.sourceMark.tag).toBe("strong");
|
101
|
-
expect(mark2!.sourceMark.tag).toBe("em");
|
102
|
-
|
103
|
-
expect(mark1!.startAfter).toBe(0);
|
104
|
-
expect(mark1!.endBefore).toBe(6);
|
105
|
-
expect(mark2!.startAfter).toBe(5);
|
106
|
-
expect(mark2!.endBefore).toBe(11);
|
107
|
-
});
|
108
|
-
|
109
|
-
test("inserting mark with additional properties", () => {
|
110
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
111
|
-
owner: me,
|
112
|
-
});
|
113
|
-
|
114
|
-
text.insertMark(0, 5, Marks.Link, {
|
115
|
-
tag: "link",
|
116
|
-
url: "https://example.com",
|
117
|
-
});
|
118
|
-
|
119
|
-
const marks = text.resolveMarks();
|
120
|
-
expect(marks).toHaveLength(1);
|
121
|
-
expect(marks[0]!.sourceMark).toHaveProperty(
|
122
|
-
"url",
|
123
|
-
"https://example.com",
|
124
|
-
);
|
125
|
-
});
|
126
|
-
|
127
|
-
test("inserting mark at text boundaries", () => {
|
128
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
129
|
-
owner: me,
|
130
|
-
});
|
131
|
-
|
132
|
-
// Mark entire text
|
133
|
-
text.insertMark(0, 11, Marks.Strong, { tag: "strong" });
|
134
|
-
|
135
|
-
const marks = text.resolveMarks();
|
136
|
-
expect(marks).toHaveLength(1);
|
137
|
-
expect(marks[0]!.startAfter).toBe(0);
|
138
|
-
expect(marks[0]!.endAfter).toBe(10);
|
139
|
-
});
|
140
|
-
|
141
|
-
test("inserting mark outside of text bounds", () => {
|
142
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
143
|
-
owner: me,
|
144
|
-
});
|
145
|
-
|
146
|
-
text.insertMark(-1, 12, Marks.Strong, { tag: "strong" });
|
147
|
-
expect(text.resolveMarks()).toHaveLength(1);
|
148
|
-
expect(text.resolveMarks()[0]!.startAfter).toBe(0);
|
149
|
-
expect(text.resolveMarks()[0]!.endAfter).toBe(10);
|
150
|
-
});
|
151
|
-
|
152
|
-
test("maintains correct mark ordering with nested marks", () => {
|
153
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
154
|
-
owner: me,
|
155
|
-
});
|
156
|
-
|
157
|
-
text.insertMark(0, 11, Marks.Strong, { tag: "strong" });
|
158
|
-
text.insertMark(2, 8, Marks.Em, { tag: "em" });
|
159
|
-
text.insertMark(4, 6, Marks.Link, {
|
160
|
-
tag: "link",
|
161
|
-
url: "https://example.com",
|
162
|
-
});
|
163
|
-
|
164
|
-
const tree = text.toTree(["strong", "em", "link"]);
|
165
|
-
// Verify the nesting structure is correct
|
166
|
-
// Strong should contain Em which should contain Link
|
167
|
-
expect(tree).toEqual({
|
168
|
-
type: "node",
|
169
|
-
tag: "root",
|
170
|
-
start: 0,
|
171
|
-
end: 11,
|
172
|
-
children: [
|
173
|
-
{
|
174
|
-
type: "node",
|
175
|
-
tag: "strong",
|
176
|
-
start: 0,
|
177
|
-
end: 2,
|
178
|
-
children: [
|
179
|
-
{
|
180
|
-
type: "leaf",
|
181
|
-
start: 0,
|
182
|
-
end: 2,
|
183
|
-
},
|
184
|
-
],
|
185
|
-
},
|
186
|
-
{
|
187
|
-
type: "node",
|
188
|
-
tag: "strong",
|
189
|
-
start: 2,
|
190
|
-
end: 4,
|
191
|
-
children: [
|
192
|
-
{
|
193
|
-
type: "node",
|
194
|
-
tag: "em",
|
195
|
-
start: 2,
|
196
|
-
end: 4,
|
197
|
-
children: [
|
198
|
-
{
|
199
|
-
type: "leaf",
|
200
|
-
start: 2,
|
201
|
-
end: 4,
|
202
|
-
},
|
203
|
-
],
|
204
|
-
},
|
205
|
-
],
|
206
|
-
},
|
207
|
-
{
|
208
|
-
type: "node",
|
209
|
-
tag: "strong",
|
210
|
-
start: 4,
|
211
|
-
end: 6,
|
212
|
-
children: [
|
213
|
-
{
|
214
|
-
type: "node",
|
215
|
-
tag: "em",
|
216
|
-
start: 4,
|
217
|
-
end: 6,
|
218
|
-
children: [
|
219
|
-
{
|
220
|
-
type: "node",
|
221
|
-
tag: "link",
|
222
|
-
start: 4,
|
223
|
-
end: 6,
|
224
|
-
children: [
|
225
|
-
{
|
226
|
-
type: "leaf",
|
227
|
-
start: 4,
|
228
|
-
end: 6,
|
229
|
-
},
|
230
|
-
],
|
231
|
-
},
|
232
|
-
],
|
233
|
-
},
|
234
|
-
],
|
235
|
-
},
|
236
|
-
{
|
237
|
-
type: "node",
|
238
|
-
tag: "strong",
|
239
|
-
start: 6,
|
240
|
-
end: 8,
|
241
|
-
children: [
|
242
|
-
{
|
243
|
-
type: "node",
|
244
|
-
tag: "em",
|
245
|
-
start: 6,
|
246
|
-
end: 8,
|
247
|
-
children: [
|
248
|
-
{
|
249
|
-
type: "leaf",
|
250
|
-
start: 6,
|
251
|
-
end: 8,
|
252
|
-
},
|
253
|
-
],
|
254
|
-
},
|
255
|
-
],
|
256
|
-
},
|
257
|
-
{
|
258
|
-
type: "node",
|
259
|
-
tag: "strong",
|
260
|
-
start: 8,
|
261
|
-
end: 11,
|
262
|
-
children: [
|
263
|
-
{
|
264
|
-
type: "leaf",
|
265
|
-
start: 8,
|
266
|
-
end: 11,
|
267
|
-
},
|
268
|
-
],
|
269
|
-
},
|
270
|
-
],
|
271
|
-
});
|
272
|
-
});
|
273
|
-
});
|
274
|
-
|
275
|
-
describe("removing marks", () => {
|
276
|
-
test("basic mark removal", () => {
|
277
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
278
|
-
owner: me,
|
279
|
-
});
|
280
|
-
|
281
|
-
// Add a mark
|
282
|
-
text.insertMark(0, 5, Marks.Strong, { tag: "strong" });
|
283
|
-
|
284
|
-
// Verify mark was added
|
285
|
-
expect(text.resolveMarks()).toHaveLength(1);
|
286
|
-
|
287
|
-
// Remove the mark
|
288
|
-
text.removeMark(0, 5, Marks.Strong, { tag: "strong" });
|
289
|
-
|
290
|
-
// Verify mark was removed
|
291
|
-
expect(text.resolveMarks()).toHaveLength(0);
|
292
|
-
});
|
293
|
-
|
294
|
-
test("skips marks that aren't in the range", () => {
|
295
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
296
|
-
owner: me,
|
297
|
-
});
|
298
|
-
|
299
|
-
text.insertMark(0, 2, Marks.Strong, { tag: "strong" });
|
300
|
-
text.removeMark(3, 6, Marks.Strong, { tag: "strong" });
|
301
|
-
text.insertMark(7, 11, Marks.Strong, { tag: "strong" });
|
302
|
-
|
303
|
-
expect(text.resolveMarks()).toHaveLength(2);
|
304
|
-
});
|
305
|
-
|
306
|
-
test("removing marks with partial overlap", () => {
|
307
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
308
|
-
owner: me,
|
309
|
-
});
|
310
|
-
|
311
|
-
// Add marks at different positions
|
312
|
-
text.insertMark(0, 6, Marks.Strong, { tag: "strong" }); // "hello "
|
313
|
-
text.insertMark(4, 11, Marks.Strong, { tag: "strong" }); // "o world"
|
314
|
-
|
315
|
-
// Verify initial marks
|
316
|
-
expect(text.resolveMarks()).toHaveLength(2);
|
317
|
-
|
318
|
-
// Remove mark in middle (4-7: "o w") where the marks overlap
|
319
|
-
// This should trim both marks to exclude the removed region:
|
320
|
-
// - First mark should become "hell" (was "hello ")
|
321
|
-
// - Second mark should become "rld" (was "o world")
|
322
|
-
text.removeMark(4, 7, Marks.Strong, { tag: "strong" });
|
323
|
-
|
324
|
-
// Should have two marks remaining - one before and one after the removal
|
325
|
-
const remainingMarks = text.resolveMarks();
|
326
|
-
expect(remainingMarks).toHaveLength(2);
|
327
|
-
|
328
|
-
// Verify the remaining marks
|
329
|
-
// First mark should be trimmed to "hell"
|
330
|
-
expect(remainingMarks[0]!.startAfter).toBe(0);
|
331
|
-
expect(remainingMarks[0]!.startBefore).toBe(1);
|
332
|
-
expect(remainingMarks[0]!.endAfter).toBe(3);
|
333
|
-
expect(remainingMarks[0]!.endBefore).toBe(4);
|
334
|
-
|
335
|
-
// Second mark should be trimmed to "rld"
|
336
|
-
expect(remainingMarks[1]!.startAfter).toBe(7);
|
337
|
-
expect(remainingMarks[1]!.startBefore).toBe(8);
|
338
|
-
expect(remainingMarks[1]!.endAfter).toBe(10);
|
339
|
-
expect(remainingMarks[1]!.endBefore).toBe(11);
|
340
|
-
|
341
|
-
// Verify the text content is still intact
|
342
|
-
expect(text.toString()).toBe("hello world");
|
343
|
-
});
|
344
|
-
|
345
|
-
test("removing marks of specific type", () => {
|
346
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
347
|
-
owner: me,
|
348
|
-
});
|
349
|
-
|
350
|
-
// Add different types of marks
|
351
|
-
text.insertMark(0, 5, Marks.Strong, { tag: "strong" });
|
352
|
-
text.insertMark(0, 5, Marks.Em, { tag: "em" });
|
353
|
-
|
354
|
-
// Verify both marks were added
|
355
|
-
expect(text.resolveMarks()).toHaveLength(2);
|
356
|
-
|
357
|
-
// Remove only Strong marks
|
358
|
-
text.removeMark(0, 5, Marks.Strong, { tag: "strong" });
|
359
|
-
|
360
|
-
// Should have one mark remaining
|
361
|
-
const remainingMarks = text.resolveMarks();
|
362
|
-
expect(remainingMarks).toHaveLength(1);
|
363
|
-
expect(remainingMarks[0]!.sourceMark!.tag).toBe("em");
|
364
|
-
});
|
365
|
-
|
366
|
-
test("removing mark that overlaps end of existing mark", () => {
|
367
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
368
|
-
owner: me,
|
369
|
-
});
|
370
|
-
|
371
|
-
// Add mark covering "hello world"
|
372
|
-
text.insertMark(0, 11, Marks.Strong, { tag: "strong" });
|
373
|
-
|
374
|
-
// Remove mark covering "world"
|
375
|
-
text.removeMark(6, 11, Marks.Strong, { tag: "strong" });
|
376
|
-
|
377
|
-
// Should have one mark remaining on "hello "
|
378
|
-
const remainingMarks = text.resolveMarks();
|
379
|
-
expect(remainingMarks).toHaveLength(1);
|
380
|
-
expect(remainingMarks[0]!.startAfter).toBe(0);
|
381
|
-
expect(remainingMarks[0]!.endAfter).toBe(5);
|
382
|
-
});
|
383
|
-
|
384
|
-
test("removing mark that overlaps start of existing mark", () => {
|
385
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
386
|
-
owner: me,
|
387
|
-
});
|
388
|
-
|
389
|
-
// Add mark covering "hello world"
|
390
|
-
text.insertMark(0, 11, Marks.Strong, { tag: "strong" });
|
391
|
-
|
392
|
-
// Remove mark covering "hello "
|
393
|
-
text.removeMark(0, 5, Marks.Strong, { tag: "strong" });
|
394
|
-
|
395
|
-
// Should have one mark remaining on "world"
|
396
|
-
const remainingMarks = text.resolveMarks();
|
397
|
-
expect(remainingMarks).toHaveLength(1);
|
398
|
-
expect(remainingMarks[0]!.startAfter).toBe(5);
|
399
|
-
expect(remainingMarks[0]!.startBefore).toBe(6);
|
400
|
-
expect(remainingMarks[0]!.endAfter).toBe(10);
|
401
|
-
expect(remainingMarks[0]!.endBefore).toBe(11);
|
402
|
-
});
|
403
|
-
|
404
|
-
test("removing mark from middle of existing mark", () => {
|
405
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
406
|
-
owner: me,
|
407
|
-
});
|
408
|
-
|
409
|
-
// Add mark covering "hello world"
|
410
|
-
text.insertMark(0, 10, Marks.Strong, { tag: "strong" });
|
411
|
-
|
412
|
-
// Remove mark covering " wo"
|
413
|
-
text.removeMark(5, 8, Marks.Strong, { tag: "strong" });
|
414
|
-
|
415
|
-
// Should have two marks remaining on "hello" and "rld"
|
416
|
-
const remainingMarks = text.resolveMarks();
|
417
|
-
expect(remainingMarks).toHaveLength(2);
|
418
|
-
|
419
|
-
// First mark should cover "hello"
|
420
|
-
expect(remainingMarks[0]!.startAfter).toBe(0);
|
421
|
-
expect(remainingMarks[0]!.startBefore).toBe(1);
|
422
|
-
expect(remainingMarks[0]!.endAfter).toBe(4);
|
423
|
-
expect(remainingMarks[0]!.endBefore).toBe(5);
|
424
|
-
|
425
|
-
// Second mark should cover "rld"
|
426
|
-
expect(remainingMarks[1]!.startAfter).toBe(8);
|
427
|
-
expect(remainingMarks[1]!.endAfter).toBe(10);
|
428
|
-
});
|
429
|
-
});
|
430
|
-
});
|
431
|
-
|
432
|
-
describe("Conversion", () => {
|
433
|
-
test("to tree", () => {
|
434
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
435
|
-
owner: me,
|
436
|
-
});
|
437
|
-
|
438
|
-
expect(text.toTree(["strong"])).toEqual({
|
439
|
-
type: "node",
|
440
|
-
tag: "root",
|
441
|
-
start: 0,
|
442
|
-
end: 11,
|
443
|
-
children: [
|
444
|
-
{
|
445
|
-
type: "leaf",
|
446
|
-
start: 0,
|
447
|
-
end: 11,
|
448
|
-
},
|
449
|
-
],
|
450
|
-
});
|
451
|
-
});
|
452
|
-
|
453
|
-
test("to string", () => {
|
454
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
455
|
-
owner: me,
|
456
|
-
});
|
457
|
-
|
458
|
-
expect(text.toString()).toEqual("hello world");
|
459
|
-
});
|
460
|
-
|
461
|
-
test("splits nested children correctly", () => {
|
462
|
-
// Create text with nested marks
|
463
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
464
|
-
owner: me,
|
465
|
-
});
|
466
|
-
|
467
|
-
// Add an outer mark spanning the whole text
|
468
|
-
text.insertMark(0, 11, Marks.Strong, { tag: "strong" });
|
469
|
-
|
470
|
-
// Add an inner mark spanning part of the text
|
471
|
-
text.insertMark(6, 11, Marks.Em, { tag: "em" });
|
472
|
-
|
473
|
-
// Split at position 8 (between 'wo' and 'rld')
|
474
|
-
const tree = text.toTree(["strong", "em"]);
|
475
|
-
|
476
|
-
expect(tree).toEqual({
|
477
|
-
type: "node",
|
478
|
-
tag: "root",
|
479
|
-
start: 0,
|
480
|
-
end: 11,
|
481
|
-
children: [
|
482
|
-
{
|
483
|
-
type: "node",
|
484
|
-
tag: "strong",
|
485
|
-
start: 0,
|
486
|
-
end: 6,
|
487
|
-
children: [
|
488
|
-
{
|
489
|
-
type: "leaf",
|
490
|
-
start: 0,
|
491
|
-
end: 6,
|
492
|
-
},
|
493
|
-
],
|
494
|
-
},
|
495
|
-
{
|
496
|
-
type: "node",
|
497
|
-
tag: "strong",
|
498
|
-
start: 6,
|
499
|
-
end: 11,
|
500
|
-
children: [
|
501
|
-
{
|
502
|
-
type: "node",
|
503
|
-
tag: "em",
|
504
|
-
start: 6,
|
505
|
-
end: 11,
|
506
|
-
children: [
|
507
|
-
{
|
508
|
-
type: "leaf",
|
509
|
-
start: 6,
|
510
|
-
end: 11,
|
511
|
-
},
|
512
|
-
],
|
513
|
-
},
|
514
|
-
],
|
515
|
-
},
|
516
|
-
],
|
517
|
-
});
|
518
|
-
|
519
|
-
// Now verify splitting works by checking a specific position
|
520
|
-
const [before, after] = splitNode(tree.children[1] as TreeNode, 8);
|
521
|
-
|
522
|
-
// Verify the structure of the split nodes
|
523
|
-
expect(before).toEqual({
|
524
|
-
type: "node",
|
525
|
-
tag: "strong",
|
526
|
-
start: 6,
|
527
|
-
end: 8,
|
528
|
-
children: [
|
529
|
-
{
|
530
|
-
type: "node",
|
531
|
-
tag: "em",
|
532
|
-
start: 6,
|
533
|
-
end: 8,
|
534
|
-
children: [
|
535
|
-
{
|
536
|
-
type: "leaf",
|
537
|
-
start: 6,
|
538
|
-
end: 8,
|
539
|
-
},
|
540
|
-
],
|
541
|
-
},
|
542
|
-
],
|
543
|
-
});
|
544
|
-
|
545
|
-
expect(after).toEqual({
|
546
|
-
type: "node",
|
547
|
-
tag: "strong",
|
548
|
-
start: 8,
|
549
|
-
end: 11,
|
550
|
-
children: [
|
551
|
-
{
|
552
|
-
type: "node",
|
553
|
-
tag: "em",
|
554
|
-
start: 8,
|
555
|
-
end: 11,
|
556
|
-
children: [
|
557
|
-
{
|
558
|
-
type: "leaf",
|
559
|
-
start: 8,
|
560
|
-
end: 11,
|
561
|
-
},
|
562
|
-
],
|
563
|
-
},
|
564
|
-
],
|
565
|
-
});
|
566
|
-
});
|
567
|
-
});
|
568
|
-
|
569
|
-
describe("Resolution", () => {
|
570
|
-
const initNodeAndText = async () => {
|
571
|
-
const me = await Account.create({
|
572
|
-
creationProps: { name: "Hermes Puggington" },
|
573
|
-
crypto: Crypto,
|
574
|
-
});
|
575
|
-
|
576
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
577
|
-
owner: me,
|
578
|
-
});
|
579
|
-
|
580
|
-
return { me, text };
|
581
|
-
};
|
582
|
-
|
583
|
-
test("Loading and availability", async () => {
|
584
|
-
const { me, text } = await initNodeAndText();
|
585
|
-
|
586
|
-
const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
|
587
|
-
peer1role: "server",
|
588
|
-
peer2role: "client",
|
589
|
-
});
|
590
|
-
if (!isControlledAccount(me)) {
|
591
|
-
throw "me is not a controlled account";
|
592
|
-
}
|
593
|
-
me._raw.core.node.syncManager.addPeer(secondPeer);
|
594
|
-
const { account: meOnSecondPeer } =
|
595
|
-
await createJazzContextFromExistingCredentials({
|
596
|
-
credentials: {
|
597
|
-
accountID: me.id,
|
598
|
-
secret: me._raw.agentSecret,
|
599
|
-
},
|
600
|
-
sessionProvider: randomSessionProvider,
|
601
|
-
peersToLoadFrom: [initialAsPeer],
|
602
|
-
crypto: Crypto,
|
603
|
-
});
|
604
|
-
|
605
|
-
const loadedText = await CoRichText.load(text.id, {
|
606
|
-
loadAs: meOnSecondPeer,
|
607
|
-
resolve: { marks: { $each: true }, text: true },
|
608
|
-
});
|
609
|
-
|
610
|
-
expect(loadedText).toBeDefined();
|
611
|
-
expect(loadedText?.toString()).toEqual("hello world");
|
612
|
-
|
613
|
-
const loadedText2 = await CoRichText.load(text.id, {
|
614
|
-
loadAs: meOnSecondPeer,
|
615
|
-
resolve: { marks: { $each: true }, text: true },
|
616
|
-
});
|
617
|
-
|
618
|
-
expect(loadedText2).toBeDefined();
|
619
|
-
expect(loadedText2?.toString()).toEqual("hello world");
|
620
|
-
});
|
621
|
-
|
622
|
-
test("Subscription & auto-resolution", async () => {
|
623
|
-
const { me, text } = await initNodeAndText();
|
624
|
-
|
625
|
-
const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
|
626
|
-
peer1role: "server",
|
627
|
-
peer2role: "client",
|
628
|
-
});
|
629
|
-
|
630
|
-
if (!isControlledAccount(me)) {
|
631
|
-
throw "me is not a controlled account";
|
632
|
-
}
|
633
|
-
me._raw.core.node.syncManager.addPeer(secondPeer);
|
634
|
-
const { account: meOnSecondPeer } =
|
635
|
-
await createJazzContextFromExistingCredentials({
|
636
|
-
credentials: {
|
637
|
-
accountID: me.id,
|
638
|
-
secret: me._raw.agentSecret,
|
639
|
-
},
|
640
|
-
sessionProvider: randomSessionProvider,
|
641
|
-
peersToLoadFrom: [initialAsPeer],
|
642
|
-
crypto: Crypto,
|
643
|
-
});
|
644
|
-
|
645
|
-
const queue = new cojsonInternals.Channel<CoRichText>();
|
646
|
-
|
647
|
-
CoRichText.subscribe(
|
648
|
-
text.id,
|
649
|
-
{
|
650
|
-
loadAs: meOnSecondPeer,
|
651
|
-
resolve: { marks: { $each: true }, text: true },
|
652
|
-
},
|
653
|
-
(subscribedText) => {
|
654
|
-
void queue.push(subscribedText);
|
655
|
-
},
|
656
|
-
);
|
657
|
-
|
658
|
-
const update1 = (await queue.next()).value;
|
659
|
-
expect(update1.toString()).toBe("hello world");
|
660
|
-
|
661
|
-
text.insertAfter(5, " beautiful");
|
662
|
-
const update2 = (await queue.next()).value;
|
663
|
-
expect(update2.toString()).toBe("hello beautiful world");
|
664
|
-
|
665
|
-
text.deleteRange({ from: 5, to: 15 });
|
666
|
-
const update3 = (await queue.next()).value;
|
667
|
-
expect(update3.toString()).toBe("hello world");
|
668
|
-
|
669
|
-
text.insertMark(0, 11, Marks.Strong, { tag: "strong" });
|
670
|
-
const update4 = (await queue.next()).value;
|
671
|
-
expect(update4.toString()).toBe("hello world");
|
672
|
-
expect(update4.resolveMarks()).toHaveLength(1);
|
673
|
-
expect(update4.resolveMarks()[0]!.tag).toBe("strong");
|
674
|
-
|
675
|
-
text.removeMark(5, 11, Marks.Strong, { tag: "strong" });
|
676
|
-
const update5 = (await queue.next()).value;
|
677
|
-
expect(update5.toString()).toBe("hello world");
|
678
|
-
expect(update5.resolveMarks()).toHaveLength(1);
|
679
|
-
expect(update5.resolveMarks()[0]!.tag).toBe("strong");
|
680
|
-
expect(update5.resolveMarks()[0]!.startAfter).toBe(0);
|
681
|
-
expect(update5.resolveMarks()[0]!.startBefore).toBe(1);
|
682
|
-
expect(update5.resolveMarks()[0]!.endAfter).toBe(4);
|
683
|
-
expect(update5.resolveMarks()[0]!.endBefore).toBe(5);
|
684
|
-
});
|
685
|
-
});
|
686
|
-
|
687
|
-
// In the sense of the mark resolving in the text, not sync resolution
|
688
|
-
describe("Mark resolution", () => {
|
689
|
-
test("basic position resolution", () => {
|
690
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
691
|
-
owner: me,
|
692
|
-
});
|
693
|
-
|
694
|
-
// Create mark directly
|
695
|
-
const mark = Marks.Strong.create(
|
696
|
-
{
|
697
|
-
tag: "strong",
|
698
|
-
startAfter: text.posAfter(0) as TextPos,
|
699
|
-
startBefore: text.posBefore(1) as TextPos,
|
700
|
-
endAfter: text.posAfter(4) as TextPos,
|
701
|
-
endBefore: text.posBefore(5) as TextPos,
|
702
|
-
},
|
703
|
-
{ owner: me },
|
704
|
-
);
|
705
|
-
|
706
|
-
// Add mark directly to marks list
|
707
|
-
text.marks!.push(mark);
|
708
|
-
|
709
|
-
const marks = text.resolveMarks();
|
710
|
-
expect(marks).toHaveLength(1);
|
711
|
-
expect(marks[0]).toMatchObject({
|
712
|
-
startAfter: 0,
|
713
|
-
startBefore: 1,
|
714
|
-
endAfter: 4,
|
715
|
-
endBefore: 5,
|
716
|
-
tag: "strong",
|
717
|
-
});
|
718
|
-
});
|
719
|
-
|
720
|
-
test("handles multiple marks", () => {
|
721
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
722
|
-
owner: me,
|
723
|
-
});
|
724
|
-
|
725
|
-
// Create marks directly
|
726
|
-
const mark1 = Marks.Strong.create(
|
727
|
-
{
|
728
|
-
tag: "strong",
|
729
|
-
startAfter: text.posAfter(0) as TextPos,
|
730
|
-
startBefore: text.posBefore(1) as TextPos,
|
731
|
-
endAfter: text.posAfter(4) as TextPos,
|
732
|
-
endBefore: text.posBefore(5) as TextPos,
|
733
|
-
},
|
734
|
-
{ owner: me },
|
735
|
-
);
|
736
|
-
|
737
|
-
const mark2 = Marks.Em.create(
|
738
|
-
{
|
739
|
-
tag: "em",
|
740
|
-
startAfter: text.posAfter(6) as TextPos,
|
741
|
-
startBefore: text.posBefore(7) as TextPos,
|
742
|
-
endAfter: text.posAfter(10) as TextPos,
|
743
|
-
endBefore: text.posBefore(11) as TextPos,
|
744
|
-
},
|
745
|
-
{ owner: me },
|
746
|
-
);
|
747
|
-
|
748
|
-
// Add marks directly
|
749
|
-
text.marks!.push(mark1);
|
750
|
-
text.marks!.push(mark2);
|
751
|
-
|
752
|
-
const marks = text.resolveMarks();
|
753
|
-
expect(marks).toHaveLength(2);
|
754
|
-
|
755
|
-
// First mark
|
756
|
-
expect(marks[0]).toMatchObject({
|
757
|
-
startAfter: 0,
|
758
|
-
startBefore: 1,
|
759
|
-
endAfter: 4,
|
760
|
-
endBefore: 5,
|
761
|
-
tag: "strong",
|
762
|
-
});
|
763
|
-
|
764
|
-
// Second mark
|
765
|
-
expect(marks[1]).toMatchObject({
|
766
|
-
startAfter: 6,
|
767
|
-
startBefore: 7,
|
768
|
-
endAfter: 10,
|
769
|
-
endBefore: 11,
|
770
|
-
tag: "em",
|
771
|
-
});
|
772
|
-
});
|
773
|
-
|
774
|
-
test("handles overlapping marks", () => {
|
775
|
-
const text = CoRichText.createFromPlainText("hello world", {
|
776
|
-
owner: me,
|
777
|
-
});
|
778
|
-
|
779
|
-
// Create overlapping marks directly
|
780
|
-
const mark1 = Marks.Strong.create(
|
781
|
-
{
|
782
|
-
tag: "strong",
|
783
|
-
startAfter: text.posAfter(0) as TextPos,
|
784
|
-
startBefore: text.posBefore(1) as TextPos,
|
785
|
-
endAfter: text.posAfter(4) as TextPos,
|
786
|
-
endBefore: text.posBefore(5) as TextPos,
|
787
|
-
},
|
788
|
-
{ owner: me },
|
789
|
-
);
|
790
|
-
|
791
|
-
const mark2 = Marks.Em.create(
|
792
|
-
{
|
793
|
-
tag: "em",
|
794
|
-
startAfter: text.posAfter(3) as TextPos,
|
795
|
-
startBefore: text.posBefore(4) as TextPos,
|
796
|
-
endAfter: text.posAfter(7) as TextPos,
|
797
|
-
endBefore: text.posBefore(8) as TextPos,
|
798
|
-
},
|
799
|
-
{ owner: me },
|
800
|
-
);
|
801
|
-
|
802
|
-
// Add marks directly
|
803
|
-
text.marks!.push(mark1);
|
804
|
-
text.marks!.push(mark2);
|
805
|
-
|
806
|
-
const marks = text.resolveMarks();
|
807
|
-
expect(marks).toHaveLength(2);
|
808
|
-
|
809
|
-
// First mark
|
810
|
-
expect(marks[0]).toMatchObject({
|
811
|
-
startAfter: 0,
|
812
|
-
startBefore: 1,
|
813
|
-
endAfter: 4,
|
814
|
-
endBefore: 5,
|
815
|
-
tag: "strong",
|
816
|
-
});
|
817
|
-
|
818
|
-
// Second mark
|
819
|
-
expect(marks[1]).toMatchObject({
|
820
|
-
startAfter: 3,
|
821
|
-
startBefore: 4,
|
822
|
-
endAfter: 7,
|
823
|
-
endBefore: 8,
|
824
|
-
tag: "em",
|
825
|
-
});
|
826
|
-
});
|
827
|
-
});
|
828
|
-
|
829
|
-
describe("Mark", () => {
|
830
|
-
test("basic mark", () => {
|
831
|
-
const mark = Marks.Strong.create(
|
832
|
-
{
|
833
|
-
tag: "strong",
|
834
|
-
startAfter: text.posAfter(0) as TextPos,
|
835
|
-
startBefore: text.posBefore(1) as TextPos,
|
836
|
-
endAfter: text.posAfter(4) as TextPos,
|
837
|
-
endBefore: text.posBefore(5) as TextPos,
|
838
|
-
},
|
839
|
-
{ owner: me },
|
840
|
-
);
|
841
|
-
expect(mark.tag).toEqual("strong");
|
842
|
-
});
|
843
|
-
|
844
|
-
test("validates positions correctly", () => {
|
845
|
-
const mark = Marks.Strong.create(
|
846
|
-
{
|
847
|
-
tag: "strong",
|
848
|
-
startAfter: text.posAfter(0) as TextPos,
|
849
|
-
startBefore: text.posBefore(1) as TextPos,
|
850
|
-
endAfter: text.posAfter(4) as TextPos,
|
851
|
-
endBefore: text.posBefore(5) as TextPos,
|
852
|
-
},
|
853
|
-
{ owner: me },
|
854
|
-
);
|
855
|
-
|
856
|
-
const result = mark.validatePositions(
|
857
|
-
11, // text length
|
858
|
-
(pos: TextPos) => text.idxAfter(pos),
|
859
|
-
(pos: TextPos) => text.idxBefore(pos),
|
860
|
-
);
|
861
|
-
|
862
|
-
expect(result).toEqual({
|
863
|
-
startAfter: 0,
|
864
|
-
startBefore: 1,
|
865
|
-
endAfter: 4,
|
866
|
-
endBefore: 5,
|
867
|
-
});
|
868
|
-
});
|
869
|
-
|
870
|
-
test("clamps positions to text bounds", () => {
|
871
|
-
const mark = Marks.Strong.create(
|
872
|
-
{
|
873
|
-
tag: "strong",
|
874
|
-
startAfter: text.posAfter(-5) as TextPos, // Invalid position
|
875
|
-
startBefore: text.posBefore(1) as TextPos,
|
876
|
-
endAfter: text.posAfter(4) as TextPos,
|
877
|
-
endBefore: text.posBefore(20) as TextPos, // Beyond text length
|
878
|
-
},
|
879
|
-
{ owner: me },
|
880
|
-
);
|
881
|
-
|
882
|
-
const result = mark.validatePositions(
|
883
|
-
11,
|
884
|
-
(pos: TextPos) => text.idxAfter(pos),
|
885
|
-
(pos: TextPos) => text.idxBefore(pos),
|
886
|
-
);
|
887
|
-
|
888
|
-
expect(result).toMatchObject({
|
889
|
-
startAfter: 0, // Clamped to start
|
890
|
-
startBefore: 1,
|
891
|
-
endAfter: 4,
|
892
|
-
endBefore: 11, // Clamped to text length
|
893
|
-
});
|
894
|
-
});
|
895
|
-
|
896
|
-
test("different mark types have correct tags", () => {
|
897
|
-
const strongMark = Marks.Strong.create(
|
898
|
-
{
|
899
|
-
tag: "strong",
|
900
|
-
startAfter: text.posAfter(0) as TextPos,
|
901
|
-
startBefore: text.posBefore(1) as TextPos,
|
902
|
-
endAfter: text.posAfter(4) as TextPos,
|
903
|
-
endBefore: text.posBefore(5) as TextPos,
|
904
|
-
},
|
905
|
-
{ owner: me },
|
906
|
-
);
|
907
|
-
|
908
|
-
const emMark = Marks.Em.create(
|
909
|
-
{
|
910
|
-
tag: "em",
|
911
|
-
startAfter: text.posAfter(0) as TextPos,
|
912
|
-
startBefore: text.posBefore(1) as TextPos,
|
913
|
-
endAfter: text.posAfter(4) as TextPos,
|
914
|
-
endBefore: text.posBefore(5) as TextPos,
|
915
|
-
},
|
916
|
-
{ owner: me },
|
917
|
-
);
|
918
|
-
|
919
|
-
const linkMark = Marks.Link.create(
|
920
|
-
{
|
921
|
-
tag: "link",
|
922
|
-
url: "https://example.com",
|
923
|
-
startAfter: text.posAfter(0) as TextPos,
|
924
|
-
startBefore: text.posBefore(1) as TextPos,
|
925
|
-
endAfter: text.posAfter(4) as TextPos,
|
926
|
-
endBefore: text.posBefore(5) as TextPos,
|
927
|
-
},
|
928
|
-
{ owner: me },
|
929
|
-
);
|
930
|
-
|
931
|
-
expect(strongMark.tag).toBe("strong");
|
932
|
-
expect(emMark.tag).toBe("em");
|
933
|
-
expect(linkMark.tag).toBe("link");
|
934
|
-
expect(linkMark).toHaveProperty("url", "https://example.com");
|
935
|
-
});
|
936
|
-
});
|
937
|
-
});
|