svedit 0.5.0 → 0.6.0
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/README.md +761 -120
- package/dist/AnnotatedTextProperty.svelte +122 -75
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.d.ts +118 -0
- package/dist/Command.svelte.d.ts.map +1 -0
- package/dist/Command.svelte.js +314 -0
- package/dist/CustomProperty.svelte +31 -42
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +36 -0
- package/dist/KeyMapper.svelte.d.ts.map +1 -0
- package/dist/KeyMapper.svelte.js +141 -0
- package/dist/Node.svelte +33 -35
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +40 -37
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/{Document.svelte.d.ts → Session.svelte.d.ts} +76 -83
- package/dist/Session.svelte.d.ts.map +1 -0
- package/dist/Session.svelte.js +594 -0
- package/dist/Svedit.svelte +1352 -1512
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +114 -40
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +845 -661
- package/dist/UnknownNode.svelte +6 -6
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +127 -0
- package/dist/doc_utils.d.ts.map +1 -0
- package/dist/doc_utils.js +482 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +20 -4
- package/dist/{commands.svelte.d.ts → transforms.svelte.d.ts} +1 -2
- package/dist/transforms.svelte.d.ts.map +1 -0
- package/dist/transforms.svelte.js +155 -0
- package/dist/types.d.ts +256 -110
- package/dist/{util.d.ts → utils.d.ts} +22 -2
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +338 -0
- package/package.json +20 -18
- package/src/lib/AnnotatedTextProperty.svelte +122 -75
- package/src/lib/Command.svelte.js +314 -0
- package/src/lib/CustomProperty.svelte +31 -42
- package/src/lib/KeyMapper.svelte.js +141 -0
- package/src/lib/Node.svelte +33 -35
- package/src/lib/NodeArrayProperty.svelte +40 -37
- package/src/lib/Session.svelte.js +594 -0
- package/src/lib/Svedit.svelte +1352 -1512
- package/src/lib/Transaction.svelte.js +845 -661
- package/src/lib/UnknownNode.svelte +6 -6
- package/src/lib/doc_utils.js +482 -0
- package/src/lib/index.js +20 -4
- package/src/lib/transforms.svelte.js +155 -0
- package/src/lib/types.d.ts +256 -110
- package/src/lib/utils.js +338 -0
- package/dist/Document.svelte.d.ts.map +0 -1
- package/dist/Document.svelte.js +0 -833
- package/dist/commands.svelte.d.ts.map +0 -1
- package/dist/commands.svelte.js +0 -266
- package/dist/util.d.ts.map +0 -1
- package/dist/util.js +0 -269
- package/src/lib/Document.svelte.js +0 -833
- package/src/lib/commands.svelte.js +0 -266
- package/src/lib/util.js +0 -269
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { NodeId, Selection, DocumentPath } from './types.js'
|
|
2
|
+
* @import { NodeId, Selection, Document, DocumentSchema, DocumentPath, Annotation } from './types.d.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
get_char_length,
|
|
7
|
+
char_slice,
|
|
8
|
+
traverse,
|
|
9
|
+
get_selection_range,
|
|
10
|
+
is_selection_collapsed
|
|
11
|
+
} from './utils.js';
|
|
12
|
+
import { join_text_node } from './transforms.svelte.js';
|
|
13
|
+
import {
|
|
14
|
+
get as doc_get,
|
|
15
|
+
property_type as doc_property_type,
|
|
16
|
+
kind as doc_kind,
|
|
17
|
+
inspect as doc_inspect,
|
|
18
|
+
apply_op,
|
|
19
|
+
count_references_excluding_deleted,
|
|
20
|
+
validate_node,
|
|
21
|
+
get_active_annotation
|
|
22
|
+
} from './doc_utils.js';
|
|
7
23
|
|
|
8
24
|
/**
|
|
9
25
|
* Transaction class for managing atomic document operations with undo/redo support.
|
|
@@ -14,667 +30,835 @@ import { join_text_node } from './commands.svelte.js';
|
|
|
14
30
|
*
|
|
15
31
|
* @example
|
|
16
32
|
* ```js
|
|
17
|
-
* const tr =
|
|
33
|
+
* const tr = session.tr;
|
|
18
34
|
* tr.set(['node_1', 'title'], 'New Title');
|
|
19
35
|
* tr.create({id: 'node_2', type: 'paragraph', content: 'Hello'});
|
|
20
|
-
*
|
|
36
|
+
* session.apply(tr); // Applies all operations atomically
|
|
21
37
|
* ```
|
|
22
38
|
*/
|
|
23
39
|
export default class Transaction {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new Transaction with the given state.
|
|
42
|
+
*
|
|
43
|
+
* @param {DocumentSchema} schema - The document schema
|
|
44
|
+
* @param {Document} doc - The document state {document_id, nodes}
|
|
45
|
+
* @param {Selection | null} selection - The current selection
|
|
46
|
+
* @param {object} config - The document config (including generate_id)
|
|
47
|
+
*/
|
|
48
|
+
constructor(schema, doc, selection, config) {
|
|
49
|
+
this.schema = schema;
|
|
50
|
+
/** @type {object} */
|
|
51
|
+
this.doc = doc;
|
|
52
|
+
/** @type {Selection} */
|
|
53
|
+
this.selection = selection;
|
|
54
|
+
this.config = config;
|
|
55
|
+
// Here we track the ops during the transaction
|
|
56
|
+
/** @type {Record<string, any>[]} */
|
|
57
|
+
this.ops = [];
|
|
58
|
+
/** @type {Record<string, any>[]} */
|
|
59
|
+
this.inverse_ops = [];
|
|
60
|
+
// Remember the selection before the transaction started
|
|
61
|
+
this.selection_before = selection;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets a value from the document at the specified path.
|
|
66
|
+
*
|
|
67
|
+
* @param {DocumentPath|string} path - The path to the value in the document, or a string node ID
|
|
68
|
+
* @returns {any} The value at the specified path
|
|
69
|
+
*/
|
|
70
|
+
get(path) {
|
|
71
|
+
return doc_get(this.schema, this.doc, path);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the type of a property from the schema.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} type - The node type
|
|
78
|
+
* @param {string} property - The property name
|
|
79
|
+
* @returns {string} The property type
|
|
80
|
+
*/
|
|
81
|
+
property_type(type, property) {
|
|
82
|
+
return doc_property_type(this.schema, type, property);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determines the kind of a node ('block', 'text', or 'annotation').
|
|
87
|
+
*
|
|
88
|
+
* @param {any} node - The node to check
|
|
89
|
+
* @returns {'block'|'text'|'annotation'} The node kind
|
|
90
|
+
*/
|
|
91
|
+
kind(node) {
|
|
92
|
+
return doc_kind(this.schema, node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Inspects a path to get metadata about the value at that location.
|
|
97
|
+
*
|
|
98
|
+
* @param {DocumentPath} path - The path to inspect
|
|
99
|
+
* @returns {{kind: 'property'|'node', [key: string]: any}} Metadata about the path
|
|
100
|
+
*/
|
|
101
|
+
inspect(path) {
|
|
102
|
+
return doc_inspect(this.schema, this.doc, path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates a new unique ID using the config's generate_id function.
|
|
107
|
+
*
|
|
108
|
+
* @returns {string} A new unique ID
|
|
109
|
+
*/
|
|
110
|
+
generate_id() {
|
|
111
|
+
return this.config.generate_id();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validates a node against the document schema.
|
|
116
|
+
*
|
|
117
|
+
* @param {any} node - The node to validate
|
|
118
|
+
* @throws {Error} Throws if the node is invalid
|
|
119
|
+
*/
|
|
120
|
+
validate_node(node) {
|
|
121
|
+
validate_node(node, this.schema, this.doc.nodes);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets all nodes referenced by a given node (recursively).
|
|
126
|
+
*
|
|
127
|
+
* @param {NodeId} node_id - The node ID to get references for
|
|
128
|
+
* @returns {NodeId[]} Array of referenced node IDs
|
|
129
|
+
*/
|
|
130
|
+
get_referenced_nodes(node_id) {
|
|
131
|
+
const traversed_nodes = traverse(node_id, this.schema, this.doc.nodes);
|
|
132
|
+
return traversed_nodes.slice(0, -1).map((node) => node.id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Gets the available annotation types for the current selection.
|
|
137
|
+
*
|
|
138
|
+
* @returns {string[]} Array of available annotation type names
|
|
139
|
+
*/
|
|
140
|
+
get available_annotation_types() {
|
|
141
|
+
if (this.selection?.type !== 'text') return [];
|
|
142
|
+
const path = this.selection.path;
|
|
143
|
+
const property_definition = this.inspect(path);
|
|
144
|
+
return property_definition.node_types || [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the annotation object that is currently "under the cursor".
|
|
149
|
+
* NOTE: Annotations in Svedit are exclusive, so there can only be one active_annotation
|
|
150
|
+
*
|
|
151
|
+
* @param {string} [annotation_type] Optional annotation type to filter by
|
|
152
|
+
* @returns {Annotation|null}
|
|
153
|
+
*/
|
|
154
|
+
active_annotation(annotation_type) {
|
|
155
|
+
return get_active_annotation(this.schema, this.doc, this.selection, annotation_type);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Applies an operation to the document (internal).
|
|
160
|
+
*
|
|
161
|
+
* @param {Array} op - The operation to apply
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
_apply_op(op) {
|
|
165
|
+
this.doc = apply_op(this.doc, op);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sets a property of a node to a new value.
|
|
170
|
+
*
|
|
171
|
+
* This is the core operation for modifying document properties. It records
|
|
172
|
+
* both the forward operation and its inverse for undo support.
|
|
173
|
+
*
|
|
174
|
+
* @param {DocumentPath} path - Array path to the property (e.g., ["node_1", "title"])
|
|
175
|
+
* @param {any} value - The new value to set
|
|
176
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```js
|
|
180
|
+
* tr.set(["list_1", "list_items"], [1, 2, 3]);
|
|
181
|
+
* tr.set(["page_1", "body", "0", "description"], {text: "Hello world", annotations: []});
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
set(path, value) {
|
|
185
|
+
const node = this.get(path.slice(0, -1));
|
|
186
|
+
|
|
187
|
+
// Turns ["page_1", "body", "0", "description"]
|
|
188
|
+
// into ["paragraph_1", "description"].
|
|
189
|
+
// Important to keep changes of multiple ops invertible.
|
|
190
|
+
const normalized_path = [node.id, path.at(-1)];
|
|
191
|
+
|
|
192
|
+
// Just to be sure, make a deep copy of the old value
|
|
193
|
+
const property_key = path.at(-1);
|
|
194
|
+
if (property_key === undefined) {
|
|
195
|
+
throw new Error('Invalid path: cannot get property key');
|
|
196
|
+
}
|
|
197
|
+
const property_key_str = String(property_key);
|
|
198
|
+
const previous_value = structuredClone($state.snapshot(node[property_key_str]));
|
|
199
|
+
|
|
200
|
+
// Collect node IDs that might need to be deleted after the set operation
|
|
201
|
+
const prop_type = this.property_type(node.type, property_key_str);
|
|
202
|
+
/** @type {NodeId[]} */
|
|
203
|
+
let removed_node_ids = [];
|
|
204
|
+
|
|
205
|
+
if (prop_type === 'node' && typeof previous_value === 'string' && previous_value !== value) {
|
|
206
|
+
removed_node_ids = [previous_value];
|
|
207
|
+
} else if (
|
|
208
|
+
prop_type === 'node_array' &&
|
|
209
|
+
Array.isArray(previous_value) &&
|
|
210
|
+
Array.isArray(value)
|
|
211
|
+
) {
|
|
212
|
+
// Only include node IDs that were in previous_value but are not in the new value
|
|
213
|
+
removed_node_ids = previous_value.filter((id) => !value.includes(id));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const op = ['set', normalized_path, value];
|
|
217
|
+
this.ops.push(op);
|
|
218
|
+
this.inverse_ops.push(['set', normalized_path, previous_value]);
|
|
219
|
+
this._apply_op(op);
|
|
220
|
+
|
|
221
|
+
for (const removed_node_id of removed_node_ids) {
|
|
222
|
+
// NOTE: This implicitly deletes childnodes as well, given that they are no longer referenced.
|
|
223
|
+
this.delete(removed_node_id);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Takes a subgraph and constructs new nodes from it
|
|
230
|
+
// NOTE: all ids will be mapped to new unique ids.
|
|
231
|
+
// NOTE: Omitted properties will be populated with default values.
|
|
232
|
+
build(node_id, nodes) {
|
|
233
|
+
const depth_first_nodes = traverse(node_id, this.schema, nodes);
|
|
234
|
+
// This maps original ids to newly generated ids
|
|
235
|
+
const id_map = {};
|
|
236
|
+
|
|
237
|
+
for (const node of depth_first_nodes) {
|
|
238
|
+
const new_id = this.generate_id();
|
|
239
|
+
id_map[node.id] = new_id;
|
|
240
|
+
const new_node = { ...node, id: new_id };
|
|
241
|
+
const node_schema = this.schema[node.type];
|
|
242
|
+
|
|
243
|
+
// Update all property references to use new IDs
|
|
244
|
+
for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
|
|
245
|
+
const prop_type = property_definition.type;
|
|
246
|
+
const value = node[property_name];
|
|
247
|
+
|
|
248
|
+
// Apply default values
|
|
249
|
+
if (prop_type === 'node_array') {
|
|
250
|
+
// [] is the default value for node arrays
|
|
251
|
+
new_node[property_name] = Array.isArray(value)
|
|
252
|
+
? value.map((ref_id) => id_map[ref_id])
|
|
253
|
+
: [];
|
|
254
|
+
} else if (prop_type === 'node') {
|
|
255
|
+
// null is the default value for node references
|
|
256
|
+
new_node[property_name] = typeof value === 'string' ? id_map[value] : null;
|
|
257
|
+
} else if (prop_type === 'annotated_text') {
|
|
258
|
+
if (value) {
|
|
259
|
+
const annotations = value.annotations.map((annotation) => {
|
|
260
|
+
const { start_offset, end_offset, node_id } = annotation;
|
|
261
|
+
return { start_offset, end_offset, node_id: id_map[node_id] || node_id };
|
|
262
|
+
});
|
|
263
|
+
new_node[property_name] = { text: value.text, annotations };
|
|
264
|
+
} else {
|
|
265
|
+
new_node[property_name] = { text: '', annotations: [] };
|
|
266
|
+
}
|
|
267
|
+
} else if (prop_type === 'string') {
|
|
268
|
+
new_node[property_name] = value ?? property_definition.default ?? '';
|
|
269
|
+
} else if (prop_type === 'integer') {
|
|
270
|
+
new_node[property_name] = value ?? property_definition.default ?? 0;
|
|
271
|
+
} else if (prop_type === 'number') {
|
|
272
|
+
new_node[property_name] = value ?? property_definition.default ?? 0;
|
|
273
|
+
} else if (prop_type === 'boolean') {
|
|
274
|
+
new_node[property_name] = value ?? property_definition.default ?? false;
|
|
275
|
+
} else if (['integer_array', 'number_array'].includes(prop_type)) {
|
|
276
|
+
new_node[property_name] = value ?? property_definition.default ?? [];
|
|
277
|
+
} else if (prop_type === 'string_array') {
|
|
278
|
+
new_node[property_name] = value ?? property_definition.default ?? [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.create(new_node);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return id_map[depth_first_nodes.at(-1).id];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Creates a new node in the document.
|
|
290
|
+
*
|
|
291
|
+
* The node must have a valid id and must not already exist in the document.
|
|
292
|
+
* The node is validated against the document schema before creation.
|
|
293
|
+
*
|
|
294
|
+
* @param {any} node - The node object to create (must include id, type, and other properties)
|
|
295
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
296
|
+
* @throws {Error} If the node ID is invalid or if the node already exists
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```js
|
|
300
|
+
* tr.create({
|
|
301
|
+
* id: 'para_123',
|
|
302
|
+
* type: 'paragraph',
|
|
303
|
+
* content: ['Hello world', []]
|
|
304
|
+
* });
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
create(node) {
|
|
308
|
+
// Validate node against schema
|
|
309
|
+
this.validate_node(node);
|
|
310
|
+
|
|
311
|
+
if (this.get(node.id)) {
|
|
312
|
+
throw new Error('Node with id ' + node.id + ' already exists');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const op = ['create', node];
|
|
316
|
+
this.ops.push(op);
|
|
317
|
+
this.inverse_ops.push(['delete', node.id]);
|
|
318
|
+
this._apply_op(op);
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Deletes a node from the document by its ID.
|
|
324
|
+
*
|
|
325
|
+
* The node's current state is captured for undo support before deletion.
|
|
326
|
+
*
|
|
327
|
+
* @param {any} id - The ID of the node to delete
|
|
328
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```js
|
|
332
|
+
* tr.delete('node_123');
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
delete(id) {
|
|
336
|
+
const previous_value = this.get(id);
|
|
337
|
+
if (!previous_value) {
|
|
338
|
+
console.warn(`Deletion of node ${id} skipped, as it does not exist.`);
|
|
339
|
+
}
|
|
340
|
+
// Get nodes referenced by this node BEFORE deleting it.
|
|
341
|
+
const referenced_nodes = this.get_referenced_nodes(id);
|
|
342
|
+
const op = ['delete', id];
|
|
343
|
+
this.ops.push(op);
|
|
344
|
+
this.inverse_ops.push(['create', previous_value]);
|
|
345
|
+
this._apply_op(op);
|
|
346
|
+
// Check if the nodes that were referenced by the deleted node are now orphaned
|
|
347
|
+
// NOTE: We don't do this yet, because we still have some manual child node removal code
|
|
348
|
+
// that causes errors. But we should soon implement this only here.
|
|
349
|
+
this._cascade_delete_unreferenced_nodes(referenced_nodes);
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Sets the document selection.
|
|
355
|
+
*
|
|
356
|
+
* @param {Selection} selection - The new selection object
|
|
357
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
358
|
+
* @throws {Error} Throws if the selection is invalid or out of bounds
|
|
359
|
+
*/
|
|
360
|
+
set_selection(selection) {
|
|
361
|
+
this._validate_selection(selection);
|
|
362
|
+
this.selection = selection;
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Validates a selection against the current document state.
|
|
368
|
+
*
|
|
369
|
+
* @param {Selection} selection - The selection to validate
|
|
370
|
+
* @throws {Error} Throws if the selection is invalid
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
_validate_selection(selection) {
|
|
374
|
+
if (!selection) return;
|
|
375
|
+
|
|
376
|
+
const selection_type = selection.type;
|
|
377
|
+
if (!['node', 'text', 'property'].includes(selection_type)) {
|
|
378
|
+
throw new Error(`Invalid selection type: ${selection_type}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (selection_type === 'node') {
|
|
382
|
+
const node_array = this.get(selection.path);
|
|
383
|
+
|
|
384
|
+
if (!Array.isArray(node_array)) {
|
|
385
|
+
throw new Error('Node selection path must point to a node_array');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const max_offset = node_array.length;
|
|
389
|
+
if (selection.anchor_offset < 0 || selection.anchor_offset > max_offset) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Node selection anchor_offset (${selection.anchor_offset}) is out of bounds. Max is ${max_offset}.`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (selection.focus_offset < 0 || selection.focus_offset > max_offset) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Node selection focus_offset (${selection.focus_offset}) is out of bounds. Max is ${max_offset}.`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
} else if (selection_type === 'text') {
|
|
400
|
+
const annotated_text = this.get(selection.path);
|
|
401
|
+
|
|
402
|
+
if (!annotated_text || typeof annotated_text.text !== 'string') {
|
|
403
|
+
throw new Error('Text selection path must point to annotated_text');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const char_length = get_char_length(annotated_text.text);
|
|
407
|
+
if (selection.anchor_offset < 0 || selection.anchor_offset > char_length) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Text selection anchor_offset (${selection.anchor_offset}) is out of bounds. Max is ${char_length}.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (selection.focus_offset < 0 || selection.focus_offset > char_length) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`Text selection focus_offset (${selection.focus_offset}) is out of bounds. Max is ${char_length}.`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Adds, updates, or removes text annotations in the current selection.
|
|
422
|
+
*
|
|
423
|
+
* Handles various annotation scenarios including adding new annotations,
|
|
424
|
+
* updating existing ones (especially for links), and removing annotations
|
|
425
|
+
* when conflicting types are applied.
|
|
426
|
+
*
|
|
427
|
+
* @param {any} annotation_type - The type of annotation (e.g., 'link', 'bold', 'italic')
|
|
428
|
+
* @param {any} annotation_properties - Additional data for the annotation (e.g., href for links)
|
|
429
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```js
|
|
433
|
+
* // Add a link annotation
|
|
434
|
+
* tr.annotate_text('link', { href: 'https://example.com' });
|
|
435
|
+
*
|
|
436
|
+
* // Add emphasis
|
|
437
|
+
* tr.annotate_text('emphasis', {});
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
annotate_text(annotation_type, annotation_properties) {
|
|
441
|
+
if (this.selection.type !== 'text') return this;
|
|
442
|
+
|
|
443
|
+
const range = get_selection_range(this.selection);
|
|
444
|
+
const annotated_text = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
445
|
+
const annotations = annotated_text.annotations;
|
|
446
|
+
const existing_annotation = this.active_annotation();
|
|
447
|
+
const existing_annotation_same_type = this.active_annotation(annotation_type);
|
|
448
|
+
|
|
449
|
+
if (existing_annotation) {
|
|
450
|
+
// If there's an existing annotation of the same type, remove it
|
|
451
|
+
if (existing_annotation_same_type) {
|
|
452
|
+
const index = annotations.findIndex(
|
|
453
|
+
/** @param {any} anno */ (anno) =>
|
|
454
|
+
anno.start_offset === existing_annotation.start_offset &&
|
|
455
|
+
anno.end_offset === existing_annotation.end_offset
|
|
456
|
+
);
|
|
457
|
+
if (index !== -1) {
|
|
458
|
+
// Remove the annotation node from the graph
|
|
459
|
+
this.delete(annotations[index].node_id);
|
|
460
|
+
annotations.splice(index, 1);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
// If there's an annotation of a different type, don't add a new one
|
|
464
|
+
return this;
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
if (is_selection_collapsed(this.selection)) {
|
|
468
|
+
console.log('Annotations can only be added to expanded text selections.');
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
if (!this.available_annotation_types.includes(annotation_type)) {
|
|
472
|
+
console.log(`Annotation type ${annotation_type} is not allowed here.`);
|
|
473
|
+
return this;
|
|
474
|
+
}
|
|
475
|
+
const new_annotation_node = {
|
|
476
|
+
id: this.generate_id(),
|
|
477
|
+
type: annotation_type,
|
|
478
|
+
...annotation_properties
|
|
479
|
+
};
|
|
480
|
+
this.create(new_annotation_node);
|
|
481
|
+
// If there's no existing annotation, add the new one
|
|
482
|
+
annotations.push({
|
|
483
|
+
start_offset: range.start_offset,
|
|
484
|
+
end_offset: range.end_offset,
|
|
485
|
+
node_id: new_annotation_node.id
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Update the annotated text
|
|
490
|
+
this.set(this.selection.path, annotated_text);
|
|
491
|
+
return this;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Deletes the currently selected text or nodes.
|
|
496
|
+
*
|
|
497
|
+
* Behavior depends on selection type:
|
|
498
|
+
* - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
|
|
499
|
+
* - For text selections: Removes selected text and adjusts annotations accordingly
|
|
500
|
+
* - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
|
|
501
|
+
* - Property selections are ignored: Those are best handled handled via commands + keyboard shortcuts.
|
|
502
|
+
*
|
|
503
|
+
* @param {'backward' | 'forward'} [direction] - Direction of deletion for collapsed selections
|
|
504
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
505
|
+
*/
|
|
506
|
+
delete_selection(direction = 'backward') {
|
|
507
|
+
if (!this.selection || this.selection.type === 'property') return this;
|
|
508
|
+
const path = this.selection.path;
|
|
509
|
+
|
|
510
|
+
// Get the start and end indices for the selection
|
|
511
|
+
let start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
512
|
+
let end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
513
|
+
let length;
|
|
514
|
+
|
|
515
|
+
if (this.selection?.type === 'text') {
|
|
516
|
+
const text_content = this.get(this.selection.path).text;
|
|
517
|
+
length = get_char_length(text_content);
|
|
518
|
+
} else if (this.selection?.type === 'node') {
|
|
519
|
+
const node_array = this.get(this.selection.path);
|
|
520
|
+
length = node_array.length;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// If selection is collapsed we delete the previous char/node (backward)
|
|
524
|
+
// or the next char/node (forward)
|
|
525
|
+
if (start === end) {
|
|
526
|
+
if (direction === 'backward' && start > 0) {
|
|
527
|
+
start = start - 1;
|
|
528
|
+
} else if (direction === 'forward' && end < length) {
|
|
529
|
+
end = end + 1;
|
|
530
|
+
} else if (direction === 'backward' && start === 0) {
|
|
531
|
+
join_text_node(this);
|
|
532
|
+
return this;
|
|
533
|
+
} else if (direction === 'forward' && end === length) {
|
|
534
|
+
// At end of text - try to join with next text node
|
|
535
|
+
const node_index = parseInt(String(this.selection.path.at(-2)), 10);
|
|
536
|
+
const successor_node = this.get([...this.selection.path.slice(0, -2), node_index + 1]);
|
|
537
|
+
// Check if next node is a text node
|
|
538
|
+
if (successor_node && this.kind(successor_node) === 'text') {
|
|
539
|
+
// Set selection to beginning of next text node
|
|
540
|
+
this.set_selection({
|
|
541
|
+
type: 'text',
|
|
542
|
+
path: [...this.selection.path.slice(0, -2), node_index + 1, 'content'],
|
|
543
|
+
anchor_offset: 0,
|
|
544
|
+
focus_offset: 0
|
|
545
|
+
});
|
|
546
|
+
// Use join_text_node to merge with previous node
|
|
547
|
+
join_text_node(this);
|
|
548
|
+
}
|
|
549
|
+
return this;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (this.selection.type === 'node') {
|
|
554
|
+
const node_array = [...this.get(path)];
|
|
555
|
+
|
|
556
|
+
// Remove the selected nodes from the node_array
|
|
557
|
+
node_array.splice(start, end - start);
|
|
558
|
+
|
|
559
|
+
// Update the node_array in the entry (this implicitly records an op via this.set)
|
|
560
|
+
// Note: this.set() will automatically cascade delete unreferenced nodes
|
|
561
|
+
this.set(path, node_array);
|
|
562
|
+
|
|
563
|
+
// Update the selection to point to the start of the deleted range
|
|
564
|
+
this.selection = {
|
|
565
|
+
type: 'node',
|
|
566
|
+
path,
|
|
567
|
+
anchor_offset: start,
|
|
568
|
+
focus_offset: start
|
|
569
|
+
};
|
|
570
|
+
} else if (this.selection.type === 'text') {
|
|
571
|
+
const path = this.selection.path;
|
|
572
|
+
let text = structuredClone($state.snapshot(this.get(path)));
|
|
573
|
+
|
|
574
|
+
// Update the text content using character-based operations
|
|
575
|
+
const original_text = text.text;
|
|
576
|
+
text.text =
|
|
577
|
+
char_slice(original_text, 0, start) +
|
|
578
|
+
char_slice(original_text, end, get_char_length(original_text));
|
|
579
|
+
|
|
580
|
+
// To mark annotation nodes for deletion.
|
|
581
|
+
/** @type {string[]} */
|
|
582
|
+
const _deleted_nodes = [];
|
|
583
|
+
const deletion_length = end - start;
|
|
584
|
+
const new_annotations = text.annotations
|
|
585
|
+
.map((/** @type {any} */ annotation) => {
|
|
586
|
+
const annotation_start = annotation.start_offset;
|
|
587
|
+
const annotation_end = annotation.end_offset;
|
|
588
|
+
const node_id = annotation.node_id;
|
|
589
|
+
|
|
590
|
+
// Case 1: Annotation is entirely before the deleted range - keep unchanged
|
|
591
|
+
if (annotation_end <= start) {
|
|
592
|
+
return annotation;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Case 2: Annotation is entirely after the deleted range - shift it
|
|
596
|
+
let new_start = annotation_start;
|
|
597
|
+
if (annotation_start >= end) {
|
|
598
|
+
new_start = annotation_start - deletion_length;
|
|
599
|
+
} else if (annotation_start > start) {
|
|
600
|
+
// Annotation starts inside deleted range
|
|
601
|
+
new_start = start;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Case 3: Annotation overlaps with deleted range - adjust end
|
|
605
|
+
let new_end = annotation_end;
|
|
606
|
+
if (annotation_end >= end) {
|
|
607
|
+
new_end = annotation_end - deletion_length;
|
|
608
|
+
} else if (annotation_end > start) {
|
|
609
|
+
// Annotation ends inside deleted range
|
|
610
|
+
new_end = start;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// If annotation is now empty, mark for deletion
|
|
614
|
+
if (new_start >= new_end) {
|
|
615
|
+
_deleted_nodes.push(node_id);
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { start_offset: new_start, end_offset: new_end, node_id };
|
|
620
|
+
})
|
|
621
|
+
.filter(Boolean);
|
|
622
|
+
|
|
623
|
+
text.annotations = new_annotations;
|
|
624
|
+
|
|
625
|
+
// Delete marked annotation nodes
|
|
626
|
+
for (const node_id of _deleted_nodes) {
|
|
627
|
+
this.delete(node_id);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this.set(path, text);
|
|
631
|
+
|
|
632
|
+
// Update the selection to the new caret position
|
|
633
|
+
this.selection = {
|
|
634
|
+
type: 'text',
|
|
635
|
+
path,
|
|
636
|
+
anchor_offset: start,
|
|
637
|
+
focus_offset: start
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Inserts nodes at the current node selection position.
|
|
646
|
+
*
|
|
647
|
+
* If the selection is expanded (not collapsed), first deletes the selected nodes
|
|
648
|
+
* before inserting the new ones.
|
|
649
|
+
*
|
|
650
|
+
* @param {NodeId[]} node_ids - Array of node IDs to insert
|
|
651
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
652
|
+
*/
|
|
653
|
+
insert_nodes(node_ids) {
|
|
654
|
+
if (this.selection.type !== 'node') return this;
|
|
655
|
+
|
|
656
|
+
// Unless cursor is collapsed, delete the selected nodes as a first step
|
|
657
|
+
if (this.selection.anchor_offset !== this.selection.focus_offset) {
|
|
658
|
+
this.delete_selection();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const path = this.selection.path;
|
|
662
|
+
const node_array = [...this.get(path)];
|
|
663
|
+
|
|
664
|
+
// Get the start and end indices for the selection
|
|
665
|
+
let start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
666
|
+
let end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
667
|
+
|
|
668
|
+
if (start !== end) {
|
|
669
|
+
// Remove the selected nodes from the node_array
|
|
670
|
+
node_array.splice(start, end - start);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Insert the new nodes
|
|
674
|
+
node_array.splice(start, 0, ...node_ids);
|
|
675
|
+
this.set(path, node_array);
|
|
676
|
+
|
|
677
|
+
this.selection = {
|
|
678
|
+
type: 'node',
|
|
679
|
+
path: [...this.selection.path],
|
|
680
|
+
anchor_offset: start,
|
|
681
|
+
focus_offset: start + node_ids.length
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
return this;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Inserts text at the current text selection position.
|
|
689
|
+
*
|
|
690
|
+
* Handles annotation adjustments when text is inserted, including:
|
|
691
|
+
* - Expanding annotations that contain the insertion point
|
|
692
|
+
* - Shifting annotations that come after the insertion point
|
|
693
|
+
* - Optionally applying new annotations to the inserted text
|
|
694
|
+
*
|
|
695
|
+
* @param {string} replaced_text - The text to insert
|
|
696
|
+
* @param {Array} [annotations] - Optional annotations to apply to the inserted text
|
|
697
|
+
* @param {object} [nodes] - Optional node definitions for annotation nodes
|
|
698
|
+
* @returns {Transaction} This transaction instance for method chaining
|
|
699
|
+
*/
|
|
700
|
+
insert_text(replaced_text, annotations = [], nodes = {}) {
|
|
701
|
+
if (this.selection?.type !== 'text') return this;
|
|
702
|
+
|
|
703
|
+
// Unless selection is collapsed, delete the selected content
|
|
704
|
+
// NOTE: This makes sure wrapped annotations are disposed correctly
|
|
705
|
+
if (!is_selection_collapsed(this.selection)) {
|
|
706
|
+
this.delete_selection();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const annotated_text = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
710
|
+
const range = get_selection_range(this.selection);
|
|
711
|
+
|
|
712
|
+
// Transform the plain text string using character-based operations
|
|
713
|
+
const text = annotated_text.text;
|
|
714
|
+
annotated_text.text =
|
|
715
|
+
char_slice(text, 0, range.start_offset) + replaced_text + char_slice(text, range.end_offset);
|
|
716
|
+
|
|
717
|
+
// Calculate the change in length
|
|
718
|
+
const delta = get_char_length(replaced_text);
|
|
719
|
+
|
|
720
|
+
const new_annotations = annotated_text.annotations.map((/** @type {any} */ annotation) => {
|
|
721
|
+
const annotation_start = annotation.start_offset;
|
|
722
|
+
const annotation_end = annotation.end_offset;
|
|
723
|
+
const node_id = annotation.node_id;
|
|
724
|
+
|
|
725
|
+
// Annotation is entirely before the insertion point
|
|
726
|
+
if (annotation_end <= range.start_offset) {
|
|
727
|
+
return annotation;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Annotation starts at the insertion point - extend it
|
|
731
|
+
if (annotation_start <= range.start_offset && annotation_end >= range.start_offset) {
|
|
732
|
+
return {
|
|
733
|
+
start_offset: annotation_start,
|
|
734
|
+
end_offset: annotation_end + delta,
|
|
735
|
+
node_id
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Annotation is entirely after the insertion point - shift it
|
|
740
|
+
if (annotation_start >= range.start_offset) {
|
|
741
|
+
return {
|
|
742
|
+
start_offset: annotation_start + delta,
|
|
743
|
+
end_offset: annotation_end + delta,
|
|
744
|
+
node_id
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Insertion point is inside the annotation - extend the annotation
|
|
749
|
+
if (annotation_start < range.start_offset && annotation_end > range.start_offset) {
|
|
750
|
+
return {
|
|
751
|
+
start_offset: annotation_start,
|
|
752
|
+
end_offset: annotation_end + delta,
|
|
753
|
+
node_id
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return annotation;
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
annotated_text.annotations = new_annotations;
|
|
761
|
+
this.set(this.selection.path, annotated_text); // this will update the current state and create a history entry
|
|
762
|
+
|
|
763
|
+
// Setting the selection automatically triggers a re-render of the corresponding DOMSelection.
|
|
764
|
+
/** @type {Selection} */
|
|
765
|
+
const new_selection = {
|
|
766
|
+
type: /** @type {const} */ ('text'),
|
|
767
|
+
path: this.selection.path,
|
|
768
|
+
anchor_offset: range.start_offset + get_char_length(replaced_text),
|
|
769
|
+
focus_offset: range.start_offset + get_char_length(replaced_text)
|
|
770
|
+
};
|
|
771
|
+
this.selection = new_selection;
|
|
772
|
+
|
|
773
|
+
// Now we apply annotations if there are any, but only if there's no active annotation
|
|
774
|
+
// at the current collapsed cursor
|
|
775
|
+
if (!this.active_annotation() && annotations.length > 0) {
|
|
776
|
+
const new_annotations = annotations
|
|
777
|
+
.map((annotation) => {
|
|
778
|
+
const original_annotation_node = nodes[annotation.node_id];
|
|
779
|
+
const text_property_definition = this.inspect(this.selection.path);
|
|
780
|
+
// console.log('original_annotation_node.type', original_annotation_node.type);
|
|
781
|
+
// console.log('text_property_definition', text_property_definition);
|
|
782
|
+
if (text_property_definition.node_types.includes(original_annotation_node.type)) {
|
|
783
|
+
const new_annotation_node_id = this.build(annotation.node_id, nodes);
|
|
784
|
+
return {
|
|
785
|
+
start_offset: range.start_offset + annotation.start_offset,
|
|
786
|
+
end_offset: range.start_offset + annotation.end_offset,
|
|
787
|
+
node_id: new_annotation_node_id
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
})
|
|
792
|
+
.filter(Boolean);
|
|
793
|
+
const next_annotated_text = structuredClone(annotated_text);
|
|
794
|
+
next_annotated_text.annotations = next_annotated_text.annotations.concat(new_annotations);
|
|
795
|
+
this.set(this.selection.path, next_annotated_text); // this will update the current state and create a history entry
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return this;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Recursively deletes nodes that are no longer referenced in the document.
|
|
803
|
+
*
|
|
804
|
+
* This handles the cascade deletion of child nodes when their parent
|
|
805
|
+
* references are removed. Uses reference counting to determine which
|
|
806
|
+
* nodes are safe to delete.
|
|
807
|
+
*
|
|
808
|
+
* @param {NodeId[]} potentially_orphaned_nodes - Array of node IDs to check
|
|
809
|
+
* @private
|
|
810
|
+
*/
|
|
811
|
+
_cascade_delete_unreferenced_nodes(potentially_orphaned_nodes) {
|
|
812
|
+
/** @type {Record<NodeId, boolean>} */
|
|
813
|
+
const nodes_to_delete = {};
|
|
814
|
+
const to_check = [...potentially_orphaned_nodes];
|
|
815
|
+
|
|
816
|
+
while (to_check.length > 0) {
|
|
817
|
+
const node_id = to_check.pop();
|
|
818
|
+
if (!node_id || nodes_to_delete[node_id]) continue;
|
|
819
|
+
|
|
820
|
+
// Count references to this node, excluding nodes already marked for deletion
|
|
821
|
+
const ref_count = this._count_references_excluding_deleted(node_id, nodes_to_delete);
|
|
822
|
+
|
|
823
|
+
if (ref_count === 0) {
|
|
824
|
+
// No more references, safe to delete this node
|
|
825
|
+
nodes_to_delete[node_id] = true;
|
|
826
|
+
|
|
827
|
+
// Also check all nodes referenced by this node
|
|
828
|
+
const referenced_nodes = this.get_referenced_nodes(node_id);
|
|
829
|
+
to_check.push(...referenced_nodes);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Now perform the actual deletions
|
|
834
|
+
for (const node_id of Object.keys(nodes_to_delete)) {
|
|
835
|
+
const previous_value = this.get([node_id]);
|
|
836
|
+
if (previous_value) {
|
|
837
|
+
const op = ['delete', node_id];
|
|
838
|
+
this.ops.push(op);
|
|
839
|
+
this.inverse_ops.push(['create', previous_value]);
|
|
840
|
+
this._apply_op(op);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Counts references to a node, excluding nodes that have been marked for deletion.
|
|
847
|
+
*
|
|
848
|
+
* This is used during cascade deletion to accurately count remaining references
|
|
849
|
+
* as nodes are being deleted.
|
|
850
|
+
*
|
|
851
|
+
* @param {NodeId} target_node_id - The node ID to count references for
|
|
852
|
+
* @param {Record<NodeId, boolean>} nodes_to_delete - Nodes already marked for deletion
|
|
853
|
+
* @returns {number} The number of references to the target node
|
|
854
|
+
* @private
|
|
855
|
+
*/
|
|
856
|
+
_count_references_excluding_deleted(target_node_id, nodes_to_delete) {
|
|
857
|
+
return count_references_excluding_deleted(
|
|
858
|
+
this.schema,
|
|
859
|
+
this.doc,
|
|
860
|
+
target_node_id,
|
|
861
|
+
nodes_to_delete
|
|
862
|
+
);
|
|
863
|
+
}
|
|
680
864
|
}
|