@manuscripts/track-changes-plugin 0.3.0 → 0.4.2

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.
Files changed (29) hide show
  1. package/dist/ChangeSet.d.ts +6 -8
  2. package/dist/{track/steps/track-utils.d.ts → change-steps/diffChangeSteps.d.ts} +4 -3
  3. package/dist/change-steps/matchInserted.d.ts +3 -0
  4. package/dist/change-steps/processChangeSteps.d.ts +21 -0
  5. package/dist/{track → changes}/applyChanges.d.ts +0 -0
  6. package/dist/{track → changes}/findChanges.d.ts +0 -0
  7. package/dist/{track → changes}/fixInconsistentChanges.d.ts +1 -1
  8. package/dist/{track → changes}/updateChangeAttrs.d.ts +0 -0
  9. package/dist/commands.d.ts +1 -1
  10. package/dist/{track/node-utils.d.ts → compute/nodeHelpers.d.ts} +3 -2
  11. package/dist/{track/steps → compute}/setFragmentAsInserted.d.ts +1 -1
  12. package/dist/compute/splitSliceIntoMergedParts.d.ts +41 -0
  13. package/dist/index.cjs +749 -393
  14. package/dist/index.js +749 -393
  15. package/dist/{track/steps → mutate}/deleteAndMergeSplitNodes.d.ts +5 -5
  16. package/dist/{track → mutate}/deleteNode.d.ts +9 -0
  17. package/dist/mutate/deleteText.d.ts +32 -0
  18. package/dist/{track → mutate}/mergeNode.d.ts +0 -0
  19. package/dist/{track/steps → mutate}/mergeTrackedMarks.d.ts +0 -0
  20. package/dist/{track/steps → steps}/trackReplaceAroundStep.d.ts +3 -2
  21. package/dist/{track/steps → steps}/trackReplaceStep.d.ts +3 -2
  22. package/dist/{track → steps}/trackTransaction.d.ts +0 -0
  23. package/dist/types/change.d.ts +23 -15
  24. package/dist/types/step.d.ts +53 -0
  25. package/dist/types/track.d.ts +5 -1
  26. package/dist/utils/track-utils.d.ts +4 -0
  27. package/package.json +3 -2
  28. package/dist/index.es.js +0 -1547
  29. package/dist/types/editor.d.ts +0 -23
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) {
@@ -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.attrs.id) && ChangeSet.isValidTrackedAttrs(c.attrs);
175
- iteratedIds.add(c.attrs.id);
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
  }
@@ -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
  });
@@ -210,13 +207,13 @@ class ChangeSet {
210
207
  return rootNodes;
211
208
  }
212
209
  get pending() {
213
- return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.pending);
210
+ return this.changeTree.filter((c) => c.dataTracked.status === CHANGE_STATUS.pending);
214
211
  }
215
212
  get accepted() {
216
- return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.accepted);
213
+ return this.changeTree.filter((c) => c.dataTracked.status === CHANGE_STATUS.accepted);
217
214
  }
218
215
  get rejected() {
219
- return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.rejected);
216
+ return this.changeTree.filter((c) => c.dataTracked.status === CHANGE_STATUS.rejected);
220
217
  }
221
218
  get textChanges() {
222
219
  return this.changes.filter((c) => c.type === 'text-change');
@@ -224,6 +221,12 @@ 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
+ get bothNodeChanges() {
228
+ return this.changes.filter((c) => c.type === 'node-change' || c.type === 'node-attr-change');
229
+ }
227
230
  get isEmpty() {
228
231
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
229
232
  }
@@ -244,7 +247,7 @@ class ChangeSet {
244
247
  });
245
248
  }
246
249
  get hasIncompleteAttrs() {
247
- return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidTrackedAttrs(c.attrs));
250
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidDataTracked(c.dataTracked));
248
251
  }
249
252
  get(id) {
250
253
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id);
@@ -264,31 +267,22 @@ class ChangeSet {
264
267
  static flattenTreeToIds(changes) {
265
268
  return changes.flatMap((c) => this.isNodeChange(c) ? [c.id, ...c.children.map((c) => c.id)] : c.id);
266
269
  }
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
- const { status, operation } = change.attrs;
273
- return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.accepted) ||
274
- (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.rejected));
275
- }
276
270
  /**
277
271
  * Determines whether a change should be deleted when applying it to the document.
278
272
  * @param change
279
273
  */
280
274
  static shouldDeleteChange(change) {
281
- const { status, operation } = change.attrs;
275
+ const { status, operation } = change.dataTracked;
282
276
  return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.rejected) ||
283
277
  (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.accepted));
284
278
  }
285
279
  /**
286
280
  * Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
287
- * @param attrs
281
+ * @param dataTracked
288
282
  */
289
- static isValidTrackedAttrs(attrs = {}) {
290
- if ('attrs' in attrs) {
291
- log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
283
+ static isValidDataTracked(dataTracked = {}) {
284
+ if ('dataTracked' in dataTracked) {
285
+ log.warn('passed "dataTracked" as property to isValidTrackedAttrs()', dataTracked);
292
286
  }
293
287
  const trackedKeys = [
294
288
  'id',
@@ -299,14 +293,14 @@ class ChangeSet {
299
293
  'updatedAt',
300
294
  ];
301
295
  // 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
296
+ // So it can be either omitted completely or at least be null or string
303
297
  const optionalKeys = ['reviewedByID'];
304
- const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
305
- const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
298
+ const entries = Object.entries(dataTracked).filter(([key, val]) => trackedKeys.includes(key));
299
+ const optionalEntries = Object.entries(dataTracked).filter(([key, val]) => optionalKeys.includes(key));
306
300
  return (entries.length === trackedKeys.length &&
307
301
  entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
308
302
  optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
309
- (attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
303
+ (dataTracked.id || '').length > 0 // Changes created with undefined id have '' as placeholder
310
304
  );
311
305
  }
312
306
  static isTextChange(change) {
@@ -315,9 +309,102 @@ class ChangeSet {
315
309
  static isNodeChange(change) {
316
310
  return change.type === 'node-change';
317
311
  }
312
+ static isNodeAttrChange(change) {
313
+ return change.type === 'node-attr-change';
314
+ }
318
315
  }
319
316
  _ChangeSet_changes = new WeakMap();
320
317
 
318
+ /*!
319
+ * © 2021 Atypon Systems LLC
320
+ *
321
+ * Licensed under the Apache License, Version 2.0 (the "License");
322
+ * you may not use this file except in compliance with the License.
323
+ * You may obtain a copy of the License at
324
+ *
325
+ * http://www.apache.org/licenses/LICENSE-2.0
326
+ *
327
+ * Unless required by applicable law or agreed to in writing, software
328
+ * distributed under the License is distributed on an "AS IS" BASIS,
329
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
330
+ * See the License for the specific language governing permissions and
331
+ * limitations under the License.
332
+ */
333
+ function uuidv4() {
334
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
335
+ const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
336
+ return v.toString(16);
337
+ });
338
+ }
339
+
340
+ function addTrackIdIfDoesntExist(attrs) {
341
+ if (!attrs.id) {
342
+ return {
343
+ id: uuidv4(),
344
+ ...attrs,
345
+ };
346
+ }
347
+ return attrs;
348
+ }
349
+ function getTextNodeTrackedMarkData(node, schema) {
350
+ if (!node || !node.isText) {
351
+ return undefined;
352
+ }
353
+ const marksTrackedData = [];
354
+ node.marks.forEach((mark) => {
355
+ if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
356
+ const operation = mark.type === schema.marks.tracked_insert
357
+ ? CHANGE_OPERATION.insert
358
+ : CHANGE_OPERATION.delete;
359
+ marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
360
+ }
361
+ });
362
+ if (marksTrackedData.length > 1) {
363
+ log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
364
+ }
365
+ return marksTrackedData[0] || undefined;
366
+ }
367
+ function getBlockInlineTrackedData(node) {
368
+ let { dataTracked } = node.attrs;
369
+ if (dataTracked && !Array.isArray(dataTracked)) {
370
+ return [dataTracked];
371
+ }
372
+ return dataTracked;
373
+ }
374
+ function getNodeTrackedData(node, schema) {
375
+ let tracked;
376
+ if (node && !node.isText) {
377
+ tracked = getBlockInlineTrackedData(node);
378
+ }
379
+ else if (node === null || node === void 0 ? void 0 : node.isText) {
380
+ tracked = getTextNodeTrackedMarkData(node, schema);
381
+ }
382
+ if (tracked && !Array.isArray(tracked)) {
383
+ tracked = [tracked];
384
+ }
385
+ return tracked;
386
+ }
387
+ function equalMarks(n1, n2) {
388
+ return (n1.marks.length === n2.marks.length &&
389
+ n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
390
+ }
391
+ function shouldMergeTrackedAttributes(left, right) {
392
+ if (!left || !right) {
393
+ log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
394
+ left,
395
+ right,
396
+ });
397
+ return false;
398
+ }
399
+ return (left.status === right.status &&
400
+ left.operation === right.operation &&
401
+ left.authorID === right.authorID);
402
+ }
403
+ function getMergeableMarkTrackedAttrs(node, attrs, schema) {
404
+ const nodeAttrs = getTextNodeTrackedMarkData(node, schema);
405
+ return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
406
+ }
407
+
321
408
  /*!
322
409
  * © 2021 Atypon Systems LLC
323
410
  *
@@ -363,6 +450,29 @@ function deleteNode(node, pos, tr) {
363
450
  // child, say some wrapper blockNode, is also deleted the content could be retained. TODO I guess.
364
451
  return tr.delete(pos, pos + node.nodeSize);
365
452
  }
453
+ }
454
+ /**
455
+ * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
456
+ * @param node
457
+ * @param pos
458
+ * @param newTr
459
+ * @param deleteAttrs
460
+ */
461
+ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
462
+ const dataTracked = getBlockInlineTrackedData(node);
463
+ const inserted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === CHANGE_OPERATION.insert);
464
+ const deleted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === CHANGE_OPERATION.delete);
465
+ const updated = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === CHANGE_OPERATION.set_node_attributes);
466
+ if (inserted && inserted.authorID === deleteAttrs.authorID) {
467
+ return deleteNode(node, pos, newTr);
468
+ }
469
+ const newDeleted = deleted
470
+ ? { ...deleted, updatedAt: deleteAttrs.updatedAt }
471
+ : addTrackIdIfDoesntExist(deleteAttrs);
472
+ newTr.setNodeMarkup(pos, undefined, {
473
+ ...node.attrs,
474
+ dataTracked: updated ? [newDeleted, updated] : [newDeleted],
475
+ }, node.marks);
366
476
  }
367
477
 
368
478
  /*!
@@ -405,101 +515,49 @@ function mergeNode(node, pos, tr) {
405
515
  return undefined;
406
516
  }
407
517
 
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
518
  function updateChangeAttrs(tr, change, trackedAttrs, schema) {
486
519
  const node = tr.doc.nodeAt(change.from);
487
520
  if (!node) {
488
- throw Error('No node at the from of change' + change);
521
+ log.error('updateChangeAttrs: no node at the from of change ', change);
522
+ return tr;
523
+ }
524
+ const { operation } = trackedAttrs;
525
+ const oldTrackData = change.type === 'text-change'
526
+ ? getTextNodeTrackedMarkData(node, schema)
527
+ : getBlockInlineTrackedData(node);
528
+ if (!operation) {
529
+ log.warn('updateChangeAttrs: unable to determine operation of change ', change);
530
+ }
531
+ else if (!oldTrackData) {
532
+ log.warn('updateChangeAttrs: no old dataTracked for change ', change);
533
+ }
534
+ if (change.type === 'text-change') {
535
+ const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
536
+ if (!oldMark) {
537
+ log.warn('updateChangeAttrs: no track marks for a text-change ', change);
538
+ return tr;
539
+ }
540
+ // TODO add operation based on mark type if it's undefined?
541
+ tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked: { ...oldTrackData, ...trackedAttrs } }));
489
542
  }
490
- const dataTracked = { ...getNodeTrackedData(node, schema), ...trackedAttrs };
491
- const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
492
- if (change.type === 'text-change' && oldMark) {
493
- tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked }));
543
+ else if ((change.type === 'node-change' || change.type === 'node-attr-change') && !operation) {
544
+ // Very weird edge-case if this happens
545
+ tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
494
546
  }
495
- else if (change.type === 'node-change') {
496
- tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked }, node.marks);
547
+ else if (change.type === 'node-change' || change.type === 'node-attr-change') {
548
+ const newDataTracked = (getBlockInlineTrackedData(node) || []).map((oldTrack) => {
549
+ if (oldTrack.operation === operation) {
550
+ return { ...oldTrack, ...trackedAttrs };
551
+ }
552
+ return oldTrack;
553
+ });
554
+ tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: newDataTracked.length === 0 ? null : newDataTracked }, node.marks);
497
555
  }
498
556
  return tr;
499
557
  }
500
558
  function updateChangeChildrenAttributes(changes, tr, mapping) {
501
559
  changes.forEach((c) => {
502
- if (c.type === 'node-change' && ChangeSet.shouldNotDelete(c)) {
560
+ if (c.type === 'node-change' && !ChangeSet.shouldDeleteChange(c)) {
503
561
  const from = mapping.map(c.from);
504
562
  const node = tr.doc.nodeAt(from);
505
563
  if (!node) {
@@ -521,12 +579,12 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
521
579
  */
522
580
  function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mapping()) {
523
581
  changes.forEach((change) => {
524
- if (change.attrs.status === CHANGE_STATUS.pending) {
582
+ if (change.dataTracked.status === CHANGE_STATUS.pending) {
525
583
  return;
526
584
  }
527
585
  // Map change.from and skip those which dont need to be applied
528
586
  // or were already deleted by an applied block delete
529
- const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || ChangeSet.shouldNotDelete(change);
587
+ const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || !ChangeSet.shouldDeleteChange(change);
530
588
  if (!node) {
531
589
  !deleted && log.warn('no node found to update for change', change);
532
590
  return;
@@ -554,6 +612,14 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mappi
554
612
  }
555
613
  deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
556
614
  }
615
+ else if (ChangeSet.isNodeAttrChange(change) &&
616
+ change.dataTracked.status === CHANGE_STATUS.accepted) {
617
+ tr.setNodeMarkup(from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
618
+ }
619
+ else if (ChangeSet.isNodeAttrChange(change) &&
620
+ change.dataTracked.status === CHANGE_STATUS.rejected) {
621
+ tr.setNodeMarkup(from, undefined, { ...change.oldAttrs, dataTracked: null }, node.marks);
622
+ }
557
623
  });
558
624
  return deleteMap;
559
625
  }
@@ -572,9 +638,10 @@ function findChanges(state) {
572
638
  // Store the last iterated change to join adjacent text changes
573
639
  let current;
574
640
  state.doc.descendants((node, pos) => {
575
- const attrs = getNodeTrackedData(node, state.schema);
576
- if (attrs) {
577
- const id = (attrs === null || attrs === void 0 ? void 0 : attrs.id) || '';
641
+ const tracked = getNodeTrackedData(node, state.schema) || [];
642
+ for (let i = 0; i < tracked.length; i += 1) {
643
+ const dataTracked = tracked[i];
644
+ const id = dataTracked.id || '';
578
645
  // Join adjacent text changes that have been broken up due to different marks
579
646
  // eg <ins><b>bold</b>norm<i>italic</i></ins> -> treated as one continuous change
580
647
  // Note the !equalMarks to leave changes separate incase the marks are equal to let fixInconsistentChanges to fix them
@@ -586,38 +653,49 @@ function findChanges(state) {
586
653
  current.change.to = pos + node.nodeSize;
587
654
  // Important to update the node as the text changes might contain multiple parts where some marks equal each other
588
655
  current.node = node;
656
+ continue;
589
657
  }
590
- else if (node.isText) {
591
- current && changes.push(current.change);
592
- current = {
593
- change: {
594
- id,
595
- type: 'text-change',
596
- from: pos,
597
- to: pos + node.nodeSize,
598
- text: node.text,
599
- attrs,
600
- },
601
- node,
658
+ current && changes.push(current.change);
659
+ let change;
660
+ if (node.isText) {
661
+ change = {
662
+ id,
663
+ type: 'text-change',
664
+ from: pos,
665
+ to: pos + node.nodeSize,
666
+ dataTracked,
667
+ text: node.text,
668
+ };
669
+ }
670
+ else if (dataTracked.operation === CHANGE_OPERATION.set_node_attributes) {
671
+ change = {
672
+ id,
673
+ type: 'node-attr-change',
674
+ from: pos,
675
+ to: pos + node.nodeSize,
676
+ dataTracked,
677
+ nodeType: node.type.name,
678
+ newAttrs: node.attrs,
679
+ oldAttrs: dataTracked.oldAttrs,
602
680
  };
603
681
  }
604
682
  else {
605
- current && changes.push(current.change);
606
- current = {
607
- change: {
608
- id,
609
- type: 'node-change',
610
- from: pos,
611
- to: pos + node.nodeSize,
612
- nodeType: node.type.name,
613
- children: [],
614
- attrs,
615
- },
616
- node,
683
+ change = {
684
+ id,
685
+ type: 'node-change',
686
+ from: pos,
687
+ to: pos + node.nodeSize,
688
+ dataTracked,
689
+ nodeType: node.type.name,
690
+ children: [],
617
691
  };
618
692
  }
693
+ current = {
694
+ change,
695
+ node,
696
+ };
619
697
  }
620
- else if (current) {
698
+ if (tracked.length === 0 && current) {
621
699
  changes.push(current.change);
622
700
  current = undefined;
623
701
  }
@@ -636,22 +714,23 @@ function findChanges(state) {
636
714
  * @param schema
637
715
  * @return docWasChanged, a boolean
638
716
  */
639
- function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
717
+ function fixInconsistentChanges(changeSet, currentUserID, newTr, schema) {
640
718
  const iteratedIds = new Set();
641
719
  let changed = false;
642
720
  changeSet.invalidChanges.forEach((c) => {
643
- const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.attrs;
721
+ const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.dataTracked;
644
722
  const newAttrs = {
645
723
  ...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
646
- ...(!authorID && { authorID: trackUserID }),
647
- ...(!operation && { operation: CHANGE_OPERATION.insert }),
724
+ ...(!authorID && { authorID: currentUserID }),
725
+ // Dont add a default operation -> rather have updateChangeAttrs delete the track data
726
+ // ...(!operation && { operation: CHANGE_OPERATION.insert }),
648
727
  ...(!reviewedByID && { reviewedByID: null }),
649
728
  ...(!status && { status: CHANGE_STATUS.pending }),
650
729
  ...(!createdAt && { createdAt: Date.now() }),
651
730
  ...(!updatedAt && { updatedAt: Date.now() }),
652
731
  };
653
732
  if (Object.keys(newAttrs).length > 0) {
654
- updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
733
+ updateChangeAttrs(newTr, c, { ...c.dataTracked, ...newAttrs }, schema);
655
734
  changed = true;
656
735
  }
657
736
  iteratedIds.add(newAttrs.id || id);
@@ -659,87 +738,6 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
659
738
  return changed;
660
739
  }
661
740
 
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
741
  /*!
744
742
  * © 2021 Atypon Systems LLC
745
743
  *
@@ -835,73 +833,135 @@ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
835
833
  firstMergedNode,
836
834
  lastMergedNode,
837
835
  };
838
- }
839
- /**
840
- * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
836
+ }
837
+
838
+ /*!
839
+ * © 2021 Atypon Systems LLC
841
840
  *
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
841
+ * Licensed under the Apache License, Version 2.0 (the "License");
842
+ * you may not use this file except in compliance with the License.
843
+ * You may obtain a copy of the License at
844
+ *
845
+ * http://www.apache.org/licenses/LICENSE-2.0
846
+ *
847
+ * Unless required by applicable law or agreed to in writing, software
848
+ * distributed under the License is distributed on an "AS IS" BASIS,
849
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
850
+ * See the License for the specific language governing permissions and
851
+ * limitations under the License.
851
852
  */
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;
862
- }
863
- 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;
881
- }
853
+ function markInlineNodeChange(node, newTrackAttrs, schema) {
854
+ const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
855
+ const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
856
+ ? schema.marks.tracked_insert
857
+ : schema.marks.tracked_delete;
858
+ const createdMark = mark.create({
859
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
860
+ });
861
+ return node.mark(filtered.concat(createdMark));
882
862
  }
883
863
  /**
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
864
+ * Iterates over fragment's content and joins pasted text with old track marks
865
+ *
866
+ * This is not strictly necessary but it's kinda bad UX if the inserted text is split into parts
867
+ * even when it's authored by the same user.
868
+ * @param content
869
+ * @param newTrackAttrs
870
+ * @param schema
871
+ * @returns
889
872
  */
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);
873
+ function loopContentAndMergeText(content, newTrackAttrs, schema) {
874
+ var _a;
875
+ const updatedChildren = [];
876
+ for (let i = 0; i < content.childCount; i += 1) {
877
+ const recursed = recurseNodeContent(content.child(i), newTrackAttrs, schema);
878
+ const prev = i > 0 ? updatedChildren[i - 1] : null;
879
+ if ((prev === null || prev === void 0 ? void 0 : prev.isText) &&
880
+ recursed.isText &&
881
+ equalMarks(prev, recursed) &&
882
+ ((_a = getTextNodeTrackedMarkData(prev, schema)) === null || _a === void 0 ? void 0 : _a.operation) === CHANGE_OPERATION.insert) {
883
+ updatedChildren.splice(i - 1, 1, schema.text('' + prev.text + recursed.text, prev.marks));
884
+ }
885
+ else {
886
+ updatedChildren.push(recursed);
887
+ }
896
888
  }
897
- else {
898
- const attrs = {
889
+ return updatedChildren;
890
+ }
891
+ function recurseNodeContent(node, newTrackAttrs, schema) {
892
+ if (node.isText) {
893
+ return markInlineNodeChange(node, newTrackAttrs, schema);
894
+ }
895
+ else if (node.isBlock || node.isInline) {
896
+ const updatedChildren = loopContentAndMergeText(node.content, newTrackAttrs, schema);
897
+ return node.type.create({
899
898
  ...node.attrs,
900
- dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
901
- };
902
- newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
899
+ dataTracked: [addTrackIdIfDoesntExist(newTrackAttrs)],
900
+ }, Fragment.fromArray(updatedChildren), node.marks);
901
+ }
902
+ else {
903
+ log.error(`unhandled node type: "${node.type.name}"`, node);
904
+ return node;
903
905
  }
904
906
  }
907
+ function setFragmentAsInserted(inserted, insertAttrs, schema) {
908
+ // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
909
+ const updatedInserted = loopContentAndMergeText(inserted, insertAttrs, schema);
910
+ return updatedInserted.length === 0 ? Fragment.empty : Fragment.fromArray(updatedInserted);
911
+ }
912
+
913
+ /*!
914
+ * © 2021 Atypon Systems LLC
915
+ *
916
+ * Licensed under the Apache License, Version 2.0 (the "License");
917
+ * you may not use this file except in compliance with the License.
918
+ * You may obtain a copy of the License at
919
+ *
920
+ * http://www.apache.org/licenses/LICENSE-2.0
921
+ *
922
+ * Unless required by applicable law or agreed to in writing, software
923
+ * distributed under the License is distributed on an "AS IS" BASIS,
924
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
925
+ * See the License for the specific language governing permissions and
926
+ * limitations under the License.
927
+ */
928
+ function createNewInsertAttrs(attrs) {
929
+ return {
930
+ ...attrs,
931
+ operation: CHANGE_OPERATION.insert,
932
+ };
933
+ }
934
+ function createNewDeleteAttrs(attrs) {
935
+ return {
936
+ ...attrs,
937
+ operation: CHANGE_OPERATION.delete,
938
+ };
939
+ }
940
+ function createNewUpdateAttrs(attrs, oldAttrs) {
941
+ // Omit dataTracked
942
+ const { dataTracked, ...restAttrs } = oldAttrs;
943
+ return {
944
+ ...attrs,
945
+ operation: CHANGE_OPERATION.set_node_attributes,
946
+ oldAttrs: restAttrs,
947
+ };
948
+ }
949
+
950
+ /*!
951
+ * © 2021 Atypon Systems LLC
952
+ *
953
+ * Licensed under the Apache License, Version 2.0 (the "License");
954
+ * you may not use this file except in compliance with the License.
955
+ * You may obtain a copy of the License at
956
+ *
957
+ * http://www.apache.org/licenses/LICENSE-2.0
958
+ *
959
+ * Unless required by applicable law or agreed to in writing, software
960
+ * distributed under the License is distributed on an "AS IS" BASIS,
961
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
962
+ * See the License for the specific language governing permissions and
963
+ * limitations under the License.
964
+ */
905
965
  /**
906
966
  * Applies deletion to the doc without actually deleting nodes that have not been inserted
907
967
  *
@@ -928,25 +988,20 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
928
988
  * @returns mapping adjusted by the applied operations & modified insert slice
929
989
  */
930
990
  function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
931
- const deleteMap = new Mapping();
932
- const mergedInsertPos = undefined;
991
+ const steps = [];
933
992
  // No deletion applied, return default values
934
993
  if (from === to) {
935
994
  return {
936
- deleteMap,
937
- mergedInsertPos,
938
995
  newSliceContent: insertSlice.content,
996
+ sliceWasSplit: false,
997
+ steps,
939
998
  };
940
999
  }
941
1000
  const { openStart, openEnd } = insertSlice;
942
1001
  const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
943
- const deleteAttrs = createNewDeleteAttrs(trackAttrs);
944
1002
  let mergingStartSide = true;
945
1003
  startDoc.nodesBetween(from, to, (node, pos) => {
946
- const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
947
- const offsetFrom = deleteMap.map(from, -1);
948
- const offsetTo = deleteMap.map(to, 1);
949
- const nodeEnd = offsetPos + node.nodeSize;
1004
+ const nodeEnd = pos + node.nodeSize;
950
1005
  // So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
951
1006
  // since the content inside gap should stay unchanged.
952
1007
  // All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
@@ -957,23 +1012,13 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
957
1012
  // are altered and should not be skipped.
958
1013
  // @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
959
1014
  const wasWithinGap = gap &&
960
- ((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
961
- (node.isText &&
962
- offsetPos <= deleteMap.map(gap.start, -1) &&
963
- nodeEnd >= deleteMap.map(gap.end, -1)));
964
- let step = newTr.steps[newTr.steps.length - 1];
1015
+ ((!node.isText && pos >= gap.start) ||
1016
+ (node.isText && pos <= gap.start && nodeEnd >= gap.start));
965
1017
  // nodeEnd > offsetFrom -> delete touches this node
966
1018
  // eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
967
- //
968
- // !nodeWasDeleted -> Check node wasn't already deleted by a previous deleteNode
969
- // This is quite tricky to wrap your head around and I've forgotten the nitty-gritty details already.
970
- // But from what I remember what it safeguards against is, when you've already deleted a node
971
- // say an inserted blockquote that had all its children deleted, nodesBetween still iterates over those
972
- // nodes and therefore we have to make this check to ensure they still exist in the doc.
973
- //
974
- if (nodeEnd > offsetFrom && !nodeWasDeleted && !wasWithinGap) {
1019
+ if (nodeEnd > from && !wasWithinGap) {
975
1020
  // |<p>asdf</p>| -> node deleted completely
976
- const nodeCompletelyDeleted = offsetPos >= offsetFrom && nodeEnd <= offsetTo;
1021
+ const nodeCompletelyDeleted = pos >= from && nodeEnd <= to;
977
1022
  // The end token deleted eg:
978
1023
  // <p 1>asdf|</p 7><p 7>bye</p 12>| + [<p>]hello</p> -> <p>asdfhello</p>
979
1024
  // (del 6 12) + (ins [<p>]hello</p> openStart 1 openEnd 0)
@@ -984,14 +1029,14 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
984
1029
  //
985
1030
  // What about:
986
1031
  // <p 1>asdf|</p 7><p 7 op="inserted">|bye</p 12> + empty -> <p>asdfbye</p>
987
- const endTokenDeleted = nodeEnd <= offsetTo;
1032
+ const endTokenDeleted = nodeEnd <= to;
988
1033
  // The start token deleted eg:
989
1034
  // |<p1 0>hey</p 6><p2 6>|asdf</p 12> + <p3>hello [</p>] -> <p3>hello asdf</p2>
990
1035
  // (del 0 7) + (ins <p>hello [</p>] openStart 0 openEnd 1)
991
1036
  // (<p1> pos 0) >= (from 0) && (nodeEnd 6) - 1 > (to 7) == false???
992
1037
  // (<p2> pos 6) >= (from 0) && (nodeEnd 12) - 1 > (to 7) == true
993
1038
  //
994
- const startTokenDeleted = offsetPos >= offsetFrom; // && nodeEnd - 1 > offsetTo
1039
+ const startTokenDeleted = pos >= from; // && nodeEnd - 1 > offsetTo
995
1040
  if (node.isText ||
996
1041
  (!endTokenDeleted && startTokenDeleted) ||
997
1042
  (endTokenDeleted && !startTokenDeleted)) {
@@ -1003,7 +1048,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1003
1048
  }
1004
1049
  // Depth is often 1 when merging paragraphs or 2 for fully open blockquotes.
1005
1050
  // Incase of merging text within a ReplaceAroundStep the depth might be 1
1006
- const depth = newTr.doc.resolve(offsetPos).depth;
1051
+ const depth = newTr.doc.resolve(pos).depth;
1007
1052
  const mergeContent = mergingStartSide
1008
1053
  ? firstMergedNode === null || firstMergedNode === void 0 ? void 0 : firstMergedNode.mergedNodeContent
1009
1054
  : lastMergedNode === null || lastMergedNode === void 0 ? void 0 : lastMergedNode.mergedNodeContent;
@@ -1016,24 +1061,18 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1016
1061
  // ProseMirror node semantics as start tokens are considered to contain the actual node itself.
1017
1062
  const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
1018
1063
  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
1064
  // Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
1032
1065
  // it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
1033
1066
  // the paragraph.
1034
- if (mergeContent.size !== 0) {
1035
- newTr.insert(insertPos, setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema));
1036
- }
1067
+ steps.push({
1068
+ type: 'merge-fragment',
1069
+ pos,
1070
+ mergePos: mergeStartNode ? nodeEnd - openStart : pos + openEnd,
1071
+ from,
1072
+ to,
1073
+ node,
1074
+ fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
1075
+ });
1037
1076
  // Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
1038
1077
  // of merged nodes & content, as just using mapped toA in that case isn't the same.
1039
1078
  // The calculation is a bit mysterious, I admit.
@@ -1046,25 +1085,32 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1046
1085
  else if (node.isText) {
1047
1086
  // Text deletion is handled even when the deletion doesn't completely wrap the text node
1048
1087
  // (which is basically the case most of the time)
1049
- deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1088
+ steps.push({
1089
+ type: 'delete-text',
1090
+ pos,
1091
+ from: Math.max(pos, from),
1092
+ to: Math.min(nodeEnd, to),
1093
+ node,
1094
+ });
1050
1095
  }
1051
1096
  else ;
1052
1097
  }
1053
1098
  else if (nodeCompletelyDeleted) {
1054
- deleteOrSetNodeDeleted(node, offsetPos, newTr, deleteAttrs);
1099
+ steps.push({
1100
+ type: 'delete-node',
1101
+ pos,
1102
+ nodeEnd,
1103
+ node,
1104
+ });
1055
1105
  }
1056
1106
  }
1057
- const newestStep = newTr.steps[newTr.steps.length - 1];
1058
- if (step !== newestStep) {
1059
- deleteMap.appendMap(newestStep.getMap());
1060
- }
1061
1107
  });
1062
1108
  return {
1063
- deleteMap,
1064
- mergedInsertPos,
1109
+ sliceWasSplit: !!(firstMergedNode || lastMergedNode),
1065
1110
  newSliceContent: updatedSliceNodes
1066
1111
  ? Fragment.fromArray(updatedSliceNodes)
1067
1112
  : insertSlice.content,
1113
+ steps,
1068
1114
  };
1069
1115
  }
1070
1116
 
@@ -1092,8 +1138,7 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
1092
1138
  if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
1093
1139
  return;
1094
1140
  }
1095
- const isLeftOlder = (leftDataTracked.createdAt || Number.MAX_VALUE) <
1096
- (rightDataTracked.createdAt || Number.MAX_VALUE);
1141
+ const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
1097
1142
  const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
1098
1143
  const dataTracked = {
1099
1144
  ...ancestorAttrs,
@@ -1149,14 +1194,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1149
1194
  const stepResult = newTr.maybeStep(newStep);
1150
1195
  if (stepResult.failed) {
1151
1196
  log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
1152
- return;
1197
+ return [];
1153
1198
  }
1154
1199
  const gap = oldState.doc.slice(gapFrom, gapTo);
1155
1200
  log.info('RETAINED GAP CONTENT', gap);
1156
1201
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1157
1202
  // 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);
1203
+ const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1204
+ let steps = deleteSteps;
1159
1205
  log.info('TR: new steps after applying delete', [...newTr.steps]);
1206
+ log.info('DELETE STEPS: ', deleteSteps);
1160
1207
  // We only want to insert when there something inside the gap (actually would this be always true?)
1161
1208
  // or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
1162
1209
  if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
@@ -1171,21 +1218,20 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1171
1218
  insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1172
1219
  log.info('insertedSlice after inserted gap', insertedSlice);
1173
1220
  }
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);
1221
+ deleteSteps.push({
1222
+ type: 'insert-slice',
1223
+ from: gapFrom,
1224
+ to: gapTo,
1225
+ slice: insertedSlice,
1226
+ sliceWasSplit,
1227
+ });
1183
1228
  }
1184
1229
  else {
1185
1230
  // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1186
- mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1187
- mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1231
+ mergeTrackedMarks(gapFrom, newTr.doc, newTr, oldState.schema);
1232
+ mergeTrackedMarks(gapTo, newTr.doc, newTr, oldState.schema);
1188
1233
  }
1234
+ return steps;
1189
1235
  }
1190
1236
 
1191
1237
  /*!
@@ -1205,7 +1251,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1205
1251
  */
1206
1252
  function trackReplaceStep(step, oldState, newTr, attrs) {
1207
1253
  log.info('###### ReplaceStep ######');
1208
- let selectionPos = 0;
1254
+ let selectionPos = 0, changeSteps = [];
1209
1255
  step.getMap().forEach((fromA, toA, fromB, toB) => {
1210
1256
  log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
1211
1257
  const { slice } = step;
@@ -1219,34 +1265,345 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1219
1265
  log.info('TR: steps before applying delete', [...newTr.steps]);
1220
1266
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1221
1267
  // eg partial nodes in an open-ended slice
1222
- const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1268
+ const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1269
+ changeSteps.push(...deleteSteps);
1223
1270
  log.info('TR: steps after applying delete', [...newTr.steps]);
1224
- const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1271
+ log.info('DELETE STEPS: ', changeSteps);
1272
+ const adjustedInsertPos = toA; // deleteMap.map(toA)
1225
1273
  if (newSliceContent.size > 0) {
1226
1274
  log.info('newSliceContent', newSliceContent);
1227
1275
  // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1228
1276
  // the sides should be equal. TODO can they be other than 0?
1229
1277
  const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1230
1278
  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);
1279
+ changeSteps.push({
1280
+ type: 'insert-slice',
1281
+ from: adjustedInsertPos,
1282
+ to: adjustedInsertPos,
1283
+ sliceWasSplit,
1284
+ slice: new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
1285
+ });
1286
+ }
1287
+ else {
1288
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1289
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1290
+ selectionPos = fromA;
1291
+ }
1292
+ });
1293
+ return [changeSteps, selectionPos];
1294
+ }
1295
+
1296
+ /*!
1297
+ * © 2021 Atypon Systems LLC
1298
+ *
1299
+ * Licensed under the Apache License, Version 2.0 (the "License");
1300
+ * you may not use this file except in compliance with the License.
1301
+ * You may obtain a copy of the License at
1302
+ *
1303
+ * http://www.apache.org/licenses/LICENSE-2.0
1304
+ *
1305
+ * Unless required by applicable law or agreed to in writing, software
1306
+ * distributed under the License is distributed on an "AS IS" BASIS,
1307
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1308
+ * See the License for the specific language governing permissions and
1309
+ * limitations under the License.
1310
+ */
1311
+ /**
1312
+ * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
1313
+ *
1314
+ * This would work for general inline nodes too, but since node marks don't work properly
1315
+ * with Yjs, attributes are used instead.
1316
+ * @param node
1317
+ * @param pos
1318
+ * @param newTr
1319
+ * @param schema
1320
+ * @param deleteAttrs
1321
+ * @param from
1322
+ * @param to
1323
+ */
1324
+ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
1325
+ const start = from ? Math.max(pos, from) : pos;
1326
+ const nodeEnd = pos + node.nodeSize;
1327
+ const end = to ? Math.min(nodeEnd, to) : nodeEnd;
1328
+ if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1329
+ // Math.max(pos, from) is for picking always the start of the node,
1330
+ // not the start of the change (which might span multiple nodes).
1331
+ // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
1332
+ newTr.replaceWith(start, end, Fragment.empty);
1333
+ return start;
1334
+ }
1335
+ else {
1336
+ const leftNode = newTr.doc.resolve(start).nodeBefore;
1337
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1338
+ const rightNode = newTr.doc.resolve(end).nodeAfter;
1339
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1340
+ const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1341
+ const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1342
+ 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);
1343
+ const dataTracked = addTrackIdIfDoesntExist({
1344
+ ...leftMarks,
1345
+ ...rightMarks,
1346
+ ...deleteAttrs,
1347
+ createdAt,
1348
+ });
1349
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1350
+ dataTracked,
1351
+ }));
1352
+ return toEndOfMark;
1353
+ }
1354
+ }
1355
+
1356
+ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
1357
+ const mapping = new Mapping();
1358
+ const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
1359
+ let selectionPos = startPos;
1360
+ // @TODO add custom handler / condition?
1361
+ changes.forEach((c) => {
1362
+ let step = newTr.steps[newTr.steps.length - 1];
1363
+ log.info('process change: ', c);
1364
+ // const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
1365
+ if (c.type === 'delete-node') {
1366
+ deleteOrSetNodeDeleted(c.node, mapping.map(c.pos), newTr, deleteAttrs);
1367
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1368
+ if (step !== newestStep) {
1369
+ mapping.appendMap(newestStep.getMap());
1370
+ step = newestStep;
1371
+ }
1372
+ mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
1373
+ }
1374
+ else if (c.type === 'delete-text') {
1375
+ const node = newTr.doc.nodeAt(mapping.map(c.pos));
1376
+ if (!node) {
1377
+ log.error(`processChangeSteps: no text node found for text-change`, c);
1378
+ return;
1379
+ }
1380
+ const where = deleteTextIfInserted(node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1381
+ mergeTrackedMarks(where, newTr.doc, newTr, schema);
1382
+ }
1383
+ else if (c.type === 'merge-fragment') {
1384
+ let insertPos = mapping.map(c.mergePos);
1385
+ // The default insert position for block nodes is either the start of the merged content or the end.
1386
+ // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1387
+ // actual position of the merge. Currently the inserted content is inserted at the start or end
1388
+ // of the merged content, TODO reverse the start/end when end/start token?
1389
+ if (c.node.isText) {
1390
+ // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1391
+ // the text node.
1392
+ insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1393
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1394
+ if (step !== newestStep) {
1395
+ mapping.appendMap(newestStep.getMap());
1396
+ step = newestStep;
1397
+ }
1398
+ }
1399
+ if (c.fragment.size > 0) {
1400
+ newTr.insert(insertPos, c.fragment);
1401
+ }
1402
+ }
1403
+ else if (c.type === 'insert-slice') {
1404
+ const newStep = new ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
1233
1405
  const stepResult = newTr.maybeStep(newStep);
1234
1406
  if (stepResult.failed) {
1235
- log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1407
+ log.error(`processChangeSteps: insert-slice ReplaceStep failed "${stepResult.failed}"`, newStep);
1236
1408
  return;
1237
1409
  }
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;
1410
+ mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
1411
+ mergeTrackedMarks(mapping.map(c.to), newTr.doc, newTr, schema);
1412
+ selectionPos = mapping.map(c.to) + c.slice.size;
1413
+ }
1414
+ else if (c.type === 'update-node-attrs') {
1415
+ const oldDataTracked = getBlockInlineTrackedData(c.node) || [];
1416
+ const oldUpdate = oldDataTracked.find((d) => d.operation === CHANGE_OPERATION.set_node_attributes);
1417
+ const { dataTracked, ...oldAttrs } = (oldUpdate === null || oldUpdate === void 0 ? void 0 : oldUpdate.oldAttrs) || c.node.attrs;
1418
+ const newDataTracked = [...oldDataTracked.filter((d) => !oldUpdate || d.id !== oldUpdate.id)];
1419
+ const newUpdate = oldUpdate
1420
+ ? {
1421
+ ...oldUpdate,
1422
+ updatedAt: emptyAttrs.updatedAt,
1423
+ }
1424
+ : addTrackIdIfDoesntExist(createNewUpdateAttrs(emptyAttrs, c.node.attrs));
1425
+ // Dont add update changes if there exists already an insert change for this node
1426
+ if (JSON.stringify(oldAttrs) !== JSON.stringify(c.newAttrs) &&
1427
+ !oldDataTracked.find((d) => d.operation === CHANGE_OPERATION.insert)) {
1428
+ newDataTracked.push(newUpdate);
1429
+ }
1430
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, {
1431
+ ...c.newAttrs,
1432
+ dataTracked: newDataTracked.length > 0 ? newDataTracked : null,
1433
+ }, c.node.marks);
1434
+ }
1435
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1436
+ if (step !== newestStep) {
1437
+ mapping.appendMap(newestStep.getMap());
1438
+ }
1439
+ });
1440
+ return [mapping, selectionPos];
1441
+ }
1442
+
1443
+ function matchText(adjDeleted, insNode, offset, matchedDeleted, deleted) {
1444
+ const { pos, from, to, node: delNode } = adjDeleted;
1445
+ let j = offset, d = from - pos, maxSteps = to - Math.max(pos, from);
1446
+ // Match text inside the inserted text node to the deleted text node
1447
+ for (; maxSteps !== j && insNode.text[j] !== undefined && insNode.text[j] === delNode.text[d]; j += 1, d += 1) {
1448
+ matchedDeleted += 1;
1449
+ }
1450
+ // this is needed incase diffing tr.doc
1451
+ // deleted.push({
1452
+ // pos: pos,
1453
+ // type: 'update-node-attrs',
1454
+ // // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1455
+ // oldAttrs: adjDeleted.node.attrs || {},
1456
+ // newAttrs: child.attrs || {},
1457
+ // })
1458
+ deleted = deleted.filter((d) => d !== adjDeleted);
1459
+ if (maxSteps !== j) {
1460
+ deleted.push({
1461
+ pos,
1462
+ from: from + j - offset,
1463
+ to,
1464
+ type: 'delete-text',
1465
+ node: delNode,
1466
+ });
1467
+ return [matchedDeleted, deleted];
1468
+ }
1469
+ const nextTextDelete = deleted.find((d) => d.type === 'delete-text' && d.pos === to);
1470
+ if (nextTextDelete) {
1471
+ return matchText(nextTextDelete, insNode, j, matchedDeleted, deleted);
1472
+ }
1473
+ return [matchedDeleted, deleted];
1474
+ }
1475
+ function matchInserted(matchedDeleted, deleted, inserted) {
1476
+ var _a;
1477
+ let matched = [matchedDeleted, deleted];
1478
+ for (let i = 0;; i += 1) {
1479
+ if (inserted.childCount === i)
1480
+ return matched;
1481
+ const insNode = inserted.child(i);
1482
+ // @ts-ignore
1483
+ let adjDeleted = matched[1].find((d) => (d.type === 'delete-text' && Math.max(d.pos, d.from) === matched[0]) ||
1484
+ (d.type === 'delete-node' && d.pos === matched[0]));
1485
+ if (insNode.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
1486
+ return matched;
1487
+ }
1488
+ else if (insNode.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
1489
+ matched = matchText(adjDeleted, insNode, 0, matched[0], matched[1]);
1490
+ continue;
1491
+ }
1492
+ else if (insNode.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
1493
+ // Move the inDeleted inside the block/inline node's boundary
1494
+ matched = matchInserted(matched[0] + 1, matched[1].filter((d) => d !== adjDeleted), insNode.content);
1242
1495
  }
1243
1496
  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;
1497
+ matched = [matched[0] + insNode.nodeSize, matched[1].filter((d) => d !== adjDeleted)];
1498
+ }
1499
+ // Omit dataTracked
1500
+ const { dataTracked, ...newAttrs } = insNode.attrs || {};
1501
+ matched[1].push({
1502
+ pos: adjDeleted.pos,
1503
+ type: 'update-node-attrs',
1504
+ node: adjDeleted.node,
1505
+ newAttrs,
1506
+ });
1507
+ }
1508
+ }
1509
+
1510
+ /*!
1511
+ * © 2021 Atypon Systems LLC
1512
+ *
1513
+ * Licensed under the Apache License, Version 2.0 (the "License");
1514
+ * you may not use this file except in compliance with the License.
1515
+ * You may obtain a copy of the License at
1516
+ *
1517
+ * http://www.apache.org/licenses/LICENSE-2.0
1518
+ *
1519
+ * Unless required by applicable law or agreed to in writing, software
1520
+ * distributed under the License is distributed on an "AS IS" BASIS,
1521
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1522
+ * See the License for the specific language governing permissions and
1523
+ * limitations under the License.
1524
+ */
1525
+ /**
1526
+ * Cuts a fragment similar to Fragment.cut but also removes the parent node.
1527
+ *
1528
+ * @TODO there is however, some silly calculation mistake so that I need to use matched - deleted + 1 > 0
1529
+ * inside it to check whether to actually cut a text node. The offset might be cascading, therefore it should
1530
+ * be fixed at some point.
1531
+ * @param matched
1532
+ * @param deleted
1533
+ * @param content
1534
+ * @returns
1535
+ */
1536
+ function cutFragment(matched, deleted, content) {
1537
+ let newContent = [];
1538
+ for (let i = 0; matched <= deleted && i < content.childCount; i += 1) {
1539
+ const child = content.child(i);
1540
+ if (!child.isText && child.content.size > 0) {
1541
+ const cut = cutFragment(matched + 1, deleted, child.content);
1542
+ matched = cut[0];
1543
+ newContent.push(...cut[1].content);
1544
+ }
1545
+ else if (child.isText && matched + child.nodeSize > deleted) {
1546
+ if (deleted - matched > 0) {
1547
+ newContent.push(child.cut(deleted - matched));
1548
+ }
1549
+ else {
1550
+ newContent.push(child);
1551
+ }
1552
+ matched = deleted + 1;
1553
+ }
1554
+ else {
1555
+ matched += child.nodeSize;
1556
+ }
1557
+ }
1558
+ return [matched, Fragment.fromArray(newContent)];
1559
+ }
1560
+ function diffChangeSteps(deleted, inserted, newTr, schema) {
1561
+ const updated = [];
1562
+ let updatedDeleted = [...deleted];
1563
+ inserted.forEach((ins) => {
1564
+ log.info('DIFF ins ', ins);
1565
+ //
1566
+ // @TODO this is a temporary workaround to prevent duplicated diffing between splitSliceIntoMergedParts and
1567
+ // matchInserted.
1568
+ //
1569
+ // As originally authored splitSliceIntoMergedParts splits open slices into their merged parts
1570
+ // leaving out the need to insert the possibly deleted nodes into the doc. However, as matchInserted now
1571
+ // traverses the deleted range checking it against the inserted slice this behaves quite in a same way
1572
+ // where the opened block nodes are traversed but left unmodified. With an openStart > 0 though the
1573
+ // node-attr-updates would additionally have to be filtered out in the processChangeSteps.
1574
+ //
1575
+ // The old logic is still left as it's as refactoring is painful and would probably break something and just
1576
+ // in general, take a lot of time. Therefore, this sliceWasSplit boolean is used to just skip diffing.
1577
+ if (ins.sliceWasSplit) {
1578
+ updated.push(ins);
1579
+ return;
1580
+ }
1581
+ // Start diffing from the start of the deleted range
1582
+ const deleteStart = updatedDeleted.reduce((acc, cur) => {
1583
+ if (cur.type === 'delete-node') {
1584
+ return Math.min(acc, cur.pos);
1585
+ }
1586
+ else if (cur.type === 'delete-text') {
1587
+ return Math.min(acc, cur.from);
1588
+ }
1589
+ return acc;
1590
+ }, Number.MAX_SAFE_INTEGER);
1591
+ const [matchedDeleted, updatedDel] = matchInserted(deleteStart, updatedDeleted, ins.slice.content);
1592
+ if (matchedDeleted === deleteStart) {
1593
+ updated.push(ins);
1594
+ return;
1595
+ }
1596
+ updatedDeleted = updatedDel;
1597
+ const [_, newInserted] = cutFragment(0, matchedDeleted - deleteStart, ins.slice.content);
1598
+ if (newInserted.size > 0) {
1599
+ updated.push({
1600
+ ...ins,
1601
+ slice: new Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
1602
+ });
1247
1603
  }
1248
1604
  });
1249
- return selectionPos;
1605
+ log.info('FINISH DIFF: ', [...updatedDeleted, ...updated]);
1606
+ return [...updatedDeleted, ...updated];
1250
1607
  }
1251
1608
 
1252
1609
  /**
@@ -1300,7 +1657,15 @@ function trackTransaction(tr, oldState, newTr, authorID) {
1300
1657
  return;
1301
1658
  }
1302
1659
  else if (step instanceof ReplaceStep) {
1303
- const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1660
+ let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1661
+ log.info('CHANGES: ', steps);
1662
+ // deleted and merged really...
1663
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1664
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1665
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1666
+ log.info('DIFFED STEPS: ', steps);
1667
+ const [mapping, selectionPos] = processChangeSteps(steps, startPos || tr.selection.head, // Incase startPos is it's default value 0, use the old selection head
1668
+ newTr, emptyAttrs, oldState.schema);
1304
1669
  if (!wasNodeSelection) {
1305
1670
  const sel = getSelectionStaticConstructor(tr.selection);
1306
1671
  // Use Selection.near to fix selections that point to a block node instead of inline content
@@ -1311,10 +1676,16 @@ function trackTransaction(tr, oldState, newTr, authorID) {
1311
1676
  }
1312
1677
  }
1313
1678
  else if (step instanceof ReplaceAroundStep) {
1314
- trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1315
- // } else if (step instanceof AddMarkStep) {
1316
- // } else if (step instanceof RemoveMarkStep) {
1679
+ let steps = trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1680
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1681
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1682
+ log.info('INSERT STEPS: ', inserted);
1683
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1684
+ log.info('DIFFED STEPS: ', steps);
1685
+ processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
1317
1686
  }
1687
+ // } else if (step instanceof AddMarkStep) {
1688
+ // } else if (step instanceof RemoveMarkStep) {
1318
1689
  // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1319
1690
  // However, this should not be done by diffing and only matching node or char by char instead since
1320
1691
  // it's A easier and B more intuitive to user.
@@ -1448,13 +1819,13 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1448
1819
  ids.forEach((changeId) => {
1449
1820
  const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
1450
1821
  if (change) {
1451
- createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
1452
- setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
1822
+ createdTr = updateChangeAttrs(createdTr, change, { ...change.dataTracked, status, reviewedByID: userID }, oldState.schema);
1453
1823
  }
1454
1824
  });
1825
+ setAction(createdTr, TrackChangesAction.updateChanges, ids);
1455
1826
  }
1456
1827
  else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
1457
- const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
1828
+ const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.bothNodeChanges);
1458
1829
  applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.textChanges, mapping);
1459
1830
  setAction(createdTr, TrackChangesAction.refreshChanges, true);
1460
1831
  }
@@ -1473,21 +1844,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1473
1844
  });
1474
1845
  };
1475
1846
 
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
1847
  /**
1492
1848
  * Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
1493
1849
  * set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.