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.
@@ -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
- });