@atlaskit/editor-plugin-show-diff 21.0.1 → 21.0.3

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 (35) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cjs/pm-plugins/NodeViewSerializer.js +33 -4
  3. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +51 -13
  4. package/dist/cjs/pm-plugins/calculateDiff/markAwareTokenEncoder.js +2 -1
  5. package/dist/cjs/pm-plugins/calculateDiff/selectTokenEncoder.js +1 -1
  6. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +1 -1
  7. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +3 -3
  8. package/dist/cjs/pm-plugins/decorations/utils/diffableAttrs.js +5 -1
  9. package/dist/cjs/pm-plugins/decorations/utils/getAttrChangeRanges.js +10 -7
  10. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -0
  11. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +6 -0
  12. package/dist/es2019/pm-plugins/NodeViewSerializer.js +31 -7
  13. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +38 -11
  14. package/dist/es2019/pm-plugins/calculateDiff/markAwareTokenEncoder.js +2 -1
  15. package/dist/es2019/pm-plugins/calculateDiff/selectTokenEncoder.js +1 -1
  16. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +1 -1
  17. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +3 -3
  18. package/dist/es2019/pm-plugins/decorations/utils/diffableAttrs.js +6 -1
  19. package/dist/es2019/pm-plugins/decorations/utils/getAttrChangeRanges.js +4 -3
  20. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -0
  21. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +6 -0
  22. package/dist/esm/pm-plugins/NodeViewSerializer.js +33 -4
  23. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +51 -13
  24. package/dist/esm/pm-plugins/calculateDiff/markAwareTokenEncoder.js +2 -1
  25. package/dist/esm/pm-plugins/calculateDiff/selectTokenEncoder.js +1 -1
  26. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +1 -1
  27. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +3 -3
  28. package/dist/esm/pm-plugins/decorations/utils/diffableAttrs.js +6 -1
  29. package/dist/esm/pm-plugins/decorations/utils/getAttrChangeRanges.js +10 -7
  30. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -0
  31. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +6 -0
  32. package/dist/types/pm-plugins/NodeViewSerializer.d.ts +2 -1
  33. package/dist/types/pm-plugins/calculateDiff/markAwareTokenEncoder.d.ts +2 -1
  34. package/dist/types/pm-plugins/calculateDiff/selectTokenEncoder.d.ts +1 -1
  35. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 21.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`bb99ff596a192`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/bb99ff596a192) -
8
+ Fix show-diff rendering a bordered box around fully-replaced paragraphs, a
9
+ platform_editor_ai_show_diff_patch_2 regression caused by paragraph falling through to the generic
10
+ block-container border treatment intended for real block containers (panels, blockquotes). Gated
11
+ behind platform_editor_ai_show_diff_patch_2, the same gate that caused the regression.
12
+
13
+ Also give an inserted atomic inline leaf (date/emoji/mention/status) the same box-shadow highlight
14
+ an in-place attribute change already gets, when it renders through `NodeViewSerializer`'s
15
+ blocklisted-container recursion under platform_editor_show_diff_deleted_nodeview_content — that
16
+ recursion gives the node its real node view instead of a lossy `toDOM()`, but previously carried
17
+ no diff styling of its own.
18
+
19
+ - Updated dependencies
20
+
21
+ ## 21.0.2
22
+
23
+ ### Patch Changes
24
+
25
+ - [`1647c1098fca5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1647c1098fca5) -
26
+ Highlight changed inline nodes and the edited portion of link display text
27
+
3
28
  ## 21.0.1
4
29
 
5
30
  ### Patch Changes
@@ -14,6 +14,8 @@ var _utils = require("@atlaskit/editor-prosemirror/utils");
14
14
  var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
15
15
  var _fg = require("@atlaskit/platform-feature-flags/fg");
16
16
  var _createEditorProxy = require("./createEditorProxy");
17
+ var _createInlineChangedDecoration = require("./decorations/createInlineChangedDecoration");
18
+ var _getAttrChangeRanges = require("./decorations/utils/getAttrChangeRanges");
17
19
  var _markViews = require("./markViews");
18
20
  /**
19
21
  * Utilities for working with ProseMirror node views and DOM serialization within the
@@ -45,6 +47,14 @@ function isEditorViewWithNodeViews(view) {
45
47
  /** Expand-family nodes whose expanded state lives in a node-keyed WeakMap. */
46
48
  var EXPAND_TYPES = new Set(['expand', 'nestedExpand']);
47
49
 
50
+ /** Merges an inline style string onto an element's existing `style` attribute. */
51
+ var appendInlineStyle = function appendInlineStyle(element, style) {
52
+ var _element$getAttribute;
53
+ var currentStyle = (_element$getAttribute = element.getAttribute('style')) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : '';
54
+ var separator = currentStyle && style && !currentStyle.trimEnd().endsWith(';') ? '; ' : '';
55
+ element.setAttribute('style', "".concat(currentStyle).concat(separator).concat(style));
56
+ };
57
+
48
58
  /** Tables are the only node views that read and write positions, so only they need a preview. */
49
59
  var containsTable = function containsTable(node) {
50
60
  var table = node.type.schema.nodes.table;
@@ -99,13 +109,28 @@ var NodeViewSerializer = exports.NodeViewSerializer = /*#__PURE__*/function () {
99
109
  var _this = this;
100
110
  var basePos = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
101
111
  var editorProxy = arguments.length > 3 ? arguments[3] : undefined;
112
+ var isInserted = arguments.length > 4 ? arguments[4] : undefined;
113
+ var colorScheme = arguments.length > 5 ? arguments[5] : undefined;
102
114
  // A node's first child sits one position inside it, and each subsequent
103
115
  // child is offset by the previous child's `nodeSize`. Tracking this lets nested node
104
116
  // views resolve to their real depth instead of every one of them seeing depth 0.
105
117
  var childPos = basePos + 1;
106
118
  children.forEach(function (child) {
107
- var childNode = _this.tryCreateNodeViewInner(child, childPos, editorProxy) || _this.serializeNode(child);
119
+ var childNode = _this.tryCreateNodeViewInner(child, childPos, editorProxy, isInserted, colorScheme) || _this.serializeNode(child);
108
120
  if (childNode) {
121
+ // Give inserted atomic children the same box-shadow
122
+ // highlight an in-place attribute change already gets (`getAtomicInlineChangedAttrs`).
123
+ if (isInserted && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_deleted_nodeview_content') && (0, _getAttrChangeRanges.isInlineAttrChangeNodeName)(child.type.name) && childNode instanceof HTMLElement) {
124
+ var _childNode$getAttribu;
125
+ var _getAtomicInlineChang = (0, _createInlineChangedDecoration.getAtomicInlineChangedAttrs)(child.type.name, colorScheme),
126
+ className = _getAtomicInlineChang.className,
127
+ styleSuffix = _getAtomicInlineChang.styleSuffix;
128
+ var existingClassName = (_childNode$getAttribu = childNode.getAttribute('class')) !== null && _childNode$getAttribu !== void 0 ? _childNode$getAttribu : '';
129
+ childNode.setAttribute('class', existingClassName ? "".concat(existingClassName, " ").concat(className) : className);
130
+ if (styleSuffix) {
131
+ appendInlineStyle(childNode, styleSuffix);
132
+ }
133
+ }
109
134
  contentDOM === null || contentDOM === void 0 || contentDOM.append(childNode);
110
135
  }
111
136
  childPos += child.nodeSize;
@@ -126,13 +151,15 @@ var NodeViewSerializer = exports.NodeViewSerializer = /*#__PURE__*/function () {
126
151
  var _preview$rootPos;
127
152
  var basePos = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
128
153
  var $sourcePos = arguments.length > 2 ? arguments[2] : undefined;
154
+ var isInserted = arguments.length > 3 ? arguments[3] : undefined;
155
+ var colorScheme = arguments.length > 4 ? arguments[4] : undefined;
129
156
  if (!this.editorView) {
130
157
  return null;
131
158
  }
132
159
  // The preview becomes the document every nested node view resolves its position against,
133
160
  // so positions stay internally consistent.
134
161
  var preview = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && containsTable(targetNode) ? (0, _createEditorProxy.createEditorProxy)(this.editorView, targetNode, $sourcePos) : null;
135
- return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy);
162
+ return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy, isInserted, colorScheme);
136
163
  }
137
164
  }, {
138
165
  key: "tryCreateNodeViewInner",
@@ -140,6 +167,8 @@ var NodeViewSerializer = exports.NodeViewSerializer = /*#__PURE__*/function () {
140
167
  var _this$nodeViews;
141
168
  var basePos = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
142
169
  var editorProxy = arguments.length > 2 ? arguments[2] : undefined;
170
+ var isInserted = arguments.length > 3 ? arguments[3] : undefined;
171
+ var colorScheme = arguments.length > 4 ? arguments[4] : undefined;
143
172
  if (!this.editorView) {
144
173
  return null;
145
174
  }
@@ -185,7 +214,7 @@ var NodeViewSerializer = exports.NodeViewSerializer = /*#__PURE__*/function () {
185
214
  if (!(0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_deleted_nodeview_content') && targetNode.type.name === 'paragraph' && targetNode.children.length === 1) {
186
215
  return this.serializeFragment(targetNode.content);
187
216
  }
188
- this.appendChildNodes(targetNode.children, _contentDOM, basePos, editorProxy);
217
+ this.appendChildNodes(targetNode.children, _contentDOM, basePos, editorProxy, isInserted, colorScheme);
189
218
  }
190
219
  return this.withMarkViews(targetNode, _dom, editorProxy);
191
220
  }
@@ -209,7 +238,7 @@ var NodeViewSerializer = exports.NodeViewSerializer = /*#__PURE__*/function () {
209
238
  dom = _constructor.dom,
210
239
  contentDOM = _constructor.contentDOM;
211
240
  // Iteratively populate children
212
- this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy);
241
+ this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy, isInserted, colorScheme);
213
242
  return this.withMarkViews(targetNode, dom, editorProxy);
214
243
  } catch (_unused) {
215
244
  return null;
@@ -464,11 +464,14 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
464
464
  };
465
465
  }
466
466
  }
467
- // The attribute-aware encoder is only needed by the smart classifier and is gated with it.
468
- var _selectTokenEncoder = (0, _selectTokenEncoder2.selectTokenEncoder)(diffType === 'smart' && (0, _fg.fg)('platform_editor_ai_smart_diff')),
467
+ // The attribute-aware encoder is needed by the smart classifier and is gated with it.
468
+ // The NCS version-history path also uses it so the main diff calculation detects attribute-only
469
+ // node replacements, e.g. changing an emoji or mention without changing the node type.
470
+ var _selectTokenEncoder = (0, _selectTokenEncoder2.selectTokenEncoder)(diffType === 'smart' && (0, _fg.fg)('platform_editor_ai_smart_diff') || (0, _fg.fg)('confluence_ncs_step_diffing_version_history')),
469
471
  tokenEncoder = _selectTokenEncoder.tokenEncoder,
470
472
  shouldHideMarkOnlyDeletions = _selectTokenEncoder.shouldHideMarkOnlyDeletions;
471
473
  var changes;
474
+ var contentChanges;
472
475
  var attributedChanges = [];
473
476
  var attributionColors;
474
477
  if (attributedChangesetEnabled) {
@@ -482,6 +485,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
482
485
  stepIndex: stepIndex
483
486
  });
484
487
  }));
488
+ contentChanges = changeset.changes;
485
489
  attributedChanges = diffType === 'step' ? (0, _diffBySteps.diffBySteps)(originalDoc, simplifiedSteps, simplifiedStepAttributions) : (0, _toConsumableArray2.default)(changeset.changes);
486
490
  // The attributed pipeline gives no guarantee that its output ranges are disjoint — see
487
491
  // `collapseOverlappingChanges`.
@@ -501,6 +505,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
501
505
  }
502
506
  } else {
503
507
  var _changeset = _prosemirrorChangeset.ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
508
+ contentChanges = _changeset.changes;
504
509
  changes = getChanges({
505
510
  changeset: _changeset,
506
511
  originalDoc: originalDoc,
@@ -760,20 +765,52 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
760
765
  changes.forEach(function (change) {
761
766
  createDecorationsForChange(change);
762
767
  });
763
- // The attribute-aware token encoder already reports mark-only changes as regular changes in the
764
- // smart path. Only add the legacy step-range decoration when no equivalent changeset range was
765
- // produced; otherwise the same content receives two inline decorations (and two navigation
766
- // changes), usually one attribution colour and one default purple decoration.
767
- (0, _getMarkChangeRanges.getMarkChangeRanges)(simplifiedSteps, simplifiedStepAttributions, (0, _fg.fg)('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(function (_ref10) {
768
+ // Compare the content outside replacement ranges at matching positions in both documents.
769
+ // This preserves real formatting edits without highlighting an unchanged link mark reapplied
770
+ // while editing its display text.
771
+ var hasUnhighlightedMarkChanges = function hasUnhighlightedMarkChanges(_ref10) {
768
772
  var fromB = _ref10.fromB,
769
773
  toB = _ref10.toB;
770
- return !isHandledByTable(fromB, toB);
771
- }).filter(function (_ref11) {
774
+ var from = fromB;
775
+ var offset = 0;
776
+ var differs = function differs(to) {
777
+ return from < to && !originalDoc.slice(from + offset, to + offset).content.eq(tr.doc.slice(from, to).content);
778
+ };
779
+ var _iterator5 = _createForOfIteratorHelper(contentChanges),
780
+ _step5;
781
+ try {
782
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
783
+ var change = _step5.value;
784
+ if (change.fromB >= toB) {
785
+ break;
786
+ }
787
+ if (change.toB > from) {
788
+ if (differs(change.fromB)) {
789
+ return true;
790
+ }
791
+ from = change.toB;
792
+ }
793
+ offset = change.toA - change.toB;
794
+ }
795
+ } catch (err) {
796
+ _iterator5.e(err);
797
+ } finally {
798
+ _iterator5.f();
799
+ }
800
+ return differs(toB);
801
+ };
802
+
803
+ // The attribute-aware token encoder already reports mark-only changes as regular changes in the
804
+ // smart path and the NCS version-history path. For NCS version history, only skip the step-range
805
+ // highlight if the main diff covers it or the remaining content has unchanged formatting.
806
+ (0, _getMarkChangeRanges.getMarkChangeRanges)(simplifiedSteps, simplifiedStepAttributions, (0, _fg.fg)('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(function (_ref11) {
772
807
  var fromB = _ref11.fromB,
773
808
  toB = _ref11.toB;
809
+ return !isHandledByTable(fromB, toB);
810
+ }).filter(function (range) {
774
811
  return !(0, _fg.fg)('confluence_ncs_step_diffing_version_history') || !changes.some(function (change) {
775
- return change.fromB === fromB && change.toB === toB;
776
- });
812
+ return change.fromB <= range.fromB && change.toB >= range.toB;
813
+ }) && hasUnhighlightedMarkChanges(range);
777
814
  }).forEach(function (change) {
778
815
  var _attributionColors$ge, _attributionColors;
779
816
  var changeColorScheme = change.attributionKey ? (_attributionColors$ge = (_attributionColors = attributionColors) === null || _attributionColors === void 0 ? void 0 : _attributionColors.get(change.attributionKey)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : colorScheme : colorScheme;
@@ -797,8 +834,9 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
797
834
  return !isHandledByTable(fromB, toB);
798
835
  }).filter(function (_ref13) {
799
836
  var fromB = _ref13.fromB,
800
- toB = _ref13.toB;
801
- return !(0, _fg.fg)('confluence_ncs_step_diffing_version_history') || !changes.some(function (change) {
837
+ toB = _ref13.toB,
838
+ isInline = _ref13.isInline;
839
+ return !(0, _fg.fg)('confluence_ncs_step_diffing_version_history') || !isInline || !changes.some(function (change) {
802
840
  return change.fromB === fromB && change.toB === toB;
803
841
  });
804
842
  }).forEach(function (change) {
@@ -20,7 +20,8 @@ var typeID = function typeID(type) {
20
20
  *
21
21
  * Deliberately narrower than `attrAwareTokenEncoder`: node-start tokens keep the library default
22
22
  * and do not fold in diffable attributes, which `getAttrChangeRanges` already reports for every
23
- * diff type.
23
+ * diff type. The NCS version-history path also uses `attrAwareTokenEncoder` so main diff calculation
24
+ * detects attribute-only node replacements.
24
25
  */
25
26
  var markAwareTokenEncoder = exports.markAwareTokenEncoder = {
26
27
  encodeCharacter: function encodeCharacter(char, marks) {
@@ -11,7 +11,7 @@ var _markAwareTokenEncoder = require("./markAwareTokenEncoder");
11
11
  * `computeDiffChanges` so the two cannot drift.
12
12
  *
13
13
  * @param useAttrAwareEncoder Caller is running the smart classifier, which needs node attrs folded
14
- * into the token.
14
+ * into the token. Also used by the NCS version-history path to detect attribute-only node changes.
15
15
  */
16
16
  var selectTokenEncoder = exports.selectTokenEncoder = function selectTokenEncoder(useAttrAwareEncoder) {
17
17
  if (useAttrAwareEncoder) {
@@ -182,7 +182,7 @@ var createChangedRowDOM = function createChangedRowDOM(_ref3) {
182
182
  var cellIndex = 0;
183
183
  rowNode.content.forEach(function (cellNode) {
184
184
  if (cellNode.type.name === 'tableCell' || cellNode.type.name === 'tableHeader') {
185
- var nodeView = nodeViewSerializer.tryCreateNodeView(cellNode);
185
+ var nodeView = nodeViewSerializer.tryCreateNodeView(cellNode, 0, undefined, isInserted, colorScheme);
186
186
  if (nodeView) {
187
187
  if (nodeView instanceof HTMLElement) {
188
188
  (0, _tableCellEdgeAttrs.applyCellEdgeAttrs)(nodeView, cellEdgeAttrs === null || cellEdgeAttrs === void 0 ? void 0 : cellEdgeAttrs[cellIndex]);
@@ -82,7 +82,7 @@ var createTableCellContentWidgets = function createTableCellContentWidgets(_ref)
82
82
  // A cell contains block content (paragraphs); serialize the cell's content
83
83
  // so every block inside the cell is rendered.
84
84
  cellNode.content.forEach(function (blockNode) {
85
- var nodeView = nodeViewSerializer.tryCreateNodeView(blockNode);
85
+ var nodeView = nodeViewSerializer.tryCreateNodeView(blockNode, 0, undefined, isInserted, colorScheme);
86
86
  var wrapper = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, false, isInserted);
87
87
  if (nodeView) {
88
88
  wrapper.append(nodeView);
@@ -322,7 +322,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
322
322
  var handleMultipleChildNodes = function handleMultipleChildNodes(node) {
323
323
  if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
324
324
  node.content.forEach(function (childNode) {
325
- var childNodeView = serializer.tryCreateNodeView(childNode);
325
+ var childNodeView = serializer.tryCreateNodeView(childNode, 0, undefined, isInserted, colorScheme);
326
326
  if (childNodeView) {
327
327
  var lineBreak = document.createElement('br');
328
328
  dom.append(lineBreak);
@@ -378,7 +378,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
378
378
 
379
379
  // Try to create node view, fallback to serialization
380
380
  var $sourcePos = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? getSourcePos(doc, $changeFromA, slice.openStart, index) : null;
381
- var nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined);
381
+ var nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined, isInserted, colorScheme);
382
382
  if (nodeView) {
383
383
  if (node.isInline) {
384
384
  var wrapper = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, isActive, isInserted, reveal);
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.isDiffableAttr = exports.getRequiredIncludedDiffableAttrs = exports.getIncludedDiffableAttrs = exports.getDiffableAttrNames = exports.DIFFABLE_ATTRS_BY_NODE_TYPE = void 0;
7
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
7
8
  /**
8
9
  * Single source of truth for which attributes of a node type represent a
9
10
  * meaningful change. Shared by `attrAwareTokenEncoder` (changeset detection)
@@ -94,10 +95,13 @@ var getRequiredIncludedDiffableAttrs = exports.getRequiredIncludedDiffableAttrs
94
95
  * order is not stable across the two diffed documents.
95
96
  */
96
97
  var getDiffableAttrNames = exports.getDiffableAttrNames = function getDiffableAttrNames(nodeTypeName, attrs) {
98
+ // Adding a column updates existing widths, ignore this attribute change. Move this into
99
+ // DIFFABLE_ATTRS_BY_NODE_TYPE when cleaning up confluence_ncs_step_diffing_version_history
100
+ var ignoreColumnWidth = nodeTypeName === 'layoutColumn' && (0, _fg.fg)('confluence_ncs_step_diffing_version_history');
97
101
  var rule = DIFFABLE_ATTRS_BY_NODE_TYPE[nodeTypeName];
98
102
  if (!rule) {
99
103
  return Object.keys(attrs).filter(function (name) {
100
- return !DEFAULT_EXCLUDED_ATTRS.includes(name);
104
+ return !DEFAULT_EXCLUDED_ATTRS.includes(name) && !(ignoreColumnWidth && name === 'width');
101
105
  }).sort();
102
106
  }
103
107
  if ('include' in rule) {
@@ -90,6 +90,11 @@ var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRa
90
90
  var $pos = doc.resolve(finalPos);
91
91
  var nodeAtPos = doc.nodeAt(finalPos);
92
92
  var originalNodeAtPos = originalPos === undefined ? null : originalDoc.nodeAt(originalPos);
93
+ var hasChangedAttr = function hasChangedAttr(watchedAttrs) {
94
+ return attrsToCheck.some(function (attr) {
95
+ return watchedAttrs.includes(attr) && (!(0, _fg.fg)('confluence_ncs_step_diffing_version_history') || !originalNodeAtPos || originalNodeAtPos.type !== (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type) || !(0, _isEqual.default)(originalNodeAtPos.attrs[attr], nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.attrs[attr]));
96
+ });
97
+ };
93
98
 
94
99
  // The changeset path (createDecorationsForChange) handles the deletion widget via
95
100
  // prosemirror-changeset; we only need to add the inline insertion highlight here.
@@ -97,9 +102,7 @@ var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRa
97
102
  var nodeName = nodeAtPos.type.name;
98
103
  if (isInlineAttrChangeNodeName(nodeName)) {
99
104
  var watchedAttrs = inlineNodeAttrMap[nodeName];
100
- if (attrsToCheck.some(function (v) {
101
- return watchedAttrs.includes(v);
102
- })) {
105
+ if (hasChangedAttr(watchedAttrs)) {
103
106
  return _objectSpread(_objectSpread({}, attributionKey ? {
104
107
  attributionKey: attributionKey
105
108
  } : {}), {}, {
@@ -131,9 +134,7 @@ var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRa
131
134
  // (e.g. note -> warning) — highlight the new panel and, when the original node
132
135
  // is resolvable, expose its range (fromA/toA) so the caller can render the old
133
136
  // panel as a "deleted" widget for a before/after comparison.
134
- if (attrsToCheck.some(function (v) {
135
- return panelAttrs.includes(v);
136
- }) && nodeAtPos && (0, _nodeTypeUtils.getBaseNodeTypeName)(nodeAtPos.type) === 'panel') {
137
+ if (hasChangedAttr(panelAttrs) && nodeAtPos && (0, _nodeTypeUtils.getBaseNodeTypeName)(nodeAtPos.type) === 'panel') {
137
138
  return _objectSpread(_objectSpread({}, attributionKey ? {
138
139
  attributionKey: attributionKey
139
140
  } : {}), {}, {
@@ -181,7 +182,9 @@ var getAttrChangeRanges = exports.getAttrChangeRanges = function getAttrChangeRa
181
182
  // (e.g. setNodeAttribute(pos, 'text', ...) + setNodeAttribute(pos, 'color', ...))
182
183
  // should produce only one decoration, not one per step.
183
184
  .filter(function (range, i, arr) {
184
- return arr.findIndex(function (r) {
185
+ return (0, _fg.fg)('confluence_ncs_step_diffing_version_history') ? !arr.some(function (r, nextIndex) {
186
+ return nextIndex > i && r.fromB === range.fromB;
187
+ }) : arr.findIndex(function (r) {
185
188
  return r.fromB === range.fromB;
186
189
  }) === i;
187
190
  });
@@ -41,6 +41,10 @@ var isMultiContainerBlockNode = exports.isMultiContainerBlockNode = function isM
41
41
  return ['decisionList', 'layoutSection'].includes(nodeName);
42
42
  };
43
43
  var isTextLikeBlockNode = exports.isTextLikeBlockNode = function isTextLikeBlockNode(nodeName) {
44
+ // When cleaning up platform_editor_ai_show_diff_patch_2 just put paragraph into the list
45
+ if (nodeName === 'paragraph') {
46
+ return (0, _fg.fg)('platform_editor_ai_show_diff_patch_2');
47
+ }
44
48
  return ['heading', 'bulletList', 'orderedList', 'listItem', 'taskList', 'blockquote'].includes(nodeName);
45
49
  };
46
50
  var getChangedContentStyleNext = function getChangedContentStyleNext(colorScheme) {
@@ -78,6 +78,12 @@ var isMultiContainerBlockNode = function isMultiContainerBlockNode(nodeName) {
78
78
  return ['decisionList', 'layoutSection'].includes(nodeName);
79
79
  };
80
80
  var isTextLikeBlockNode = function isTextLikeBlockNode(nodeName) {
81
+ // `paragraph` only reaches this classification via patch_2's `shouldRenderAsBlockNode` routing
82
+ // (createNodeChangedDecorationWidget.ts), which fully replaced paragraphs never hit otherwise —
83
+ // gating it here keeps that dependency explicit rather than implicit.
84
+ if (nodeName === 'paragraph') {
85
+ return (0, _fg.fg)('platform_editor_ai_show_diff_patch_2');
86
+ }
81
87
  return ['heading', 'bulletList', 'orderedList', 'listItem', 'taskList', 'blockquote'].includes(nodeName);
82
88
  };
83
89
 
@@ -4,6 +4,8 @@ import { contains } from '@atlaskit/editor-prosemirror/utils';
4
4
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
5
5
  import { fg } from '@atlaskit/platform-feature-flags/fg';
6
6
  import { createEditorProxy } from './createEditorProxy';
7
+ import { getAtomicInlineChangedAttrs } from './decorations/createInlineChangedDecoration';
8
+ import { isInlineAttrChangeNodeName } from './decorations/utils/getAttrChangeRanges';
7
9
  import { wrapInMarkViews } from './markViews';
8
10
 
9
11
  /**
@@ -36,6 +38,14 @@ export function isEditorViewWithNodeViews(view) {
36
38
  /** Expand-family nodes whose expanded state lives in a node-keyed WeakMap. */
37
39
  const EXPAND_TYPES = new Set(['expand', 'nestedExpand']);
38
40
 
41
+ /** Merges an inline style string onto an element's existing `style` attribute. */
42
+ const appendInlineStyle = (element, style) => {
43
+ var _element$getAttribute;
44
+ const currentStyle = (_element$getAttribute = element.getAttribute('style')) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : '';
45
+ const separator = currentStyle && style && !currentStyle.trimEnd().endsWith(';') ? '; ' : '';
46
+ element.setAttribute('style', `${currentStyle}${separator}${style}`);
47
+ };
48
+
39
49
  /** Tables are the only node views that read and write positions, so only they need a preview. */
40
50
  const containsTable = node => {
41
51
  const {
@@ -83,14 +93,28 @@ export class NodeViewSerializer {
83
93
  /**
84
94
  * Appends serialized child nodes to the given contentDOM element.
85
95
  */
86
- appendChildNodes(children, contentDOM, basePos = 0, editorProxy) {
96
+ appendChildNodes(children, contentDOM, basePos = 0, editorProxy, isInserted, colorScheme) {
87
97
  // A node's first child sits one position inside it, and each subsequent
88
98
  // child is offset by the previous child's `nodeSize`. Tracking this lets nested node
89
99
  // views resolve to their real depth instead of every one of them seeing depth 0.
90
100
  let childPos = basePos + 1;
91
101
  children.forEach(child => {
92
- const childNode = this.tryCreateNodeViewInner(child, childPos, editorProxy) || this.serializeNode(child);
102
+ const childNode = this.tryCreateNodeViewInner(child, childPos, editorProxy, isInserted, colorScheme) || this.serializeNode(child);
93
103
  if (childNode) {
104
+ // Give inserted atomic children the same box-shadow
105
+ // highlight an in-place attribute change already gets (`getAtomicInlineChangedAttrs`).
106
+ if (isInserted && isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && isInlineAttrChangeNodeName(child.type.name) && childNode instanceof HTMLElement) {
107
+ var _childNode$getAttribu;
108
+ const {
109
+ className,
110
+ styleSuffix
111
+ } = getAtomicInlineChangedAttrs(child.type.name, colorScheme);
112
+ const existingClassName = (_childNode$getAttribu = childNode.getAttribute('class')) !== null && _childNode$getAttribu !== void 0 ? _childNode$getAttribu : '';
113
+ childNode.setAttribute('class', existingClassName ? `${existingClassName} ${className}` : className);
114
+ if (styleSuffix) {
115
+ appendInlineStyle(childNode, styleSuffix);
116
+ }
117
+ }
94
118
  contentDOM === null || contentDOM === void 0 ? void 0 : contentDOM.append(childNode);
95
119
  }
96
120
  childPos += child.nodeSize;
@@ -105,7 +129,7 @@ export class NodeViewSerializer {
105
129
  *
106
130
  * `$sourcePos` is where the node sits in the doc it came from, so nested tables keep their depth.
107
131
  */
108
- tryCreateNodeView(targetNode, basePos = 0, $sourcePos) {
132
+ tryCreateNodeView(targetNode, basePos = 0, $sourcePos, isInserted, colorScheme) {
109
133
  var _preview$rootPos;
110
134
  if (!this.editorView) {
111
135
  return null;
@@ -113,9 +137,9 @@ export class NodeViewSerializer {
113
137
  // The preview becomes the document every nested node view resolves its position against,
114
138
  // so positions stay internally consistent.
115
139
  const preview = fg('platform_editor_ai_show_diff_patch_2') && containsTable(targetNode) ? createEditorProxy(this.editorView, targetNode, $sourcePos) : null;
116
- return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy);
140
+ return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy, isInserted, colorScheme);
117
141
  }
118
- tryCreateNodeViewInner(targetNode, basePos = 0, editorProxy) {
142
+ tryCreateNodeViewInner(targetNode, basePos = 0, editorProxy, isInserted, colorScheme) {
119
143
  var _this$nodeViews;
120
144
  if (!this.editorView) {
121
145
  return null;
@@ -163,7 +187,7 @@ export class NodeViewSerializer {
163
187
  if (!isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && targetNode.type.name === 'paragraph' && targetNode.children.length === 1) {
164
188
  return this.serializeFragment(targetNode.content);
165
189
  }
166
- this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy);
190
+ this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy, isInserted, colorScheme);
167
191
  }
168
192
  return this.withMarkViews(targetNode, dom, editorProxy);
169
193
  }
@@ -186,7 +210,7 @@ export class NodeViewSerializer {
186
210
  contentDOM
187
211
  } = constructor(targetNode, view, () => resolvedPos, [], {});
188
212
  // Iteratively populate children
189
- this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy);
213
+ this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy, isInserted, colorScheme);
190
214
  return this.withMarkViews(targetNode, dom, editorProxy);
191
215
  } catch {
192
216
  return null;
@@ -418,12 +418,15 @@ const calculateDiffDecorationsInner = ({
418
418
  };
419
419
  }
420
420
  }
421
- // The attribute-aware encoder is only needed by the smart classifier and is gated with it.
421
+ // The attribute-aware encoder is needed by the smart classifier and is gated with it.
422
+ // The NCS version-history path also uses it so the main diff calculation detects attribute-only
423
+ // node replacements, e.g. changing an emoji or mention without changing the node type.
422
424
  const {
423
425
  tokenEncoder,
424
426
  shouldHideMarkOnlyDeletions
425
- } = selectTokenEncoder(diffType === 'smart' && fg('platform_editor_ai_smart_diff'));
427
+ } = selectTokenEncoder(diffType === 'smart' && fg('platform_editor_ai_smart_diff') || fg('confluence_ncs_step_diffing_version_history'));
426
428
  let changes;
429
+ let contentChanges;
427
430
  let attributedChanges = [];
428
431
  let attributionColors;
429
432
  if (attributedChangesetEnabled) {
@@ -434,6 +437,7 @@ const calculateDiffDecorationsInner = ({
434
437
  ...attribution,
435
438
  stepIndex
436
439
  })));
440
+ contentChanges = changeset.changes;
437
441
  attributedChanges = diffType === 'step' ? diffBySteps(originalDoc, simplifiedSteps, simplifiedStepAttributions) : [...changeset.changes];
438
442
  // The attributed pipeline gives no guarantee that its output ranges are disjoint — see
439
443
  // `collapseOverlappingChanges`.
@@ -453,6 +457,7 @@ const calculateDiffDecorationsInner = ({
453
457
  }
454
458
  } else {
455
459
  const changeset = ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
460
+ contentChanges = changeset.changes;
456
461
  changes = getChanges({
457
462
  changeset,
458
463
  originalDoc,
@@ -686,17 +691,38 @@ const calculateDiffDecorationsInner = ({
686
691
  changes.forEach(change => {
687
692
  createDecorationsForChange(change);
688
693
  });
689
- // The attribute-aware token encoder already reports mark-only changes as regular changes in the
690
- // smart path. Only add the legacy step-range decoration when no equivalent changeset range was
691
- // produced; otherwise the same content receives two inline decorations (and two navigation
692
- // changes), usually one attribution colour and one default purple decoration.
693
- getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(({
694
+ // Compare the content outside replacement ranges at matching positions in both documents.
695
+ // This preserves real formatting edits without highlighting an unchanged link mark reapplied
696
+ // while editing its display text.
697
+ const hasUnhighlightedMarkChanges = ({
694
698
  fromB,
695
699
  toB
696
- }) => !isHandledByTable(fromB, toB)).filter(({
700
+ }) => {
701
+ let from = fromB;
702
+ let offset = 0;
703
+ const differs = to => from < to && !originalDoc.slice(from + offset, to + offset).content.eq(tr.doc.slice(from, to).content);
704
+ for (const change of contentChanges) {
705
+ if (change.fromB >= toB) {
706
+ break;
707
+ }
708
+ if (change.toB > from) {
709
+ if (differs(change.fromB)) {
710
+ return true;
711
+ }
712
+ from = change.toB;
713
+ }
714
+ offset = change.toA - change.toB;
715
+ }
716
+ return differs(toB);
717
+ };
718
+
719
+ // The attribute-aware token encoder already reports mark-only changes as regular changes in the
720
+ // smart path and the NCS version-history path. For NCS version history, only skip the step-range
721
+ // highlight if the main diff covers it or the remaining content has unchanged formatting.
722
+ getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(({
697
723
  fromB,
698
724
  toB
699
- }) => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
725
+ }) => !isHandledByTable(fromB, toB)).filter(range => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB <= range.fromB && change.toB >= range.toB) && hasUnhighlightedMarkChanges(range)).forEach(change => {
700
726
  var _attributionColors$ge, _attributionColors;
701
727
  const changeColorScheme = change.attributionKey ? (_attributionColors$ge = (_attributionColors = attributionColors) === null || _attributionColors === void 0 ? void 0 : _attributionColors.get(change.attributionKey)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : colorScheme : colorScheme;
702
728
  const attributionKey = showContributorTags ? change.attributionKey : undefined;
@@ -718,8 +744,9 @@ const calculateDiffDecorationsInner = ({
718
744
  toB
719
745
  }) => !isHandledByTable(fromB, toB)).filter(({
720
746
  fromB,
721
- toB
722
- }) => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
747
+ toB,
748
+ isInline
749
+ }) => !fg('confluence_ncs_step_diffing_version_history') || !isInline || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
723
750
  var _attributionColors$ge2, _attributionColors2;
724
751
  const changeColorScheme = change.attributionKey ? (_attributionColors$ge2 = (_attributionColors2 = attributionColors) === null || _attributionColors2 === void 0 ? void 0 : _attributionColors2.get(change.attributionKey)) !== null && _attributionColors$ge2 !== void 0 ? _attributionColors$ge2 : colorScheme : colorScheme;
725
752
  const attributionKey = showContributorTags ? change.attributionKey : undefined;
@@ -15,7 +15,8 @@ const typeID = type => {
15
15
  *
16
16
  * Deliberately narrower than `attrAwareTokenEncoder`: node-start tokens keep the library default
17
17
  * and do not fold in diffable attributes, which `getAttrChangeRanges` already reports for every
18
- * diff type.
18
+ * diff type. The NCS version-history path also uses `attrAwareTokenEncoder` so main diff calculation
19
+ * detects attribute-only node replacements.
19
20
  */
20
21
  export const markAwareTokenEncoder = {
21
22
  encodeCharacter: (char, marks) => encodeCharacterWithMarks(char, marks),
@@ -5,7 +5,7 @@ import { markAwareTokenEncoder } from './markAwareTokenEncoder';
5
5
  * `computeDiffChanges` so the two cannot drift.
6
6
  *
7
7
  * @param useAttrAwareEncoder Caller is running the smart classifier, which needs node attrs folded
8
- * into the token.
8
+ * into the token. Also used by the NCS version-history path to detect attribute-only node changes.
9
9
  */
10
10
  export const selectTokenEncoder = useAttrAwareEncoder => {
11
11
  if (useAttrAwareEncoder) {
@@ -157,7 +157,7 @@ const createChangedRowDOM = ({
157
157
  let cellIndex = 0;
158
158
  rowNode.content.forEach(cellNode => {
159
159
  if (cellNode.type.name === 'tableCell' || cellNode.type.name === 'tableHeader') {
160
- const nodeView = nodeViewSerializer.tryCreateNodeView(cellNode);
160
+ const nodeView = nodeViewSerializer.tryCreateNodeView(cellNode, 0, undefined, isInserted, colorScheme);
161
161
  if (nodeView) {
162
162
  if (nodeView instanceof HTMLElement) {
163
163
  applyCellEdgeAttrs(nodeView, cellEdgeAttrs === null || cellEdgeAttrs === void 0 ? void 0 : cellEdgeAttrs[cellIndex]);
@@ -72,7 +72,7 @@ const createTableCellContentWidgets = ({
72
72
  // A cell contains block content (paragraphs); serialize the cell's content
73
73
  // so every block inside the cell is rendered.
74
74
  cellNode.content.forEach(blockNode => {
75
- const nodeView = nodeViewSerializer.tryCreateNodeView(blockNode);
75
+ const nodeView = nodeViewSerializer.tryCreateNodeView(blockNode, 0, undefined, isInserted, colorScheme);
76
76
  const wrapper = createContentWrapper(colorScheme, false, isInserted);
77
77
  if (nodeView) {
78
78
  wrapper.append(nodeView);
@@ -314,7 +314,7 @@ export const createNodeChangedDecorationWidget = ({
314
314
  const handleMultipleChildNodes = node => {
315
315
  if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
316
316
  node.content.forEach(childNode => {
317
- const childNodeView = serializer.tryCreateNodeView(childNode);
317
+ const childNodeView = serializer.tryCreateNodeView(childNode, 0, undefined, isInserted, colorScheme);
318
318
  if (childNodeView) {
319
319
  const lineBreak = document.createElement('br');
320
320
  dom.append(lineBreak);
@@ -364,7 +364,7 @@ export const createNodeChangedDecorationWidget = ({
364
364
 
365
365
  // Try to create node view, fallback to serialization
366
366
  const $sourcePos = fg('platform_editor_ai_show_diff_patch_2') ? getSourcePos(doc, $changeFromA, slice.openStart, index) : null;
367
- const nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined);
367
+ const nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined, isInserted, colorScheme);
368
368
  if (nodeView) {
369
369
  if (node.isInline) {
370
370
  const wrapper = createContentWrapper(colorScheme, isActive, isInserted, reveal);
@@ -1,3 +1,5 @@
1
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
2
+
1
3
  /**
2
4
  * Single source of truth for which attributes of a node type represent a
3
5
  * meaningful change. Shared by `attrAwareTokenEncoder` (changeset detection)
@@ -88,9 +90,12 @@ export const getRequiredIncludedDiffableAttrs = nodeTypeName => {
88
90
  * order is not stable across the two diffed documents.
89
91
  */
90
92
  export const getDiffableAttrNames = (nodeTypeName, attrs) => {
93
+ // Adding a column updates existing widths, ignore this attribute change. Move this into
94
+ // DIFFABLE_ATTRS_BY_NODE_TYPE when cleaning up confluence_ncs_step_diffing_version_history
95
+ const ignoreColumnWidth = nodeTypeName === 'layoutColumn' && fg('confluence_ncs_step_diffing_version_history');
91
96
  const rule = DIFFABLE_ATTRS_BY_NODE_TYPE[nodeTypeName];
92
97
  if (!rule) {
93
- return Object.keys(attrs).filter(name => !DEFAULT_EXCLUDED_ATTRS.includes(name)).sort();
98
+ return Object.keys(attrs).filter(name => !DEFAULT_EXCLUDED_ATTRS.includes(name) && !(ignoreColumnWidth && name === 'width')).sort();
94
99
  }
95
100
  if ('include' in rule) {
96
101
  return rule.include;
@@ -74,6 +74,7 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
74
74
  const $pos = doc.resolve(finalPos);
75
75
  const nodeAtPos = doc.nodeAt(finalPos);
76
76
  const originalNodeAtPos = originalPos === undefined ? null : originalDoc.nodeAt(originalPos);
77
+ const hasChangedAttr = watchedAttrs => attrsToCheck.some(attr => watchedAttrs.includes(attr) && (!fg('confluence_ncs_step_diffing_version_history') || !originalNodeAtPos || originalNodeAtPos.type !== (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type) || !isEqual(originalNodeAtPos.attrs[attr], nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.attrs[attr])));
77
78
 
78
79
  // The changeset path (createDecorationsForChange) handles the deletion widget via
79
80
  // prosemirror-changeset; we only need to add the inline insertion highlight here.
@@ -81,7 +82,7 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
81
82
  const nodeName = nodeAtPos.type.name;
82
83
  if (isInlineAttrChangeNodeName(nodeName)) {
83
84
  const watchedAttrs = inlineNodeAttrMap[nodeName];
84
- if (attrsToCheck.some(v => watchedAttrs.includes(v))) {
85
+ if (hasChangedAttr(watchedAttrs)) {
85
86
  return {
86
87
  ...(attributionKey ? {
87
88
  attributionKey
@@ -114,7 +115,7 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
114
115
  // (e.g. note -> warning) — highlight the new panel and, when the original node
115
116
  // is resolvable, expose its range (fromA/toA) so the caller can render the old
116
117
  // panel as a "deleted" widget for a before/after comparison.
117
- if (attrsToCheck.some(v => panelAttrs.includes(v)) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') {
118
+ if (hasChangedAttr(panelAttrs) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') {
118
119
  return {
119
120
  ...(attributionKey ? {
120
121
  attributionKey
@@ -161,7 +162,7 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
161
162
  // Deduplicate by node position: multiple AttrSteps on the same node
162
163
  // (e.g. setNodeAttribute(pos, 'text', ...) + setNodeAttribute(pos, 'color', ...))
163
164
  // should produce only one decoration, not one per step.
164
- .filter((range, i, arr) => arr.findIndex(r => r.fromB === range.fromB) === i);
165
+ .filter((range, i, arr) => fg('confluence_ncs_step_diffing_version_history') ? !arr.some((r, nextIndex) => nextIndex > i && r.fromB === range.fromB) : arr.findIndex(r => r.fromB === range.fromB) === i);
165
166
  };
166
167
 
167
168
  /**
@@ -30,6 +30,10 @@ export const isMultiContainerBlockNode = nodeName => {
30
30
  return ['decisionList', 'layoutSection'].includes(nodeName);
31
31
  };
32
32
  export const isTextLikeBlockNode = nodeName => {
33
+ // When cleaning up platform_editor_ai_show_diff_patch_2 just put paragraph into the list
34
+ if (nodeName === 'paragraph') {
35
+ return fg('platform_editor_ai_show_diff_patch_2');
36
+ }
33
37
  return ['heading', 'bulletList', 'orderedList', 'listItem', 'taskList', 'blockquote'].includes(nodeName);
34
38
  };
35
39
  const getChangedContentStyleNext = (colorScheme, isActive = false, isInserted = false, hideAddedDiffsUnderline = false) => {
@@ -71,6 +71,12 @@ const isMultiContainerBlockNode = nodeName => {
71
71
  return ['decisionList', 'layoutSection'].includes(nodeName);
72
72
  };
73
73
  const isTextLikeBlockNode = nodeName => {
74
+ // `paragraph` only reaches this classification via patch_2's `shouldRenderAsBlockNode` routing
75
+ // (createNodeChangedDecorationWidget.ts), which fully replaced paragraphs never hit otherwise —
76
+ // gating it here keeps that dependency explicit rather than implicit.
77
+ if (nodeName === 'paragraph') {
78
+ return fg('platform_editor_ai_show_diff_patch_2');
79
+ }
74
80
  return ['heading', 'bulletList', 'orderedList', 'listItem', 'taskList', 'blockquote'].includes(nodeName);
75
81
  };
76
82
 
@@ -6,6 +6,8 @@ import { contains } from '@atlaskit/editor-prosemirror/utils';
6
6
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
7
7
  import { fg } from '@atlaskit/platform-feature-flags/fg';
8
8
  import { createEditorProxy } from './createEditorProxy';
9
+ import { getAtomicInlineChangedAttrs } from './decorations/createInlineChangedDecoration';
10
+ import { isInlineAttrChangeNodeName } from './decorations/utils/getAttrChangeRanges';
9
11
  import { wrapInMarkViews } from './markViews';
10
12
 
11
13
  /**
@@ -38,6 +40,14 @@ export function isEditorViewWithNodeViews(view) {
38
40
  /** Expand-family nodes whose expanded state lives in a node-keyed WeakMap. */
39
41
  var EXPAND_TYPES = new Set(['expand', 'nestedExpand']);
40
42
 
43
+ /** Merges an inline style string onto an element's existing `style` attribute. */
44
+ var appendInlineStyle = function appendInlineStyle(element, style) {
45
+ var _element$getAttribute;
46
+ var currentStyle = (_element$getAttribute = element.getAttribute('style')) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : '';
47
+ var separator = currentStyle && style && !currentStyle.trimEnd().endsWith(';') ? '; ' : '';
48
+ element.setAttribute('style', "".concat(currentStyle).concat(separator).concat(style));
49
+ };
50
+
41
51
  /** Tables are the only node views that read and write positions, so only they need a preview. */
42
52
  var containsTable = function containsTable(node) {
43
53
  var table = node.type.schema.nodes.table;
@@ -92,13 +102,28 @@ export var NodeViewSerializer = /*#__PURE__*/function () {
92
102
  var _this = this;
93
103
  var basePos = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
94
104
  var editorProxy = arguments.length > 3 ? arguments[3] : undefined;
105
+ var isInserted = arguments.length > 4 ? arguments[4] : undefined;
106
+ var colorScheme = arguments.length > 5 ? arguments[5] : undefined;
95
107
  // A node's first child sits one position inside it, and each subsequent
96
108
  // child is offset by the previous child's `nodeSize`. Tracking this lets nested node
97
109
  // views resolve to their real depth instead of every one of them seeing depth 0.
98
110
  var childPos = basePos + 1;
99
111
  children.forEach(function (child) {
100
- var childNode = _this.tryCreateNodeViewInner(child, childPos, editorProxy) || _this.serializeNode(child);
112
+ var childNode = _this.tryCreateNodeViewInner(child, childPos, editorProxy, isInserted, colorScheme) || _this.serializeNode(child);
101
113
  if (childNode) {
114
+ // Give inserted atomic children the same box-shadow
115
+ // highlight an in-place attribute change already gets (`getAtomicInlineChangedAttrs`).
116
+ if (isInserted && isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && isInlineAttrChangeNodeName(child.type.name) && childNode instanceof HTMLElement) {
117
+ var _childNode$getAttribu;
118
+ var _getAtomicInlineChang = getAtomicInlineChangedAttrs(child.type.name, colorScheme),
119
+ className = _getAtomicInlineChang.className,
120
+ styleSuffix = _getAtomicInlineChang.styleSuffix;
121
+ var existingClassName = (_childNode$getAttribu = childNode.getAttribute('class')) !== null && _childNode$getAttribu !== void 0 ? _childNode$getAttribu : '';
122
+ childNode.setAttribute('class', existingClassName ? "".concat(existingClassName, " ").concat(className) : className);
123
+ if (styleSuffix) {
124
+ appendInlineStyle(childNode, styleSuffix);
125
+ }
126
+ }
102
127
  contentDOM === null || contentDOM === void 0 || contentDOM.append(childNode);
103
128
  }
104
129
  childPos += child.nodeSize;
@@ -119,13 +144,15 @@ export var NodeViewSerializer = /*#__PURE__*/function () {
119
144
  var _preview$rootPos;
120
145
  var basePos = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
121
146
  var $sourcePos = arguments.length > 2 ? arguments[2] : undefined;
147
+ var isInserted = arguments.length > 3 ? arguments[3] : undefined;
148
+ var colorScheme = arguments.length > 4 ? arguments[4] : undefined;
122
149
  if (!this.editorView) {
123
150
  return null;
124
151
  }
125
152
  // The preview becomes the document every nested node view resolves its position against,
126
153
  // so positions stay internally consistent.
127
154
  var preview = fg('platform_editor_ai_show_diff_patch_2') && containsTable(targetNode) ? createEditorProxy(this.editorView, targetNode, $sourcePos) : null;
128
- return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy);
155
+ return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy, isInserted, colorScheme);
129
156
  }
130
157
  }, {
131
158
  key: "tryCreateNodeViewInner",
@@ -133,6 +160,8 @@ export var NodeViewSerializer = /*#__PURE__*/function () {
133
160
  var _this$nodeViews;
134
161
  var basePos = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
135
162
  var editorProxy = arguments.length > 2 ? arguments[2] : undefined;
163
+ var isInserted = arguments.length > 3 ? arguments[3] : undefined;
164
+ var colorScheme = arguments.length > 4 ? arguments[4] : undefined;
136
165
  if (!this.editorView) {
137
166
  return null;
138
167
  }
@@ -178,7 +207,7 @@ export var NodeViewSerializer = /*#__PURE__*/function () {
178
207
  if (!isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && targetNode.type.name === 'paragraph' && targetNode.children.length === 1) {
179
208
  return this.serializeFragment(targetNode.content);
180
209
  }
181
- this.appendChildNodes(targetNode.children, _contentDOM, basePos, editorProxy);
210
+ this.appendChildNodes(targetNode.children, _contentDOM, basePos, editorProxy, isInserted, colorScheme);
182
211
  }
183
212
  return this.withMarkViews(targetNode, _dom, editorProxy);
184
213
  }
@@ -202,7 +231,7 @@ export var NodeViewSerializer = /*#__PURE__*/function () {
202
231
  dom = _constructor.dom,
203
232
  contentDOM = _constructor.contentDOM;
204
233
  // Iteratively populate children
205
- this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy);
234
+ this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy, isInserted, colorScheme);
206
235
  return this.withMarkViews(targetNode, dom, editorProxy);
207
236
  } catch (_unused) {
208
237
  return null;
@@ -457,11 +457,14 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
457
457
  };
458
458
  }
459
459
  }
460
- // The attribute-aware encoder is only needed by the smart classifier and is gated with it.
461
- var _selectTokenEncoder = selectTokenEncoder(diffType === 'smart' && fg('platform_editor_ai_smart_diff')),
460
+ // The attribute-aware encoder is needed by the smart classifier and is gated with it.
461
+ // The NCS version-history path also uses it so the main diff calculation detects attribute-only
462
+ // node replacements, e.g. changing an emoji or mention without changing the node type.
463
+ var _selectTokenEncoder = selectTokenEncoder(diffType === 'smart' && fg('platform_editor_ai_smart_diff') || fg('confluence_ncs_step_diffing_version_history')),
462
464
  tokenEncoder = _selectTokenEncoder.tokenEncoder,
463
465
  shouldHideMarkOnlyDeletions = _selectTokenEncoder.shouldHideMarkOnlyDeletions;
464
466
  var changes;
467
+ var contentChanges;
465
468
  var attributedChanges = [];
466
469
  var attributionColors;
467
470
  if (attributedChangesetEnabled) {
@@ -475,6 +478,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
475
478
  stepIndex: stepIndex
476
479
  });
477
480
  }));
481
+ contentChanges = changeset.changes;
478
482
  attributedChanges = diffType === 'step' ? diffBySteps(originalDoc, simplifiedSteps, simplifiedStepAttributions) : _toConsumableArray(changeset.changes);
479
483
  // The attributed pipeline gives no guarantee that its output ranges are disjoint — see
480
484
  // `collapseOverlappingChanges`.
@@ -494,6 +498,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
494
498
  }
495
499
  } else {
496
500
  var _changeset = ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
501
+ contentChanges = _changeset.changes;
497
502
  changes = getChanges({
498
503
  changeset: _changeset,
499
504
  originalDoc: originalDoc,
@@ -753,20 +758,52 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
753
758
  changes.forEach(function (change) {
754
759
  createDecorationsForChange(change);
755
760
  });
756
- // The attribute-aware token encoder already reports mark-only changes as regular changes in the
757
- // smart path. Only add the legacy step-range decoration when no equivalent changeset range was
758
- // produced; otherwise the same content receives two inline decorations (and two navigation
759
- // changes), usually one attribution colour and one default purple decoration.
760
- getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(function (_ref10) {
761
+ // Compare the content outside replacement ranges at matching positions in both documents.
762
+ // This preserves real formatting edits without highlighting an unchanged link mark reapplied
763
+ // while editing its display text.
764
+ var hasUnhighlightedMarkChanges = function hasUnhighlightedMarkChanges(_ref10) {
761
765
  var fromB = _ref10.fromB,
762
766
  toB = _ref10.toB;
763
- return !isHandledByTable(fromB, toB);
764
- }).filter(function (_ref11) {
767
+ var from = fromB;
768
+ var offset = 0;
769
+ var differs = function differs(to) {
770
+ return from < to && !originalDoc.slice(from + offset, to + offset).content.eq(tr.doc.slice(from, to).content);
771
+ };
772
+ var _iterator5 = _createForOfIteratorHelper(contentChanges),
773
+ _step5;
774
+ try {
775
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
776
+ var change = _step5.value;
777
+ if (change.fromB >= toB) {
778
+ break;
779
+ }
780
+ if (change.toB > from) {
781
+ if (differs(change.fromB)) {
782
+ return true;
783
+ }
784
+ from = change.toB;
785
+ }
786
+ offset = change.toA - change.toB;
787
+ }
788
+ } catch (err) {
789
+ _iterator5.e(err);
790
+ } finally {
791
+ _iterator5.f();
792
+ }
793
+ return differs(toB);
794
+ };
795
+
796
+ // The attribute-aware token encoder already reports mark-only changes as regular changes in the
797
+ // smart path and the NCS version-history path. For NCS version history, only skip the step-range
798
+ // highlight if the main diff covers it or the remaining content has unchanged formatting.
799
+ getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(function (_ref11) {
765
800
  var fromB = _ref11.fromB,
766
801
  toB = _ref11.toB;
802
+ return !isHandledByTable(fromB, toB);
803
+ }).filter(function (range) {
767
804
  return !fg('confluence_ncs_step_diffing_version_history') || !changes.some(function (change) {
768
- return change.fromB === fromB && change.toB === toB;
769
- });
805
+ return change.fromB <= range.fromB && change.toB >= range.toB;
806
+ }) && hasUnhighlightedMarkChanges(range);
770
807
  }).forEach(function (change) {
771
808
  var _attributionColors$ge, _attributionColors;
772
809
  var changeColorScheme = change.attributionKey ? (_attributionColors$ge = (_attributionColors = attributionColors) === null || _attributionColors === void 0 ? void 0 : _attributionColors.get(change.attributionKey)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : colorScheme : colorScheme;
@@ -790,8 +827,9 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
790
827
  return !isHandledByTable(fromB, toB);
791
828
  }).filter(function (_ref13) {
792
829
  var fromB = _ref13.fromB,
793
- toB = _ref13.toB;
794
- return !fg('confluence_ncs_step_diffing_version_history') || !changes.some(function (change) {
830
+ toB = _ref13.toB,
831
+ isInline = _ref13.isInline;
832
+ return !fg('confluence_ncs_step_diffing_version_history') || !isInline || !changes.some(function (change) {
795
833
  return change.fromB === fromB && change.toB === toB;
796
834
  });
797
835
  }).forEach(function (change) {
@@ -15,7 +15,8 @@ var typeID = function typeID(type) {
15
15
  *
16
16
  * Deliberately narrower than `attrAwareTokenEncoder`: node-start tokens keep the library default
17
17
  * and do not fold in diffable attributes, which `getAttrChangeRanges` already reports for every
18
- * diff type.
18
+ * diff type. The NCS version-history path also uses `attrAwareTokenEncoder` so main diff calculation
19
+ * detects attribute-only node replacements.
19
20
  */
20
21
  export var markAwareTokenEncoder = {
21
22
  encodeCharacter: function encodeCharacter(char, marks) {
@@ -5,7 +5,7 @@ import { markAwareTokenEncoder } from './markAwareTokenEncoder';
5
5
  * `computeDiffChanges` so the two cannot drift.
6
6
  *
7
7
  * @param useAttrAwareEncoder Caller is running the smart classifier, which needs node attrs folded
8
- * into the token.
8
+ * into the token. Also used by the NCS version-history path to detect attribute-only node changes.
9
9
  */
10
10
  export var selectTokenEncoder = function selectTokenEncoder(useAttrAwareEncoder) {
11
11
  if (useAttrAwareEncoder) {
@@ -175,7 +175,7 @@ var createChangedRowDOM = function createChangedRowDOM(_ref3) {
175
175
  var cellIndex = 0;
176
176
  rowNode.content.forEach(function (cellNode) {
177
177
  if (cellNode.type.name === 'tableCell' || cellNode.type.name === 'tableHeader') {
178
- var nodeView = nodeViewSerializer.tryCreateNodeView(cellNode);
178
+ var nodeView = nodeViewSerializer.tryCreateNodeView(cellNode, 0, undefined, isInserted, colorScheme);
179
179
  if (nodeView) {
180
180
  if (nodeView instanceof HTMLElement) {
181
181
  applyCellEdgeAttrs(nodeView, cellEdgeAttrs === null || cellEdgeAttrs === void 0 ? void 0 : cellEdgeAttrs[cellIndex]);
@@ -76,7 +76,7 @@ var createTableCellContentWidgets = function createTableCellContentWidgets(_ref)
76
76
  // A cell contains block content (paragraphs); serialize the cell's content
77
77
  // so every block inside the cell is rendered.
78
78
  cellNode.content.forEach(function (blockNode) {
79
- var nodeView = nodeViewSerializer.tryCreateNodeView(blockNode);
79
+ var nodeView = nodeViewSerializer.tryCreateNodeView(blockNode, 0, undefined, isInserted, colorScheme);
80
80
  var wrapper = createContentWrapper(colorScheme, false, isInserted);
81
81
  if (nodeView) {
82
82
  wrapper.append(nodeView);
@@ -316,7 +316,7 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
316
316
  var handleMultipleChildNodes = function handleMultipleChildNodes(node) {
317
317
  if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
318
318
  node.content.forEach(function (childNode) {
319
- var childNodeView = serializer.tryCreateNodeView(childNode);
319
+ var childNodeView = serializer.tryCreateNodeView(childNode, 0, undefined, isInserted, colorScheme);
320
320
  if (childNodeView) {
321
321
  var lineBreak = document.createElement('br');
322
322
  dom.append(lineBreak);
@@ -372,7 +372,7 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
372
372
 
373
373
  // Try to create node view, fallback to serialization
374
374
  var $sourcePos = fg('platform_editor_ai_show_diff_patch_2') ? getSourcePos(doc, $changeFromA, slice.openStart, index) : null;
375
- var nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined);
375
+ var nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined, isInserted, colorScheme);
376
376
  if (nodeView) {
377
377
  if (node.isInline) {
378
378
  var wrapper = createContentWrapper(colorScheme, isActive, isInserted, reveal);
@@ -1,3 +1,5 @@
1
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
2
+
1
3
  /**
2
4
  * Single source of truth for which attributes of a node type represent a
3
5
  * meaningful change. Shared by `attrAwareTokenEncoder` (changeset detection)
@@ -88,10 +90,13 @@ export var getRequiredIncludedDiffableAttrs = function getRequiredIncludedDiffab
88
90
  * order is not stable across the two diffed documents.
89
91
  */
90
92
  export var getDiffableAttrNames = function getDiffableAttrNames(nodeTypeName, attrs) {
93
+ // Adding a column updates existing widths, ignore this attribute change. Move this into
94
+ // DIFFABLE_ATTRS_BY_NODE_TYPE when cleaning up confluence_ncs_step_diffing_version_history
95
+ var ignoreColumnWidth = nodeTypeName === 'layoutColumn' && fg('confluence_ncs_step_diffing_version_history');
91
96
  var rule = DIFFABLE_ATTRS_BY_NODE_TYPE[nodeTypeName];
92
97
  if (!rule) {
93
98
  return Object.keys(attrs).filter(function (name) {
94
- return !DEFAULT_EXCLUDED_ATTRS.includes(name);
99
+ return !DEFAULT_EXCLUDED_ATTRS.includes(name) && !(ignoreColumnWidth && name === 'width');
95
100
  }).sort();
96
101
  }
97
102
  if ('include' in rule) {
@@ -83,6 +83,11 @@ export var getAttrChangeRanges = function getAttrChangeRanges(doc, attrStepConte
83
83
  var $pos = doc.resolve(finalPos);
84
84
  var nodeAtPos = doc.nodeAt(finalPos);
85
85
  var originalNodeAtPos = originalPos === undefined ? null : originalDoc.nodeAt(originalPos);
86
+ var hasChangedAttr = function hasChangedAttr(watchedAttrs) {
87
+ return attrsToCheck.some(function (attr) {
88
+ return watchedAttrs.includes(attr) && (!fg('confluence_ncs_step_diffing_version_history') || !originalNodeAtPos || originalNodeAtPos.type !== (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type) || !isEqual(originalNodeAtPos.attrs[attr], nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.attrs[attr]));
89
+ });
90
+ };
86
91
 
87
92
  // The changeset path (createDecorationsForChange) handles the deletion widget via
88
93
  // prosemirror-changeset; we only need to add the inline insertion highlight here.
@@ -90,9 +95,7 @@ export var getAttrChangeRanges = function getAttrChangeRanges(doc, attrStepConte
90
95
  var nodeName = nodeAtPos.type.name;
91
96
  if (isInlineAttrChangeNodeName(nodeName)) {
92
97
  var watchedAttrs = inlineNodeAttrMap[nodeName];
93
- if (attrsToCheck.some(function (v) {
94
- return watchedAttrs.includes(v);
95
- })) {
98
+ if (hasChangedAttr(watchedAttrs)) {
96
99
  return _objectSpread(_objectSpread({}, attributionKey ? {
97
100
  attributionKey: attributionKey
98
101
  } : {}), {}, {
@@ -124,9 +127,7 @@ export var getAttrChangeRanges = function getAttrChangeRanges(doc, attrStepConte
124
127
  // (e.g. note -> warning) — highlight the new panel and, when the original node
125
128
  // is resolvable, expose its range (fromA/toA) so the caller can render the old
126
129
  // panel as a "deleted" widget for a before/after comparison.
127
- if (attrsToCheck.some(function (v) {
128
- return panelAttrs.includes(v);
129
- }) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') {
130
+ if (hasChangedAttr(panelAttrs) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') {
130
131
  return _objectSpread(_objectSpread({}, attributionKey ? {
131
132
  attributionKey: attributionKey
132
133
  } : {}), {}, {
@@ -174,7 +175,9 @@ export var getAttrChangeRanges = function getAttrChangeRanges(doc, attrStepConte
174
175
  // (e.g. setNodeAttribute(pos, 'text', ...) + setNodeAttribute(pos, 'color', ...))
175
176
  // should produce only one decoration, not one per step.
176
177
  .filter(function (range, i, arr) {
177
- return arr.findIndex(function (r) {
178
+ return fg('confluence_ncs_step_diffing_version_history') ? !arr.some(function (r, nextIndex) {
179
+ return nextIndex > i && r.fromB === range.fromB;
180
+ }) : arr.findIndex(function (r) {
178
181
  return r.fromB === range.fromB;
179
182
  }) === i;
180
183
  });
@@ -35,6 +35,10 @@ export var isMultiContainerBlockNode = function isMultiContainerBlockNode(nodeNa
35
35
  return ['decisionList', 'layoutSection'].includes(nodeName);
36
36
  };
37
37
  export var isTextLikeBlockNode = function isTextLikeBlockNode(nodeName) {
38
+ // When cleaning up platform_editor_ai_show_diff_patch_2 just put paragraph into the list
39
+ if (nodeName === 'paragraph') {
40
+ return fg('platform_editor_ai_show_diff_patch_2');
41
+ }
38
42
  return ['heading', 'bulletList', 'orderedList', 'listItem', 'taskList', 'blockquote'].includes(nodeName);
39
43
  };
40
44
  var getChangedContentStyleNext = function getChangedContentStyleNext(colorScheme) {
@@ -71,6 +71,12 @@ var isMultiContainerBlockNode = function isMultiContainerBlockNode(nodeName) {
71
71
  return ['decisionList', 'layoutSection'].includes(nodeName);
72
72
  };
73
73
  var isTextLikeBlockNode = function isTextLikeBlockNode(nodeName) {
74
+ // `paragraph` only reaches this classification via patch_2's `shouldRenderAsBlockNode` routing
75
+ // (createNodeChangedDecorationWidget.ts), which fully replaced paragraphs never hit otherwise —
76
+ // gating it here keeps that dependency explicit rather than implicit.
77
+ if (nodeName === 'paragraph') {
78
+ return fg('platform_editor_ai_show_diff_patch_2');
79
+ }
74
80
  return ['heading', 'bulletList', 'orderedList', 'listItem', 'taskList', 'blockquote'].includes(nodeName);
75
81
  };
76
82
 
@@ -1,6 +1,7 @@
1
1
  import type { NodeViewConstructor } from '@atlaskit/editor-common/lazy-node-view';
2
2
  import type { Node as PMNode, Fragment, ResolvedPos } from '@atlaskit/editor-prosemirror/model';
3
3
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
4
+ import type { ColorScheme } from './decorations/colorSchemes/types';
4
5
  /**
5
6
  * Utilities for working with ProseMirror node views and DOM serialization within the
6
7
  * Show Diff editor plugin.
@@ -61,7 +62,7 @@ export declare class NodeViewSerializer {
61
62
  *
62
63
  * `$sourcePos` is where the node sits in the doc it came from, so nested tables keep their depth.
63
64
  */
64
- tryCreateNodeView(targetNode: PMNode, basePos?: number, $sourcePos?: ResolvedPos): Node | null;
65
+ tryCreateNodeView(targetNode: PMNode, basePos?: number, $sourcePos?: ResolvedPos, isInserted?: boolean, colorScheme?: ColorScheme): Node | null;
65
66
  private tryCreateNodeViewInner;
66
67
  /** Wraps rendered DOM in its marks, rendered against the editor proxy when there is one. */
67
68
  private withMarkViews;
@@ -4,6 +4,7 @@ import type { TokenEncoder } from 'prosemirror-changeset';
4
4
  *
5
5
  * Deliberately narrower than `attrAwareTokenEncoder`: node-start tokens keep the library default
6
6
  * and do not fold in diffable attributes, which `getAttrChangeRanges` already reports for every
7
- * diff type.
7
+ * diff type. The NCS version-history path also uses `attrAwareTokenEncoder` so main diff calculation
8
+ * detects attribute-only node replacements.
8
9
  */
9
10
  export declare const markAwareTokenEncoder: TokenEncoder<string | number>;
@@ -10,6 +10,6 @@ export type TokenEncoderSelection = {
10
10
  * `computeDiffChanges` so the two cannot drift.
11
11
  *
12
12
  * @param useAttrAwareEncoder Caller is running the smart classifier, which needs node attrs folded
13
- * into the token.
13
+ * into the token. Also used by the NCS version-history path to detect attribute-only node changes.
14
14
  */
15
15
  export declare const selectTokenEncoder: (useAttrAwareEncoder: boolean) => TokenEncoderSelection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-show-diff",
3
- "version": "21.0.1",
3
+ "version": "21.0.3",
4
4
  "description": "ShowDiff plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,7 @@
39
39
  "@atlaskit/platform-feature-experiments": "^0.3.0",
40
40
  "@atlaskit/platform-feature-flags": "^2.2.0",
41
41
  "@atlaskit/primitives": "^22.5.0",
42
- "@atlaskit/tmp-editor-statsig": "^219.0.0",
42
+ "@atlaskit/tmp-editor-statsig": "^220.0.0",
43
43
  "@atlaskit/tokens": "^20.1.0",
44
44
  "@babel/runtime": "^7.0.0",
45
45
  "@compiled/react": "^1.0.2",
@@ -71,7 +71,7 @@
71
71
  "react-intl": "^7.0.0"
72
72
  },
73
73
  "peerDependencies": {
74
- "@atlaskit/editor-common": "^127.1.0",
74
+ "@atlaskit/editor-common": "^127.2.0",
75
75
  "react": "^18.2.0 || ^19.2.0",
76
76
  "react-dom": "^18.2.0 || ^19.2.0",
77
77
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"