@atlaskit/editor-plugin-show-diff 8.4.0 → 8.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/cjs/pm-plugins/areDocsEqualByBlockStructureAndText.js +32 -2
  3. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +20 -9
  4. package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +45 -4
  5. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +3 -1
  6. package/dist/cjs/pm-plugins/decorations/utils/getAttrChangeRanges.js +71 -12
  7. package/dist/es2019/pm-plugins/areDocsEqualByBlockStructureAndText.js +33 -2
  8. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +20 -9
  9. package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +45 -4
  10. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +1 -1
  11. package/dist/es2019/pm-plugins/decorations/utils/getAttrChangeRanges.js +63 -8
  12. package/dist/esm/pm-plugins/areDocsEqualByBlockStructureAndText.js +33 -2
  13. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +20 -9
  14. package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +45 -4
  15. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +2 -1
  16. package/dist/esm/pm-plugins/decorations/utils/getAttrChangeRanges.js +71 -11
  17. package/dist/types/pm-plugins/areDocsEqualByBlockStructureAndText.d.ts +5 -0
  18. package/dist/types/pm-plugins/decorations/utils/getAttrChangeRanges.d.ts +2 -0
  19. package/dist/types-ts4.5/pm-plugins/areDocsEqualByBlockStructureAndText.d.ts +5 -0
  20. package/dist/types-ts4.5/pm-plugins/decorations/utils/getAttrChangeRanges.d.ts +2 -0
  21. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 8.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`5789b1638025b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/5789b1638025b) -
8
+ EDITOR-6631: If only marks has changed, don't use granular diffing as that won't show any diffs.
9
+ - Updated dependencies
10
+
11
+ ## 8.4.1
12
+
13
+ ### Patch Changes
14
+
15
+ - [`8a9f26c6c71bc`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/8a9f26c6c71bc) -
16
+ [ux] Improve diff logic for some nodes and edge cases where marks are causing the diff to fail
17
+ - Updated dependencies
18
+
3
19
  ## 8.4.0
4
20
 
5
21
  ### Minor Changes
6
22
 
7
- - [`41168b2bd2790`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/41168b2bd2790) -
23
+ - [`ebab8f80bfc40`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ebab8f80bfc40) -
8
24
  Autofix: add explicit package exports (barrel removal)
9
25
 
10
26
  ### Patch Changes
@@ -4,8 +4,22 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.areDocsEqualByBlockStructureAndText = areDocsEqualByBlockStructureAndText;
7
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
8
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
7
9
  /**
8
- * Returns true if both nodes have the same tree structure (type and child count at every level).
10
+ * Returns a copy of the document with all marks removed from all text.
11
+ * This normalises mark fragmentation — e.g. annotation marks reordering can split
12
+ * a single text run into many text nodes, making structural comparison unreliable.
13
+ * After stripping, ProseMirror will merge adjacent text nodes so childCounts are stable.
14
+ */
15
+ function stripMarks(doc) {
16
+ var tr = new _transform.Transform(doc);
17
+ tr.removeMark(0, doc.content.size);
18
+ return tr.doc;
19
+ }
20
+
21
+ /**
22
+ * Returns true if both (mark-stripped) nodes have the same block tree structure (node type and child count at every level)
9
23
  */
10
24
  function isBlockStructureEqual(node1, node2) {
11
25
  if (node1.type !== node2.type || node1.childCount !== node2.childCount) {
@@ -23,7 +37,23 @@ function isBlockStructureEqual(node1, node2) {
23
37
  * Looser equality for "safe diff" cases: same full text content and same block structure
24
38
  * (e.g. text moved across text-node boundaries). Used when strict areNodesEqualIgnoreAttrs fails.
25
39
  * This is safe because we ensure decorations get applied to valid positions.
40
+ *
41
+ * Marks are intentionally ignored — two documents that differ only in mark application
42
+ * (e.g. bold/italic boundaries or annotation mark ordering) are considered equal here.
43
+ * Both documents are mark-stripped before comparison so that mark-driven text fragmentation
44
+ * does not produce false inequalities.
26
45
  */
27
46
  function areDocsEqualByBlockStructureAndText(doc1, doc2) {
28
- return doc1.textContent === doc2.textContent && doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(doc1, doc2);
47
+ if (doc1.textContent !== doc2.textContent) {
48
+ return false;
49
+ }
50
+ if ((0, _expValEquals.expValEquals)('platform_editor_show_diff_improvements', 'isEnabled', true)) {
51
+ // Strip marks before comparing so that mark-driven text fragmentation
52
+ // (e.g. annotation mark reordering producing different childCounts) does not
53
+ // cause false inequalities.
54
+ var stripped1 = stripMarks(doc1);
55
+ var stripped2 = stripMarks(doc2);
56
+ return doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(stripped1, stripped2);
57
+ }
58
+ return doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(doc1, doc2);
29
59
  }
@@ -221,15 +221,26 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref3
221
221
  }));
222
222
  });
223
223
  (0, _getAttrChangeRanges.getAttrChangeRanges)(tr.doc, attrSteps).forEach(function (change) {
224
- decorations.push.apply(decorations, (0, _toConsumableArray2.default)(calculateNodesForBlockDecoration({
225
- doc: tr.doc,
226
- from: change.fromB,
227
- to: change.toB,
228
- colorScheme: colorScheme,
229
- isInserted: true,
230
- activeIndexPos: activeIndexPos,
231
- intl: intl
232
- })));
224
+ if ((0, _expValEquals.expValEquals)('platform_editor_show_diff_improvements', 'isEnabled', true) && change.isInline) {
225
+ // Inline nodes (e.g. date) need an inline decoration rather than a block decoration
226
+ var isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
227
+ decorations.push((0, _createInlineChangedDecoration.createInlineChangedDecoration)({
228
+ change: change,
229
+ colorScheme: colorScheme,
230
+ isActive: isActive,
231
+ isInserted: true
232
+ }));
233
+ } else {
234
+ decorations.push.apply(decorations, (0, _toConsumableArray2.default)(calculateNodesForBlockDecoration({
235
+ doc: tr.doc,
236
+ from: change.fromB,
237
+ to: change.toB,
238
+ colorScheme: colorScheme,
239
+ isInserted: true,
240
+ activeIndexPos: activeIndexPos,
241
+ intl: intl
242
+ })));
243
+ }
233
244
  });
234
245
  return _view.DecorationSet.empty.add(tr.doc, decorations);
235
246
  };
@@ -8,6 +8,7 @@ exports.diffBySteps = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
10
  var _prosemirrorChangeset = require("prosemirror-changeset");
11
+ var _model = require("@atlaskit/editor-prosemirror/model");
11
12
  var _transform = require("@atlaskit/editor-prosemirror/transform");
12
13
  var _optimizeChanges = require("./optimizeChanges");
13
14
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -18,6 +19,22 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
18
19
  var mapPosition = function mapPosition(mapping, pos) {
19
20
  return mapping.map(pos);
20
21
  };
22
+
23
+ /**
24
+ * Compare marks between two nodes
25
+ * We have to check each child because adding a mark splits text into multiple nodes
26
+ */
27
+ var hasSameChildMarks = function hasSameChildMarks(left, right) {
28
+ if (left.childCount !== right.childCount) {
29
+ return false;
30
+ }
31
+ for (var index = 0; index < left.childCount; index++) {
32
+ if (!_model.Mark.sameSet(left.child(index).marks, right.child(index).marks)) {
33
+ return false;
34
+ }
35
+ }
36
+ return true;
37
+ };
21
38
  var createMapping = function createMapping(maps) {
22
39
  var mapping = new _transform.Mapping();
23
40
  var _iterator = _createForOfIteratorHelper(maps),
@@ -69,8 +86,20 @@ var mergeOverlappingByNewDocRange = function mergeOverlappingByNewDocRange(chang
69
86
  merged.push(current);
70
87
  return merged;
71
88
  };
72
- var isReplaceStepForTextBlockNode = function isReplaceStepForTextBlockNode(step, before, from, to) {
73
- var _replacedSlice$conten, _replacingSlice$conte, _replacedSlice$conten2;
89
+
90
+ /**
91
+ * This function checks whether to do granular diffing.
92
+ * We should do granular diffing if:
93
+ * - The step is a replace step
94
+ * - The step is not open
95
+ * - The replaced slice is not open
96
+ * - The replaced slice has only one child
97
+ * - The replacing slice has only one child
98
+ * - The replaced slice and replacing slice have the same text content
99
+ * - The replaced slice and replacing slice have the same child marks (if text content is equal)
100
+ */
101
+ var shouldCheckGranularDiff = function shouldCheckGranularDiff(step, before, from, to) {
102
+ var _replacedNode$marks, _replacingNode$marks;
74
103
  if (!(step instanceof _transform.ReplaceStep)) {
75
104
  return false;
76
105
  }
@@ -79,7 +108,19 @@ var isReplaceStepForTextBlockNode = function isReplaceStepForTextBlockNode(step,
79
108
  }
80
109
  var replacedSlice = before.slice(from, to);
81
110
  var replacingSlice = step.slice;
82
- return Boolean(replacedSlice.openStart === 0 && replacedSlice.openEnd === 0 && replacedSlice.content.childCount === 1 && replacingSlice.content.childCount === 1 && ((_replacedSlice$conten = replacedSlice.content.firstChild) === null || _replacedSlice$conten === void 0 ? void 0 : _replacedSlice$conten.type.name) === ((_replacingSlice$conte = replacingSlice.content.firstChild) === null || _replacingSlice$conte === void 0 ? void 0 : _replacingSlice$conte.type.name) && ((_replacedSlice$conten2 = replacedSlice.content.firstChild) === null || _replacedSlice$conten2 === void 0 ? void 0 : _replacedSlice$conten2.type.isTextblock));
111
+ if (replacedSlice.openStart !== 0 || replacedSlice.openEnd !== 0 || replacedSlice.content.childCount !== 1 || replacingSlice.content.childCount !== 1) {
112
+ return false;
113
+ }
114
+ var replacedNode = replacedSlice.content.firstChild;
115
+ var replacingNode = replacingSlice.content.firstChild;
116
+ if ((replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.type.name) !== (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.type.name) || !(replacedNode !== null && replacedNode !== void 0 && replacedNode.type.isTextblock)) {
117
+ return false;
118
+ }
119
+ if (!_model.Mark.sameSet((_replacedNode$marks = replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.marks) !== null && _replacedNode$marks !== void 0 ? _replacedNode$marks : [], (_replacingNode$marks = replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.marks) !== null && _replacingNode$marks !== void 0 ? _replacingNode$marks : [])) {
120
+ return false;
121
+ }
122
+ var isTextContentEqual = (replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.textContent) === (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.textContent);
123
+ return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
83
124
  };
84
125
  var diffBySteps = exports.diffBySteps = function diffBySteps(originalDoc, steps) {
85
126
  var changes = [];
@@ -131,7 +172,7 @@ var diffBySteps = exports.diffBySteps = function diffBySteps(originalDoc, steps)
131
172
  var afterStepToFinal = createMapping(successfulStepMaps.slice(rangedStep.mapIndex + 1));
132
173
  var fromB = mapPosition(afterStepToFinal, fromAfterStep);
133
174
  var toB = mapPosition(afterStepToFinal, toAfterStep);
134
- if (isReplaceStepForTextBlockNode(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
175
+ if (shouldCheckGranularDiff(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
135
176
  var granularStepChanges = _prosemirrorChangeset.ChangeSet.create(rangedStep.before).addSteps(rangedStep.doc, [rangedStep.stepMap], null);
136
177
 
137
178
  // `simplifyChanges` reads text using `Change.fromB`/`toB`, which are
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
7
  exports.createBlockChangedDecoration = void 0;
8
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
7
9
  var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
8
10
  var _view = require("@atlaskit/editor-prosemirror/view");
9
11
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
@@ -33,7 +35,7 @@ var getBlockNodeStyle = function getBlockNodeStyle(_ref) {
33
35
  // Handle table separately to avoid border issues
34
36
  'tableRow', 'paragraph',
35
37
  // Paragraph and heading nodes do not need special styling
36
- 'heading', 'hardBreak', 'decisionList', 'taskList', 'taskItem', 'bulletList', 'orderedList', 'layoutSection'].includes(nodeName)) {
38
+ 'heading', 'hardBreak', 'decisionList', 'taskList'].concat((0, _toConsumableArray2.default)((0, _expValEquals.expValEquals)('platform_editor_show_diff_improvements', 'isEnabled', true) ? [] : ['taskItem']), ['bulletList', 'orderedList', 'layoutSection']).includes(nodeName)) {
37
39
  // Layout nodes do not need special styling
38
40
  return undefined;
39
41
  }
@@ -1,32 +1,91 @@
1
1
  "use strict";
2
2
 
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
3
  Object.defineProperty(exports, "__esModule", {
5
4
  value: true
6
5
  });
7
6
  exports.stepIsValidAttrChange = exports.getAttrChangeRanges = void 0;
8
- var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
9
7
  var _steps = require("@atlaskit/adf-schema/steps");
10
8
  var _transform = require("@atlaskit/editor-prosemirror/transform");
9
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
11
10
  var filterUndefined = function filterUndefined(x) {
12
11
  return !!x;
13
12
  };
14
13
 
15
- // Currently allow attributes that indicats a change in media image
16
- var allowedAttrs = ['id', 'collection', 'url'];
14
+ // Attributes that indicate a change in media image
15
+ var mediaAttrs = ['id', 'collection', 'url'];
16
+
17
+ // Attribute that indicates a date change
18
+ var dateAttrs = ['timestamp'];
19
+
20
+ // Attribute that indicates a task item state change
21
+ var taskItemAttrs = ['state'];
22
+
23
+ // Attributes excluded from extension change detection (not meaningful content changes)
24
+ var extensionExcludedAttrs = ['localId'];
25
+
26
+ // Extension node type names
27
+ var extensionNodeNames = ['extension', 'inlineExtension', 'bodiedExtension'];
28
+ var getStepAttrs = function getStepAttrs(step) {
29
+ if (step instanceof _transform.AttrStep) {
30
+ return [step.attr];
31
+ }
32
+ if (step instanceof _steps.SetAttrsStep && step.attrs) {
33
+ return Object.keys(step.attrs);
34
+ }
35
+ return [];
36
+ };
17
37
  var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRanges(doc, steps) {
18
38
  return steps.map(function (step) {
19
- if (step instanceof _transform.AttrStep && allowedAttrs.includes(step.attr) || step instanceof _steps.SetAttrsStep && step.attrs && (0, _toConsumableArray2.default)(Object.keys(step.attrs)).some(function (v) {
20
- return allowedAttrs.includes(v);
21
- })) {
22
- var $pos = doc.resolve(step.pos);
23
- if ($pos.parent.type === doc.type.schema.nodes.mediaSingle) {
24
- var startPos = $pos.pos + $pos.parentOffset;
39
+ if (!(step instanceof _transform.AttrStep) && !(step instanceof _steps.SetAttrsStep)) {
40
+ return undefined;
41
+ }
42
+ var stepAttrs = getStepAttrs(step);
43
+ var $pos = doc.resolve(step.pos);
44
+ var nodeAtPos = doc.nodeAt(step.pos);
45
+ if ((0, _expValEquals.expValEquals)('platform_editor_show_diff_improvements', 'isEnabled', true)) {
46
+ // date node: timestamp attribute change — highlight the date node itself (inline)
47
+ if (stepAttrs.some(function (v) {
48
+ return dateAttrs.includes(v);
49
+ }) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'date') {
50
+ return {
51
+ fromB: step.pos,
52
+ toB: step.pos + nodeAtPos.nodeSize,
53
+ isInline: true
54
+ };
55
+ }
56
+
57
+ // taskItem node: state attribute change — highlight the taskItem node
58
+ if (stepAttrs.some(function (v) {
59
+ return taskItemAttrs.includes(v);
60
+ }) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') {
25
61
  return {
26
- fromB: startPos,
27
- toB: startPos + $pos.parent.nodeSize - 1
62
+ fromB: step.pos,
63
+ toB: step.pos + nodeAtPos.nodeSize
28
64
  };
29
65
  }
66
+
67
+ // extension nodes: any attribute change except localId — highlight the node
68
+ if (nodeAtPos && extensionNodeNames.includes(nodeAtPos.type.name) && stepAttrs.some(function (v) {
69
+ return !extensionExcludedAttrs.includes(v);
70
+ })) {
71
+ var isInline = nodeAtPos.type.name === 'inlineExtension';
72
+ return {
73
+ fromB: step.pos,
74
+ toB: step.pos + nodeAtPos.nodeSize,
75
+ isInline: isInline
76
+ };
77
+ }
78
+ }
79
+
80
+ // media node: id/collection/url attribute change — highlight the mediaSingle parent
81
+ if (stepAttrs.some(function (v) {
82
+ return mediaAttrs.includes(v);
83
+ }) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) {
84
+ var startPos = $pos.pos + $pos.parentOffset;
85
+ return {
86
+ fromB: startPos,
87
+ toB: startPos + $pos.parent.nodeSize - 1
88
+ };
30
89
  }
31
90
  return undefined;
32
91
  }).filter(filterUndefined);
@@ -1,5 +1,20 @@
1
+ import { Transform } from '@atlaskit/editor-prosemirror/transform';
2
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
+
4
+ /**
5
+ * Returns a copy of the document with all marks removed from all text.
6
+ * This normalises mark fragmentation — e.g. annotation marks reordering can split
7
+ * a single text run into many text nodes, making structural comparison unreliable.
8
+ * After stripping, ProseMirror will merge adjacent text nodes so childCounts are stable.
9
+ */
10
+ function stripMarks(doc) {
11
+ const tr = new Transform(doc);
12
+ tr.removeMark(0, doc.content.size);
13
+ return tr.doc;
14
+ }
15
+
1
16
  /**
2
- * Returns true if both nodes have the same tree structure (type and child count at every level).
17
+ * Returns true if both (mark-stripped) nodes have the same block tree structure (node type and child count at every level)
3
18
  */
4
19
  function isBlockStructureEqual(node1, node2) {
5
20
  if (node1.type !== node2.type || node1.childCount !== node2.childCount) {
@@ -17,7 +32,23 @@ function isBlockStructureEqual(node1, node2) {
17
32
  * Looser equality for "safe diff" cases: same full text content and same block structure
18
33
  * (e.g. text moved across text-node boundaries). Used when strict areNodesEqualIgnoreAttrs fails.
19
34
  * This is safe because we ensure decorations get applied to valid positions.
35
+ *
36
+ * Marks are intentionally ignored — two documents that differ only in mark application
37
+ * (e.g. bold/italic boundaries or annotation mark ordering) are considered equal here.
38
+ * Both documents are mark-stripped before comparison so that mark-driven text fragmentation
39
+ * does not produce false inequalities.
20
40
  */
21
41
  export function areDocsEqualByBlockStructureAndText(doc1, doc2) {
22
- return doc1.textContent === doc2.textContent && doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(doc1, doc2);
42
+ if (doc1.textContent !== doc2.textContent) {
43
+ return false;
44
+ }
45
+ if (expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true)) {
46
+ // Strip marks before comparing so that mark-driven text fragmentation
47
+ // (e.g. annotation mark reordering producing different childCounts) does not
48
+ // cause false inequalities.
49
+ const stripped1 = stripMarks(doc1);
50
+ const stripped2 = stripMarks(doc2);
51
+ return doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(stripped1, stripped2);
52
+ }
53
+ return doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(doc1, doc2);
23
54
  }
@@ -202,15 +202,26 @@ const calculateDiffDecorationsInner = ({
202
202
  }));
203
203
  });
204
204
  getAttrChangeRanges(tr.doc, attrSteps).forEach(change => {
205
- decorations.push(...calculateNodesForBlockDecoration({
206
- doc: tr.doc,
207
- from: change.fromB,
208
- to: change.toB,
209
- colorScheme,
210
- isInserted: true,
211
- activeIndexPos,
212
- intl
213
- }));
205
+ if (expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true) && change.isInline) {
206
+ // Inline nodes (e.g. date) need an inline decoration rather than a block decoration
207
+ const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
208
+ decorations.push(createInlineChangedDecoration({
209
+ change,
210
+ colorScheme,
211
+ isActive,
212
+ isInserted: true
213
+ }));
214
+ } else {
215
+ decorations.push(...calculateNodesForBlockDecoration({
216
+ doc: tr.doc,
217
+ from: change.fromB,
218
+ to: change.toB,
219
+ colorScheme,
220
+ isInserted: true,
221
+ activeIndexPos,
222
+ intl
223
+ }));
224
+ }
214
225
  });
215
226
  return DecorationSet.empty.add(tr.doc, decorations);
216
227
  };
@@ -1,7 +1,24 @@
1
1
  import { simplifyChanges, ChangeSet } from 'prosemirror-changeset';
2
+ import { Mark } from '@atlaskit/editor-prosemirror/model';
2
3
  import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
3
4
  import { optimizeChanges } from './optimizeChanges';
4
5
  const mapPosition = (mapping, pos) => mapping.map(pos);
6
+
7
+ /**
8
+ * Compare marks between two nodes
9
+ * We have to check each child because adding a mark splits text into multiple nodes
10
+ */
11
+ const hasSameChildMarks = (left, right) => {
12
+ if (left.childCount !== right.childCount) {
13
+ return false;
14
+ }
15
+ for (let index = 0; index < left.childCount; index++) {
16
+ if (!Mark.sameSet(left.child(index).marks, right.child(index).marks)) {
17
+ return false;
18
+ }
19
+ }
20
+ return true;
21
+ };
5
22
  const createMapping = maps => {
6
23
  const mapping = new Mapping();
7
24
  for (const map of maps) {
@@ -44,8 +61,20 @@ const mergeOverlappingByNewDocRange = changes => {
44
61
  merged.push(current);
45
62
  return merged;
46
63
  };
47
- const isReplaceStepForTextBlockNode = (step, before, from, to) => {
48
- var _replacedSlice$conten, _replacingSlice$conte, _replacedSlice$conten2;
64
+
65
+ /**
66
+ * This function checks whether to do granular diffing.
67
+ * We should do granular diffing if:
68
+ * - The step is a replace step
69
+ * - The step is not open
70
+ * - The replaced slice is not open
71
+ * - The replaced slice has only one child
72
+ * - The replacing slice has only one child
73
+ * - The replaced slice and replacing slice have the same text content
74
+ * - The replaced slice and replacing slice have the same child marks (if text content is equal)
75
+ */
76
+ const shouldCheckGranularDiff = (step, before, from, to) => {
77
+ var _replacedNode$marks, _replacingNode$marks;
49
78
  if (!(step instanceof ReplaceStep)) {
50
79
  return false;
51
80
  }
@@ -54,7 +83,19 @@ const isReplaceStepForTextBlockNode = (step, before, from, to) => {
54
83
  }
55
84
  const replacedSlice = before.slice(from, to);
56
85
  const replacingSlice = step.slice;
57
- return Boolean(replacedSlice.openStart === 0 && replacedSlice.openEnd === 0 && replacedSlice.content.childCount === 1 && replacingSlice.content.childCount === 1 && ((_replacedSlice$conten = replacedSlice.content.firstChild) === null || _replacedSlice$conten === void 0 ? void 0 : _replacedSlice$conten.type.name) === ((_replacingSlice$conte = replacingSlice.content.firstChild) === null || _replacingSlice$conte === void 0 ? void 0 : _replacingSlice$conte.type.name) && ((_replacedSlice$conten2 = replacedSlice.content.firstChild) === null || _replacedSlice$conten2 === void 0 ? void 0 : _replacedSlice$conten2.type.isTextblock));
86
+ if (replacedSlice.openStart !== 0 || replacedSlice.openEnd !== 0 || replacedSlice.content.childCount !== 1 || replacingSlice.content.childCount !== 1) {
87
+ return false;
88
+ }
89
+ const replacedNode = replacedSlice.content.firstChild;
90
+ const replacingNode = replacingSlice.content.firstChild;
91
+ if ((replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.type.name) !== (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.type.name) || !(replacedNode !== null && replacedNode !== void 0 && replacedNode.type.isTextblock)) {
92
+ return false;
93
+ }
94
+ if (!Mark.sameSet((_replacedNode$marks = replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.marks) !== null && _replacedNode$marks !== void 0 ? _replacedNode$marks : [], (_replacingNode$marks = replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.marks) !== null && _replacingNode$marks !== void 0 ? _replacingNode$marks : [])) {
95
+ return false;
96
+ }
97
+ const isTextContentEqual = (replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.textContent) === (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.textContent);
98
+ return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
58
99
  };
59
100
  export const diffBySteps = (originalDoc, steps) => {
60
101
  const changes = [];
@@ -96,7 +137,7 @@ export const diffBySteps = (originalDoc, steps) => {
96
137
  const afterStepToFinal = createMapping(successfulStepMaps.slice(rangedStep.mapIndex + 1));
97
138
  const fromB = mapPosition(afterStepToFinal, fromAfterStep);
98
139
  const toB = mapPosition(afterStepToFinal, toAfterStep);
99
- if (isReplaceStepForTextBlockNode(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
140
+ if (shouldCheckGranularDiff(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
100
141
  const granularStepChanges = ChangeSet.create(rangedStep.before).addSteps(rangedStep.doc, [rangedStep.stepMap], null);
101
142
 
102
143
  // `simplifyChanges` reads text using `Change.fromB`/`toB`, which are
@@ -26,7 +26,7 @@ const getBlockNodeStyle = ({
26
26
  // Handle table separately to avoid border issues
27
27
  'tableRow', 'paragraph',
28
28
  // Paragraph and heading nodes do not need special styling
29
- 'heading', 'hardBreak', 'decisionList', 'taskList', 'taskItem', 'bulletList', 'orderedList', 'layoutSection'].includes(nodeName)) {
29
+ 'heading', 'hardBreak', 'decisionList', 'taskList', ...(expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true) ? [] : ['taskItem']), 'bulletList', 'orderedList', 'layoutSection'].includes(nodeName)) {
30
30
  // Layout nodes do not need special styling
31
31
  return undefined;
32
32
  }
@@ -1,20 +1,75 @@
1
1
  import { SetAttrsStep } from '@atlaskit/adf-schema/steps';
2
2
  import { AttrStep } from '@atlaskit/editor-prosemirror/transform';
3
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
4
  const filterUndefined = x => !!x;
4
5
 
5
- // Currently allow attributes that indicats a change in media image
6
- const allowedAttrs = ['id', 'collection', 'url'];
6
+ // Attributes that indicate a change in media image
7
+ const mediaAttrs = ['id', 'collection', 'url'];
8
+
9
+ // Attribute that indicates a date change
10
+ const dateAttrs = ['timestamp'];
11
+
12
+ // Attribute that indicates a task item state change
13
+ const taskItemAttrs = ['state'];
14
+
15
+ // Attributes excluded from extension change detection (not meaningful content changes)
16
+ const extensionExcludedAttrs = ['localId'];
17
+
18
+ // Extension node type names
19
+ const extensionNodeNames = ['extension', 'inlineExtension', 'bodiedExtension'];
20
+ const getStepAttrs = step => {
21
+ if (step instanceof AttrStep) {
22
+ return [step.attr];
23
+ }
24
+ if (step instanceof SetAttrsStep && step.attrs) {
25
+ return Object.keys(step.attrs);
26
+ }
27
+ return [];
28
+ };
7
29
  export const getAttrChangeRanges = (doc, steps) => {
8
30
  return steps.map(step => {
9
- if (step instanceof AttrStep && allowedAttrs.includes(step.attr) || step instanceof SetAttrsStep && step.attrs && [...Object.keys(step.attrs)].some(v => allowedAttrs.includes(v))) {
10
- const $pos = doc.resolve(step.pos);
11
- if ($pos.parent.type === doc.type.schema.nodes.mediaSingle) {
12
- const startPos = $pos.pos + $pos.parentOffset;
31
+ if (!(step instanceof AttrStep) && !(step instanceof SetAttrsStep)) {
32
+ return undefined;
33
+ }
34
+ const stepAttrs = getStepAttrs(step);
35
+ const $pos = doc.resolve(step.pos);
36
+ const nodeAtPos = doc.nodeAt(step.pos);
37
+ if (expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true)) {
38
+ // date node: timestamp attribute change — highlight the date node itself (inline)
39
+ if (stepAttrs.some(v => dateAttrs.includes(v)) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'date') {
40
+ return {
41
+ fromB: step.pos,
42
+ toB: step.pos + nodeAtPos.nodeSize,
43
+ isInline: true
44
+ };
45
+ }
46
+
47
+ // taskItem node: state attribute change — highlight the taskItem node
48
+ if (stepAttrs.some(v => taskItemAttrs.includes(v)) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') {
13
49
  return {
14
- fromB: startPos,
15
- toB: startPos + $pos.parent.nodeSize - 1
50
+ fromB: step.pos,
51
+ toB: step.pos + nodeAtPos.nodeSize
16
52
  };
17
53
  }
54
+
55
+ // extension nodes: any attribute change except localId — highlight the node
56
+ if (nodeAtPos && extensionNodeNames.includes(nodeAtPos.type.name) && stepAttrs.some(v => !extensionExcludedAttrs.includes(v))) {
57
+ const isInline = nodeAtPos.type.name === 'inlineExtension';
58
+ return {
59
+ fromB: step.pos,
60
+ toB: step.pos + nodeAtPos.nodeSize,
61
+ isInline
62
+ };
63
+ }
64
+ }
65
+
66
+ // media node: id/collection/url attribute change — highlight the mediaSingle parent
67
+ if (stepAttrs.some(v => mediaAttrs.includes(v)) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) {
68
+ const startPos = $pos.pos + $pos.parentOffset;
69
+ return {
70
+ fromB: startPos,
71
+ toB: startPos + $pos.parent.nodeSize - 1
72
+ };
18
73
  }
19
74
  return undefined;
20
75
  }).filter(filterUndefined);
@@ -1,5 +1,20 @@
1
+ import { Transform } from '@atlaskit/editor-prosemirror/transform';
2
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
+
4
+ /**
5
+ * Returns a copy of the document with all marks removed from all text.
6
+ * This normalises mark fragmentation — e.g. annotation marks reordering can split
7
+ * a single text run into many text nodes, making structural comparison unreliable.
8
+ * After stripping, ProseMirror will merge adjacent text nodes so childCounts are stable.
9
+ */
10
+ function stripMarks(doc) {
11
+ var tr = new Transform(doc);
12
+ tr.removeMark(0, doc.content.size);
13
+ return tr.doc;
14
+ }
15
+
1
16
  /**
2
- * Returns true if both nodes have the same tree structure (type and child count at every level).
17
+ * Returns true if both (mark-stripped) nodes have the same block tree structure (node type and child count at every level)
3
18
  */
4
19
  function isBlockStructureEqual(node1, node2) {
5
20
  if (node1.type !== node2.type || node1.childCount !== node2.childCount) {
@@ -17,7 +32,23 @@ function isBlockStructureEqual(node1, node2) {
17
32
  * Looser equality for "safe diff" cases: same full text content and same block structure
18
33
  * (e.g. text moved across text-node boundaries). Used when strict areNodesEqualIgnoreAttrs fails.
19
34
  * This is safe because we ensure decorations get applied to valid positions.
35
+ *
36
+ * Marks are intentionally ignored — two documents that differ only in mark application
37
+ * (e.g. bold/italic boundaries or annotation mark ordering) are considered equal here.
38
+ * Both documents are mark-stripped before comparison so that mark-driven text fragmentation
39
+ * does not produce false inequalities.
20
40
  */
21
41
  export function areDocsEqualByBlockStructureAndText(doc1, doc2) {
22
- return doc1.textContent === doc2.textContent && doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(doc1, doc2);
42
+ if (doc1.textContent !== doc2.textContent) {
43
+ return false;
44
+ }
45
+ if (expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true)) {
46
+ // Strip marks before comparing so that mark-driven text fragmentation
47
+ // (e.g. annotation mark reordering producing different childCounts) does not
48
+ // cause false inequalities.
49
+ var stripped1 = stripMarks(doc1);
50
+ var stripped2 = stripMarks(doc2);
51
+ return doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(stripped1, stripped2);
52
+ }
53
+ return doc1.nodeSize === doc2.nodeSize && isBlockStructureEqual(doc1, doc2);
23
54
  }
@@ -215,15 +215,26 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref3
215
215
  }));
216
216
  });
217
217
  getAttrChangeRanges(tr.doc, attrSteps).forEach(function (change) {
218
- decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration({
219
- doc: tr.doc,
220
- from: change.fromB,
221
- to: change.toB,
222
- colorScheme: colorScheme,
223
- isInserted: true,
224
- activeIndexPos: activeIndexPos,
225
- intl: intl
226
- })));
218
+ if (expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true) && change.isInline) {
219
+ // Inline nodes (e.g. date) need an inline decoration rather than a block decoration
220
+ var isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
221
+ decorations.push(createInlineChangedDecoration({
222
+ change: change,
223
+ colorScheme: colorScheme,
224
+ isActive: isActive,
225
+ isInserted: true
226
+ }));
227
+ } else {
228
+ decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration({
229
+ doc: tr.doc,
230
+ from: change.fromB,
231
+ to: change.toB,
232
+ colorScheme: colorScheme,
233
+ isInserted: true,
234
+ activeIndexPos: activeIndexPos,
235
+ intl: intl
236
+ })));
237
+ }
227
238
  });
228
239
  return DecorationSet.empty.add(tr.doc, decorations);
229
240
  };
@@ -6,11 +6,28 @@ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol
6
6
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
7
7
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
8
8
  import { simplifyChanges, ChangeSet } from 'prosemirror-changeset';
9
+ import { Mark } from '@atlaskit/editor-prosemirror/model';
9
10
  import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
10
11
  import { optimizeChanges } from './optimizeChanges';
11
12
  var mapPosition = function mapPosition(mapping, pos) {
12
13
  return mapping.map(pos);
13
14
  };
15
+
16
+ /**
17
+ * Compare marks between two nodes
18
+ * We have to check each child because adding a mark splits text into multiple nodes
19
+ */
20
+ var hasSameChildMarks = function hasSameChildMarks(left, right) {
21
+ if (left.childCount !== right.childCount) {
22
+ return false;
23
+ }
24
+ for (var index = 0; index < left.childCount; index++) {
25
+ if (!Mark.sameSet(left.child(index).marks, right.child(index).marks)) {
26
+ return false;
27
+ }
28
+ }
29
+ return true;
30
+ };
14
31
  var createMapping = function createMapping(maps) {
15
32
  var mapping = new Mapping();
16
33
  var _iterator = _createForOfIteratorHelper(maps),
@@ -62,8 +79,20 @@ var mergeOverlappingByNewDocRange = function mergeOverlappingByNewDocRange(chang
62
79
  merged.push(current);
63
80
  return merged;
64
81
  };
65
- var isReplaceStepForTextBlockNode = function isReplaceStepForTextBlockNode(step, before, from, to) {
66
- var _replacedSlice$conten, _replacingSlice$conte, _replacedSlice$conten2;
82
+
83
+ /**
84
+ * This function checks whether to do granular diffing.
85
+ * We should do granular diffing if:
86
+ * - The step is a replace step
87
+ * - The step is not open
88
+ * - The replaced slice is not open
89
+ * - The replaced slice has only one child
90
+ * - The replacing slice has only one child
91
+ * - The replaced slice and replacing slice have the same text content
92
+ * - The replaced slice and replacing slice have the same child marks (if text content is equal)
93
+ */
94
+ var shouldCheckGranularDiff = function shouldCheckGranularDiff(step, before, from, to) {
95
+ var _replacedNode$marks, _replacingNode$marks;
67
96
  if (!(step instanceof ReplaceStep)) {
68
97
  return false;
69
98
  }
@@ -72,7 +101,19 @@ var isReplaceStepForTextBlockNode = function isReplaceStepForTextBlockNode(step,
72
101
  }
73
102
  var replacedSlice = before.slice(from, to);
74
103
  var replacingSlice = step.slice;
75
- return Boolean(replacedSlice.openStart === 0 && replacedSlice.openEnd === 0 && replacedSlice.content.childCount === 1 && replacingSlice.content.childCount === 1 && ((_replacedSlice$conten = replacedSlice.content.firstChild) === null || _replacedSlice$conten === void 0 ? void 0 : _replacedSlice$conten.type.name) === ((_replacingSlice$conte = replacingSlice.content.firstChild) === null || _replacingSlice$conte === void 0 ? void 0 : _replacingSlice$conte.type.name) && ((_replacedSlice$conten2 = replacedSlice.content.firstChild) === null || _replacedSlice$conten2 === void 0 ? void 0 : _replacedSlice$conten2.type.isTextblock));
104
+ if (replacedSlice.openStart !== 0 || replacedSlice.openEnd !== 0 || replacedSlice.content.childCount !== 1 || replacingSlice.content.childCount !== 1) {
105
+ return false;
106
+ }
107
+ var replacedNode = replacedSlice.content.firstChild;
108
+ var replacingNode = replacingSlice.content.firstChild;
109
+ if ((replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.type.name) !== (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.type.name) || !(replacedNode !== null && replacedNode !== void 0 && replacedNode.type.isTextblock)) {
110
+ return false;
111
+ }
112
+ if (!Mark.sameSet((_replacedNode$marks = replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.marks) !== null && _replacedNode$marks !== void 0 ? _replacedNode$marks : [], (_replacingNode$marks = replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.marks) !== null && _replacingNode$marks !== void 0 ? _replacingNode$marks : [])) {
113
+ return false;
114
+ }
115
+ var isTextContentEqual = (replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.textContent) === (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.textContent);
116
+ return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
76
117
  };
77
118
  export var diffBySteps = function diffBySteps(originalDoc, steps) {
78
119
  var changes = [];
@@ -124,7 +165,7 @@ export var diffBySteps = function diffBySteps(originalDoc, steps) {
124
165
  var afterStepToFinal = createMapping(successfulStepMaps.slice(rangedStep.mapIndex + 1));
125
166
  var fromB = mapPosition(afterStepToFinal, fromAfterStep);
126
167
  var toB = mapPosition(afterStepToFinal, toAfterStep);
127
- if (isReplaceStepForTextBlockNode(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
168
+ if (shouldCheckGranularDiff(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
128
169
  var granularStepChanges = ChangeSet.create(rangedStep.before).addSteps(rangedStep.doc, [rangedStep.stepMap], null);
129
170
 
130
171
  // `simplifyChanges` reads text using `Change.fromB`/`toB`, which are
@@ -1,3 +1,4 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
2
3
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
4
  import { fg } from '@atlaskit/platform-feature-flags';
@@ -27,7 +28,7 @@ var getBlockNodeStyle = function getBlockNodeStyle(_ref) {
27
28
  // Handle table separately to avoid border issues
28
29
  'tableRow', 'paragraph',
29
30
  // Paragraph and heading nodes do not need special styling
30
- 'heading', 'hardBreak', 'decisionList', 'taskList', 'taskItem', 'bulletList', 'orderedList', 'layoutSection'].includes(nodeName)) {
31
+ 'heading', 'hardBreak', 'decisionList', 'taskList'].concat(_toConsumableArray(expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true) ? [] : ['taskItem']), ['bulletList', 'orderedList', 'layoutSection']).includes(nodeName)) {
31
32
  // Layout nodes do not need special styling
32
33
  return undefined;
33
34
  }
@@ -1,25 +1,85 @@
1
- import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
1
  import { SetAttrsStep } from '@atlaskit/adf-schema/steps';
3
2
  import { AttrStep } from '@atlaskit/editor-prosemirror/transform';
3
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
4
  var filterUndefined = function filterUndefined(x) {
5
5
  return !!x;
6
6
  };
7
7
 
8
- // Currently allow attributes that indicats a change in media image
9
- var allowedAttrs = ['id', 'collection', 'url'];
8
+ // Attributes that indicate a change in media image
9
+ var mediaAttrs = ['id', 'collection', 'url'];
10
+
11
+ // Attribute that indicates a date change
12
+ var dateAttrs = ['timestamp'];
13
+
14
+ // Attribute that indicates a task item state change
15
+ var taskItemAttrs = ['state'];
16
+
17
+ // Attributes excluded from extension change detection (not meaningful content changes)
18
+ var extensionExcludedAttrs = ['localId'];
19
+
20
+ // Extension node type names
21
+ var extensionNodeNames = ['extension', 'inlineExtension', 'bodiedExtension'];
22
+ var getStepAttrs = function getStepAttrs(step) {
23
+ if (step instanceof AttrStep) {
24
+ return [step.attr];
25
+ }
26
+ if (step instanceof SetAttrsStep && step.attrs) {
27
+ return Object.keys(step.attrs);
28
+ }
29
+ return [];
30
+ };
10
31
  export var getAttrChangeRanges = function getAttrChangeRanges(doc, steps) {
11
32
  return steps.map(function (step) {
12
- if (step instanceof AttrStep && allowedAttrs.includes(step.attr) || step instanceof SetAttrsStep && step.attrs && _toConsumableArray(Object.keys(step.attrs)).some(function (v) {
13
- return allowedAttrs.includes(v);
14
- })) {
15
- var $pos = doc.resolve(step.pos);
16
- if ($pos.parent.type === doc.type.schema.nodes.mediaSingle) {
17
- var startPos = $pos.pos + $pos.parentOffset;
33
+ if (!(step instanceof AttrStep) && !(step instanceof SetAttrsStep)) {
34
+ return undefined;
35
+ }
36
+ var stepAttrs = getStepAttrs(step);
37
+ var $pos = doc.resolve(step.pos);
38
+ var nodeAtPos = doc.nodeAt(step.pos);
39
+ if (expValEquals('platform_editor_show_diff_improvements', 'isEnabled', true)) {
40
+ // date node: timestamp attribute change — highlight the date node itself (inline)
41
+ if (stepAttrs.some(function (v) {
42
+ return dateAttrs.includes(v);
43
+ }) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'date') {
44
+ return {
45
+ fromB: step.pos,
46
+ toB: step.pos + nodeAtPos.nodeSize,
47
+ isInline: true
48
+ };
49
+ }
50
+
51
+ // taskItem node: state attribute change — highlight the taskItem node
52
+ if (stepAttrs.some(function (v) {
53
+ return taskItemAttrs.includes(v);
54
+ }) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') {
18
55
  return {
19
- fromB: startPos,
20
- toB: startPos + $pos.parent.nodeSize - 1
56
+ fromB: step.pos,
57
+ toB: step.pos + nodeAtPos.nodeSize
21
58
  };
22
59
  }
60
+
61
+ // extension nodes: any attribute change except localId — highlight the node
62
+ if (nodeAtPos && extensionNodeNames.includes(nodeAtPos.type.name) && stepAttrs.some(function (v) {
63
+ return !extensionExcludedAttrs.includes(v);
64
+ })) {
65
+ var isInline = nodeAtPos.type.name === 'inlineExtension';
66
+ return {
67
+ fromB: step.pos,
68
+ toB: step.pos + nodeAtPos.nodeSize,
69
+ isInline: isInline
70
+ };
71
+ }
72
+ }
73
+
74
+ // media node: id/collection/url attribute change — highlight the mediaSingle parent
75
+ if (stepAttrs.some(function (v) {
76
+ return mediaAttrs.includes(v);
77
+ }) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) {
78
+ var startPos = $pos.pos + $pos.parentOffset;
79
+ return {
80
+ fromB: startPos,
81
+ toB: startPos + $pos.parent.nodeSize - 1
82
+ };
23
83
  }
24
84
  return undefined;
25
85
  }).filter(filterUndefined);
@@ -3,5 +3,10 @@ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
3
  * Looser equality for "safe diff" cases: same full text content and same block structure
4
4
  * (e.g. text moved across text-node boundaries). Used when strict areNodesEqualIgnoreAttrs fails.
5
5
  * This is safe because we ensure decorations get applied to valid positions.
6
+ *
7
+ * Marks are intentionally ignored — two documents that differ only in mark application
8
+ * (e.g. bold/italic boundaries or annotation mark ordering) are considered equal here.
9
+ * Both documents are mark-stripped before comparison so that mark-driven text fragmentation
10
+ * does not produce false inequalities.
6
11
  */
7
12
  export declare function areDocsEqualByBlockStructureAndText(doc1: PMNode, doc2: PMNode): boolean;
@@ -2,6 +2,8 @@ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
2
  import { type Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
3
3
  type StepRange = {
4
4
  fromB: number;
5
+ /** Whether the changed node is inline (true) or block (false/undefined) */
6
+ isInline?: boolean;
5
7
  toB: number;
6
8
  };
7
9
  export declare const getAttrChangeRanges: (doc: PMNode, steps: ProseMirrorStep[]) => StepRange[];
@@ -3,5 +3,10 @@ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
3
  * Looser equality for "safe diff" cases: same full text content and same block structure
4
4
  * (e.g. text moved across text-node boundaries). Used when strict areNodesEqualIgnoreAttrs fails.
5
5
  * This is safe because we ensure decorations get applied to valid positions.
6
+ *
7
+ * Marks are intentionally ignored — two documents that differ only in mark application
8
+ * (e.g. bold/italic boundaries or annotation mark ordering) are considered equal here.
9
+ * Both documents are mark-stripped before comparison so that mark-driven text fragmentation
10
+ * does not produce false inequalities.
6
11
  */
7
12
  export declare function areDocsEqualByBlockStructureAndText(doc1: PMNode, doc2: PMNode): boolean;
@@ -2,6 +2,8 @@ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
2
  import { type Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
3
3
  type StepRange = {
4
4
  fromB: number;
5
+ /** Whether the changed node is inline (true) or block (false/undefined) */
6
+ isInline?: boolean;
5
7
  toB: number;
6
8
  };
7
9
  export declare const getAttrChangeRanges: (doc: PMNode, steps: ProseMirrorStep[]) => StepRange[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-show-diff",
3
- "version": "8.4.0",
3
+ "version": "8.4.2",
4
4
  "description": "ShowDiff plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -35,7 +35,7 @@
35
35
  "@atlaskit/editor-prosemirror": "^7.3.0",
36
36
  "@atlaskit/editor-tables": "^2.9.0",
37
37
  "@atlaskit/platform-feature-flags": "^1.1.0",
38
- "@atlaskit/tmp-editor-statsig": "^80.0.0",
38
+ "@atlaskit/tmp-editor-statsig": "^80.1.0",
39
39
  "@atlaskit/tokens": "^13.0.0",
40
40
  "@babel/runtime": "^7.0.0",
41
41
  "lodash": "^4.17.21",