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