@manuscripts/track-changes-plugin 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -86,11 +86,11 @@ var CHANGE_OPERATION;
86
86
  (function (CHANGE_OPERATION) {
87
87
  CHANGE_OPERATION["insert"] = "insert";
88
88
  CHANGE_OPERATION["delete"] = "delete";
89
- CHANGE_OPERATION["set_node_attributes"] = "set_node_attributes";
90
- CHANGE_OPERATION["wrap_with_node"] = "wrap_with_node";
91
- CHANGE_OPERATION["unwrap_from_node"] = "unwrap_from_node";
92
- CHANGE_OPERATION["add_mark"] = "add_mark";
93
- CHANGE_OPERATION["remove_mark"] = "remove_mark";
89
+ CHANGE_OPERATION["set_node_attributes"] = "set_attrs";
90
+ // wrap_with_node = 'wrap_with_node',
91
+ // unwrap_from_node = 'unwrap_from_node',
92
+ // add_mark = 'add_mark',
93
+ // remove_mark = 'remove_mark',
94
94
  })(CHANGE_OPERATION || (CHANGE_OPERATION = {}));
95
95
  var CHANGE_STATUS;
96
96
  (function (CHANGE_STATUS) {
@@ -191,16 +191,13 @@ class ChangeSet {
191
191
  rootNodes.push(currentNodeChange);
192
192
  currentNodeChange = undefined;
193
193
  }
194
- if (c.type === 'node-change' && currentNodeChange && c.from < currentNodeChange.to) {
194
+ if (currentNodeChange && c.from < currentNodeChange.to) {
195
195
  currentNodeChange.children.push(c);
196
196
  }
197
197
  else if (c.type === 'node-change') {
198
198
  currentNodeChange = { ...c, children: [] };
199
199
  }
200
- else if (c.type === 'text-change' && currentNodeChange && c.from < currentNodeChange.to) {
201
- currentNodeChange.children.push(c);
202
- }
203
- else if (c.type === 'text-change') {
200
+ else {
204
201
  rootNodes.push(c);
205
202
  }
206
203
  });
@@ -224,6 +221,9 @@ class ChangeSet {
224
221
  get nodeChanges() {
225
222
  return this.changes.filter((c) => c.type === 'node-change');
226
223
  }
224
+ get nodeAttrChanges() {
225
+ return this.changes.filter((c) => c.type === 'node-attr-change');
226
+ }
227
227
  get isEmpty() {
228
228
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
229
229
  }
@@ -269,9 +269,7 @@ class ChangeSet {
269
269
  * @param change
270
270
  */
271
271
  static shouldNotDelete(change) {
272
- const { status, operation } = change.attrs;
273
- return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.accepted) ||
274
- (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.rejected));
272
+ return !ChangeSet.shouldDeleteChange(change);
275
273
  }
276
274
  /**
277
275
  * Determines whether a change should be deleted when applying it to the document.
@@ -299,7 +297,7 @@ class ChangeSet {
299
297
  'updatedAt',
300
298
  ];
301
299
  // reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
302
- // So it can be either omitted completely or at least null or string
300
+ // So it can be either omitted completely or at least be null or string
303
301
  const optionalKeys = ['reviewedByID'];
304
302
  const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
305
303
  const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
@@ -315,9 +313,89 @@ class ChangeSet {
315
313
  static isNodeChange(change) {
316
314
  return change.type === 'node-change';
317
315
  }
316
+ static isNodeAttrChange(change) {
317
+ return change.type === 'node-attr-change';
318
+ }
318
319
  }
319
320
  _ChangeSet_changes = new WeakMap();
320
321
 
322
+ /*!
323
+ * © 2021 Atypon Systems LLC
324
+ *
325
+ * Licensed under the Apache License, Version 2.0 (the "License");
326
+ * you may not use this file except in compliance with the License.
327
+ * You may obtain a copy of the License at
328
+ *
329
+ * http://www.apache.org/licenses/LICENSE-2.0
330
+ *
331
+ * Unless required by applicable law or agreed to in writing, software
332
+ * distributed under the License is distributed on an "AS IS" BASIS,
333
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
334
+ * See the License for the specific language governing permissions and
335
+ * limitations under the License.
336
+ */
337
+ function uuidv4() {
338
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
339
+ const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
340
+ return v.toString(16);
341
+ });
342
+ }
343
+
344
+ function addTrackIdIfDoesntExist(attrs) {
345
+ if (!attrs.id) {
346
+ return {
347
+ id: uuidv4(),
348
+ ...attrs,
349
+ };
350
+ }
351
+ return attrs;
352
+ }
353
+ function getInlineNodeTrackedMarkData(node, schema) {
354
+ if (!node || !node.isInline) {
355
+ return undefined;
356
+ }
357
+ const marksTrackedData = [];
358
+ node.marks.forEach((mark) => {
359
+ if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
360
+ const operation = mark.type === schema.marks.tracked_insert
361
+ ? CHANGE_OPERATION.insert
362
+ : CHANGE_OPERATION.delete;
363
+ marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
364
+ }
365
+ });
366
+ if (marksTrackedData.length > 1) {
367
+ log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
368
+ }
369
+ return marksTrackedData[0] || undefined;
370
+ }
371
+ function getNodeTrackedData(node, schema) {
372
+ return !node
373
+ ? undefined
374
+ : node.isText
375
+ ? getInlineNodeTrackedMarkData(node, schema)
376
+ : node.attrs.dataTracked;
377
+ }
378
+ function equalMarks(n1, n2) {
379
+ return (n1.marks.length === n2.marks.length &&
380
+ n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
381
+ }
382
+ function shouldMergeTrackedAttributes(left, right) {
383
+ if (!left || !right) {
384
+ log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
385
+ left,
386
+ right,
387
+ });
388
+ return false;
389
+ }
390
+ return (left.status === right.status &&
391
+ left.operation === right.operation &&
392
+ left.authorID === right.authorID);
393
+ }
394
+ function getMergeableMarkTrackedAttrs(node, attrs, schema) {
395
+ const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
396
+ return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
397
+ }
398
+
321
399
  /*!
322
400
  * © 2021 Atypon Systems LLC
323
401
  *
@@ -405,83 +483,6 @@ function mergeNode(node, pos, tr) {
405
483
  return undefined;
406
484
  }
407
485
 
408
- /*!
409
- * © 2021 Atypon Systems LLC
410
- *
411
- * Licensed under the Apache License, Version 2.0 (the "License");
412
- * you may not use this file except in compliance with the License.
413
- * You may obtain a copy of the License at
414
- *
415
- * http://www.apache.org/licenses/LICENSE-2.0
416
- *
417
- * Unless required by applicable law or agreed to in writing, software
418
- * distributed under the License is distributed on an "AS IS" BASIS,
419
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
420
- * See the License for the specific language governing permissions and
421
- * limitations under the License.
422
- */
423
- function uuidv4() {
424
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
425
- const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
426
- return v.toString(16);
427
- });
428
- }
429
-
430
- function addTrackIdIfDoesntExist(attrs) {
431
- if (!attrs.id) {
432
- return {
433
- id: uuidv4(),
434
- ...attrs,
435
- };
436
- }
437
- return attrs;
438
- }
439
- function getInlineNodeTrackedMarkData(node, schema) {
440
- if (!node || !node.isInline) {
441
- return undefined;
442
- }
443
- const marksTrackedData = [];
444
- node.marks.forEach((mark) => {
445
- if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
446
- const operation = mark.type === schema.marks.tracked_insert
447
- ? CHANGE_OPERATION.insert
448
- : CHANGE_OPERATION.delete;
449
- marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
450
- }
451
- });
452
- if (marksTrackedData.length > 1) {
453
- log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
454
- }
455
- return marksTrackedData[0] || undefined;
456
- }
457
- function getNodeTrackedData(node, schema) {
458
- return !node
459
- ? undefined
460
- : node.isText
461
- ? getInlineNodeTrackedMarkData(node, schema)
462
- : node.attrs.dataTracked;
463
- }
464
- function equalMarks(n1, n2) {
465
- return (n1.marks.length === n2.marks.length &&
466
- n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
467
- }
468
- function shouldMergeTrackedAttributes(left, right) {
469
- if (!left || !right) {
470
- log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
471
- left,
472
- right,
473
- });
474
- return false;
475
- }
476
- return (left.status === right.status &&
477
- left.operation === right.operation &&
478
- left.authorID === right.authorID);
479
- }
480
- function getMergeableMarkTrackedAttrs(node, attrs, schema) {
481
- const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
482
- return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
483
- }
484
-
485
486
  function updateChangeAttrs(tr, change, trackedAttrs, schema) {
486
487
  const node = tr.doc.nodeAt(change.from);
487
488
  if (!node) {
@@ -554,6 +555,16 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mappi
554
555
  }
555
556
  deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
556
557
  }
558
+ else if (ChangeSet.isNodeAttrChange(change) &&
559
+ change.attrs.status === CHANGE_STATUS.accepted) {
560
+ const attrs = { ...node.attrs, dataTracked: null };
561
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
562
+ }
563
+ else if (ChangeSet.isNodeAttrChange(change) &&
564
+ change.attrs.status === CHANGE_STATUS.rejected) {
565
+ const attrs = { ...change.oldAttrs, dataTracked: null };
566
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
567
+ }
557
568
  });
558
569
  return deleteMap;
559
570
  }
@@ -659,87 +670,6 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
659
670
  return changed;
660
671
  }
661
672
 
662
- /*!
663
- * © 2021 Atypon Systems LLC
664
- *
665
- * Licensed under the Apache License, Version 2.0 (the "License");
666
- * you may not use this file except in compliance with the License.
667
- * You may obtain a copy of the License at
668
- *
669
- * http://www.apache.org/licenses/LICENSE-2.0
670
- *
671
- * Unless required by applicable law or agreed to in writing, software
672
- * distributed under the License is distributed on an "AS IS" BASIS,
673
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
674
- * See the License for the specific language governing permissions and
675
- * limitations under the License.
676
- */
677
- function markInlineNodeChange(node, newTrackAttrs, schema) {
678
- const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
679
- const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
680
- ? schema.marks.tracked_insert
681
- : schema.marks.tracked_delete;
682
- const createdMark = mark.create({
683
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
684
- });
685
- return node.mark(filtered.concat(createdMark));
686
- }
687
- function recurseNodeContent(node, newTrackAttrs, schema) {
688
- if (node.isText) {
689
- return markInlineNodeChange(node, newTrackAttrs, schema);
690
- }
691
- else if (node.isBlock || node.isInline) {
692
- const updatedChildren = [];
693
- node.content.forEach((child) => {
694
- updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
695
- });
696
- return node.type.create({
697
- ...node.attrs,
698
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
699
- }, Fragment.fromArray(updatedChildren), node.marks);
700
- }
701
- else {
702
- log.error(`unhandled node type: "${node.type.name}"`, node);
703
- return node;
704
- }
705
- }
706
- function setFragmentAsInserted(inserted, insertAttrs, schema) {
707
- // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
708
- const updatedInserted = [];
709
- inserted.forEach((n) => {
710
- updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
711
- });
712
- return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
713
- }
714
-
715
- /*!
716
- * © 2021 Atypon Systems LLC
717
- *
718
- * Licensed under the Apache License, Version 2.0 (the "License");
719
- * you may not use this file except in compliance with the License.
720
- * You may obtain a copy of the License at
721
- *
722
- * http://www.apache.org/licenses/LICENSE-2.0
723
- *
724
- * Unless required by applicable law or agreed to in writing, software
725
- * distributed under the License is distributed on an "AS IS" BASIS,
726
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
727
- * See the License for the specific language governing permissions and
728
- * limitations under the License.
729
- */
730
- function createNewInsertAttrs(attrs) {
731
- return {
732
- ...attrs,
733
- operation: CHANGE_OPERATION.insert,
734
- };
735
- }
736
- function createNewDeleteAttrs(attrs) {
737
- return {
738
- ...attrs,
739
- operation: CHANGE_OPERATION.delete,
740
- };
741
- }
742
-
743
673
  /*!
744
674
  * © 2021 Atypon Systems LLC
745
675
  *
@@ -835,73 +765,104 @@ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
835
765
  firstMergedNode,
836
766
  lastMergedNode,
837
767
  };
838
- }
839
- /**
840
- * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
768
+ }
769
+
770
+ /*!
771
+ * © 2021 Atypon Systems LLC
841
772
  *
842
- * This would work for general inline nodes too, but since node marks don't work properly
843
- * with Yjs, attributes are used instead.
844
- * @param node
845
- * @param pos
846
- * @param newTr
847
- * @param schema
848
- * @param deleteAttrs
849
- * @param from
850
- * @param to
773
+ * Licensed under the Apache License, Version 2.0 (the "License");
774
+ * you may not use this file except in compliance with the License.
775
+ * You may obtain a copy of the License at
776
+ *
777
+ * http://www.apache.org/licenses/LICENSE-2.0
778
+ *
779
+ * Unless required by applicable law or agreed to in writing, software
780
+ * distributed under the License is distributed on an "AS IS" BASIS,
781
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
782
+ * See the License for the specific language governing permissions and
783
+ * limitations under the License.
851
784
  */
852
- function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
853
- const start = from ? Math.max(pos, from) : pos;
854
- const nodeEnd = pos + node.nodeSize;
855
- const end = to ? Math.min(nodeEnd, to) : nodeEnd;
856
- if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
857
- // Math.max(pos, from) is for picking always the start of the node,
858
- // not the start of the change (which might span multiple nodes).
859
- // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
860
- newTr.replaceWith(start, end, Fragment.empty);
861
- return start;
785
+ function markInlineNodeChange(node, newTrackAttrs, schema) {
786
+ const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
787
+ const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
788
+ ? schema.marks.tracked_insert
789
+ : schema.marks.tracked_delete;
790
+ const createdMark = mark.create({
791
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
792
+ });
793
+ return node.mark(filtered.concat(createdMark));
794
+ }
795
+ function recurseNodeContent(node, newTrackAttrs, schema) {
796
+ if (node.isText) {
797
+ return markInlineNodeChange(node, newTrackAttrs, schema);
798
+ }
799
+ else if (node.isBlock || node.isInline) {
800
+ const updatedChildren = [];
801
+ node.content.forEach((child) => {
802
+ updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
803
+ });
804
+ return node.type.create({
805
+ ...node.attrs,
806
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
807
+ }, Fragment.fromArray(updatedChildren), node.marks);
862
808
  }
863
809
  else {
864
- const leftNode = newTr.doc.resolve(start).nodeBefore;
865
- const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
866
- const rightNode = newTr.doc.resolve(end).nodeAfter;
867
- const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
868
- const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
869
- const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
870
- const createdAt = Math.min((leftMarks === null || leftMarks === void 0 ? void 0 : leftMarks.createdAt) || Number.MAX_VALUE, (rightMarks === null || rightMarks === void 0 ? void 0 : rightMarks.createdAt) || Number.MAX_VALUE, deleteAttrs.createdAt);
871
- const dataTracked = addTrackIdIfDoesntExist({
872
- ...leftMarks,
873
- ...rightMarks,
874
- ...deleteAttrs,
875
- createdAt,
876
- });
877
- newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
878
- dataTracked,
879
- }));
880
- return toEndOfMark;
810
+ log.error(`unhandled node type: "${node.type.name}"`, node);
811
+ return node;
881
812
  }
882
813
  }
883
- /**
884
- * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
885
- * @param node
886
- * @param pos
887
- * @param newTr
888
- * @param deleteAttrs
814
+ function setFragmentAsInserted(inserted, insertAttrs, schema) {
815
+ // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
816
+ const updatedInserted = [];
817
+ inserted.forEach((n) => {
818
+ updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
819
+ });
820
+ return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
821
+ }
822
+
823
+ /*!
824
+ * © 2021 Atypon Systems LLC
825
+ *
826
+ * Licensed under the Apache License, Version 2.0 (the "License");
827
+ * you may not use this file except in compliance with the License.
828
+ * You may obtain a copy of the License at
829
+ *
830
+ * http://www.apache.org/licenses/LICENSE-2.0
831
+ *
832
+ * Unless required by applicable law or agreed to in writing, software
833
+ * distributed under the License is distributed on an "AS IS" BASIS,
834
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
835
+ * See the License for the specific language governing permissions and
836
+ * limitations under the License.
889
837
  */
890
- function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
891
- const dataTracked = node.attrs.dataTracked;
892
- const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === CHANGE_OPERATION.insert &&
893
- dataTracked.authorID === deleteAttrs.authorID;
894
- if (wasInsertedBySameUser) {
895
- deleteNode(node, pos, newTr);
896
- }
897
- else {
898
- const attrs = {
899
- ...node.attrs,
900
- dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
901
- };
902
- newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
903
- }
838
+ function createNewInsertAttrs(attrs) {
839
+ return {
840
+ ...attrs,
841
+ operation: CHANGE_OPERATION.insert,
842
+ };
904
843
  }
844
+ function createNewDeleteAttrs(attrs) {
845
+ return {
846
+ ...attrs,
847
+ operation: CHANGE_OPERATION.delete,
848
+ };
849
+ }
850
+
851
+ /*!
852
+ * © 2021 Atypon Systems LLC
853
+ *
854
+ * Licensed under the Apache License, Version 2.0 (the "License");
855
+ * you may not use this file except in compliance with the License.
856
+ * You may obtain a copy of the License at
857
+ *
858
+ * http://www.apache.org/licenses/LICENSE-2.0
859
+ *
860
+ * Unless required by applicable law or agreed to in writing, software
861
+ * distributed under the License is distributed on an "AS IS" BASIS,
862
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
863
+ * See the License for the specific language governing permissions and
864
+ * limitations under the License.
865
+ */
905
866
  /**
906
867
  * Applies deletion to the doc without actually deleting nodes that have not been inserted
907
868
  *
@@ -929,18 +890,17 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
929
890
  */
930
891
  function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
931
892
  const deleteMap = new Mapping();
932
- const mergedInsertPos = undefined;
893
+ const steps = [];
933
894
  // No deletion applied, return default values
934
895
  if (from === to) {
935
896
  return {
936
897
  deleteMap,
937
- mergedInsertPos,
938
898
  newSliceContent: insertSlice.content,
899
+ steps,
939
900
  };
940
901
  }
941
902
  const { openStart, openEnd } = insertSlice;
942
903
  const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
943
- const deleteAttrs = createNewDeleteAttrs(trackAttrs);
944
904
  let mergingStartSide = true;
945
905
  startDoc.nodesBetween(from, to, (node, pos) => {
946
906
  const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
@@ -1016,24 +976,18 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1016
976
  // ProseMirror node semantics as start tokens are considered to contain the actual node itself.
1017
977
  const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
1018
978
  if (mergeStartNode || mergeEndNode) {
1019
- // The default insert position for block nodes is either the start of the merged content or the end.
1020
- // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1021
- // actual position of the merge. Currently the inserted content is inserted at the start or end
1022
- // of the merged content, TODO reverse the start/end when end/start token?
1023
- let insertPos = mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd;
1024
- if (node.isText) {
1025
- // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1026
- // the text node.
1027
- insertPos = deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1028
- deleteMap.appendMap(newTr.steps[newTr.steps.length - 1].getMap());
1029
- step = newTr.steps[newTr.steps.length - 1];
1030
- }
1031
979
  // Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
1032
980
  // it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
1033
981
  // the paragraph.
1034
- if (mergeContent.size !== 0) {
1035
- newTr.insert(insertPos, setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema));
1036
- }
982
+ steps.push({
983
+ type: 'merge-fragment',
984
+ pos: offsetPos,
985
+ mergePos: mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd,
986
+ from: offsetFrom,
987
+ to: offsetTo,
988
+ node,
989
+ fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
990
+ });
1037
991
  // Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
1038
992
  // of merged nodes & content, as just using mapped toA in that case isn't the same.
1039
993
  // The calculation is a bit mysterious, I admit.
@@ -1046,12 +1000,23 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1046
1000
  else if (node.isText) {
1047
1001
  // Text deletion is handled even when the deletion doesn't completely wrap the text node
1048
1002
  // (which is basically the case most of the time)
1049
- deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1003
+ steps.push({
1004
+ type: 'delete-text',
1005
+ pos: offsetPos,
1006
+ from: Math.max(offsetPos, offsetFrom),
1007
+ to: Math.min(nodeEnd, offsetTo),
1008
+ node,
1009
+ });
1050
1010
  }
1051
1011
  else ;
1052
1012
  }
1053
1013
  else if (nodeCompletelyDeleted) {
1054
- deleteOrSetNodeDeleted(node, offsetPos, newTr, deleteAttrs);
1014
+ steps.push({
1015
+ type: 'delete-node',
1016
+ pos: offsetPos,
1017
+ nodeEnd: nodeEnd,
1018
+ node,
1019
+ });
1055
1020
  }
1056
1021
  }
1057
1022
  const newestStep = newTr.steps[newTr.steps.length - 1];
@@ -1061,10 +1026,10 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1061
1026
  });
1062
1027
  return {
1063
1028
  deleteMap,
1064
- mergedInsertPos,
1065
1029
  newSliceContent: updatedSliceNodes
1066
1030
  ? Fragment.fromArray(updatedSliceNodes)
1067
1031
  : insertSlice.content,
1032
+ steps,
1068
1033
  };
1069
1034
  }
1070
1035
 
@@ -1092,8 +1057,7 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
1092
1057
  if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
1093
1058
  return;
1094
1059
  }
1095
- const isLeftOlder = (leftDataTracked.createdAt || Number.MAX_VALUE) <
1096
- (rightDataTracked.createdAt || Number.MAX_VALUE);
1060
+ const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
1097
1061
  const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
1098
1062
  const dataTracked = {
1099
1063
  ...ancestorAttrs,
@@ -1149,14 +1113,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1149
1113
  const stepResult = newTr.maybeStep(newStep);
1150
1114
  if (stepResult.failed) {
1151
1115
  log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
1152
- return;
1116
+ return [];
1153
1117
  }
1154
1118
  const gap = oldState.doc.slice(gapFrom, gapTo);
1155
1119
  log.info('RETAINED GAP CONTENT', gap);
1156
1120
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1157
1121
  // eg partial nodes in an open-ended slice
1158
- const { deleteMap, newSliceContent } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1122
+ const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1123
+ let steps = deleteSteps;
1159
1124
  log.info('TR: new steps after applying delete', [...newTr.steps]);
1125
+ log.info('DELETE STEPS: ', deleteSteps);
1160
1126
  // We only want to insert when there something inside the gap (actually would this be always true?)
1161
1127
  // or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
1162
1128
  if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
@@ -1171,21 +1137,19 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1171
1137
  insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1172
1138
  log.info('insertedSlice after inserted gap', insertedSlice);
1173
1139
  }
1174
- const newStep = new ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1175
- const stepResult = newTr.maybeStep(newStep);
1176
- if (stepResult.failed) {
1177
- log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1178
- return;
1179
- }
1180
- log.info('new steps after applying insert', [...newTr.steps]);
1181
- mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1182
- mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1140
+ deleteSteps.push({
1141
+ type: 'insert-slice',
1142
+ from: deleteMap.map(gapFrom),
1143
+ to: deleteMap.map(gapTo),
1144
+ slice: insertedSlice,
1145
+ });
1183
1146
  }
1184
1147
  else {
1185
1148
  // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1186
1149
  mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1187
1150
  mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1188
1151
  }
1152
+ return steps;
1189
1153
  }
1190
1154
 
1191
1155
  /*!
@@ -1205,7 +1169,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1205
1169
  */
1206
1170
  function trackReplaceStep(step, oldState, newTr, attrs) {
1207
1171
  log.info('###### ReplaceStep ######');
1208
- let selectionPos = 0;
1172
+ let selectionPos = 0, changeSteps = [];
1209
1173
  step.getMap().forEach((fromA, toA, fromB, toB) => {
1210
1174
  log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
1211
1175
  const { slice } = step;
@@ -1219,34 +1183,306 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1219
1183
  log.info('TR: steps before applying delete', [...newTr.steps]);
1220
1184
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1221
1185
  // eg partial nodes in an open-ended slice
1222
- const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1186
+ const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1187
+ changeSteps.push(...deleteSteps);
1223
1188
  log.info('TR: steps after applying delete', [...newTr.steps]);
1224
- const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1189
+ log.info('DELETE STEPS: ', changeSteps);
1190
+ const adjustedInsertPos = deleteMap.map(toA);
1225
1191
  if (newSliceContent.size > 0) {
1226
1192
  log.info('newSliceContent', newSliceContent);
1227
1193
  // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1228
1194
  // the sides should be equal. TODO can they be other than 0?
1229
1195
  const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1230
1196
  const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
1231
- const insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1232
- const newStep = new ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1197
+ changeSteps.push({
1198
+ type: 'insert-slice',
1199
+ from: adjustedInsertPos,
1200
+ to: adjustedInsertPos,
1201
+ slice: new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
1202
+ });
1203
+ }
1204
+ else {
1205
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1206
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1207
+ selectionPos = fromA;
1208
+ }
1209
+ });
1210
+ return [changeSteps, selectionPos];
1211
+ }
1212
+
1213
+ /*!
1214
+ * © 2021 Atypon Systems LLC
1215
+ *
1216
+ * Licensed under the Apache License, Version 2.0 (the "License");
1217
+ * you may not use this file except in compliance with the License.
1218
+ * You may obtain a copy of the License at
1219
+ *
1220
+ * http://www.apache.org/licenses/LICENSE-2.0
1221
+ *
1222
+ * Unless required by applicable law or agreed to in writing, software
1223
+ * distributed under the License is distributed on an "AS IS" BASIS,
1224
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1225
+ * See the License for the specific language governing permissions and
1226
+ * limitations under the License.
1227
+ */
1228
+ /**
1229
+ * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
1230
+ *
1231
+ * This would work for general inline nodes too, but since node marks don't work properly
1232
+ * with Yjs, attributes are used instead.
1233
+ * @param node
1234
+ * @param pos
1235
+ * @param newTr
1236
+ * @param schema
1237
+ * @param deleteAttrs
1238
+ * @param from
1239
+ * @param to
1240
+ */
1241
+ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
1242
+ const start = from ? Math.max(pos, from) : pos;
1243
+ const nodeEnd = pos + node.nodeSize;
1244
+ const end = to ? Math.min(nodeEnd, to) : nodeEnd;
1245
+ if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1246
+ // Math.max(pos, from) is for picking always the start of the node,
1247
+ // not the start of the change (which might span multiple nodes).
1248
+ // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
1249
+ newTr.replaceWith(start, end, Fragment.empty);
1250
+ return start;
1251
+ }
1252
+ else {
1253
+ const leftNode = newTr.doc.resolve(start).nodeBefore;
1254
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1255
+ const rightNode = newTr.doc.resolve(end).nodeAfter;
1256
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1257
+ const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1258
+ const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1259
+ const createdAt = Math.min((leftMarks === null || leftMarks === void 0 ? void 0 : leftMarks.createdAt) || Number.MAX_VALUE, (rightMarks === null || rightMarks === void 0 ? void 0 : rightMarks.createdAt) || Number.MAX_VALUE, deleteAttrs.createdAt);
1260
+ const dataTracked = addTrackIdIfDoesntExist({
1261
+ ...leftMarks,
1262
+ ...rightMarks,
1263
+ ...deleteAttrs,
1264
+ createdAt,
1265
+ });
1266
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1267
+ dataTracked,
1268
+ }));
1269
+ return toEndOfMark;
1270
+ }
1271
+ }
1272
+
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
+ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
1289
+ const mapping = new Mapping();
1290
+ const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
1291
+ let selectionPos = startPos;
1292
+ // @TODO add custom handler / condition?
1293
+ changes.forEach((c) => {
1294
+ let step = newTr.steps[newTr.steps.length - 1];
1295
+ log.info('process change: ', c);
1296
+ // const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
1297
+ if (c.type === 'delete-node') {
1298
+ const dataTracked = c.node.attrs.dataTracked;
1299
+ const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === CHANGE_OPERATION.insert &&
1300
+ dataTracked.authorID === emptyAttrs.authorID;
1301
+ if (wasInsertedBySameUser) {
1302
+ deleteNode(c.node, mapping.map(c.pos), newTr);
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);
1316
+ }
1317
+ }
1318
+ else if (c.type === 'delete-text') {
1319
+ const from = mapping.map(c.from, -1);
1320
+ const to = mapping.map(c.to, 1);
1321
+ const node = newTr.doc.nodeAt(mapping.map(c.pos));
1322
+ if (node === null || node === void 0 ? void 0 : node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1323
+ newTr.replaceWith(from, to, Fragment.empty);
1324
+ mergeTrackedMarks(from, newTr.doc, newTr, schema);
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
+ }));
1341
+ }
1342
+ }
1343
+ else if (c.type === 'merge-fragment') {
1344
+ let insertPos = mapping.map(c.mergePos);
1345
+ // The default insert position for block nodes is either the start of the merged content or the end.
1346
+ // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1347
+ // actual position of the merge. Currently the inserted content is inserted at the start or end
1348
+ // of the merged content, TODO reverse the start/end when end/start token?
1349
+ if (c.node.isText) {
1350
+ // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1351
+ // the text node.
1352
+ insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1353
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1354
+ if (step !== newestStep) {
1355
+ mapping.appendMap(newestStep.getMap());
1356
+ step = newestStep;
1357
+ }
1358
+ }
1359
+ if (c.fragment.size > 0) {
1360
+ newTr.insert(insertPos, c.fragment);
1361
+ }
1362
+ }
1363
+ else if (c.type === 'insert-slice') {
1364
+ const newStep = new ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
1233
1365
  const stepResult = newTr.maybeStep(newStep);
1234
1366
  if (stepResult.failed) {
1235
1367
  log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1236
1368
  return;
1237
1369
  }
1238
- log.info('new steps after applying insert', [...newTr.steps]);
1239
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1240
- mergeTrackedMarks(adjustedInsertPos + insertedSlice.size, newTr.doc, newTr, oldState.schema);
1241
- selectionPos = adjustedInsertPos + insertedSlice.size;
1370
+ mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
1371
+ mergeTrackedMarks(mapping.map(c.to), newTr.doc, newTr, schema);
1372
+ selectionPos = mapping.map(c.to) + c.slice.size;
1242
1373
  }
1243
- else {
1244
- // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1245
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1246
- selectionPos = fromA;
1374
+ else if (c.type === 'update-node-attrs') {
1375
+ const oldDataTracked = c.oldAttrs.dataTracked;
1376
+ const oldAttrs = (oldDataTracked === null || oldDataTracked === void 0 ? void 0 : oldDataTracked.operation) === CHANGE_OPERATION.set_node_attributes
1377
+ ? oldDataTracked.oldAttrs
1378
+ : c.oldAttrs;
1379
+ const dataTracked = addTrackIdIfDoesntExist({
1380
+ ...oldDataTracked,
1381
+ oldAttrs,
1382
+ ...emptyAttrs,
1383
+ operation: CHANGE_OPERATION.set_node_attributes,
1384
+ });
1385
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, { ...c.newAttrs, dataTracked });
1386
+ }
1387
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1388
+ if (step !== newestStep) {
1389
+ mapping.appendMap(newestStep.getMap());
1390
+ }
1391
+ });
1392
+ return [mapping, selectionPos];
1393
+ }
1394
+
1395
+ /*!
1396
+ * © 2021 Atypon Systems LLC
1397
+ *
1398
+ * Licensed under the Apache License, Version 2.0 (the "License");
1399
+ * you may not use this file except in compliance with the License.
1400
+ * You may obtain a copy of the License at
1401
+ *
1402
+ * http://www.apache.org/licenses/LICENSE-2.0
1403
+ *
1404
+ * Unless required by applicable law or agreed to in writing, software
1405
+ * distributed under the License is distributed on an "AS IS" BASIS,
1406
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1407
+ * See the License for the specific language governing permissions and
1408
+ * limitations under the License.
1409
+ */
1410
+ function matchInserted(inDeleted, deleted, inserted, newTr, schema) {
1411
+ var _a;
1412
+ for (let i = 0;; i += 1) {
1413
+ if (inserted.childCount === i)
1414
+ return [inDeleted, deleted];
1415
+ const child = inserted.child(i);
1416
+ // @ts-ignore
1417
+ let adjDeleted = deleted.find((d) => (d.type === 'delete-text' && d.to === inDeleted) ||
1418
+ (d.type === 'delete-node' && d.nodeEnd === inDeleted));
1419
+ if (child.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
1420
+ return [inDeleted, deleted];
1421
+ }
1422
+ else if (child.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
1423
+ adjDeleted = adjDeleted;
1424
+ const { pos, from, to, node } = adjDeleted;
1425
+ let j = 0, d = from - pos, maxSteps = Math.max(pos, from) - to;
1426
+ // Match text inside the inserted text node to the deleted text node
1427
+ for (; maxSteps !== j && child.text[j] === node.text[d]; j += 1, d += 1) {
1428
+ inDeleted -= 1;
1429
+ }
1430
+ // this is needed incase diffing tr.doc
1431
+ // deleted.push({
1432
+ // pos: pos,
1433
+ // type: 'update-node-attrs',
1434
+ // // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1435
+ // oldAttrs: adjDeleted.node.attrs || {},
1436
+ // newAttrs: child.attrs || {},
1437
+ // })
1438
+ if (maxSteps !== j) {
1439
+ deleted.push({
1440
+ pos,
1441
+ from: Math.max(pos, from) + j,
1442
+ to,
1443
+ type: 'delete-text',
1444
+ node,
1445
+ });
1446
+ }
1447
+ return [inDeleted, deleted.filter((d) => d !== adjDeleted)];
1448
+ }
1449
+ else if (child.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
1450
+ // Move the inDeleted inside the block node's boundary
1451
+ return matchInserted(inDeleted - 1, deleted.filter((d) => d !== adjDeleted), child.content);
1452
+ }
1453
+ deleted.push({
1454
+ pos: adjDeleted.pos,
1455
+ type: 'update-node-attrs',
1456
+ // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1457
+ oldAttrs: adjDeleted.node.attrs || {},
1458
+ newAttrs: child.attrs || {},
1459
+ });
1460
+ deleted = deleted.filter((d) => d !== adjDeleted);
1461
+ inDeleted -= child.nodeSize;
1462
+ }
1463
+ }
1464
+ function diffChangeSteps(deleted, inserted, newTr, schema) {
1465
+ const updated = [];
1466
+ let updatedDeleted = [...deleted];
1467
+ inserted.forEach((ins) => {
1468
+ const [inDeleted, updatedDel] = matchInserted(ins.from, updatedDeleted, ins.slice.content);
1469
+ if (inDeleted === ins.from) {
1470
+ updated.push(ins);
1471
+ return;
1472
+ }
1473
+ updatedDeleted = updatedDel;
1474
+ const newInsertedA = ins.slice.content.cut(ins.from - inDeleted);
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;
1478
+ if (newInserted.size > 0) {
1479
+ updated.push({
1480
+ ...ins,
1481
+ slice: new Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
1482
+ });
1247
1483
  }
1248
1484
  });
1249
- return selectionPos;
1485
+ return [...updatedDeleted, ...updated];
1250
1486
  }
1251
1487
 
1252
1488
  /**
@@ -1300,7 +1536,14 @@ function trackTransaction(tr, oldState, newTr, authorID) {
1300
1536
  return;
1301
1537
  }
1302
1538
  else if (step instanceof ReplaceStep) {
1303
- const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1539
+ let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1540
+ log.info('CHANGES: ', steps);
1541
+ // deleted and merged really...
1542
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1543
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1544
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1545
+ log.info('DIFFED STEPS: ', steps);
1546
+ const [mapping, selectionPos] = processChangeSteps(steps, startPos, newTr, emptyAttrs, oldState.schema);
1304
1547
  if (!wasNodeSelection) {
1305
1548
  const sel = getSelectionStaticConstructor(tr.selection);
1306
1549
  // Use Selection.near to fix selections that point to a block node instead of inline content
@@ -1311,10 +1554,16 @@ function trackTransaction(tr, oldState, newTr, authorID) {
1311
1554
  }
1312
1555
  }
1313
1556
  else if (step instanceof ReplaceAroundStep) {
1314
- trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1315
- // } else if (step instanceof AddMarkStep) {
1316
- // } else if (step instanceof RemoveMarkStep) {
1557
+ let steps = trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1558
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1559
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1560
+ log.info('INSERT STEPS: ', inserted);
1561
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1562
+ log.info('DIFFED STEPS: ', steps);
1563
+ processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
1317
1564
  }
1565
+ // } else if (step instanceof AddMarkStep) {
1566
+ // } else if (step instanceof RemoveMarkStep) {
1318
1567
  // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1319
1568
  // However, this should not be done by diffing and only matching node or char by char instead since
1320
1569
  // it's A easier and B more intuitive to user.
@@ -1473,21 +1722,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1473
1722
  });
1474
1723
  };
1475
1724
 
1476
- /*!
1477
- * © 2021 Atypon Systems LLC
1478
- *
1479
- * Licensed under the Apache License, Version 2.0 (the "License");
1480
- * you may not use this file except in compliance with the License.
1481
- * You may obtain a copy of the License at
1482
- *
1483
- * http://www.apache.org/licenses/LICENSE-2.0
1484
- *
1485
- * Unless required by applicable law or agreed to in writing, software
1486
- * distributed under the License is distributed on an "AS IS" BASIS,
1487
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1488
- * See the License for the specific language governing permissions and
1489
- * limitations under the License.
1490
- */
1491
1725
  /**
1492
1726
  * Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
1493
1727
  * set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.