@atlaskit/editor-plugin-show-diff 16.1.1 → 16.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/afm-products/tsconfig.json +3 -0
  3. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +67 -22
  4. package/dist/cjs/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +242 -26
  5. package/dist/cjs/pm-plugins/decorations/colorSchemes/factory.js +71 -37
  6. package/dist/cjs/pm-plugins/decorations/colorSchemes/schemes.js +5 -1
  7. package/dist/cjs/pm-plugins/decorations/colorSchemes/types.js +0 -2
  8. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +21 -14
  9. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +123 -43
  10. package/dist/cjs/pm-plugins/decorations/revealStyles.js +5 -1
  11. package/dist/cjs/pm-plugins/getScrollableDecorations.js +48 -19
  12. package/dist/cjs/pm-plugins/utils/baseNodeName.js +25 -0
  13. package/dist/cjs/pm-plugins/utils/taggableBlockNodes.js +37 -0
  14. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +39 -2
  15. package/dist/es2019/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +196 -10
  16. package/dist/es2019/pm-plugins/decorations/colorSchemes/factory.js +51 -19
  17. package/dist/es2019/pm-plugins/decorations/colorSchemes/schemes.js +3 -0
  18. package/dist/es2019/pm-plugins/decorations/colorSchemes/types.js +0 -2
  19. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +23 -14
  20. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +60 -7
  21. package/dist/es2019/pm-plugins/decorations/revealStyles.js +5 -1
  22. package/dist/es2019/pm-plugins/getScrollableDecorations.js +44 -19
  23. package/dist/es2019/pm-plugins/utils/baseNodeName.js +18 -0
  24. package/dist/es2019/pm-plugins/utils/taggableBlockNodes.js +28 -0
  25. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +67 -22
  26. package/dist/esm/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +243 -26
  27. package/dist/esm/pm-plugins/decorations/colorSchemes/factory.js +71 -37
  28. package/dist/esm/pm-plugins/decorations/colorSchemes/schemes.js +5 -1
  29. package/dist/esm/pm-plugins/decorations/colorSchemes/types.js +0 -2
  30. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +21 -14
  31. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +123 -43
  32. package/dist/esm/pm-plugins/decorations/revealStyles.js +5 -1
  33. package/dist/esm/pm-plugins/getScrollableDecorations.js +48 -19
  34. package/dist/esm/pm-plugins/utils/baseNodeName.js +20 -0
  35. package/dist/esm/pm-plugins/utils/taggableBlockNodes.js +32 -0
  36. package/dist/types/pm-plugins/calculateDiff/simplifyChangesWithAttribution.d.ts +20 -1
  37. package/dist/types/pm-plugins/decorations/colorSchemes/factory.d.ts +4 -11
  38. package/dist/types/pm-plugins/decorations/colorSchemes/types.d.ts +9 -8
  39. package/dist/types/pm-plugins/getScrollableDecorations.d.ts +2 -2
  40. package/dist/types/pm-plugins/utils/baseNodeName.d.ts +16 -0
  41. package/dist/types/pm-plugins/utils/taggableBlockNodes.d.ts +14 -0
  42. package/package.json +5 -4
@@ -4,6 +4,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
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 { isExtendedEnabled } from '../isExtendedEnabled';
7
+ import { isTaggableBlockNode } from '../utils/taggableBlockNodes';
7
8
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedBlockNodeStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildInsertedBlockNodeStyle } from './colorSchemes/factory';
8
9
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
9
10
  import { createBlockIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
@@ -23,23 +24,28 @@ const UNSTYLED_NODES = ['mediaSingle', 'mediaGroup', 'table',
23
24
  'heading', 'hardBreak', 'decisionList', 'taskList', 'bulletList', 'orderedList', 'layoutSection'];
24
25
  const CELL_NODES = ['tableCell', 'tableHeader'];
25
26
 
26
- /**
27
- * Block nodes that host a contributor tag of their own — those whose whole box reads as one change.
28
- * Lists and tables are excluded: they decorate per item and per cell, with no one-tag-per-block
29
- * design yet. `blockCard` covers the datasource variant, which is the same node type.
30
- */
31
- const TAGGABLE_BLOCK_NODES = new Set(['blockCard', 'blockquote', 'bodiedExtension', 'codeBlock', 'embedCard', 'expand', 'extension', 'media', 'multiBodiedExtension', 'panel', 'rule']);
32
-
33
27
  /** Panels, rules and media can also sit in a table cell, which is out of scope for block tags. */
34
28
  const isInsideTable = (doc, pos) => findParentNodeClosestToPos(doc.resolve(pos), node => node.type.name === 'table') !== undefined;
35
29
 
36
- /** Whether this block can host a contributor tag of its own. */
30
+ /**
31
+ * Whether this block can host a contributor tag of its own.
32
+ *
33
+ * The node type is resolved from the document rather than matched on `change.name`, so a schema
34
+ * variant is recognised as the type it varies — see `isTaggableBlockNode`. `from` is the node's own
35
+ * position, so `nodeAt` returns exactly the node being decorated.
36
+ */
37
37
  const canTagBlock = ({
38
38
  diffType,
39
39
  doc,
40
- from,
41
- name
42
- }) => doc !== undefined && isExtendedEnabled(diffType) && TAGGABLE_BLOCK_NODES.has(name) && !isInsideTable(doc, from);
40
+ from
41
+ }) => {
42
+ var _doc$nodeAt;
43
+ if (doc === undefined || !isExtendedEnabled(diffType)) {
44
+ return false;
45
+ }
46
+ const nodeType = (_doc$nodeAt = doc.nodeAt(from)) === null || _doc$nodeAt === void 0 ? void 0 : _doc$nodeAt.type;
47
+ return nodeType !== undefined && isTaggableBlockNode(nodeType) && !isInsideTable(doc, from);
48
+ };
43
49
 
44
50
  /** Positioning context for the cell overlay widget decorations. */
45
51
  const cellPositionStyle = convertToInlineCss({
@@ -176,8 +182,7 @@ export const createBlockChangedDecoration = ({
176
182
  const shouldTagBlock = showContributorTags && canTagBlock({
177
183
  diffType,
178
184
  doc,
179
- from: change.from,
180
- name: change.name
185
+ from: change.from
181
186
  });
182
187
  // Named so this block's own tag can position against it; see `createContributorTagWidget`.
183
188
  const tagAnchorName = shouldTagBlock && isContributorTagWidgetEnabled() ? buildAnchorDecorationKey({
@@ -221,7 +226,9 @@ export const createBlockChangedDecoration = ({
221
226
  key: 'cell-overlay-decoration'
222
227
  }));
223
228
  }
224
- // isInserted is only read under the extended experience, so pass it unconditionally.
229
+ // isInserted is only read under the extended experience, so pass it unconditionally. `change.name`
230
+ // verbatim: base-name resolution stays in `canTagBlock`, which needs tags on, so a schema
231
+ // variant's legacy style is untouched — see `resolveBaseNodeName`.
225
232
  const nodeStyle = getBlockNodeStyle({
226
233
  nodeName: change.name,
227
234
  colorScheme,
@@ -254,6 +261,8 @@ export const createBlockChangedDecoration = ({
254
261
  })));
255
262
  }
256
263
  if (decorations.length === 0) {
264
+ // An unstyled node type (paragraph, heading, list, layout, …) emits no decoration at all, so
265
+ // there is nothing for a tag to hang off even when the node type would otherwise be taggable.
257
266
  return decorations;
258
267
  }
259
268
  if (showIndicators && doc && isExtendedEnabled(diffType)) {
@@ -34,6 +34,12 @@ const leadsReplacement = widget => fg('confluence_ncs_step_diffing_version_histo
34
34
  */
35
35
  const isSameBlockChange = (inner, block) => inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
36
36
 
37
+ /**
38
+ * Whether `outer` is the box `inner` sits in, for one contributor. Nested blocks they inserted read
39
+ * as one change, so only the outermost keeps a tag (EDITOR-8932).
40
+ */
41
+ const containsBlock = (outer, inner) => outer.spec.attributionKey === inner.spec.attributionKey && outer.from <= inner.from && inner.to <= outer.to && outer.to - outer.from > inner.to - inner.from;
42
+
37
43
  /**
38
44
  * Which of two changes in the same range leads, and so is the one a tag captions. Position first,
39
45
  * then `side`, since deleted content shares its `from` with the content that replaced it and paints
@@ -52,6 +58,15 @@ const containedBy = (targets, {
52
58
  decoration
53
59
  }) => from <= decoration.from && decoration.to <= to).sort(byLeadingPosition);
54
60
 
61
+ /**
62
+ * The stop a target is stepped to by: the narrowest one that wholly covers it. A stop's range is
63
+ * the union of the decorations grouped into it, so one inserted run's inline highlight stretches
64
+ * its stop over every block the run spans — including blocks that are a stop of their own. Picking
65
+ * the narrowest keeps each of those blocks in the stop that actually reaches it, rather than
66
+ * letting the widest stop claim them all and fold away every tag but the first (EDITOR-8932).
67
+ */
68
+ const owningStop = (target, stops) => stops.reduce((narrowest, stop) => stop.from <= target.decoration.from && target.decoration.to <= stop.to && (narrowest === undefined || stop.to - stop.from < narrowest.to - narrowest.from) ? stop : narrowest, undefined);
69
+
55
70
  /**
56
71
  * The one tag a navigation step reveals. `spec.isActive` is no use here: it covers everything the
57
72
  * group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
@@ -108,6 +123,10 @@ export const extractContributorTags = (decorations, contributors, activeIndexPos
108
123
  }) => decoration.spec.decorationType === 'widget');
109
124
  const linkedDiffIds = new Map();
110
125
  const folded = new Set();
126
+
127
+ /**
128
+ * A folded target keeps no tag of its own — it becomes a hover target of the host instead.
129
+ */
111
130
  const fold = (host, target) => {
112
131
  var _linkedDiffIds$get, _linkedDiffIds$get2;
113
132
  folded.add(target);
@@ -158,20 +177,54 @@ export const extractContributorTags = (decorations, contributors, activeIndexPos
158
177
  }
159
178
  }
160
179
 
180
+ // A nested block folds into the outermost one of the same contributor, so nesting adds no tags.
181
+ if (fg('confluence_ncs_step_diffing_version_history')) {
182
+ const blocksByContributor = new Map();
183
+ for (const target of blockTargets) {
184
+ var _target$decoration$sp, _blocksByContributor$;
185
+ const key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
186
+ blocksByContributor.set(key, [...((_blocksByContributor$ = blocksByContributor.get(key)) !== null && _blocksByContributor$ !== void 0 ? _blocksByContributor$ : []), target]);
187
+ }
188
+ for (const contributorBlocks of blocksByContributor.values()) {
189
+ // Block ranges nest rather than partially overlap, so ascending `from` then descending `to`
190
+ // reaches every block after the ones containing it. A stack of the blocks still open at
191
+ // this position then yields each block's container in one pass, rather than rescanning
192
+ // every block for the widest that contains it.
193
+ const nested = [...contributorBlocks].sort((left, right) => left.decoration.from - right.decoration.from || right.decoration.to - left.decoration.to);
194
+ const open = [];
195
+ for (const target of nested) {
196
+ while (open.length > 0 && !containsBlock(open[open.length - 1].decoration, target.decoration)) {
197
+ open.pop();
198
+ }
199
+ // Outermost first, so the bottom of the stack is the block the tag survives on.
200
+ if (open.length > 0) {
201
+ fold(open[0], target);
202
+ }
203
+ open.push(target);
204
+ }
205
+ }
206
+ }
207
+
161
208
  // One tag per contributor per navigation stop. The folds above only catch a replacement's two
162
209
  // halves and a block's own highlights; two of one contributor's changes that merely touch are a
163
210
  // single stop (`groupTouchingDecorations`) yet kept a tag each, so the stop's trailing tag was
164
211
  // drawn but could never be stepped to (EDITOR-8971). It folds into the leading tag of its
165
212
  // contributor, staying a hover target. Contributors are kept apart: a stop spanning two of them
166
213
  // still captions each, rather than crediting one for the other's change.
167
- for (const stop of stops !== null && stops !== void 0 ? stops : []) {
168
- const byContributor = new Map();
169
- for (const target of containedBy(targets.filter(target => !folded.has(target)), stop)) {
170
- var _target$decoration$sp, _byContributor$get;
171
- const key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
172
- byContributor.set(key, [...((_byContributor$get = byContributor.get(key)) !== null && _byContributor$get !== void 0 ? _byContributor$get : []), target]);
214
+ // Each target is counted against one stop only see `owningStop`.
215
+ if (stops !== null && stops !== void 0 && stops.length) {
216
+ const byStopAndContributor = new Map();
217
+ for (const target of targets.filter(target => !folded.has(target))) {
218
+ var _target$decoration$sp2, _byStopAndContributor;
219
+ const stop = owningStop(target, stops);
220
+ if (stop === undefined) {
221
+ continue;
222
+ }
223
+ const key = `${stop.from}-${stop.to}|${(_target$decoration$sp2 = target.decoration.spec.attributionKey) !== null && _target$decoration$sp2 !== void 0 ? _target$decoration$sp2 : ''}`;
224
+ byStopAndContributor.set(key, [...((_byStopAndContributor = byStopAndContributor.get(key)) !== null && _byStopAndContributor !== void 0 ? _byStopAndContributor : []), target]);
173
225
  }
174
- for (const [leader, ...trailing] of byContributor.values()) {
226
+ for (const members of byStopAndContributor.values()) {
227
+ const [leader, ...trailing] = [...members].sort(byLeadingPosition);
175
228
  trailing.forEach(target => fold(leader, target));
176
229
  }
177
230
  }
@@ -1,3 +1,4 @@
1
+ import { agentBrandColorSchemes } from '@atlaskit/agent-color/agent-brand-color-schemes';
1
2
  import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
2
3
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
3
4
  import { buildDeletedInlineStyleStandard, getDeletedInlineRevealColors, getInsertedInlineRevealColors } from './colorSchemes/factory';
@@ -96,6 +97,7 @@ export const resolveRevealStyle = ({
96
97
  return undefined;
97
98
  }
98
99
  const colors = getColorScheme(colorScheme);
100
+ const brand = isInserted && Object.prototype.hasOwnProperty.call(agentBrandColorSchemes, colors.insertColor) ? agentBrandColorSchemes[colors.insertColor] : undefined;
99
101
  const {
100
102
  background,
101
103
  border
@@ -106,7 +108,9 @@ export const resolveRevealStyle = ({
106
108
  });
107
109
  return {
108
110
  role: isInserted ? 'added' : 'deleted',
109
- style: (isInserted || !includeDeletedTextStyle ? '' : deletedTextOnlyStyle(colors, isActive)) + revealBaseStyle + buildWipeableBackground(background) + buildRevealVariables({
111
+ style: (brand ? convertToInlineCss({
112
+ color: brand.text
113
+ }) : '') + (isInserted || !includeDeletedTextStyle ? '' : deletedTextOnlyStyle(colors, isActive)) + revealBaseStyle + buildWipeableBackground(background) + buildRevealVariables({
110
114
  background,
111
115
  border
112
116
  })
@@ -49,6 +49,17 @@ function decorationSide(decoration) {
49
49
  return isDiffDecoration(decoration) && decoration.spec.side || 0;
50
50
  }
51
51
 
52
+ /**
53
+ * Whose change a decoration is part of, or `''` on an unattributed diff — where every decoration
54
+ * reads as one contributor and the grouping below is unchanged.
55
+ *
56
+ * Contributors are kept apart so one stop never covers two of them, since only the tag on the
57
+ * change stepped to reveals (EDITOR-8932).
58
+ */
59
+ function contributorOf(decoration) {
60
+ return isDiffDecoration(decoration) && decoration.spec.attributionKey || '';
61
+ }
62
+
52
63
  /**
53
64
  * Collapses decorations that represent one continuous customer-facing edit.
54
65
  *
@@ -57,32 +68,46 @@ function decorationSide(decoration) {
57
68
  * deletion at the same location should be treated as one replacement. Zero-width decorations
58
69
  * (normally deleted-content widgets) can join a group, but cannot extend its range and therefore
59
70
  * cannot bridge two otherwise separate edits.
71
+ *
72
+ * One group per contributor within a run of touching ranges — see `contributorOf`.
60
73
  */
61
74
  function groupTouchingDecorations(decorations, isInlineDecoration) {
62
75
  if (decorations.length < 2) {
63
76
  return decorations;
64
77
  }
65
78
  const groups = [];
66
- let currentGroup = [];
67
- let currentGroupFrom = 0;
68
- let currentGroupTo = 0;
79
+ // The groups a following decoration can still join — one per contributor, dropped at every gap.
80
+ let openGroups = [];
81
+ let clusterTo = 0;
69
82
  const sortedDecorations = [...decorations].sort((a, b) => a.from === b.from ? a.to - b.to : a.from - b.from);
70
83
  sortedDecorations.forEach(decoration => {
71
- if (currentGroup.length === 0 || decoration.from > currentGroupTo) {
72
- currentGroup = [decoration];
73
- currentGroupFrom = decoration.from;
74
- currentGroupTo = decoration.to;
75
- groups.push({
76
- decorations: currentGroup,
77
- from: currentGroupFrom,
78
- to: currentGroupTo
79
- });
80
- return;
84
+ var _openGroups$find;
85
+ // A gap ends the run: nothing before it can be joined.
86
+ if (decoration.from > clusterTo) {
87
+ openGroups = [];
81
88
  }
82
- currentGroup.push(decoration);
83
- // A zero-width decoration must not extend the group and bridge a gap.
84
- currentGroupTo = Math.max(currentGroupTo, decoration.to);
85
- groups[groups.length - 1].to = currentGroupTo;
89
+ const contributor = contributorOf(decoration);
90
+ // An unattributed decoration names nobody to keep apart, so it joins whatever is open — a
91
+ // decision item's own highlight must not split the stop from the tag on its list.
92
+ const openGroup = (_openGroups$find = openGroups.find(group => group.contributor === contributor)) !== null && _openGroups$find !== void 0 ? _openGroups$find : contributor === '' ? openGroups[0] : openGroups.find(group => group.contributor === '');
93
+ if (openGroup === undefined) {
94
+ const group = {
95
+ contributor,
96
+ decorations: [decoration],
97
+ from: decoration.from,
98
+ to: decoration.to
99
+ };
100
+ groups.push(group);
101
+ openGroups = [...openGroups, group];
102
+ } else {
103
+ // The first contributor to join claims the group, so the next one still starts its own.
104
+ openGroup.contributor = openGroup.contributor || contributor;
105
+ openGroup.decorations.push(decoration);
106
+ openGroup.to = Math.max(openGroup.to, decoration.to);
107
+ }
108
+
109
+ // A zero-width decoration must not extend the run and bridge a gap.
110
+ clusterTo = Math.max(clusterTo, decoration.to);
86
111
  });
87
112
  return groups.map(({
88
113
  decorations: group,
@@ -127,8 +152,8 @@ function groupTouchingDecorations(decorations, isInlineDecoration) {
127
152
  * 4. When `doc` is passed: excludes diff-inline decorations whose range has no inline content
128
153
  * (invalid positions, or block-only slices with no text/atoms — e.g. empty blocks)
129
154
  * 5. When `confluence_ncs_step_diffing_version_history` is enabled, groups overlapping or
130
- * directly touching ranges across decoration types into one result, using the union of all
131
- * grouped ranges
155
+ * directly touching ranges across decoration types into one result per contributor, using the
156
+ * union of all grouped ranges
132
157
  * (zero-width widgets can join a group without extending it). Under
133
158
  * `platform_editor_ai_show_diff_patch_1`, a group that starts with content painting above
134
159
  * the content that replaced it — reports that widget as its `scrollTarget` spec,
@@ -0,0 +1,18 @@
1
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
2
+
3
+ /**
4
+ * A node type's base name, with a schema variant's suffix stripped: `panel`, `panel_c1` and
5
+ * `panel_c1_root_only` all resolve to `panel`.
6
+ *
7
+ * Package-local rather than `getBaseNodeTypeName` from editor-common, which enumerates the variants
8
+ * it happens to know (`panel_c1` alone) and is shared with features outside this one. Derived
9
+ * instead of listed, so a variant added later needs no change here: the schema generator names a
10
+ * variant `<type>_<variant>`, and every other node name in the ADF schema is camelCase, so the
11
+ * first `_` is the boundary.
12
+ *
13
+ * Gated, so the diff behaves exactly as before for anyone outside the rollout.
14
+ *
15
+ * For taggability only — this gate is weaker than the ones attribution needs, so resolving a variant
16
+ * in the styling decisions would move the legacy diff for readers who see no tags.
17
+ */
18
+ export const resolveBaseNodeName = name => fg('confluence_ncs_step_diffing_version_history') ? name.split('_')[0] : name;
@@ -0,0 +1,28 @@
1
+ import { resolveBaseNodeName } from './baseNodeName';
2
+
3
+ /**
4
+ * Block nodes that host a contributor tag of their own — those whose whole box reads as one change.
5
+ * Everything that draws a box whatever it holds, plus the two lists that are inserted whole: without
6
+ * them the tag fell through to the text inside the first item (EDITOR-8932). Bullet and ordered lists
7
+ * are left out, since their items accrue one at a time. Tables and list items decorate per cell and
8
+ * per item, with no one-tag-per-block design yet. `blockCard` covers the datasource variant, which is
9
+ * the same node type.
10
+ *
11
+ * Base names only — match with the predicates below, never against `type.name` directly.
12
+ */
13
+ const TAGGABLE_BLOCK_NODES = new Set(['blockCard', 'blockquote', 'bodiedExtension', 'codeBlock', 'decisionList', 'embedCard', 'expand', 'extension', 'media', 'multiBodiedExtension', 'panel', 'rule', 'taskList']);
14
+
15
+ /**
16
+ * Whether this node hosts a contributor tag of its own. Navigation grouping does not consult this:
17
+ * a run of these one contributor inserted is a single stop, tagged on the leading block.
18
+ *
19
+ * Resolved through `resolveBaseNodeName` so a schema variant counts as the type it is a variant of:
20
+ * `panel_c1` is the panel the table-in-panel schema inserts, and matching on `type.name` left it
21
+ * untagged (EDITOR-8932).
22
+ *
23
+ * By name, because the decoration spec carries `nodeName` rather than the type it came from.
24
+ */
25
+ export const isTaggableBlockNodeName = name => name !== undefined && TAGGABLE_BLOCK_NODES.has(resolveBaseNodeName(name));
26
+
27
+ /** As above, from the node type itself. */
28
+ export const isTaggableBlockNode = nodeType => isTaggableBlockNodeName(nodeType.name);
@@ -190,8 +190,51 @@ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration
190
190
  diffType = _ref5.diffType,
191
191
  _ref5$coarseTableCell = _ref5.coarseTableCellsOnly,
192
192
  coarseTableCellsOnly = _ref5$coarseTableCell === void 0 ? false : _ref5$coarseTableCell,
193
- tagMountContext = _ref5.tagMountContext;
193
+ tagMountContext = _ref5.tagMountContext,
194
+ nodeRanges = _ref5.nodeRanges;
194
195
  var decorations = [];
196
+
197
+ // Exact-nodes path: only reached for a split change (EDITOR-8932), which names the nodes it
198
+ // owns. Deliberately kept separate from the walk below rather than sharing it.
199
+ if (nodeRanges) {
200
+ var _iterator = _createForOfIteratorHelper(nodeRanges),
201
+ _step;
202
+ try {
203
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
204
+ var _step$value = _step.value,
205
+ pos = _step$value.from,
206
+ nodeEnd = _step$value.to;
207
+ var node = doc.nodeAt(pos);
208
+ if (!(node !== null && node !== void 0 && node.isBlock)) {
209
+ continue;
210
+ }
211
+ var isActive = isRangeActive(activeIndexPos, pos, nodeEnd);
212
+ decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
213
+ attributionKey: attributionKey,
214
+ change: {
215
+ from: pos,
216
+ to: nodeEnd,
217
+ name: node.type.name
218
+ },
219
+ colorScheme: colorScheme,
220
+ isInserted: isInserted,
221
+ isActive: isActive,
222
+ shouldHideDeleted: shouldHideDeleted,
223
+ showContributorTags: showContributorTags,
224
+ showIndicators: showIndicators,
225
+ doc: doc,
226
+ diffType: diffType,
227
+ tagMountContext: tagMountContext
228
+ })));
229
+ }
230
+ } catch (err) {
231
+ _iterator.e(err);
232
+ } finally {
233
+ _iterator.f();
234
+ }
235
+ return decorations;
236
+ }
237
+
195
238
  // Coarse path: inside the large table, only cell/header overlays are kept — the
196
239
  // table/row/paragraph decorations there are redundant and cause expensive per-cell
197
240
  // re-render. Blocks outside the table keep their normal decorations.
@@ -205,18 +248,18 @@ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration
205
248
  }
206
249
  }
207
250
  if (node.isBlock && (!isExtendedEnabled(diffType) || pos + node.nodeSize <= to)) {
208
- var nodeEnd = pos + node.nodeSize;
209
- var isActive = isRangeActive(activeIndexPos, pos, nodeEnd);
251
+ var _nodeEnd = pos + node.nodeSize;
252
+ var _isActive = isRangeActive(activeIndexPos, pos, _nodeEnd);
210
253
  decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
211
254
  attributionKey: attributionKey,
212
255
  change: {
213
256
  from: pos,
214
- to: nodeEnd,
257
+ to: _nodeEnd,
215
258
  name: node.type.name
216
259
  },
217
260
  colorScheme: colorScheme,
218
261
  isInserted: isInserted,
219
- isActive: isActive,
262
+ isActive: _isActive,
220
263
  shouldHideDeleted: shouldHideDeleted,
221
264
  showContributorTags: showContributorTags,
222
265
  showIndicators: showIndicators,
@@ -339,13 +382,13 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
339
382
  }
340
383
  var pendingAttrSteps = [];
341
384
  var stepMaps = [];
342
- var _iterator = _createForOfIteratorHelper(simplifiedSteps.entries()),
343
- _step;
385
+ var _iterator2 = _createForOfIteratorHelper(simplifiedSteps.entries()),
386
+ _step2;
344
387
  try {
345
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
346
- var _step$value = _slicedToArray(_step.value, 2),
347
- stepIndex = _step$value[0],
348
- step = _step$value[1];
388
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
389
+ var _step2$value = _slicedToArray(_step2.value, 2),
390
+ stepIndex = _step2$value[0],
391
+ step = _step2$value[1];
349
392
  var result = step.apply(steppedDoc);
350
393
  if (result.failed === null && result.doc) {
351
394
  if (stepIsValidAttrChange(step, steppedDoc, result.doc)) {
@@ -364,9 +407,9 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
364
407
  // Gate the complete attribution fix: mapped positions and step-time nodes are used together.
365
408
  // The fallback deliberately preserves the legacy raw-position lookups for rollout safety.
366
409
  } catch (err) {
367
- _iterator.e(err);
410
+ _iterator2.e(err);
368
411
  } finally {
369
- _iterator.f();
412
+ _iterator2.f();
370
413
  }
371
414
  var attrStepContexts = fg('platform_editor_reduce_diff_attr_sensitivity') ? pendingAttrSteps.flatMap(function (_ref1) {
372
415
  var afterNode = _ref1.afterNode,
@@ -454,7 +497,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
454
497
  tr: tr,
455
498
  attributedChanges: attributedChanges
456
499
  });
457
- changes = collapseOverlappingChanges(uncollapsedChanges);
500
+ changes = collapseOverlappingChanges(uncollapsedChanges, tr.doc);
458
501
 
459
502
  // Still colours-only: a tags-only diff keeps the plain colour scheme.
460
503
  if (attributionColoringEnabled) {
@@ -551,13 +594,13 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
551
594
  // table (the cell overlays added below provide the highlight without the
552
595
  // expensive content re-render); leaf blocks outside the table still get theirs.
553
596
  var coarseTables = useCoarseTableDecoration ? tableRanges(tr.doc, change.fromB, change.toB) : [];
554
- var _iterator2 = _createForOfIteratorHelper(leafTextblockRanges(tr.doc, change.fromB, change.toB)),
555
- _step2;
597
+ var _iterator3 = _createForOfIteratorHelper(leafTextblockRanges(tr.doc, change.fromB, change.toB)),
598
+ _step3;
556
599
  try {
557
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
558
- var _step2$value = _slicedToArray(_step2.value, 2),
559
- from = _step2$value[0],
560
- to = _step2$value[1];
600
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
601
+ var _step3$value = _slicedToArray(_step3.value, 2),
602
+ from = _step3$value[0],
603
+ to = _step3$value[1];
561
604
  if (useCoarseTableDecoration && isInsideTable(coarseTables, from)) {
562
605
  continue;
563
606
  }
@@ -584,9 +627,9 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
584
627
  }))));
585
628
  }
586
629
  } catch (err) {
587
- _iterator2.e(err);
630
+ _iterator3.e(err);
588
631
  } finally {
589
- _iterator2.f();
632
+ _iterator3.f();
590
633
  }
591
634
  } else {
592
635
  decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration(_objectSpread({
@@ -613,6 +656,8 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
613
656
  doc: tr.doc,
614
657
  from: change.fromB,
615
658
  to: change.toB,
659
+ // A split piece's own range no longer covers the container it opened (EDITOR-8932).
660
+ nodeRanges: change.blockNodeRangesB,
616
661
  colorScheme: changeColorScheme
617
662
  }, isExtendedEnabled(diffType) && {
618
663
  isInserted: isInserted,