@manuscripts/track-changes-plugin 0.4.0 → 0.4.1
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/dist/ChangeSet.d.ts +2 -7
- package/dist/change-steps/diffChangeSteps.d.ts +1 -1
- package/dist/changes/fixInconsistentChanges.d.ts +1 -1
- package/dist/compute/nodeHelpers.d.ts +3 -2
- package/dist/index.cjs +337 -224
- package/dist/index.es.js +1547 -0
- package/dist/index.js +337 -224
- package/dist/mutate/deleteAndMergeSplitNodes.d.ts +1 -2
- package/dist/mutate/deleteNode.d.ts +1 -1
- package/dist/track/applyChanges.d.ts +28 -0
- package/dist/track/deleteNode.d.ts +27 -0
- package/dist/track/findChanges.d.ts +27 -0
- package/dist/track/fixInconsistentChanges.d.ts +29 -0
- package/dist/track/mergeNode.d.ts +25 -0
- package/dist/track/node-utils.d.ts +27 -0
- package/dist/track/steps/deleteAndMergeSplitNodes.d.ts +53 -0
- package/dist/track/steps/mergeTrackedMarks.d.ts +29 -0
- package/dist/track/steps/setFragmentAsInserted.d.ts +18 -0
- package/dist/track/steps/track-utils.d.ts +18 -0
- package/dist/track/steps/trackReplaceAroundStep.d.ts +4 -0
- package/dist/track/steps/trackReplaceStep.d.ts +4 -0
- package/dist/track/trackTransaction.d.ts +17 -0
- package/dist/track/updateChangeAttrs.d.ts +21 -0
- package/dist/types/change.d.ts +5 -5
- package/dist/types/editor.d.ts +23 -0
- package/dist/types/step.d.ts +2 -1
- package/dist/types/track.d.ts +5 -1
- package/dist/utils/track-utils.d.ts +2 -16
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -171,8 +171,8 @@ class ChangeSet {
|
|
|
171
171
|
get changes() {
|
|
172
172
|
const iteratedIds = new Set();
|
|
173
173
|
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => {
|
|
174
|
-
const valid = !iteratedIds.has(c.
|
|
175
|
-
iteratedIds.add(c.
|
|
174
|
+
const valid = !iteratedIds.has(c.dataTracked.id) && ChangeSet.isValidDataTracked(c.dataTracked);
|
|
175
|
+
iteratedIds.add(c.dataTracked.id);
|
|
176
176
|
return valid;
|
|
177
177
|
});
|
|
178
178
|
}
|
|
@@ -207,13 +207,13 @@ class ChangeSet {
|
|
|
207
207
|
return rootNodes;
|
|
208
208
|
}
|
|
209
209
|
get pending() {
|
|
210
|
-
return this.changeTree.filter((c) => c.
|
|
210
|
+
return this.changeTree.filter((c) => c.dataTracked.status === CHANGE_STATUS.pending);
|
|
211
211
|
}
|
|
212
212
|
get accepted() {
|
|
213
|
-
return this.changeTree.filter((c) => c.
|
|
213
|
+
return this.changeTree.filter((c) => c.dataTracked.status === CHANGE_STATUS.accepted);
|
|
214
214
|
}
|
|
215
215
|
get rejected() {
|
|
216
|
-
return this.changeTree.filter((c) => c.
|
|
216
|
+
return this.changeTree.filter((c) => c.dataTracked.status === CHANGE_STATUS.rejected);
|
|
217
217
|
}
|
|
218
218
|
get textChanges() {
|
|
219
219
|
return this.changes.filter((c) => c.type === 'text-change');
|
|
@@ -244,7 +244,7 @@ class ChangeSet {
|
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
246
|
get hasIncompleteAttrs() {
|
|
247
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.
|
|
247
|
+
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidDataTracked(c.dataTracked));
|
|
248
248
|
}
|
|
249
249
|
get(id) {
|
|
250
250
|
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id);
|
|
@@ -264,29 +264,22 @@ class ChangeSet {
|
|
|
264
264
|
static flattenTreeToIds(changes) {
|
|
265
265
|
return changes.flatMap((c) => this.isNodeChange(c) ? [c.id, ...c.children.map((c) => c.id)] : c.id);
|
|
266
266
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Determines whether a change should not be deleted when applying it to the document.
|
|
269
|
-
* @param change
|
|
270
|
-
*/
|
|
271
|
-
static shouldNotDelete(change) {
|
|
272
|
-
return !ChangeSet.shouldDeleteChange(change);
|
|
273
|
-
}
|
|
274
267
|
/**
|
|
275
268
|
* Determines whether a change should be deleted when applying it to the document.
|
|
276
269
|
* @param change
|
|
277
270
|
*/
|
|
278
271
|
static shouldDeleteChange(change) {
|
|
279
|
-
const { status, operation } = change.
|
|
272
|
+
const { status, operation } = change.dataTracked;
|
|
280
273
|
return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.rejected) ||
|
|
281
274
|
(operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.accepted));
|
|
282
275
|
}
|
|
283
276
|
/**
|
|
284
277
|
* Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
|
|
285
|
-
* @param
|
|
278
|
+
* @param dataTracked
|
|
286
279
|
*/
|
|
287
|
-
static
|
|
288
|
-
if ('
|
|
289
|
-
log.warn('passed "
|
|
280
|
+
static isValidDataTracked(dataTracked = {}) {
|
|
281
|
+
if ('dataTracked' in dataTracked) {
|
|
282
|
+
log.warn('passed "dataTracked" as property to isValidTrackedAttrs()', dataTracked);
|
|
290
283
|
}
|
|
291
284
|
const trackedKeys = [
|
|
292
285
|
'id',
|
|
@@ -299,12 +292,12 @@ class ChangeSet {
|
|
|
299
292
|
// reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
|
|
300
293
|
// So it can be either omitted completely or at least be null or string
|
|
301
294
|
const optionalKeys = ['reviewedByID'];
|
|
302
|
-
const entries = Object.entries(
|
|
303
|
-
const optionalEntries = Object.entries(
|
|
295
|
+
const entries = Object.entries(dataTracked).filter(([key, val]) => trackedKeys.includes(key));
|
|
296
|
+
const optionalEntries = Object.entries(dataTracked).filter(([key, val]) => optionalKeys.includes(key));
|
|
304
297
|
return (entries.length === trackedKeys.length &&
|
|
305
298
|
entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
|
|
306
299
|
optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
|
|
307
|
-
(
|
|
300
|
+
(dataTracked.id || '').length > 0 // Changes created with undefined id have '' as placeholder
|
|
308
301
|
);
|
|
309
302
|
}
|
|
310
303
|
static isTextChange(change) {
|
|
@@ -350,8 +343,8 @@ function addTrackIdIfDoesntExist(attrs) {
|
|
|
350
343
|
}
|
|
351
344
|
return attrs;
|
|
352
345
|
}
|
|
353
|
-
function
|
|
354
|
-
if (!node || !node.
|
|
346
|
+
function getTextNodeTrackedMarkData(node, schema) {
|
|
347
|
+
if (!node || !node.isText) {
|
|
355
348
|
return undefined;
|
|
356
349
|
}
|
|
357
350
|
const marksTrackedData = [];
|
|
@@ -368,12 +361,25 @@ function getInlineNodeTrackedMarkData(node, schema) {
|
|
|
368
361
|
}
|
|
369
362
|
return marksTrackedData[0] || undefined;
|
|
370
363
|
}
|
|
364
|
+
function getBlockInlineTrackedData(node) {
|
|
365
|
+
let { dataTracked } = node.attrs;
|
|
366
|
+
if (dataTracked && !Array.isArray(dataTracked)) {
|
|
367
|
+
return [dataTracked];
|
|
368
|
+
}
|
|
369
|
+
return dataTracked;
|
|
370
|
+
}
|
|
371
371
|
function getNodeTrackedData(node, schema) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
372
|
+
let tracked;
|
|
373
|
+
if (node && !node.isText) {
|
|
374
|
+
tracked = getBlockInlineTrackedData(node);
|
|
375
|
+
}
|
|
376
|
+
else if (node === null || node === void 0 ? void 0 : node.isText) {
|
|
377
|
+
tracked = getTextNodeTrackedMarkData(node, schema);
|
|
378
|
+
}
|
|
379
|
+
if (tracked && !Array.isArray(tracked)) {
|
|
380
|
+
tracked = [tracked];
|
|
381
|
+
}
|
|
382
|
+
return tracked;
|
|
377
383
|
}
|
|
378
384
|
function equalMarks(n1, n2) {
|
|
379
385
|
return (n1.marks.length === n2.marks.length &&
|
|
@@ -392,7 +398,7 @@ function shouldMergeTrackedAttributes(left, right) {
|
|
|
392
398
|
left.authorID === right.authorID);
|
|
393
399
|
}
|
|
394
400
|
function getMergeableMarkTrackedAttrs(node, attrs, schema) {
|
|
395
|
-
const nodeAttrs =
|
|
401
|
+
const nodeAttrs = getTextNodeTrackedMarkData(node, schema);
|
|
396
402
|
return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
|
|
397
403
|
}
|
|
398
404
|
|
|
@@ -441,6 +447,29 @@ function deleteNode(node, pos, tr) {
|
|
|
441
447
|
// child, say some wrapper blockNode, is also deleted the content could be retained. TODO I guess.
|
|
442
448
|
return tr.delete(pos, pos + node.nodeSize);
|
|
443
449
|
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
|
|
453
|
+
* @param node
|
|
454
|
+
* @param pos
|
|
455
|
+
* @param newTr
|
|
456
|
+
* @param deleteAttrs
|
|
457
|
+
*/
|
|
458
|
+
function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
|
|
459
|
+
const dataTracked = getBlockInlineTrackedData(node);
|
|
460
|
+
const inserted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === CHANGE_OPERATION.insert);
|
|
461
|
+
const deleted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === CHANGE_OPERATION.delete);
|
|
462
|
+
const updated = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === CHANGE_OPERATION.set_node_attributes);
|
|
463
|
+
if (inserted && inserted.authorID === deleteAttrs.authorID) {
|
|
464
|
+
return deleteNode(node, pos, newTr);
|
|
465
|
+
}
|
|
466
|
+
const newDeleted = deleted
|
|
467
|
+
? { ...deleted, updatedAt: deleteAttrs.updatedAt }
|
|
468
|
+
: addTrackIdIfDoesntExist(deleteAttrs);
|
|
469
|
+
newTr.setNodeMarkup(pos, undefined, {
|
|
470
|
+
...node.attrs,
|
|
471
|
+
dataTracked: updated ? [newDeleted, updated] : [newDeleted],
|
|
472
|
+
}, node.marks);
|
|
444
473
|
}
|
|
445
474
|
|
|
446
475
|
/*!
|
|
@@ -486,21 +515,46 @@ function mergeNode(node, pos, tr) {
|
|
|
486
515
|
function updateChangeAttrs(tr, change, trackedAttrs, schema) {
|
|
487
516
|
const node = tr.doc.nodeAt(change.from);
|
|
488
517
|
if (!node) {
|
|
489
|
-
|
|
518
|
+
log.error('updateChangeAttrs: no node at the from of change ', change);
|
|
519
|
+
return tr;
|
|
520
|
+
}
|
|
521
|
+
const { operation } = trackedAttrs;
|
|
522
|
+
const oldTrackData = change.type === 'text-change'
|
|
523
|
+
? getTextNodeTrackedMarkData(node, schema)
|
|
524
|
+
: getBlockInlineTrackedData(node);
|
|
525
|
+
if (!operation) {
|
|
526
|
+
log.warn('updateChangeAttrs: unable to determine operation of change ', change);
|
|
527
|
+
}
|
|
528
|
+
else if (!oldTrackData) {
|
|
529
|
+
log.warn('updateChangeAttrs: no old dataTracked for change ', change);
|
|
530
|
+
}
|
|
531
|
+
if (change.type === 'text-change') {
|
|
532
|
+
const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
|
|
533
|
+
if (!oldMark) {
|
|
534
|
+
log.warn('updateChangeAttrs: no track marks for a text-change ', change);
|
|
535
|
+
return tr;
|
|
536
|
+
}
|
|
537
|
+
// TODO add operation based on mark type if it's undefined?
|
|
538
|
+
tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked: { ...oldTrackData, ...trackedAttrs } }));
|
|
490
539
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked }));
|
|
540
|
+
else if (change.type === 'node-change' && !operation) {
|
|
541
|
+
// Very weird edge-case if this happens
|
|
542
|
+
tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
|
|
495
543
|
}
|
|
496
544
|
else if (change.type === 'node-change') {
|
|
497
|
-
|
|
545
|
+
const newDataTracked = (getBlockInlineTrackedData(node) || []).map((oldTrack) => {
|
|
546
|
+
if (oldTrack.operation === operation) {
|
|
547
|
+
return { ...oldTrack, ...trackedAttrs };
|
|
548
|
+
}
|
|
549
|
+
return oldTrack;
|
|
550
|
+
});
|
|
551
|
+
tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: newDataTracked.length === 0 ? null : newDataTracked }, node.marks);
|
|
498
552
|
}
|
|
499
553
|
return tr;
|
|
500
554
|
}
|
|
501
555
|
function updateChangeChildrenAttributes(changes, tr, mapping) {
|
|
502
556
|
changes.forEach((c) => {
|
|
503
|
-
if (c.type === 'node-change' && ChangeSet.
|
|
557
|
+
if (c.type === 'node-change' && !ChangeSet.shouldDeleteChange(c)) {
|
|
504
558
|
const from = mapping.map(c.from);
|
|
505
559
|
const node = tr.doc.nodeAt(from);
|
|
506
560
|
if (!node) {
|
|
@@ -522,12 +576,12 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
|
|
|
522
576
|
*/
|
|
523
577
|
function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mapping()) {
|
|
524
578
|
changes.forEach((change) => {
|
|
525
|
-
if (change.
|
|
579
|
+
if (change.dataTracked.status === CHANGE_STATUS.pending) {
|
|
526
580
|
return;
|
|
527
581
|
}
|
|
528
582
|
// Map change.from and skip those which dont need to be applied
|
|
529
583
|
// or were already deleted by an applied block delete
|
|
530
|
-
const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || ChangeSet.
|
|
584
|
+
const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || !ChangeSet.shouldDeleteChange(change);
|
|
531
585
|
if (!node) {
|
|
532
586
|
!deleted && log.warn('no node found to update for change', change);
|
|
533
587
|
return;
|
|
@@ -556,12 +610,12 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mappi
|
|
|
556
610
|
deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
|
|
557
611
|
}
|
|
558
612
|
else if (ChangeSet.isNodeAttrChange(change) &&
|
|
559
|
-
change.
|
|
613
|
+
change.dataTracked.status === CHANGE_STATUS.accepted) {
|
|
560
614
|
const attrs = { ...node.attrs, dataTracked: null };
|
|
561
615
|
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
562
616
|
}
|
|
563
617
|
else if (ChangeSet.isNodeAttrChange(change) &&
|
|
564
|
-
change.
|
|
618
|
+
change.dataTracked.status === CHANGE_STATUS.rejected) {
|
|
565
619
|
const attrs = { ...change.oldAttrs, dataTracked: null };
|
|
566
620
|
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
567
621
|
}
|
|
@@ -583,9 +637,10 @@ function findChanges(state) {
|
|
|
583
637
|
// Store the last iterated change to join adjacent text changes
|
|
584
638
|
let current;
|
|
585
639
|
state.doc.descendants((node, pos) => {
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
const
|
|
640
|
+
const tracked = getNodeTrackedData(node, state.schema) || [];
|
|
641
|
+
for (let i = 0; i < tracked.length; i += 1) {
|
|
642
|
+
const dataTracked = tracked[i];
|
|
643
|
+
const id = dataTracked.id || '';
|
|
589
644
|
// Join adjacent text changes that have been broken up due to different marks
|
|
590
645
|
// eg <ins><b>bold</b>norm<i>italic</i></ins> -> treated as one continuous change
|
|
591
646
|
// Note the !equalMarks to leave changes separate incase the marks are equal to let fixInconsistentChanges to fix them
|
|
@@ -597,38 +652,49 @@ function findChanges(state) {
|
|
|
597
652
|
current.change.to = pos + node.nodeSize;
|
|
598
653
|
// Important to update the node as the text changes might contain multiple parts where some marks equal each other
|
|
599
654
|
current.node = node;
|
|
655
|
+
continue;
|
|
600
656
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
657
|
+
current && changes.push(current.change);
|
|
658
|
+
let change;
|
|
659
|
+
if (node.isText) {
|
|
660
|
+
change = {
|
|
661
|
+
id,
|
|
662
|
+
type: 'text-change',
|
|
663
|
+
from: pos,
|
|
664
|
+
to: pos + node.nodeSize,
|
|
665
|
+
dataTracked,
|
|
666
|
+
text: node.text,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
else if (dataTracked.operation === CHANGE_OPERATION.set_node_attributes) {
|
|
670
|
+
change = {
|
|
671
|
+
id,
|
|
672
|
+
type: 'node-attr-change',
|
|
673
|
+
from: pos,
|
|
674
|
+
to: pos + node.nodeSize,
|
|
675
|
+
dataTracked,
|
|
676
|
+
nodeType: node.type.name,
|
|
677
|
+
newAttrs: node.attrs,
|
|
678
|
+
oldAttrs: dataTracked.oldAttrs,
|
|
613
679
|
};
|
|
614
680
|
}
|
|
615
681
|
else {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
children: [],
|
|
625
|
-
attrs,
|
|
626
|
-
},
|
|
627
|
-
node,
|
|
682
|
+
change = {
|
|
683
|
+
id,
|
|
684
|
+
type: 'node-change',
|
|
685
|
+
from: pos,
|
|
686
|
+
to: pos + node.nodeSize,
|
|
687
|
+
dataTracked,
|
|
688
|
+
nodeType: node.type.name,
|
|
689
|
+
children: [],
|
|
628
690
|
};
|
|
629
691
|
}
|
|
692
|
+
current = {
|
|
693
|
+
change,
|
|
694
|
+
node,
|
|
695
|
+
};
|
|
630
696
|
}
|
|
631
|
-
|
|
697
|
+
if (tracked.length === 0 && current) {
|
|
632
698
|
changes.push(current.change);
|
|
633
699
|
current = undefined;
|
|
634
700
|
}
|
|
@@ -647,22 +713,23 @@ function findChanges(state) {
|
|
|
647
713
|
* @param schema
|
|
648
714
|
* @return docWasChanged, a boolean
|
|
649
715
|
*/
|
|
650
|
-
function fixInconsistentChanges(changeSet,
|
|
716
|
+
function fixInconsistentChanges(changeSet, currentUserID, newTr, schema) {
|
|
651
717
|
const iteratedIds = new Set();
|
|
652
718
|
let changed = false;
|
|
653
719
|
changeSet.invalidChanges.forEach((c) => {
|
|
654
|
-
const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.
|
|
720
|
+
const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.dataTracked;
|
|
655
721
|
const newAttrs = {
|
|
656
722
|
...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
|
|
657
|
-
...(!authorID && { authorID:
|
|
658
|
-
|
|
723
|
+
...(!authorID && { authorID: currentUserID }),
|
|
724
|
+
// Dont add a default operation -> rather have updateChangeAttrs delete the track data
|
|
725
|
+
// ...(!operation && { operation: CHANGE_OPERATION.insert }),
|
|
659
726
|
...(!reviewedByID && { reviewedByID: null }),
|
|
660
727
|
...(!status && { status: CHANGE_STATUS.pending }),
|
|
661
728
|
...(!createdAt && { createdAt: Date.now() }),
|
|
662
729
|
...(!updatedAt && { updatedAt: Date.now() }),
|
|
663
730
|
};
|
|
664
731
|
if (Object.keys(newAttrs).length > 0) {
|
|
665
|
-
updateChangeAttrs(newTr, c, { ...c.
|
|
732
|
+
updateChangeAttrs(newTr, c, { ...c.dataTracked, ...newAttrs }, schema);
|
|
666
733
|
changed = true;
|
|
667
734
|
}
|
|
668
735
|
iteratedIds.add(newAttrs.id || id);
|
|
@@ -792,18 +859,43 @@ function markInlineNodeChange(node, newTrackAttrs, schema) {
|
|
|
792
859
|
});
|
|
793
860
|
return node.mark(filtered.concat(createdMark));
|
|
794
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* Iterates over fragment's content and joins pasted text with old track marks
|
|
864
|
+
*
|
|
865
|
+
* This is not strictly necessary but it's kinda bad UX if the inserted text is split into parts
|
|
866
|
+
* even when it's authored by the same user.
|
|
867
|
+
* @param content
|
|
868
|
+
* @param newTrackAttrs
|
|
869
|
+
* @param schema
|
|
870
|
+
* @returns
|
|
871
|
+
*/
|
|
872
|
+
function loopContentAndMergeText(content, newTrackAttrs, schema) {
|
|
873
|
+
var _a;
|
|
874
|
+
const updatedChildren = [];
|
|
875
|
+
for (let i = 0; i < content.childCount; i += 1) {
|
|
876
|
+
const recursed = recurseNodeContent(content.child(i), newTrackAttrs, schema);
|
|
877
|
+
const prev = i > 0 ? updatedChildren[i - 1] : null;
|
|
878
|
+
if ((prev === null || prev === void 0 ? void 0 : prev.isText) &&
|
|
879
|
+
recursed.isText &&
|
|
880
|
+
equalMarks(prev, recursed) &&
|
|
881
|
+
((_a = getTextNodeTrackedMarkData(prev, schema)) === null || _a === void 0 ? void 0 : _a.operation) === CHANGE_OPERATION.insert) {
|
|
882
|
+
updatedChildren.splice(i - 1, 1, schema.text('' + prev.text + recursed.text, prev.marks));
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
updatedChildren.push(recursed);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return updatedChildren;
|
|
889
|
+
}
|
|
795
890
|
function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
796
891
|
if (node.isText) {
|
|
797
892
|
return markInlineNodeChange(node, newTrackAttrs, schema);
|
|
798
893
|
}
|
|
799
894
|
else if (node.isBlock || node.isInline) {
|
|
800
|
-
const updatedChildren =
|
|
801
|
-
node.content.forEach((child) => {
|
|
802
|
-
updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
|
|
803
|
-
});
|
|
895
|
+
const updatedChildren = loopContentAndMergeText(node.content, newTrackAttrs, schema);
|
|
804
896
|
return node.type.create({
|
|
805
897
|
...node.attrs,
|
|
806
|
-
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
898
|
+
dataTracked: [addTrackIdIfDoesntExist(newTrackAttrs)],
|
|
807
899
|
}, Fragment.fromArray(updatedChildren), node.marks);
|
|
808
900
|
}
|
|
809
901
|
else {
|
|
@@ -813,11 +905,8 @@ function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
|
813
905
|
}
|
|
814
906
|
function setFragmentAsInserted(inserted, insertAttrs, schema) {
|
|
815
907
|
// Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
|
|
816
|
-
const updatedInserted =
|
|
817
|
-
|
|
818
|
-
updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
|
|
819
|
-
});
|
|
820
|
-
return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
|
|
908
|
+
const updatedInserted = loopContentAndMergeText(inserted, insertAttrs, schema);
|
|
909
|
+
return updatedInserted.length === 0 ? Fragment.empty : Fragment.fromArray(updatedInserted);
|
|
821
910
|
}
|
|
822
911
|
|
|
823
912
|
/*!
|
|
@@ -846,6 +935,13 @@ function createNewDeleteAttrs(attrs) {
|
|
|
846
935
|
...attrs,
|
|
847
936
|
operation: CHANGE_OPERATION.delete,
|
|
848
937
|
};
|
|
938
|
+
}
|
|
939
|
+
function createNewUpdateAttrs(attrs, oldAttrs) {
|
|
940
|
+
return {
|
|
941
|
+
...attrs,
|
|
942
|
+
operation: CHANGE_OPERATION.set_node_attributes,
|
|
943
|
+
oldAttrs,
|
|
944
|
+
};
|
|
849
945
|
}
|
|
850
946
|
|
|
851
947
|
/*!
|
|
@@ -889,13 +985,12 @@ function createNewDeleteAttrs(attrs) {
|
|
|
889
985
|
* @returns mapping adjusted by the applied operations & modified insert slice
|
|
890
986
|
*/
|
|
891
987
|
function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
|
|
892
|
-
const deleteMap = new Mapping();
|
|
893
988
|
const steps = [];
|
|
894
989
|
// No deletion applied, return default values
|
|
895
990
|
if (from === to) {
|
|
896
991
|
return {
|
|
897
|
-
deleteMap,
|
|
898
992
|
newSliceContent: insertSlice.content,
|
|
993
|
+
sliceWasSplit: false,
|
|
899
994
|
steps,
|
|
900
995
|
};
|
|
901
996
|
}
|
|
@@ -903,10 +998,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
903
998
|
const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
|
|
904
999
|
let mergingStartSide = true;
|
|
905
1000
|
startDoc.nodesBetween(from, to, (node, pos) => {
|
|
906
|
-
const
|
|
907
|
-
const offsetFrom = deleteMap.map(from, -1);
|
|
908
|
-
const offsetTo = deleteMap.map(to, 1);
|
|
909
|
-
const nodeEnd = offsetPos + node.nodeSize;
|
|
1001
|
+
const nodeEnd = pos + node.nodeSize;
|
|
910
1002
|
// So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
|
|
911
1003
|
// since the content inside gap should stay unchanged.
|
|
912
1004
|
// All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
|
|
@@ -917,23 +1009,13 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
917
1009
|
// are altered and should not be skipped.
|
|
918
1010
|
// @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
|
|
919
1011
|
const wasWithinGap = gap &&
|
|
920
|
-
((!node.isText &&
|
|
921
|
-
(node.isText &&
|
|
922
|
-
offsetPos <= deleteMap.map(gap.start, -1) &&
|
|
923
|
-
nodeEnd >= deleteMap.map(gap.end, -1)));
|
|
924
|
-
let step = newTr.steps[newTr.steps.length - 1];
|
|
1012
|
+
((!node.isText && pos >= gap.start) ||
|
|
1013
|
+
(node.isText && pos <= gap.start && nodeEnd >= gap.start));
|
|
925
1014
|
// nodeEnd > offsetFrom -> delete touches this node
|
|
926
1015
|
// eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
|
|
927
|
-
|
|
928
|
-
// !nodeWasDeleted -> Check node wasn't already deleted by a previous deleteNode
|
|
929
|
-
// This is quite tricky to wrap your head around and I've forgotten the nitty-gritty details already.
|
|
930
|
-
// But from what I remember what it safeguards against is, when you've already deleted a node
|
|
931
|
-
// say an inserted blockquote that had all its children deleted, nodesBetween still iterates over those
|
|
932
|
-
// nodes and therefore we have to make this check to ensure they still exist in the doc.
|
|
933
|
-
//
|
|
934
|
-
if (nodeEnd > offsetFrom && !nodeWasDeleted && !wasWithinGap) {
|
|
1016
|
+
if (nodeEnd > from && !wasWithinGap) {
|
|
935
1017
|
// |<p>asdf</p>| -> node deleted completely
|
|
936
|
-
const nodeCompletelyDeleted =
|
|
1018
|
+
const nodeCompletelyDeleted = pos >= from && nodeEnd <= to;
|
|
937
1019
|
// The end token deleted eg:
|
|
938
1020
|
// <p 1>asdf|</p 7><p 7>bye</p 12>| + [<p>]hello</p> -> <p>asdfhello</p>
|
|
939
1021
|
// (del 6 12) + (ins [<p>]hello</p> openStart 1 openEnd 0)
|
|
@@ -944,14 +1026,14 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
944
1026
|
//
|
|
945
1027
|
// What about:
|
|
946
1028
|
// <p 1>asdf|</p 7><p 7 op="inserted">|bye</p 12> + empty -> <p>asdfbye</p>
|
|
947
|
-
const endTokenDeleted = nodeEnd <=
|
|
1029
|
+
const endTokenDeleted = nodeEnd <= to;
|
|
948
1030
|
// The start token deleted eg:
|
|
949
1031
|
// |<p1 0>hey</p 6><p2 6>|asdf</p 12> + <p3>hello [</p>] -> <p3>hello asdf</p2>
|
|
950
1032
|
// (del 0 7) + (ins <p>hello [</p>] openStart 0 openEnd 1)
|
|
951
1033
|
// (<p1> pos 0) >= (from 0) && (nodeEnd 6) - 1 > (to 7) == false???
|
|
952
1034
|
// (<p2> pos 6) >= (from 0) && (nodeEnd 12) - 1 > (to 7) == true
|
|
953
1035
|
//
|
|
954
|
-
const startTokenDeleted =
|
|
1036
|
+
const startTokenDeleted = pos >= from; // && nodeEnd - 1 > offsetTo
|
|
955
1037
|
if (node.isText ||
|
|
956
1038
|
(!endTokenDeleted && startTokenDeleted) ||
|
|
957
1039
|
(endTokenDeleted && !startTokenDeleted)) {
|
|
@@ -963,7 +1045,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
963
1045
|
}
|
|
964
1046
|
// Depth is often 1 when merging paragraphs or 2 for fully open blockquotes.
|
|
965
1047
|
// Incase of merging text within a ReplaceAroundStep the depth might be 1
|
|
966
|
-
const depth = newTr.doc.resolve(
|
|
1048
|
+
const depth = newTr.doc.resolve(pos).depth;
|
|
967
1049
|
const mergeContent = mergingStartSide
|
|
968
1050
|
? firstMergedNode === null || firstMergedNode === void 0 ? void 0 : firstMergedNode.mergedNodeContent
|
|
969
1051
|
: lastMergedNode === null || lastMergedNode === void 0 ? void 0 : lastMergedNode.mergedNodeContent;
|
|
@@ -981,10 +1063,10 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
981
1063
|
// the paragraph.
|
|
982
1064
|
steps.push({
|
|
983
1065
|
type: 'merge-fragment',
|
|
984
|
-
pos
|
|
985
|
-
mergePos: mergeStartNode ? nodeEnd - openStart :
|
|
986
|
-
from
|
|
987
|
-
to
|
|
1066
|
+
pos,
|
|
1067
|
+
mergePos: mergeStartNode ? nodeEnd - openStart : pos + openEnd,
|
|
1068
|
+
from,
|
|
1069
|
+
to,
|
|
988
1070
|
node,
|
|
989
1071
|
fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
|
|
990
1072
|
});
|
|
@@ -1002,9 +1084,9 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
1002
1084
|
// (which is basically the case most of the time)
|
|
1003
1085
|
steps.push({
|
|
1004
1086
|
type: 'delete-text',
|
|
1005
|
-
pos
|
|
1006
|
-
from: Math.max(
|
|
1007
|
-
to: Math.min(nodeEnd,
|
|
1087
|
+
pos,
|
|
1088
|
+
from: Math.max(pos, from),
|
|
1089
|
+
to: Math.min(nodeEnd, to),
|
|
1008
1090
|
node,
|
|
1009
1091
|
});
|
|
1010
1092
|
}
|
|
@@ -1013,19 +1095,15 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
1013
1095
|
else if (nodeCompletelyDeleted) {
|
|
1014
1096
|
steps.push({
|
|
1015
1097
|
type: 'delete-node',
|
|
1016
|
-
pos
|
|
1017
|
-
nodeEnd
|
|
1098
|
+
pos,
|
|
1099
|
+
nodeEnd,
|
|
1018
1100
|
node,
|
|
1019
1101
|
});
|
|
1020
1102
|
}
|
|
1021
1103
|
}
|
|
1022
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1023
|
-
if (step !== newestStep) {
|
|
1024
|
-
deleteMap.appendMap(newestStep.getMap());
|
|
1025
|
-
}
|
|
1026
1104
|
});
|
|
1027
1105
|
return {
|
|
1028
|
-
|
|
1106
|
+
sliceWasSplit: !!(firstMergedNode || lastMergedNode),
|
|
1029
1107
|
newSliceContent: updatedSliceNodes
|
|
1030
1108
|
? Fragment.fromArray(updatedSliceNodes)
|
|
1031
1109
|
: insertSlice.content,
|
|
@@ -1119,7 +1197,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1119
1197
|
log.info('RETAINED GAP CONTENT', gap);
|
|
1120
1198
|
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1121
1199
|
// eg partial nodes in an open-ended slice
|
|
1122
|
-
const {
|
|
1200
|
+
const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
|
|
1123
1201
|
let steps = deleteSteps;
|
|
1124
1202
|
log.info('TR: new steps after applying delete', [...newTr.steps]);
|
|
1125
1203
|
log.info('DELETE STEPS: ', deleteSteps);
|
|
@@ -1139,15 +1217,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1139
1217
|
}
|
|
1140
1218
|
deleteSteps.push({
|
|
1141
1219
|
type: 'insert-slice',
|
|
1142
|
-
from:
|
|
1143
|
-
to:
|
|
1220
|
+
from: gapFrom,
|
|
1221
|
+
to: gapTo,
|
|
1144
1222
|
slice: insertedSlice,
|
|
1223
|
+
sliceWasSplit,
|
|
1145
1224
|
});
|
|
1146
1225
|
}
|
|
1147
1226
|
else {
|
|
1148
1227
|
// Incase only deletion was applied, check whether tracked marks around deleted content can be merged
|
|
1149
|
-
mergeTrackedMarks(
|
|
1150
|
-
mergeTrackedMarks(
|
|
1228
|
+
mergeTrackedMarks(gapFrom, newTr.doc, newTr, oldState.schema);
|
|
1229
|
+
mergeTrackedMarks(gapTo, newTr.doc, newTr, oldState.schema);
|
|
1151
1230
|
}
|
|
1152
1231
|
return steps;
|
|
1153
1232
|
}
|
|
@@ -1183,11 +1262,11 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
|
|
|
1183
1262
|
log.info('TR: steps before applying delete', [...newTr.steps]);
|
|
1184
1263
|
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1185
1264
|
// eg partial nodes in an open-ended slice
|
|
1186
|
-
const {
|
|
1265
|
+
const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
|
|
1187
1266
|
changeSteps.push(...deleteSteps);
|
|
1188
1267
|
log.info('TR: steps after applying delete', [...newTr.steps]);
|
|
1189
1268
|
log.info('DELETE STEPS: ', changeSteps);
|
|
1190
|
-
const adjustedInsertPos = deleteMap.map(toA)
|
|
1269
|
+
const adjustedInsertPos = toA; // deleteMap.map(toA)
|
|
1191
1270
|
if (newSliceContent.size > 0) {
|
|
1192
1271
|
log.info('newSliceContent', newSliceContent);
|
|
1193
1272
|
// Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
|
|
@@ -1198,6 +1277,7 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
|
|
|
1198
1277
|
type: 'insert-slice',
|
|
1199
1278
|
from: adjustedInsertPos,
|
|
1200
1279
|
to: adjustedInsertPos,
|
|
1280
|
+
sliceWasSplit,
|
|
1201
1281
|
slice: new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
|
|
1202
1282
|
});
|
|
1203
1283
|
}
|
|
@@ -1270,21 +1350,6 @@ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
|
|
|
1270
1350
|
}
|
|
1271
1351
|
}
|
|
1272
1352
|
|
|
1273
|
-
/*!
|
|
1274
|
-
* © 2021 Atypon Systems LLC
|
|
1275
|
-
*
|
|
1276
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1277
|
-
* you may not use this file except in compliance with the License.
|
|
1278
|
-
* You may obtain a copy of the License at
|
|
1279
|
-
*
|
|
1280
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1281
|
-
*
|
|
1282
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1283
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1284
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1285
|
-
* See the License for the specific language governing permissions and
|
|
1286
|
-
* limitations under the License.
|
|
1287
|
-
*/
|
|
1288
1353
|
function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
1289
1354
|
const mapping = new Mapping();
|
|
1290
1355
|
const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
|
|
@@ -1295,50 +1360,22 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1295
1360
|
log.info('process change: ', c);
|
|
1296
1361
|
// const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
|
|
1297
1362
|
if (c.type === 'delete-node') {
|
|
1298
|
-
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1304
|
-
if (step !== newestStep) {
|
|
1305
|
-
mapping.appendMap(newestStep.getMap());
|
|
1306
|
-
step = newestStep;
|
|
1307
|
-
}
|
|
1308
|
-
mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
|
|
1309
|
-
}
|
|
1310
|
-
else {
|
|
1311
|
-
const attrs = {
|
|
1312
|
-
...c.node.attrs,
|
|
1313
|
-
dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
|
|
1314
|
-
};
|
|
1315
|
-
newTr.setNodeMarkup(mapping.map(c.pos), undefined, attrs, c.node.marks);
|
|
1363
|
+
deleteOrSetNodeDeleted(c.node, mapping.map(c.pos), newTr, deleteAttrs);
|
|
1364
|
+
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1365
|
+
if (step !== newestStep) {
|
|
1366
|
+
mapping.appendMap(newestStep.getMap());
|
|
1367
|
+
step = newestStep;
|
|
1316
1368
|
}
|
|
1369
|
+
mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
|
|
1317
1370
|
}
|
|
1318
1371
|
else if (c.type === 'delete-text') {
|
|
1319
|
-
const from = mapping.map(c.from, -1);
|
|
1320
|
-
const to = mapping.map(c.to, 1);
|
|
1321
1372
|
const node = newTr.doc.nodeAt(mapping.map(c.pos));
|
|
1322
|
-
if (node
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
else {
|
|
1327
|
-
const leftNode = newTr.doc.resolve(from).nodeBefore;
|
|
1328
|
-
const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
|
|
1329
|
-
const rightNode = newTr.doc.resolve(to).nodeAfter;
|
|
1330
|
-
const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
|
|
1331
|
-
const fromStartOfMark = from - (leftNode && leftMarks ? leftNode.nodeSize : 0);
|
|
1332
|
-
const toEndOfMark = to + (rightNode && rightMarks ? rightNode.nodeSize : 0);
|
|
1333
|
-
const dataTracked = addTrackIdIfDoesntExist({
|
|
1334
|
-
...leftMarks,
|
|
1335
|
-
...rightMarks,
|
|
1336
|
-
...deleteAttrs,
|
|
1337
|
-
});
|
|
1338
|
-
newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
|
|
1339
|
-
dataTracked,
|
|
1340
|
-
}));
|
|
1373
|
+
if (!node) {
|
|
1374
|
+
log.error(`processChangeSteps: no text node found for text-change`, c);
|
|
1375
|
+
return;
|
|
1341
1376
|
}
|
|
1377
|
+
const where = deleteTextIfInserted(node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
|
|
1378
|
+
mergeTrackedMarks(where, newTr.doc, newTr, schema);
|
|
1342
1379
|
}
|
|
1343
1380
|
else if (c.type === 'merge-fragment') {
|
|
1344
1381
|
let insertPos = mapping.map(c.mergePos);
|
|
@@ -1364,7 +1401,7 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1364
1401
|
const newStep = new ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
|
|
1365
1402
|
const stepResult = newTr.maybeStep(newStep);
|
|
1366
1403
|
if (stepResult.failed) {
|
|
1367
|
-
log.error(`insert ReplaceStep failed
|
|
1404
|
+
log.error(`processChangeSteps: insert-slice ReplaceStep failed "${stepResult.failed}"`, newStep);
|
|
1368
1405
|
return;
|
|
1369
1406
|
}
|
|
1370
1407
|
mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
|
|
@@ -1372,17 +1409,29 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1372
1409
|
selectionPos = mapping.map(c.to) + c.slice.size;
|
|
1373
1410
|
}
|
|
1374
1411
|
else if (c.type === 'update-node-attrs') {
|
|
1375
|
-
const oldDataTracked = c.
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1412
|
+
const oldDataTracked = getBlockInlineTrackedData(c.node) || [];
|
|
1413
|
+
const oldUpdate = oldDataTracked.find((d) => d.operation === CHANGE_OPERATION.set_node_attributes);
|
|
1414
|
+
let newDataTracked = oldDataTracked;
|
|
1415
|
+
if (oldUpdate) {
|
|
1416
|
+
newDataTracked = [
|
|
1417
|
+
...oldDataTracked.filter((d) => d === oldUpdate),
|
|
1418
|
+
{
|
|
1419
|
+
...oldUpdate,
|
|
1420
|
+
updatedAt: emptyAttrs.updatedAt,
|
|
1421
|
+
},
|
|
1422
|
+
];
|
|
1423
|
+
}
|
|
1424
|
+
else if (oldDataTracked.length === 0 ||
|
|
1425
|
+
oldDataTracked.find((d) => d.operation === CHANGE_OPERATION.delete)) {
|
|
1426
|
+
newDataTracked = [
|
|
1427
|
+
...oldDataTracked,
|
|
1428
|
+
addTrackIdIfDoesntExist(createNewUpdateAttrs(emptyAttrs, c.node.attrs)),
|
|
1429
|
+
];
|
|
1430
|
+
}
|
|
1431
|
+
newTr.setNodeMarkup(mapping.map(c.pos), undefined, {
|
|
1432
|
+
...c.newAttrs,
|
|
1433
|
+
dataTracked: newDataTracked.length > 0 ? newDataTracked : null,
|
|
1434
|
+
}, c.node.marks);
|
|
1386
1435
|
}
|
|
1387
1436
|
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1388
1437
|
if (step !== newestStep) {
|
|
@@ -1407,25 +1456,26 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1407
1456
|
* See the License for the specific language governing permissions and
|
|
1408
1457
|
* limitations under the License.
|
|
1409
1458
|
*/
|
|
1410
|
-
function matchInserted(
|
|
1459
|
+
function matchInserted(matchedDeleted, deleted, inserted, newTr, schema) {
|
|
1411
1460
|
var _a;
|
|
1461
|
+
let matched = [matchedDeleted, deleted];
|
|
1412
1462
|
for (let i = 0;; i += 1) {
|
|
1413
1463
|
if (inserted.childCount === i)
|
|
1414
|
-
return
|
|
1415
|
-
const
|
|
1464
|
+
return matched;
|
|
1465
|
+
const insNode = inserted.child(i);
|
|
1416
1466
|
// @ts-ignore
|
|
1417
|
-
let adjDeleted =
|
|
1418
|
-
(d.type === 'delete-node' && d.
|
|
1419
|
-
if (
|
|
1420
|
-
return
|
|
1467
|
+
let adjDeleted = matched[1].find((d) => (d.type === 'delete-text' && Math.max(d.pos, d.from) === matched[0]) ||
|
|
1468
|
+
(d.type === 'delete-node' && d.pos === matched[0]));
|
|
1469
|
+
if (insNode.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
|
|
1470
|
+
return matched;
|
|
1421
1471
|
}
|
|
1422
|
-
else if (
|
|
1472
|
+
else if (insNode.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
|
|
1423
1473
|
adjDeleted = adjDeleted;
|
|
1424
|
-
const { pos, from, to, node } = adjDeleted;
|
|
1425
|
-
let j = 0, d = from - pos, maxSteps = Math.max(pos, from)
|
|
1474
|
+
const { pos, from, to, node: delNode } = adjDeleted;
|
|
1475
|
+
let j = 0, d = from - pos, maxSteps = to - Math.max(pos, from);
|
|
1426
1476
|
// Match text inside the inserted text node to the deleted text node
|
|
1427
|
-
for (; maxSteps !== j &&
|
|
1428
|
-
|
|
1477
|
+
for (; maxSteps !== j && insNode.text[j] !== undefined && insNode.text[j] === delNode.text[d]; j += 1, d += 1) {
|
|
1478
|
+
matched[0] += 1;
|
|
1429
1479
|
}
|
|
1430
1480
|
// this is needed incase diffing tr.doc
|
|
1431
1481
|
// deleted.push({
|
|
@@ -1435,46 +1485,108 @@ function matchInserted(inDeleted, deleted, inserted, newTr, schema) {
|
|
|
1435
1485
|
// oldAttrs: adjDeleted.node.attrs || {},
|
|
1436
1486
|
// newAttrs: child.attrs || {},
|
|
1437
1487
|
// })
|
|
1488
|
+
matched = [matched[0], matched[1].filter((d) => d !== adjDeleted)];
|
|
1438
1489
|
if (maxSteps !== j) {
|
|
1439
|
-
|
|
1490
|
+
matched[1].push({
|
|
1440
1491
|
pos,
|
|
1441
1492
|
from: Math.max(pos, from) + j,
|
|
1442
1493
|
to,
|
|
1443
1494
|
type: 'delete-text',
|
|
1444
|
-
node,
|
|
1495
|
+
node: delNode,
|
|
1445
1496
|
});
|
|
1497
|
+
return matched;
|
|
1446
1498
|
}
|
|
1447
|
-
|
|
1499
|
+
continue;
|
|
1448
1500
|
}
|
|
1449
|
-
else if (
|
|
1450
|
-
// Move the inDeleted inside the block node's boundary
|
|
1451
|
-
|
|
1501
|
+
else if (insNode.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
|
|
1502
|
+
// Move the inDeleted inside the block/inline node's boundary
|
|
1503
|
+
matched = matchInserted(matched[0] + 1, matched[1].filter((d) => d !== adjDeleted), insNode.content);
|
|
1452
1504
|
}
|
|
1453
|
-
|
|
1505
|
+
else {
|
|
1506
|
+
matched = [matched[0] + insNode.nodeSize, matched[1].filter((d) => d !== adjDeleted)];
|
|
1507
|
+
}
|
|
1508
|
+
matched[1].push({
|
|
1454
1509
|
pos: adjDeleted.pos,
|
|
1455
1510
|
type: 'update-node-attrs',
|
|
1511
|
+
node: adjDeleted.node,
|
|
1456
1512
|
// Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
|
|
1457
|
-
|
|
1458
|
-
newAttrs: child.attrs || {},
|
|
1513
|
+
newAttrs: insNode.attrs || {},
|
|
1459
1514
|
});
|
|
1460
|
-
deleted = deleted.filter((d) => d !== adjDeleted);
|
|
1461
|
-
inDeleted -= child.nodeSize;
|
|
1462
1515
|
}
|
|
1463
1516
|
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Cuts a fragment similar to Fragment.cut but also removes the parent node.
|
|
1519
|
+
*
|
|
1520
|
+
* @TODO there is however, some silly calculation mistake so that I need to use matched - deleted + 1 > 0
|
|
1521
|
+
* inside it to check whether to actually cut a text node. The offset might be cascading, therefore it should
|
|
1522
|
+
* be fixed at some point.
|
|
1523
|
+
* @param matched
|
|
1524
|
+
* @param deleted
|
|
1525
|
+
* @param content
|
|
1526
|
+
* @returns
|
|
1527
|
+
*/
|
|
1528
|
+
function cutFragment(matched, deleted, content) {
|
|
1529
|
+
let newContent = [];
|
|
1530
|
+
for (let i = 0; matched <= deleted && i < content.childCount; i += 1) {
|
|
1531
|
+
const child = content.child(i);
|
|
1532
|
+
if (!child.isText && child.content.size > 0) {
|
|
1533
|
+
const cut = cutFragment(matched + 1, deleted, child.content);
|
|
1534
|
+
matched = cut[0];
|
|
1535
|
+
newContent.push(...cut[1].content);
|
|
1536
|
+
}
|
|
1537
|
+
else if (child.isText && matched + child.nodeSize > deleted) {
|
|
1538
|
+
if (matched - deleted + 1 > 0) {
|
|
1539
|
+
newContent.push(child.cut(0, matched - deleted + 1));
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
newContent.push(child);
|
|
1543
|
+
}
|
|
1544
|
+
matched = deleted + 1;
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
matched += child.nodeSize;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return [matched, Fragment.fromArray(newContent)];
|
|
1551
|
+
}
|
|
1464
1552
|
function diffChangeSteps(deleted, inserted, newTr, schema) {
|
|
1465
1553
|
const updated = [];
|
|
1466
1554
|
let updatedDeleted = [...deleted];
|
|
1467
1555
|
inserted.forEach((ins) => {
|
|
1468
|
-
|
|
1469
|
-
|
|
1556
|
+
log.info('DIFF ins ', ins);
|
|
1557
|
+
//
|
|
1558
|
+
// @TODO this is a temporary workaround to prevent duplicated diffing between splitSliceIntoMergedParts and
|
|
1559
|
+
// matchInserted.
|
|
1560
|
+
//
|
|
1561
|
+
// As originally authored splitSliceIntoMergedParts splits open slices into their merged parts
|
|
1562
|
+
// leaving out the need to insert the possibly deleted nodes into the doc. However, as matchInserted now
|
|
1563
|
+
// traverses the deleted range checking it against the inserted slice this behaves quite in a same way
|
|
1564
|
+
// where the opened block nodes are traversed but left unmodified. With an openStart > 0 though the
|
|
1565
|
+
// node-attr-updates would additionally have to be filtered out in the processChangeSteps.
|
|
1566
|
+
//
|
|
1567
|
+
// The old logic is still left as it's as refactoring is painful and would probably break something and just
|
|
1568
|
+
// in general, take a lot of time. Therefore, this sliceWasSplit boolean is used to just skip diffing.
|
|
1569
|
+
if (ins.sliceWasSplit) {
|
|
1570
|
+
updated.push(ins);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
// Start diffing from the start of the deleted range
|
|
1574
|
+
const deleteStart = deleted.reduce((acc, cur) => {
|
|
1575
|
+
if (cur.type === 'delete-node') {
|
|
1576
|
+
return Math.min(acc, cur.pos);
|
|
1577
|
+
}
|
|
1578
|
+
else if (cur.type === 'delete-text') {
|
|
1579
|
+
return Math.min(acc, cur.from);
|
|
1580
|
+
}
|
|
1581
|
+
return acc;
|
|
1582
|
+
}, Number.MAX_SAFE_INTEGER);
|
|
1583
|
+
const [inDeleted, updatedDel] = matchInserted(deleteStart, updatedDeleted, ins.slice.content);
|
|
1584
|
+
if (inDeleted === deleteStart) {
|
|
1470
1585
|
updated.push(ins);
|
|
1471
1586
|
return;
|
|
1472
1587
|
}
|
|
1473
1588
|
updatedDeleted = updatedDel;
|
|
1474
|
-
const
|
|
1475
|
-
const newInsertedB = ins.slice.content.cut(ins.from - inDeleted + 1);
|
|
1476
|
-
// Super hax to cut over block node boundaries in the inserted fragment
|
|
1477
|
-
const newInserted = newInsertedA.size === newInsertedB.size + 2 ? newInsertedB : newInsertedA;
|
|
1589
|
+
const newInserted = cutFragment(0, inDeleted, ins.slice.content)[1];
|
|
1478
1590
|
if (newInserted.size > 0) {
|
|
1479
1591
|
updated.push({
|
|
1480
1592
|
...ins,
|
|
@@ -1482,6 +1594,7 @@ function diffChangeSteps(deleted, inserted, newTr, schema) {
|
|
|
1482
1594
|
});
|
|
1483
1595
|
}
|
|
1484
1596
|
});
|
|
1597
|
+
log.info('FINISH DIFF: ', [...updatedDeleted, ...updated]);
|
|
1485
1598
|
return [...updatedDeleted, ...updated];
|
|
1486
1599
|
}
|
|
1487
1600
|
|
|
@@ -1697,10 +1810,10 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1697
1810
|
ids.forEach((changeId) => {
|
|
1698
1811
|
const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
|
|
1699
1812
|
if (change) {
|
|
1700
|
-
createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
|
|
1701
|
-
setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
|
|
1813
|
+
createdTr = updateChangeAttrs(createdTr, change, { ...change.dataTracked, status, reviewedByID: userID }, oldState.schema);
|
|
1702
1814
|
}
|
|
1703
1815
|
});
|
|
1816
|
+
setAction(createdTr, TrackChangesAction.updateChanges, ids);
|
|
1704
1817
|
}
|
|
1705
1818
|
else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
|
|
1706
1819
|
const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
|