svedit 0.11.1 → 0.13.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 +260 -82
- package/dist/Command.svelte.d.ts +54 -29
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +286 -283
- package/dist/CustomProperty.svelte +3 -5
- package/dist/CustomProperty.svelte.d.ts +3 -6
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +19 -6
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +95 -112
- package/dist/Node.svelte +40 -9
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +142 -18
- package/dist/NodeArrayProperty.svelte.d.ts +3 -5
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +1 -1
- package/dist/NodeCaret.svelte.d.ts +6 -14
- package/dist/NodeCaret.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +60 -12
- package/dist/NodeGap.svelte.d.ts +7 -14
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +19 -10
- package/dist/NodeGapMarkers.svelte.d.ts +6 -11
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +20 -6
- package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +85 -116
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +543 -571
- package/dist/Svedit.svelte +243 -216
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
- package/dist/TextProperty.svelte.d.ts +5 -0
- package/dist/TextProperty.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +101 -126
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +843 -827
- package/dist/UnknownNode.svelte +4 -3
- package/dist/UnknownNode.svelte.d.ts +5 -9
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +109 -122
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +726 -698
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -18
- package/dist/node_visibility.svelte.d.ts +132 -5
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +535 -642
- package/dist/paste_utils.d.ts +11 -43
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -149
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +126 -152
- package/dist/types.d.ts +329 -291
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +79 -95
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +392 -305
- package/package.json +69 -67
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +111 -41
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +40 -9
- package/src/lib/NodeArrayProperty.svelte +142 -18
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGap.svelte +60 -12
- package/src/lib/NodeGapMarkers.svelte +19 -10
- package/src/lib/NodeSelectionMarkers.svelte +20 -6
- package/src/lib/Session.svelte.ts +656 -0
- package/src/lib/Svedit.svelte +243 -216
- package/src/lib/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
- package/src/lib/Transaction.svelte.ts +1020 -0
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/doc_utils.ts +1120 -0
- package/src/lib/{index.js → index.ts} +48 -1
- package/src/lib/node_visibility.svelte.ts +658 -0
- package/src/lib/{paste_utils.js → paste_utils.ts} +51 -59
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +44 -27
- package/src/lib/{types.d.ts → types.ts} +250 -103
- package/src/lib/utils.ts +660 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +0 -1
- package/src/lib/Session.svelte.js +0 -575
- package/src/lib/Transaction.svelte.js +0 -846
- package/src/lib/doc_utils.js +0 -807
- package/src/lib/node_visibility.svelte.js +0 -718
- package/src/lib/utils.js +0 -465
|
@@ -1,29 +1,16 @@
|
|
|
1
|
+
import { get_char_length, char_slice, traverse, traverse_ids, get_selection_range, is_selection_collapsed, adjust_ranges_for_deletion, adjust_ranges_for_insertion, are_ranges_exclusive } from './utils.js';
|
|
2
|
+
import { join_text_node } from './transforms.svelte.js';
|
|
3
|
+
import { get as doc_get, property_type as doc_property_type, kind as doc_kind, inspect as doc_inspect, create_document_draft, apply_op_to_draft, build_reference_counts, visit_node_references, validate_node, is_id_valid, fill_node_defaults, can_switch_mark_type, get_selected_marks, get_selected_annotations, get_selected_range_types, validate_selection } from './doc_utils.js';
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
5
|
+
* Check whether a selection-relative range fits within a container length.
|
|
3
6
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
fill_node_defaults,
|
|
22
|
-
get_active_annotation,
|
|
23
|
-
validate_selection,
|
|
24
|
-
can_switch_annotation_type
|
|
25
|
-
} from './doc_utils.js';
|
|
26
|
-
|
|
7
|
+
function is_range_within_bounds(range, length) {
|
|
8
|
+
return (Number.isInteger(range.start_offset) &&
|
|
9
|
+
Number.isInteger(range.end_offset) &&
|
|
10
|
+
range.start_offset >= 0 &&
|
|
11
|
+
range.start_offset < range.end_offset &&
|
|
12
|
+
range.end_offset <= length);
|
|
13
|
+
}
|
|
27
14
|
/**
|
|
28
15
|
* Transaction class for managing atomic document operations with undo/redo support.
|
|
29
16
|
*
|
|
@@ -40,807 +27,836 @@ import {
|
|
|
40
27
|
* ```
|
|
41
28
|
*/
|
|
42
29
|
export default class Transaction {
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
30
|
+
schema;
|
|
31
|
+
doc;
|
|
32
|
+
// NOTE: Typed non-null for historical reasons — most methods assume a
|
|
33
|
+
// selection is present. A null selection is cast here and would surface at
|
|
34
|
+
// runtime exactly as before the TypeScript conversion.
|
|
35
|
+
selection;
|
|
36
|
+
config;
|
|
37
|
+
ops;
|
|
38
|
+
inverse_ops;
|
|
39
|
+
selection_before;
|
|
40
|
+
created_node_ids;
|
|
41
|
+
modified_node_ids;
|
|
42
|
+
deleted_node_ids;
|
|
43
|
+
changed_node_types;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a new Transaction with the given state.
|
|
46
|
+
*
|
|
47
|
+
* @param schema - The document schema
|
|
48
|
+
* @param doc - The document state {document_id, nodes}
|
|
49
|
+
* @param selection - The current selection
|
|
50
|
+
* @param config - The document config (including generate_id)
|
|
51
|
+
*/
|
|
52
|
+
constructor(schema, doc, selection, config) {
|
|
53
|
+
this.schema = schema;
|
|
54
|
+
// The transaction works on a draft: one shallow copy of the nodes map
|
|
55
|
+
// up front, then ops mutate the draft in place (copying node objects
|
|
56
|
+
// on write). This keeps the original document untouched and node
|
|
57
|
+
// identity stable for unchanged nodes, without the previous
|
|
58
|
+
// one-full-map-copy-per-op cost.
|
|
59
|
+
this.doc = create_document_draft(doc);
|
|
60
|
+
this.selection = selection;
|
|
61
|
+
this.config = config;
|
|
62
|
+
// Here we track the ops during the transaction
|
|
63
|
+
this.ops = [];
|
|
64
|
+
this.inverse_ops = [];
|
|
65
|
+
// Remember the selection before the transaction started
|
|
66
|
+
this.selection_before = selection;
|
|
67
|
+
this.created_node_ids = [];
|
|
68
|
+
this.modified_node_ids = [];
|
|
69
|
+
this.deleted_node_ids = [];
|
|
70
|
+
// True when any op set a node's 'type' property — referrers must be
|
|
71
|
+
// re-validated against type constraints in that case.
|
|
72
|
+
this.changed_node_types = false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Gets a value from the document at the specified path.
|
|
76
|
+
*
|
|
77
|
+
* @param path - The path to the value in the document, or a string node ID
|
|
78
|
+
* @returns The value at the specified path
|
|
79
|
+
*/
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
get(path) {
|
|
82
|
+
return doc_get(this.schema, this.doc, path);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets the type of a property from the schema.
|
|
86
|
+
*/
|
|
87
|
+
property_type(type, property) {
|
|
88
|
+
return doc_property_type(this.schema, type, property);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Determines the kind of a node ('document', 'block', 'text', 'mark', or 'annotation').
|
|
92
|
+
*/
|
|
93
|
+
kind(node) {
|
|
94
|
+
return doc_kind(this.schema, node);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Inspects a path to get metadata about the value at that location.
|
|
98
|
+
*/
|
|
99
|
+
inspect(path) {
|
|
100
|
+
return doc_inspect(this.schema, this.doc, path);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generates a new unique ID using the config's generate_id function.
|
|
104
|
+
*/
|
|
105
|
+
generate_id() {
|
|
106
|
+
const id = this.config?.generate_id ? this.config.generate_id() : `node_${crypto.randomUUID()}`;
|
|
107
|
+
if (!is_id_valid(id)) {
|
|
108
|
+
throw new Error(`Generated node id ${JSON.stringify(id)} is invalid. Node ids must be non-empty strings that start with a letter or underscore, contain only letters, numbers, underscores, or dashes, and must not contain "__".`);
|
|
109
|
+
}
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Validates a node against the document schema.
|
|
114
|
+
*
|
|
115
|
+
* @throws {Error} Throws if the node is invalid
|
|
116
|
+
*/
|
|
117
|
+
validate_node(node) {
|
|
118
|
+
validate_node(node, this.schema, this.doc.nodes, { require_references: false });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Gets all nodes referenced by a given node (recursively).
|
|
122
|
+
*/
|
|
123
|
+
get_referenced_nodes(node_id) {
|
|
124
|
+
return traverse_ids(node_id, this.schema, this.doc.nodes).slice(0, -1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Gets the available mark types for the current selection.
|
|
128
|
+
*/
|
|
129
|
+
get available_mark_types() {
|
|
130
|
+
if (this.selection?.type !== 'text' && this.selection?.type !== 'node')
|
|
131
|
+
return [];
|
|
132
|
+
const property_definition = this.inspect(this.selection.path);
|
|
133
|
+
return property_definition.mark_types || [];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Gets the available annotation types for the current selection.
|
|
137
|
+
*/
|
|
138
|
+
get available_annotation_types() {
|
|
139
|
+
if (this.selection?.type !== 'text' && this.selection?.type !== 'node')
|
|
140
|
+
return [];
|
|
141
|
+
const property_definition = this.inspect(this.selection.path);
|
|
142
|
+
return property_definition.annotation_types || [];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Returns marks touched by the current selection.
|
|
146
|
+
*/
|
|
147
|
+
get selected_marks() {
|
|
148
|
+
return get_selected_marks(this.schema, this.doc, this.selection);
|
|
149
|
+
}
|
|
150
|
+
get active_mark() {
|
|
151
|
+
return this.selected_marks.length === 1 ? this.selected_marks[0] : null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Returns annotations touched by the current selection.
|
|
155
|
+
*/
|
|
156
|
+
get selected_annotations() {
|
|
157
|
+
return get_selected_annotations(this.schema, this.doc, this.selection);
|
|
158
|
+
}
|
|
159
|
+
get active_annotation() {
|
|
160
|
+
return this.selected_annotations.length === 1 ? this.selected_annotations[0] : null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Applies an operation to the document (internal).
|
|
164
|
+
*/
|
|
165
|
+
_apply_op(op) {
|
|
166
|
+
apply_op_to_draft(this.doc, op);
|
|
167
|
+
}
|
|
168
|
+
_track_node_id(node_ids, node_id) {
|
|
169
|
+
if (!node_ids.includes(node_id))
|
|
170
|
+
node_ids.push(node_id);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Sets a property of a node to a new value.
|
|
174
|
+
*
|
|
175
|
+
* This is the core operation for modifying document properties. It records
|
|
176
|
+
* both the forward operation and its inverse for undo support.
|
|
177
|
+
*
|
|
178
|
+
* @param path - Array path to the property (e.g., ["node_1", "title"])
|
|
179
|
+
* @param value - The new value to set
|
|
180
|
+
* @returns This transaction instance for method chaining
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```js
|
|
184
|
+
* tr.set(["list_1", "list_items"], [1, 2, 3]);
|
|
185
|
+
* tr.set(["page_1", "body", "0", "description"], {content: "Hello world", marks: [], annotations: []});
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
+
set(path, value) {
|
|
190
|
+
const path_info = this.inspect(path);
|
|
191
|
+
if (path_info?.kind !== 'property') {
|
|
192
|
+
throw new Error(`Transaction.set requires a path that points to a property, got ${JSON.stringify(path)}`);
|
|
193
|
+
}
|
|
194
|
+
const node = this.get(path.slice(0, -1));
|
|
195
|
+
// Turns ["page_1", "body", "0", "description"]
|
|
196
|
+
// into ["paragraph_1", "description"].
|
|
197
|
+
// Important to keep changes of multiple ops invertible.
|
|
198
|
+
const normalized_path = [node.id, path.at(-1)];
|
|
199
|
+
// Just to be sure, make a deep copy of the old value
|
|
200
|
+
const property_key = path.at(-1);
|
|
201
|
+
const property_key_str = String(property_key);
|
|
202
|
+
const previous_value = structuredClone($state.snapshot(node[property_key_str]));
|
|
203
|
+
// Collect node IDs that might need to be deleted after the set operation
|
|
204
|
+
const prop_type = this.property_type(node.type, property_key_str);
|
|
205
|
+
let removed_node_ids = [];
|
|
206
|
+
if (prop_type === 'node' && typeof previous_value === 'string' && previous_value !== value) {
|
|
207
|
+
removed_node_ids = [previous_value];
|
|
208
|
+
}
|
|
209
|
+
else if (prop_type === 'node_array') {
|
|
210
|
+
const previous_node_ids = previous_value.nodes;
|
|
211
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- Non-reactive local lookup set.
|
|
212
|
+
const next_node_ids = new Set(value.nodes);
|
|
213
|
+
// Only include node IDs that were in previous_value but are not in the new value
|
|
214
|
+
removed_node_ids = previous_node_ids.filter((id) => !next_node_ids.has(id));
|
|
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
|
+
this._track_node_id(this.modified_node_ids, node.id);
|
|
221
|
+
if (property_key_str === 'type')
|
|
222
|
+
this.changed_node_types = true;
|
|
223
|
+
// Garbage-collect nodes whose only reference was via this property.
|
|
224
|
+
// We must use a ref-count-aware sweep instead of unconditional deletion
|
|
225
|
+
// because a node ID removed from this array may still be referenced
|
|
226
|
+
// elsewhere — either from a pre-existing reference (e.g. shared nodes)
|
|
227
|
+
// or from a node created earlier in the same transaction (the
|
|
228
|
+
// wrap-in-group pattern: create a wrapper that points at the children,
|
|
229
|
+
// then set the parent array to drop them).
|
|
230
|
+
if (removed_node_ids.length > 0) {
|
|
231
|
+
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
232
|
+
}
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
// Takes a subgraph and constructs new nodes from it
|
|
236
|
+
// NOTE: all ids will be mapped to new unique ids.
|
|
237
|
+
// NOTE: Omitted properties will be populated with default values.
|
|
238
|
+
build(node_id, nodes) {
|
|
239
|
+
const depth_first_nodes = traverse(node_id, this.schema, nodes);
|
|
240
|
+
// This maps original ids to newly generated ids
|
|
241
|
+
const id_map = {};
|
|
242
|
+
for (const node of depth_first_nodes) {
|
|
243
|
+
const new_id = this.generate_id();
|
|
244
|
+
id_map[node.id] = new_id;
|
|
245
|
+
let new_node = { ...node, id: new_id };
|
|
246
|
+
const node_schema = this.schema[node.type];
|
|
247
|
+
// Update all property references to use new IDs
|
|
248
|
+
for (const [property_name, property_definition] of Object.entries(node_schema.properties)) {
|
|
249
|
+
const prop_type = property_definition.type;
|
|
250
|
+
const value = new_node[property_name];
|
|
251
|
+
const remap_ranges = (ranges) => (ranges ?? []).map(({ start_offset, end_offset, node_id }) => {
|
|
252
|
+
return { start_offset, end_offset, node_id: id_map[node_id] || node_id };
|
|
253
|
+
});
|
|
254
|
+
if (prop_type === 'node_array' && value && typeof value === 'object') {
|
|
255
|
+
new_node[property_name] = {
|
|
256
|
+
nodes: value.nodes.map((ref_id) => id_map[ref_id]),
|
|
257
|
+
marks: remap_ranges(value.marks),
|
|
258
|
+
annotations: remap_ranges(value.annotations)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
else if (prop_type === 'node' && typeof value === 'string') {
|
|
262
|
+
new_node[property_name] = id_map[value];
|
|
263
|
+
}
|
|
264
|
+
else if (prop_type === 'text' && value) {
|
|
265
|
+
new_node[property_name] = {
|
|
266
|
+
content: value.content,
|
|
267
|
+
marks: remap_ranges(value.marks),
|
|
268
|
+
annotations: remap_ranges(value.annotations)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
new_node = fill_node_defaults(new_node, this.schema);
|
|
273
|
+
this.create(new_node);
|
|
274
|
+
}
|
|
275
|
+
return id_map[depth_first_nodes.at(-1).id];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Creates a new node in the document.
|
|
279
|
+
*
|
|
280
|
+
* The node must have a valid id and must not already exist in the document.
|
|
281
|
+
* Omitted properties with schema defaults are filled before validation.
|
|
282
|
+
*
|
|
283
|
+
* @param node - The node object to create (must include id, type, and required properties)
|
|
284
|
+
* @returns This transaction instance for method chaining
|
|
285
|
+
* @throws {Error} If the node ID is invalid or if the node already exists
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```js
|
|
289
|
+
* tr.create({
|
|
290
|
+
* id: 'para_123',
|
|
291
|
+
* type: 'paragraph',
|
|
292
|
+
* content: ['Hello world', []]
|
|
293
|
+
* });
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
create(node) {
|
|
297
|
+
const node_with_defaults = fill_node_defaults(node, this.schema);
|
|
298
|
+
// Validate node against schema
|
|
299
|
+
this.validate_node(node_with_defaults);
|
|
300
|
+
if (this.get(node_with_defaults.id)) {
|
|
301
|
+
throw new Error('Node with id ' + node_with_defaults.id + ' already exists');
|
|
302
|
+
}
|
|
303
|
+
const op = ['create', node_with_defaults];
|
|
304
|
+
this.ops.push(op);
|
|
305
|
+
this.inverse_ops.push(['delete', node_with_defaults.id]);
|
|
306
|
+
this._apply_op(op);
|
|
307
|
+
this._track_node_id(this.created_node_ids, node_with_defaults.id);
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Deletes a node from the document by its ID.
|
|
312
|
+
*
|
|
313
|
+
* The node's current state is captured for undo support before deletion.
|
|
314
|
+
*
|
|
315
|
+
* NOTE: This is a force-delete and intentionally does NOT check whether
|
|
316
|
+
* the node is still referenced from elsewhere — callers (e.g.
|
|
317
|
+
* `toggle_mark`) sometimes need to remove a node while a stale
|
|
318
|
+
* reference to it still exists in the property they are about to update.
|
|
319
|
+
* For ref-count-aware cleanup, see how `set` calls
|
|
320
|
+
* `_cascade_delete_unreferenced_nodes`.
|
|
321
|
+
*
|
|
322
|
+
* @param id - The ID of the node to delete
|
|
323
|
+
* @returns This transaction instance for method chaining
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```js
|
|
327
|
+
* tr.delete('node_123');
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
delete(id) {
|
|
331
|
+
const previous_value = this.get(id);
|
|
332
|
+
if (!previous_value) {
|
|
333
|
+
console.warn(`Deletion of node ${id} skipped, as it does not exist.`);
|
|
334
|
+
return this;
|
|
335
|
+
}
|
|
336
|
+
// Get nodes referenced by this node BEFORE deleting it.
|
|
337
|
+
const referenced_nodes = this.get_referenced_nodes(id);
|
|
338
|
+
const op = ['delete', id];
|
|
339
|
+
this.ops.push(op);
|
|
340
|
+
this.inverse_ops.push(['create', previous_value]);
|
|
341
|
+
this._apply_op(op);
|
|
342
|
+
this._track_node_id(this.deleted_node_ids, id);
|
|
343
|
+
// Cascade delete any nodes that are now orphaned after removing this node
|
|
344
|
+
this._cascade_delete_unreferenced_nodes(referenced_nodes);
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Sets the document selection.
|
|
349
|
+
*
|
|
350
|
+
* @param selection - The new selection object
|
|
351
|
+
* @returns This transaction instance for method chaining
|
|
352
|
+
* @throws {Error} Throws if the selection is invalid or out of bounds
|
|
353
|
+
*/
|
|
354
|
+
set_selection(selection) {
|
|
355
|
+
this._validate_selection(selection);
|
|
356
|
+
this.selection = selection;
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Validates a selection against the current document state.
|
|
361
|
+
*
|
|
362
|
+
* @throws {Error} Throws if the selection is invalid
|
|
363
|
+
*/
|
|
364
|
+
_validate_selection(selection) {
|
|
365
|
+
validate_selection(selection, this);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Toggles a mark on the current text or node selection.
|
|
369
|
+
*
|
|
370
|
+
* Marks are mutually exclusive. Whole marks are toggled, removed, or
|
|
371
|
+
* switched; ranges are never truncated or expanded by this command.
|
|
372
|
+
*
|
|
373
|
+
* @param mark_type - The type of mark (e.g., 'link', 'strong', 'emphasis')
|
|
374
|
+
* @param mark_properties - Additional data for the mark (e.g., href for links)
|
|
375
|
+
* @returns This transaction instance for method chaining
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* ```js
|
|
379
|
+
* // Add a link mark
|
|
380
|
+
* tr.toggle_mark('link', { href: 'https://example.com' });
|
|
381
|
+
*
|
|
382
|
+
* // Add emphasis
|
|
383
|
+
* tr.toggle_mark('emphasis', {});
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
toggle_mark(mark_type, mark_properties) {
|
|
387
|
+
if (this.selection.type !== 'text' && this.selection.type !== 'node')
|
|
388
|
+
return this;
|
|
389
|
+
if (this.selection.type === 'node' && is_selection_collapsed(this.selection))
|
|
390
|
+
return this;
|
|
391
|
+
if (!this.available_mark_types.includes(mark_type)) {
|
|
392
|
+
console.warn(`Mark type ${mark_type} is not allowed here.`);
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
const range = get_selection_range(this.selection);
|
|
396
|
+
if (!range)
|
|
397
|
+
return this;
|
|
398
|
+
const annotated_value = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
399
|
+
// All marks compete for toggling, since marks are mutually exclusive.
|
|
400
|
+
// Annotations never block a mark toggle.
|
|
401
|
+
const selected_marks = this.selected_marks;
|
|
402
|
+
const selected_mark_types = get_selected_range_types(selected_marks);
|
|
403
|
+
if (selected_mark_types.size > 1)
|
|
404
|
+
return this;
|
|
405
|
+
if (selected_marks.length === 0) {
|
|
406
|
+
if (is_selection_collapsed(this.selection)) {
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
const new_mark_node = {
|
|
410
|
+
id: this.generate_id(),
|
|
411
|
+
type: mark_type,
|
|
412
|
+
...mark_properties
|
|
413
|
+
};
|
|
414
|
+
this.create(new_mark_node);
|
|
415
|
+
// If there's no existing mark, add the new one
|
|
416
|
+
annotated_value.marks.push({
|
|
417
|
+
start_offset: range.start_offset,
|
|
418
|
+
end_offset: range.end_offset,
|
|
419
|
+
node_id: new_mark_node.id
|
|
420
|
+
});
|
|
421
|
+
this.set(this.selection.path, annotated_value);
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
const first_selected_mark = selected_marks[0];
|
|
425
|
+
const selected_mark_type = first_selected_mark.node.type;
|
|
426
|
+
if (selected_mark_type === mark_type) {
|
|
427
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
428
|
+
const selected_indices = new Set(selected_marks.map(({ index }) => index));
|
|
429
|
+
const removed_node_ids = selected_marks.map(({ node_id }) => node_id);
|
|
430
|
+
annotated_value.marks = annotated_value.marks.filter((_, index) => !selected_indices.has(index));
|
|
431
|
+
this.set(this.selection.path, annotated_value);
|
|
432
|
+
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
if (selected_marks.length === 1 &&
|
|
436
|
+
can_switch_mark_type(this.schema, selected_mark_type, mark_type)) {
|
|
437
|
+
const selected_mark = first_selected_mark;
|
|
438
|
+
this.set([selected_mark.node_id, 'type'], mark_type);
|
|
439
|
+
this.set_selection({
|
|
440
|
+
type: this.selection.type,
|
|
441
|
+
path: this.selection.path,
|
|
442
|
+
anchor_offset: selected_mark.start_offset,
|
|
443
|
+
focus_offset: selected_mark.end_offset
|
|
444
|
+
});
|
|
445
|
+
return this;
|
|
446
|
+
}
|
|
447
|
+
return this;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Toggles an annotation on the current text or node selection.
|
|
451
|
+
*
|
|
452
|
+
* Annotations only compete with touched annotations of the same type:
|
|
453
|
+
* touched same-type annotations are removed, otherwise a new annotation is
|
|
454
|
+
* created. Marks and other annotation types never block a toggle, and
|
|
455
|
+
* same-type annotations may still overlap when created through lower-level
|
|
456
|
+
* APIs.
|
|
457
|
+
*
|
|
458
|
+
* @param annotation_type - The type of annotation (e.g., 'comment', 'marker')
|
|
459
|
+
* @param annotation_properties - Additional data for the annotation
|
|
460
|
+
* @returns This transaction instance for method chaining
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```js
|
|
464
|
+
* // Add a comment annotation
|
|
465
|
+
* tr.toggle_annotation('comment', { text: 'Please review' });
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
toggle_annotation(annotation_type, annotation_properties) {
|
|
469
|
+
if (this.selection.type !== 'text' && this.selection.type !== 'node')
|
|
470
|
+
return this;
|
|
471
|
+
if (this.selection.type === 'node' && is_selection_collapsed(this.selection))
|
|
472
|
+
return this;
|
|
473
|
+
if (!this.available_annotation_types.includes(annotation_type)) {
|
|
474
|
+
console.warn(`Annotation type ${annotation_type} is not allowed here.`);
|
|
475
|
+
return this;
|
|
476
|
+
}
|
|
477
|
+
const range = get_selection_range(this.selection);
|
|
478
|
+
if (!range)
|
|
479
|
+
return this;
|
|
480
|
+
const annotated_value = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
481
|
+
// Only touched annotations of the same type compete
|
|
482
|
+
const selected_annotations = this.selected_annotations.filter(({ node }) => node?.type === annotation_type);
|
|
483
|
+
if (selected_annotations.length === 0) {
|
|
484
|
+
if (is_selection_collapsed(this.selection)) {
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
const new_annotation_node = {
|
|
488
|
+
id: this.generate_id(),
|
|
489
|
+
type: annotation_type,
|
|
490
|
+
...annotation_properties
|
|
491
|
+
};
|
|
492
|
+
this.create(new_annotation_node);
|
|
493
|
+
annotated_value.annotations.push({
|
|
494
|
+
start_offset: range.start_offset,
|
|
495
|
+
end_offset: range.end_offset,
|
|
496
|
+
node_id: new_annotation_node.id
|
|
497
|
+
});
|
|
498
|
+
this.set(this.selection.path, annotated_value);
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
// Remove touched same-type annotations
|
|
502
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
503
|
+
const selected_indices = new Set(selected_annotations.map(({ index }) => index));
|
|
504
|
+
const removed_node_ids = selected_annotations.map(({ node_id }) => node_id);
|
|
505
|
+
annotated_value.annotations = annotated_value.annotations.filter((_, index) => !selected_indices.has(index));
|
|
506
|
+
this.set(this.selection.path, annotated_value);
|
|
507
|
+
this._cascade_delete_unreferenced_nodes(removed_node_ids);
|
|
508
|
+
return this;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Deletes the currently selected text, node, or property.
|
|
512
|
+
*
|
|
513
|
+
* Behavior depends on selection type:
|
|
514
|
+
* - For node selections: Removes selected nodes and cascades deletion of unreferenced nodes
|
|
515
|
+
* - For text selections: Removes selected text and adjusts annotations accordingly
|
|
516
|
+
* - For collapsed selections: Deletes the previous character/node (backward) or next character/node (forward)
|
|
517
|
+
* - For property selections: Delegates to the config's handle_property_deletion hook
|
|
518
|
+
*
|
|
519
|
+
* @param direction - Direction of deletion for collapsed selections
|
|
520
|
+
* @returns This transaction instance for method chaining
|
|
521
|
+
*/
|
|
522
|
+
delete_selection(direction = 'backward') {
|
|
523
|
+
if (!this.selection)
|
|
524
|
+
return this;
|
|
525
|
+
if (this.selection.type === 'property') {
|
|
526
|
+
this.config.handle_property_deletion?.(this, this.selection.path);
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
const path = this.selection.path;
|
|
530
|
+
// Get the start and end indices for the selection
|
|
531
|
+
let start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
532
|
+
let end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
533
|
+
let length = 0;
|
|
534
|
+
if (this.selection?.type === 'text') {
|
|
535
|
+
const text_content = this.get(this.selection.path).content;
|
|
536
|
+
length = get_char_length(text_content);
|
|
537
|
+
}
|
|
538
|
+
else if (this.selection?.type === 'node') {
|
|
539
|
+
const node_array = this.get(this.selection.path);
|
|
540
|
+
length = node_array.nodes.length;
|
|
541
|
+
}
|
|
542
|
+
// If selection is collapsed we delete the previous char/node (backward)
|
|
543
|
+
// or the next char/node (forward)
|
|
544
|
+
if (start === end) {
|
|
545
|
+
if (direction === 'backward' && start > 0) {
|
|
546
|
+
start = start - 1;
|
|
547
|
+
}
|
|
548
|
+
else if (direction === 'forward' && end < length) {
|
|
549
|
+
end = end + 1;
|
|
550
|
+
}
|
|
551
|
+
else if (direction === 'backward' && start === 0) {
|
|
552
|
+
join_text_node(this);
|
|
553
|
+
return this;
|
|
554
|
+
}
|
|
555
|
+
else if (direction === 'forward' && end === length) {
|
|
556
|
+
// At end of text - try to join with next text node
|
|
557
|
+
const node_index = this.selection.path.at(-2);
|
|
558
|
+
const successor_node = this.get([...this.selection.path.slice(0, -2), node_index + 1]);
|
|
559
|
+
// Check if next node is a text node
|
|
560
|
+
if (successor_node && this.kind(successor_node) === 'text') {
|
|
561
|
+
// Set selection to beginning of next text node
|
|
562
|
+
this.set_selection({
|
|
563
|
+
type: 'text',
|
|
564
|
+
path: [...this.selection.path.slice(0, -2), node_index + 1, 'content'],
|
|
565
|
+
anchor_offset: 0,
|
|
566
|
+
focus_offset: 0
|
|
567
|
+
});
|
|
568
|
+
// Use join_text_node to merge with previous node
|
|
569
|
+
join_text_node(this);
|
|
570
|
+
}
|
|
571
|
+
return this;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (this.selection.type === 'node') {
|
|
575
|
+
const current_value = structuredClone($state.snapshot(this.get(path)));
|
|
576
|
+
const node_array = [...current_value.nodes];
|
|
577
|
+
const deletion_length = end - start;
|
|
578
|
+
// Remove the selected nodes from the node_array
|
|
579
|
+
node_array.splice(start, deletion_length);
|
|
580
|
+
const marks_result = adjust_ranges_for_deletion(current_value.marks, start, end);
|
|
581
|
+
const annotations_result = adjust_ranges_for_deletion(current_value.annotations, start, end);
|
|
582
|
+
// Update the node_array in the entry (this implicitly records an op via this.set)
|
|
583
|
+
// Note: this.set() will automatically cascade delete unreferenced nodes
|
|
584
|
+
this.set(path, {
|
|
585
|
+
nodes: node_array,
|
|
586
|
+
marks: marks_result.ranges,
|
|
587
|
+
annotations: annotations_result.ranges
|
|
588
|
+
});
|
|
589
|
+
this._cascade_delete_unreferenced_nodes([
|
|
590
|
+
...marks_result.removed_node_ids,
|
|
591
|
+
...annotations_result.removed_node_ids
|
|
592
|
+
]);
|
|
593
|
+
// Update the selection to point to the start of the deleted range
|
|
594
|
+
this.selection = {
|
|
595
|
+
type: 'node',
|
|
596
|
+
path,
|
|
597
|
+
anchor_offset: start,
|
|
598
|
+
focus_offset: start
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
else if (this.selection.type === 'text') {
|
|
602
|
+
const path = this.selection.path;
|
|
603
|
+
const text = structuredClone($state.snapshot(this.get(path)));
|
|
604
|
+
// Update the text content using character-based operations
|
|
605
|
+
const original_text = text.content;
|
|
606
|
+
text.content =
|
|
607
|
+
char_slice(original_text, 0, start) +
|
|
608
|
+
char_slice(original_text, end, get_char_length(original_text));
|
|
609
|
+
const marks_result = adjust_ranges_for_deletion(text.marks, start, end);
|
|
610
|
+
const annotations_result = adjust_ranges_for_deletion(text.annotations, start, end);
|
|
611
|
+
text.marks = marks_result.ranges;
|
|
612
|
+
text.annotations = annotations_result.ranges;
|
|
613
|
+
this.set(path, text);
|
|
614
|
+
// Delete removed mark/annotation nodes only if no other references remain.
|
|
615
|
+
this._cascade_delete_unreferenced_nodes([
|
|
616
|
+
...marks_result.removed_node_ids,
|
|
617
|
+
...annotations_result.removed_node_ids
|
|
618
|
+
]);
|
|
619
|
+
// Update the selection to the new caret position
|
|
620
|
+
this.selection = {
|
|
621
|
+
type: 'text',
|
|
622
|
+
path,
|
|
623
|
+
anchor_offset: start,
|
|
624
|
+
focus_offset: start
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Inserts nodes at the current node selection position.
|
|
631
|
+
*
|
|
632
|
+
* If the selection is expanded (not collapsed), first deletes the selected nodes
|
|
633
|
+
* before inserting the new ones.
|
|
634
|
+
*
|
|
635
|
+
* @param node_ids - Array of node IDs to insert
|
|
636
|
+
* @param marks - Selection-relative marks to restore
|
|
637
|
+
* @param annotations - Selection-relative annotations to restore
|
|
638
|
+
* @param nodes - Node graph for restored marks and annotations
|
|
639
|
+
* @returns This transaction instance for method chaining
|
|
640
|
+
*/
|
|
641
|
+
insert_nodes(node_ids, marks = [], annotations = [], nodes = {}) {
|
|
642
|
+
if (this.selection.type !== 'node')
|
|
643
|
+
return this;
|
|
644
|
+
// Unless caret is collapsed, delete the selected nodes as a first step
|
|
645
|
+
if (this.selection.anchor_offset !== this.selection.focus_offset) {
|
|
646
|
+
this.delete_selection();
|
|
647
|
+
}
|
|
648
|
+
const path = this.selection.path;
|
|
649
|
+
const current_value = structuredClone($state.snapshot(this.get(path)));
|
|
650
|
+
const node_array = [...current_value.nodes];
|
|
651
|
+
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
652
|
+
let next_marks = adjust_ranges_for_insertion(current_value.marks, start, node_ids.length);
|
|
653
|
+
let next_annotations = adjust_ranges_for_insertion(current_value.annotations, start, node_ids.length);
|
|
654
|
+
// Insert the new nodes
|
|
655
|
+
node_array.splice(start, 0, ...node_ids);
|
|
656
|
+
this.selection = {
|
|
657
|
+
type: 'node',
|
|
658
|
+
path: [...this.selection.path],
|
|
659
|
+
anchor_offset: start,
|
|
660
|
+
focus_offset: start + node_ids.length
|
|
661
|
+
};
|
|
662
|
+
const property_definition = this.inspect(path);
|
|
663
|
+
// Restore marks only when they stay mutually exclusive
|
|
664
|
+
if (!this.active_mark && marks.length > 0 && are_ranges_exclusive(marks, node_ids.length)) {
|
|
665
|
+
const restored_marks = marks
|
|
666
|
+
.map((mark) => {
|
|
667
|
+
const mark_node = nodes[mark.node_id];
|
|
668
|
+
if (!property_definition.mark_types?.includes(mark_node?.type))
|
|
669
|
+
return null;
|
|
670
|
+
const new_mark_node_id = this.build(mark.node_id, nodes);
|
|
671
|
+
return {
|
|
672
|
+
start_offset: start + mark.start_offset,
|
|
673
|
+
end_offset: start + mark.end_offset,
|
|
674
|
+
node_id: new_mark_node_id
|
|
675
|
+
};
|
|
676
|
+
})
|
|
677
|
+
.filter((mark) => mark !== null);
|
|
678
|
+
const combined_marks = next_marks.concat(restored_marks);
|
|
679
|
+
if (are_ranges_exclusive(combined_marks)) {
|
|
680
|
+
next_marks = combined_marks;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
for (const mark of restored_marks) {
|
|
684
|
+
this.delete(mark.node_id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Restore annotations; overlap is allowed
|
|
689
|
+
const restored_annotations = annotations
|
|
690
|
+
.map((annotation) => {
|
|
691
|
+
if (!is_range_within_bounds(annotation, node_ids.length))
|
|
692
|
+
return null;
|
|
693
|
+
const annotation_node = nodes[annotation.node_id];
|
|
694
|
+
if (!property_definition.annotation_types?.includes(annotation_node?.type))
|
|
695
|
+
return null;
|
|
696
|
+
const new_annotation_node_id = this.build(annotation.node_id, nodes);
|
|
697
|
+
return {
|
|
698
|
+
start_offset: start + annotation.start_offset,
|
|
699
|
+
end_offset: start + annotation.end_offset,
|
|
700
|
+
node_id: new_annotation_node_id
|
|
701
|
+
};
|
|
702
|
+
})
|
|
703
|
+
.filter((annotation) => annotation !== null);
|
|
704
|
+
next_annotations = next_annotations.concat(restored_annotations);
|
|
705
|
+
this.set(path, { nodes: node_array, marks: next_marks, annotations: next_annotations });
|
|
706
|
+
return this;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Inserts text at the current text selection position.
|
|
710
|
+
*
|
|
711
|
+
* Handles mark and annotation adjustments when text is inserted, including:
|
|
712
|
+
* - Expanding ranges that contain the insertion point
|
|
713
|
+
* - Shifting ranges that come after the insertion point
|
|
714
|
+
* - Optionally applying new marks and annotations to the inserted text
|
|
715
|
+
*
|
|
716
|
+
* @param replaced_text - The text to insert
|
|
717
|
+
* @param marks - Optional marks to apply to the inserted text
|
|
718
|
+
* @param annotations - Optional annotations to apply to the inserted text
|
|
719
|
+
* @param nodes - Optional node definitions for mark and annotation nodes
|
|
720
|
+
* @returns This transaction instance for method chaining
|
|
721
|
+
*/
|
|
722
|
+
insert_text(replaced_text, marks = [], annotations = [], nodes = {}) {
|
|
723
|
+
if (this.selection?.type !== 'text')
|
|
724
|
+
return this;
|
|
725
|
+
// Unless selection is collapsed, delete the selected content
|
|
726
|
+
// NOTE: This makes sure wrapped marks and annotations are disposed correctly
|
|
727
|
+
if (!is_selection_collapsed(this.selection)) {
|
|
728
|
+
this.delete_selection();
|
|
729
|
+
}
|
|
730
|
+
const text_value = structuredClone($state.snapshot(this.get(this.selection.path)));
|
|
731
|
+
const range = get_selection_range(this.selection);
|
|
732
|
+
// Transform the plain text string using character-based operations
|
|
733
|
+
const current_text = text_value.content;
|
|
734
|
+
text_value.content =
|
|
735
|
+
char_slice(current_text, 0, range.start_offset) +
|
|
736
|
+
replaced_text +
|
|
737
|
+
char_slice(current_text, range.end_offset);
|
|
738
|
+
// Calculate the change in length
|
|
739
|
+
const delta = get_char_length(replaced_text);
|
|
740
|
+
text_value.marks = adjust_ranges_for_insertion(text_value.marks, range.start_offset, delta);
|
|
741
|
+
text_value.annotations = adjust_ranges_for_insertion(text_value.annotations, range.start_offset, delta);
|
|
742
|
+
this.set(this.selection.path, text_value); // this will update the current state and create a history entry
|
|
743
|
+
// Setting the selection automatically triggers a re-render of the corresponding DOMSelection.
|
|
744
|
+
const new_selection = {
|
|
745
|
+
type: 'text',
|
|
746
|
+
path: this.selection.path,
|
|
747
|
+
anchor_offset: range.start_offset + get_char_length(replaced_text),
|
|
748
|
+
focus_offset: range.start_offset + get_char_length(replaced_text)
|
|
749
|
+
};
|
|
750
|
+
this.selection = new_selection;
|
|
751
|
+
const text_property_definition = this.inspect(this.selection.path);
|
|
752
|
+
const next_text = structuredClone(text_value);
|
|
753
|
+
let text_changed = false;
|
|
754
|
+
// Now we apply marks if there are any, but only if there's no active mark
|
|
755
|
+
// at the current collapsed caret
|
|
756
|
+
if (!this.active_mark && marks.length > 0 && are_ranges_exclusive(marks, delta)) {
|
|
757
|
+
const restored_marks = marks
|
|
758
|
+
.map((mark) => {
|
|
759
|
+
const original_mark_node = nodes[mark.node_id];
|
|
760
|
+
if (text_property_definition.mark_types?.includes(original_mark_node?.type)) {
|
|
761
|
+
const new_mark_node_id = this.build(mark.node_id, nodes);
|
|
762
|
+
return {
|
|
763
|
+
start_offset: range.start_offset + mark.start_offset,
|
|
764
|
+
end_offset: range.start_offset + mark.end_offset,
|
|
765
|
+
node_id: new_mark_node_id
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
})
|
|
770
|
+
.filter((mark) => mark !== null);
|
|
771
|
+
const combined_marks = text_value.marks.concat(restored_marks);
|
|
772
|
+
if (are_ranges_exclusive(combined_marks)) {
|
|
773
|
+
next_text.marks = combined_marks;
|
|
774
|
+
text_changed = true;
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
for (const mark of restored_marks) {
|
|
778
|
+
this.delete(mark.node_id);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Apply annotations to the inserted text; overlap is allowed
|
|
783
|
+
if (annotations.length > 0) {
|
|
784
|
+
const restored_annotations = annotations
|
|
785
|
+
.map((annotation) => {
|
|
786
|
+
if (!is_range_within_bounds(annotation, delta))
|
|
787
|
+
return null;
|
|
788
|
+
const original_annotation_node = nodes[annotation.node_id];
|
|
789
|
+
if (text_property_definition.annotation_types?.includes(original_annotation_node?.type)) {
|
|
790
|
+
const new_annotation_node_id = this.build(annotation.node_id, nodes);
|
|
791
|
+
return {
|
|
792
|
+
start_offset: range.start_offset + annotation.start_offset,
|
|
793
|
+
end_offset: range.start_offset + annotation.end_offset,
|
|
794
|
+
node_id: new_annotation_node_id
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
})
|
|
799
|
+
.filter((annotation) => annotation !== null);
|
|
800
|
+
if (restored_annotations.length > 0) {
|
|
801
|
+
next_text.annotations = text_value.annotations.concat(restored_annotations);
|
|
802
|
+
text_changed = true;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (text_changed) {
|
|
806
|
+
this.set(this.selection.path, next_text);
|
|
807
|
+
}
|
|
808
|
+
return this;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Recursively deletes nodes that are no longer referenced in the document.
|
|
812
|
+
*
|
|
813
|
+
* This handles the cascade deletion of child nodes when their parent
|
|
814
|
+
* references are removed. Uses reference counting to determine which
|
|
815
|
+
* nodes are safe to delete.
|
|
816
|
+
*
|
|
817
|
+
* @param potentially_orphaned_nodes - Array of node IDs to check
|
|
818
|
+
*/
|
|
819
|
+
_cascade_delete_unreferenced_nodes(potentially_orphaned_nodes) {
|
|
820
|
+
if (potentially_orphaned_nodes.length === 0)
|
|
821
|
+
return;
|
|
822
|
+
// Build reference counts in ONE full-document scan, then maintain them
|
|
823
|
+
// incrementally: marking a node deleted decrements the counts of every
|
|
824
|
+
// node it references (per occurrence). This replaces the previous
|
|
825
|
+
// per-candidate full-document re-scan, which made deleting M nodes
|
|
826
|
+
// O(M·N) instead of O(N + M).
|
|
827
|
+
const ref_counts = build_reference_counts(this.schema, this.doc);
|
|
828
|
+
const nodes_to_delete = {};
|
|
829
|
+
const to_check = [...potentially_orphaned_nodes];
|
|
830
|
+
while (to_check.length > 0) {
|
|
831
|
+
const node_id = to_check.pop();
|
|
832
|
+
if (!node_id || nodes_to_delete[node_id])
|
|
833
|
+
continue;
|
|
834
|
+
if ((ref_counts.get(node_id) || 0) === 0) {
|
|
835
|
+
// No more references, safe to delete this node
|
|
836
|
+
nodes_to_delete[node_id] = true;
|
|
837
|
+
// Release this node's outgoing references and re-check the
|
|
838
|
+
// nodes it pointed at — they may have become unreferenced.
|
|
839
|
+
const node = this.doc.nodes[node_id];
|
|
840
|
+
if (node) {
|
|
841
|
+
visit_node_references(this.schema, node, (referenced_id) => {
|
|
842
|
+
const count = ref_counts.get(referenced_id);
|
|
843
|
+
if (count !== undefined)
|
|
844
|
+
ref_counts.set(referenced_id, count - 1);
|
|
845
|
+
to_check.push(referenced_id);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Now perform the actual deletions
|
|
851
|
+
for (const node_id of Object.keys(nodes_to_delete)) {
|
|
852
|
+
const previous_value = this.get([node_id]);
|
|
853
|
+
if (previous_value) {
|
|
854
|
+
const op = ['delete', node_id];
|
|
855
|
+
this.ops.push(op);
|
|
856
|
+
this.inverse_ops.push(['create', previous_value]);
|
|
857
|
+
this._apply_op(op);
|
|
858
|
+
this._track_node_id(this.deleted_node_ids, node_id);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
846
862
|
}
|