@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/ChangeSet.d.ts +3 -1
- package/dist/change-steps/diffChangeSteps.d.ts +21 -0
- package/dist/change-steps/processChangeSteps.d.ts +21 -0
- package/dist/{track → changes}/applyChanges.d.ts +0 -0
- package/dist/{track → changes}/findChanges.d.ts +0 -0
- package/dist/{track → changes}/fixInconsistentChanges.d.ts +0 -0
- package/dist/{track → changes}/updateChangeAttrs.d.ts +0 -0
- package/dist/commands.d.ts +1 -1
- package/dist/{track/node-utils.d.ts → compute/nodeHelpers.d.ts} +0 -0
- package/dist/{track/steps → compute}/setFragmentAsInserted.d.ts +1 -1
- package/dist/compute/splitSliceIntoMergedParts.d.ts +41 -0
- package/dist/index.cjs +532 -298
- package/dist/index.js +532 -298
- package/dist/{track/steps → mutate}/deleteAndMergeSplitNodes.d.ts +4 -3
- package/dist/{track → mutate}/deleteNode.d.ts +9 -0
- package/dist/mutate/deleteText.d.ts +32 -0
- package/dist/{track → mutate}/mergeNode.d.ts +0 -0
- package/dist/{track/steps → mutate}/mergeTrackedMarks.d.ts +0 -0
- package/dist/{track/steps → steps}/trackReplaceAroundStep.d.ts +3 -2
- package/dist/{track/steps → steps}/trackReplaceStep.d.ts +3 -2
- package/dist/{track → steps}/trackTransaction.d.ts +0 -0
- package/dist/types/change.d.ts +18 -10
- package/dist/types/step.d.ts +52 -0
- package/dist/{track/steps → utils}/track-utils.d.ts +1 -1
- package/package.json +2 -2
- package/dist/index.es.js +0 -1547
- 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"] = "
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/*!
|
|
771
|
+
* © 2021 Atypon Systems LLC
|
|
841
772
|
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
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
|
|
853
|
-
const
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
865
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
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
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1239
|
-
mergeTrackedMarks(
|
|
1240
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
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'.
|