@atlaskit/editor-plugin-show-diff 16.1.1 → 17.0.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 +23 -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 +12 -11
@@ -61,6 +61,17 @@ function decorationSide(decoration) {
61
61
  return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.side || 0;
62
62
  }
63
63
 
64
+ /**
65
+ * Whose change a decoration is part of, or `''` on an unattributed diff — where every decoration
66
+ * reads as one contributor and the grouping below is unchanged.
67
+ *
68
+ * Contributors are kept apart so one stop never covers two of them, since only the tag on the
69
+ * change stepped to reveals (EDITOR-8932).
70
+ */
71
+ function contributorOf(decoration) {
72
+ return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.attributionKey || '';
73
+ }
74
+
64
75
  /**
65
76
  * Collapses decorations that represent one continuous customer-facing edit.
66
77
  *
@@ -69,34 +80,52 @@ function decorationSide(decoration) {
69
80
  * deletion at the same location should be treated as one replacement. Zero-width decorations
70
81
  * (normally deleted-content widgets) can join a group, but cannot extend its range and therefore
71
82
  * cannot bridge two otherwise separate edits.
83
+ *
84
+ * One group per contributor within a run of touching ranges — see `contributorOf`.
72
85
  */
73
86
  function groupTouchingDecorations(decorations, isInlineDecoration) {
74
87
  if (decorations.length < 2) {
75
88
  return decorations;
76
89
  }
77
90
  var groups = [];
78
- var currentGroup = [];
79
- var currentGroupFrom = 0;
80
- var currentGroupTo = 0;
91
+ // The groups a following decoration can still join — one per contributor, dropped at every gap.
92
+ var openGroups = [];
93
+ var clusterTo = 0;
81
94
  var sortedDecorations = (0, _toConsumableArray2.default)(decorations).sort(function (a, b) {
82
95
  return a.from === b.from ? a.to - b.to : a.from - b.from;
83
96
  });
84
97
  sortedDecorations.forEach(function (decoration) {
85
- if (currentGroup.length === 0 || decoration.from > currentGroupTo) {
86
- currentGroup = [decoration];
87
- currentGroupFrom = decoration.from;
88
- currentGroupTo = decoration.to;
89
- groups.push({
90
- decorations: currentGroup,
91
- from: currentGroupFrom,
92
- to: currentGroupTo
93
- });
94
- return;
98
+ var _openGroups$find;
99
+ // A gap ends the run: nothing before it can be joined.
100
+ if (decoration.from > clusterTo) {
101
+ openGroups = [];
95
102
  }
96
- currentGroup.push(decoration);
97
- // A zero-width decoration must not extend the group and bridge a gap.
98
- currentGroupTo = Math.max(currentGroupTo, decoration.to);
99
- groups[groups.length - 1].to = currentGroupTo;
103
+ var contributor = contributorOf(decoration);
104
+ // An unattributed decoration names nobody to keep apart, so it joins whatever is open — a
105
+ // decision item's own highlight must not split the stop from the tag on its list.
106
+ var openGroup = (_openGroups$find = openGroups.find(function (group) {
107
+ return group.contributor === contributor;
108
+ })) !== null && _openGroups$find !== void 0 ? _openGroups$find : contributor === '' ? openGroups[0] : openGroups.find(function (group) {
109
+ return group.contributor === '';
110
+ });
111
+ if (openGroup === undefined) {
112
+ var group = {
113
+ contributor: contributor,
114
+ decorations: [decoration],
115
+ from: decoration.from,
116
+ to: decoration.to
117
+ };
118
+ groups.push(group);
119
+ openGroups = [].concat((0, _toConsumableArray2.default)(openGroups), [group]);
120
+ } else {
121
+ // The first contributor to join claims the group, so the next one still starts its own.
122
+ openGroup.contributor = openGroup.contributor || contributor;
123
+ openGroup.decorations.push(decoration);
124
+ openGroup.to = Math.max(openGroup.to, decoration.to);
125
+ }
126
+
127
+ // A zero-width decoration must not extend the run and bridge a gap.
128
+ clusterTo = Math.max(clusterTo, decoration.to);
100
129
  });
101
130
  return groups.map(function (_ref) {
102
131
  var _ref2, _group$find;
@@ -143,8 +172,8 @@ function groupTouchingDecorations(decorations, isInlineDecoration) {
143
172
  * 4. When `doc` is passed: excludes diff-inline decorations whose range has no inline content
144
173
  * (invalid positions, or block-only slices with no text/atoms — e.g. empty blocks)
145
174
  * 5. When `confluence_ncs_step_diffing_version_history` is enabled, groups overlapping or
146
- * directly touching ranges across decoration types into one result, using the union of all
147
- * grouped ranges
175
+ * directly touching ranges across decoration types into one result per contributor, using the
176
+ * union of all grouped ranges
148
177
  * (zero-width widgets can join a group without extending it). Under
149
178
  * `platform_editor_ai_show_diff_patch_1`, a group that starts with content painting above
150
179
  * the content that replaced it — reports that widget as its `scrollTarget` spec,
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.resolveBaseNodeName = void 0;
7
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
8
+ /**
9
+ * A node type's base name, with a schema variant's suffix stripped: `panel`, `panel_c1` and
10
+ * `panel_c1_root_only` all resolve to `panel`.
11
+ *
12
+ * Package-local rather than `getBaseNodeTypeName` from editor-common, which enumerates the variants
13
+ * it happens to know (`panel_c1` alone) and is shared with features outside this one. Derived
14
+ * instead of listed, so a variant added later needs no change here: the schema generator names a
15
+ * variant `<type>_<variant>`, and every other node name in the ADF schema is camelCase, so the
16
+ * first `_` is the boundary.
17
+ *
18
+ * Gated, so the diff behaves exactly as before for anyone outside the rollout.
19
+ *
20
+ * For taggability only — this gate is weaker than the ones attribution needs, so resolving a variant
21
+ * in the styling decisions would move the legacy diff for readers who see no tags.
22
+ */
23
+ var resolveBaseNodeName = exports.resolveBaseNodeName = function resolveBaseNodeName(name) {
24
+ return (0, _fg.fg)('confluence_ncs_step_diffing_version_history') ? name.split('_')[0] : name;
25
+ };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isTaggableBlockNodeName = exports.isTaggableBlockNode = void 0;
7
+ var _baseNodeName = require("./baseNodeName");
8
+ /**
9
+ * Block nodes that host a contributor tag of their own — those whose whole box reads as one change.
10
+ * Everything that draws a box whatever it holds, plus the two lists that are inserted whole: without
11
+ * them the tag fell through to the text inside the first item (EDITOR-8932). Bullet and ordered lists
12
+ * are left out, since their items accrue one at a time. Tables and list items decorate per cell and
13
+ * per item, with no one-tag-per-block design yet. `blockCard` covers the datasource variant, which is
14
+ * the same node type.
15
+ *
16
+ * Base names only — match with the predicates below, never against `type.name` directly.
17
+ */
18
+ var TAGGABLE_BLOCK_NODES = new Set(['blockCard', 'blockquote', 'bodiedExtension', 'codeBlock', 'decisionList', 'embedCard', 'expand', 'extension', 'media', 'multiBodiedExtension', 'panel', 'rule', 'taskList']);
19
+
20
+ /**
21
+ * Whether this node hosts a contributor tag of its own. Navigation grouping does not consult this:
22
+ * a run of these one contributor inserted is a single stop, tagged on the leading block.
23
+ *
24
+ * Resolved through `resolveBaseNodeName` so a schema variant counts as the type it is a variant of:
25
+ * `panel_c1` is the panel the table-in-panel schema inserts, and matching on `type.name` left it
26
+ * untagged (EDITOR-8932).
27
+ *
28
+ * By name, because the decoration spec carries `nodeName` rather than the type it came from.
29
+ */
30
+ var isTaggableBlockNodeName = exports.isTaggableBlockNodeName = function isTaggableBlockNodeName(name) {
31
+ return name !== undefined && TAGGABLE_BLOCK_NODES.has((0, _baseNodeName.resolveBaseNodeName)(name));
32
+ };
33
+
34
+ /** As above, from the node type itself. */
35
+ var isTaggableBlockNode = exports.isTaggableBlockNode = function isTaggableBlockNode(nodeType) {
36
+ return isTaggableBlockNodeName(nodeType.name);
37
+ };
@@ -172,9 +172,44 @@ const calculateNodesForBlockDecoration = ({
172
172
  showIndicators = false,
173
173
  diffType,
174
174
  coarseTableCellsOnly = false,
175
- tagMountContext
175
+ tagMountContext,
176
+ nodeRanges
176
177
  }) => {
177
178
  const decorations = [];
179
+
180
+ // Exact-nodes path: only reached for a split change (EDITOR-8932), which names the nodes it
181
+ // owns. Deliberately kept separate from the walk below rather than sharing it.
182
+ if (nodeRanges) {
183
+ for (const {
184
+ from: pos,
185
+ to: nodeEnd
186
+ } of nodeRanges) {
187
+ const node = doc.nodeAt(pos);
188
+ if (!(node !== null && node !== void 0 && node.isBlock)) {
189
+ continue;
190
+ }
191
+ const isActive = isRangeActive(activeIndexPos, pos, nodeEnd);
192
+ decorations.push(...createBlockChangedDecoration({
193
+ attributionKey,
194
+ change: {
195
+ from: pos,
196
+ to: nodeEnd,
197
+ name: node.type.name
198
+ },
199
+ colorScheme,
200
+ isInserted,
201
+ isActive,
202
+ shouldHideDeleted,
203
+ showContributorTags,
204
+ showIndicators,
205
+ doc,
206
+ diffType,
207
+ tagMountContext
208
+ }));
209
+ }
210
+ return decorations;
211
+ }
212
+
178
213
  // Coarse path: inside the large table, only cell/header overlays are kept — the
179
214
  // table/row/paragraph decorations there are redundant and cause expensive per-cell
180
215
  // re-render. Blocks outside the table keep their normal decorations.
@@ -420,7 +455,7 @@ const calculateDiffDecorationsInner = ({
420
455
  tr,
421
456
  attributedChanges
422
457
  });
423
- changes = collapseOverlappingChanges(uncollapsedChanges);
458
+ changes = collapseOverlappingChanges(uncollapsedChanges, tr.doc);
424
459
 
425
460
  // Still colours-only: a tags-only diff keeps the plain colour scheme.
426
461
  if (attributionColoringEnabled) {
@@ -570,6 +605,8 @@ const calculateDiffDecorationsInner = ({
570
605
  doc: tr.doc,
571
606
  from: change.fromB,
572
607
  to: change.toB,
608
+ // A split piece's own range no longer covers the container it opened (EDITOR-8932).
609
+ nodeRanges: change.blockNodeRangesB,
573
610
  colorScheme: changeColorScheme,
574
611
  ...(isExtendedEnabled(diffType) && {
575
612
  isInserted,
@@ -1,6 +1,13 @@
1
1
  import { simplifyChanges } from 'prosemirror-changeset';
2
2
  import { getAttributionKey } from '../decorations/colorSchemes/attributions';
3
3
  import { optimizeChanges } from './optimizeChanges';
4
+
5
+ /**
6
+ * A change, plus the block nodes it owns when it is one piece of a split — a piece's own range no
7
+ * longer covers the container it opened. Absent on an unsplit change, which decorates the blocks
8
+ * inside its own range as before.
9
+ */
10
+
4
11
  const getAttributionIdentity = change => {
5
12
  const identities = new Set();
6
13
  for (const span of [...change.deleted, ...change.inserted]) {
@@ -19,20 +26,178 @@ const getAttributionIdentity = change => {
19
26
  };
20
27
  const byNewDocRange = (left, right) => left.fromB - right.fromB || left.toB - right.toB;
21
28
 
29
+ /** Union of the given ranges, with overlapping and touching ones merged. */
30
+ const mergeRanges = ranges => [...ranges].sort((left, right) => left.from - right.from).reduce((merged, range) => {
31
+ const last = merged[merged.length - 1];
32
+ if (last && range.from <= last.to) {
33
+ last.to = Math.max(last.to, range.to);
34
+ return merged;
35
+ }
36
+ return [...merged, {
37
+ ...range
38
+ }];
39
+ }, []);
40
+
41
+ /** Whether `inner` sits wholly inside `outer` and covers less of the document. */
42
+ const isContained = (outer, inner) => inner.toB > inner.fromB && outer.fromB <= inner.fromB && inner.toB <= outer.toB && inner.toB - inner.fromB < outer.toB - outer.fromB;
43
+
22
44
  /**
23
- * Collapses changes whose new-document ranges strictly overlap. Attribution runs are simplified
24
- * independently, so runs either side of an unattributed step can come back covering the same
25
- * characters which stacks two inline decorations and renders one contributor tag per copy.
45
+ * The block nodes in `change` whose opening position `ownsPosition` claims. Ownership is by opening
46
+ * position so a block another contributor added inside a container is not claimed by whoever opened
47
+ * the container. `undefined` for a range this document cannot resolve, which falls the piece back
48
+ * to decorating its own range.
49
+ */
50
+ const blockNodeRanges = (doc, change, ownsPosition) => {
51
+ if (!doc || change.from >= change.to || change.to > doc.content.size) {
52
+ return undefined;
53
+ }
54
+ const ranges = [];
55
+ doc.nodesBetween(change.from, change.to, (node, pos) => {
56
+ if (node.isBlock && ownsPosition(pos) && pos + node.nodeSize <= change.to) {
57
+ ranges.push({
58
+ from: pos,
59
+ to: pos + node.nodeSize
60
+ });
61
+ }
62
+ return true;
63
+ });
64
+ return ranges;
65
+ };
66
+
67
+ /**
68
+ * Splits a change whose spans name more than one contributor into one change per run of consecutive
69
+ * spans sharing a key.
26
70
  *
27
- * Merging keeps every span from both sides, so the latest-writer rule still decides whose tag is
28
- * shown. Ranges that merely touch are left alone, so an unattributed edit next to an attributed one
29
- * is not credited to it.
71
+ * `prosemirror-changeset` merges contiguous inserted content into one change while keeping per-span
72
+ * data, so a panel inserted by one contributor and edited inside by another arrives as a single
73
+ * change holding every span — and the latest-writer rule credited all of it, panel included, to
74
+ * whoever wrote last (EDITOR-8932).
30
75
  */
31
- export const collapseOverlappingChanges = changes => {
32
- if (changes.length <= 1) {
33
- return changes;
76
+ const splitMixedIdentityChanges = (changes, doc) => changes.flatMap(change => {
77
+ if (getAttributionIdentity(change).mergeable) {
78
+ return [change];
79
+ }
80
+ const runs = [];
81
+ let offset = change.fromB;
82
+ for (const span of change.inserted) {
83
+ const key = getAttributionKey(span.data);
84
+ const last = runs[runs.length - 1];
85
+ if (last && last.key === key) {
86
+ last.to = offset + span.length;
87
+ last.spans.push(span);
88
+ } else {
89
+ runs.push({
90
+ key,
91
+ from: offset,
92
+ to: offset + span.length,
93
+ spans: [span]
94
+ });
95
+ }
96
+ offset += span.length;
34
97
  }
35
98
 
99
+ // The offsets above are only positions if the spans partition the inserted range.
100
+ if (runs.length < 2 || offset !== change.toB) {
101
+ return [change];
102
+ }
103
+ return runs.map((run, index) => {
104
+ const owned = blockNodeRanges(doc, {
105
+ from: change.fromB,
106
+ to: change.toB
107
+ }, pos => pos >= run.from && pos < run.to);
108
+ return {
109
+ ...change,
110
+ fromA: index === 0 ? change.fromA : change.toA,
111
+ fromB: run.from,
112
+ toB: run.to,
113
+ deleted: index === 0 ? change.deleted : [],
114
+ inserted: run.spans,
115
+ ...(owned ? {
116
+ blockNodeRangesB: owned
117
+ } : {})
118
+ };
119
+ });
120
+ });
121
+
122
+ /**
123
+ * Subtracts every other author's contained change from a change's range, so both contributions
124
+ * survive. Applies when the contributors arrive as separate changes rather than as spans of one —
125
+ * the `step` diff type. A contained change by the same author, and one that only partially
126
+ * overlaps, are left for the collapse below.
127
+ *
128
+ * The leading fragment keeps the deleted side, so one deletion is not drawn once per fragment.
129
+ */
130
+ const splitContainedChanges = (changes, doc) => changes.flatMap(change => {
131
+ const {
132
+ key
133
+ } = getAttributionIdentity(change);
134
+ const inner = mergeRanges(changes.filter(other => {
135
+ const otherKey = getAttributionIdentity(other).key;
136
+ return other !== change && key !== undefined && otherKey !== undefined && otherKey !== key && isContained(change, other);
137
+ }).map(({
138
+ fromB,
139
+ toB
140
+ }) => ({
141
+ from: fromB,
142
+ to: toB
143
+ })));
144
+ if (inner.length === 0) {
145
+ return [change];
146
+ }
147
+ const gaps = [];
148
+ let cursor = change.fromB;
149
+ for (const {
150
+ from,
151
+ to
152
+ } of inner) {
153
+ if (from > cursor) {
154
+ gaps.push({
155
+ from: cursor,
156
+ to: from
157
+ });
158
+ }
159
+ cursor = Math.max(cursor, to);
160
+ }
161
+ if (change.toB > cursor) {
162
+ gaps.push({
163
+ from: cursor,
164
+ to: change.toB
165
+ });
166
+ }
167
+
168
+ // Subtracted end to end, so nothing is left to highlight — but the blocks are still owned.
169
+ const fragments = gaps.length > 0 ? gaps : [{
170
+ from: change.toB,
171
+ to: change.toB
172
+ }];
173
+ // The outer author's: every block in their range that no inner change opened.
174
+ const owned = blockNodeRanges(doc, {
175
+ from: change.fromB,
176
+ to: change.toB
177
+ }, pos => !inner.some(({
178
+ from,
179
+ to
180
+ }) => pos >= from && pos < to));
181
+ return fragments.map((fragment, index) => ({
182
+ ...change,
183
+ fromA: index === 0 ? change.fromA : change.toA,
184
+ fromB: fragment.from,
185
+ toB: fragment.to,
186
+ deleted: index === 0 ? change.deleted : [],
187
+ ...(owned ? {
188
+ blockNodeRangesB: index === 0 ? owned : []
189
+ } : {})
190
+ }));
191
+ });
192
+
193
+ /**
194
+ * The blocks a merged change owns. Both sides must name theirs, or the merge falls back to its own
195
+ * range — narrowing it could drop a block altogether.
196
+ */
197
+ const mergeBlockNodeRanges = (left, right) => left.blockNodeRangesB && right.blockNodeRangesB ? [...left.blockNodeRangesB, ...right.blockNodeRangesB] : undefined;
198
+
199
+ /** Unions the changes whose new-document ranges strictly overlap, leaving touching ones alone. */
200
+ const collapse = changes => {
36
201
  // A zero-length new-document range paints no inline decoration, so it is kept aside rather than
37
202
  // folded into a neighbouring insertion.
38
203
  const emptyRanges = changes.filter(change => change.toB <= change.fromB);
@@ -40,13 +205,17 @@ export const collapseOverlappingChanges = changes => {
40
205
  for (const change of changes.filter(change => change.toB > change.fromB).sort(byNewDocRange)) {
41
206
  const current = collapsed[collapsed.length - 1];
42
207
  if (current && change.fromB < current.toB) {
208
+ const blockNodeRangesB = mergeBlockNodeRanges(current, change);
43
209
  collapsed[collapsed.length - 1] = {
44
210
  fromA: Math.min(current.fromA, change.fromA),
45
211
  toA: Math.max(current.toA, change.toA),
46
212
  fromB: Math.min(current.fromB, change.fromB),
47
213
  toB: Math.max(current.toB, change.toB),
48
214
  deleted: [...current.deleted, ...change.deleted],
49
- inserted: [...current.inserted, ...change.inserted]
215
+ inserted: [...current.inserted, ...change.inserted],
216
+ ...(blockNodeRangesB ? {
217
+ blockNodeRangesB
218
+ } : {})
50
219
  };
51
220
  } else {
52
221
  collapsed.push({
@@ -57,6 +226,23 @@ export const collapseOverlappingChanges = changes => {
57
226
  return [...collapsed, ...emptyRanges].sort(byNewDocRange);
58
227
  };
59
228
 
229
+ /**
230
+ * Collapses changes whose new-document ranges strictly overlap. Attribution runs are simplified
231
+ * independently, so runs either side of an unattributed step can come back covering the same
232
+ * characters — which stacks two inline decorations and renders one contributor tag per copy.
233
+ *
234
+ * Merging keeps every span from both sides, so the latest-writer rule still decides whose tag is
235
+ * shown. Ranges that merely touch are left alone, so an unattributed edit next to an attributed one
236
+ * is not credited to it.
237
+ *
238
+ * Contributors are separated first, by `splitMixedIdentityChanges` and `splitContainedChanges`.
239
+ * Both leave ranges that only touch, so they survive the collapse.
240
+ *
241
+ * Carries no gate of its own: the only caller reaches it through the attributed changeset, which
242
+ * already requires `confluence_ncs_step_diffing_version_history`.
243
+ */
244
+ export const collapseOverlappingChanges = (changes, doc) => collapse(splitContainedChanges(splitMixedIdentityChanges(changes, doc), doc));
245
+
60
246
  /**
61
247
  * Simplifies and optimizes consecutive changes without crossing an attribution boundary.
62
248
  * A change which already contains more than one identity is deliberately kept isolated.
@@ -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 { getStandardDeletedTextDecorationStyle } from './getStandardDeletedTextDecorationStyle';
3
4
 
@@ -115,40 +116,46 @@ const textAccentMap = {
115
116
  gray: "var(--ds-text-accent-gray, #505258)",
116
117
  lime: "var(--ds-text-accent-lime, #4C6B1F)"
117
118
  };
119
+ const getBrand = color => Object.prototype.hasOwnProperty.call(agentBrandColorSchemes, color) ? agentBrandColorSchemes[color] : undefined;
118
120
  function bgSubtlest(color) {
119
- return bgSubtlestMap[color];
121
+ var _getBrand$background, _getBrand;
122
+ return (_getBrand$background = (_getBrand = getBrand(color)) === null || _getBrand === void 0 ? void 0 : _getBrand.background) !== null && _getBrand$background !== void 0 ? _getBrand$background : bgSubtlestMap[color];
120
123
  }
121
124
  function bgSubtlestPressed(color) {
122
- return bgSubtlestPressedMap[color];
125
+ var _getBrand$background2, _getBrand2;
126
+ return (_getBrand$background2 = (_getBrand2 = getBrand(color)) === null || _getBrand2 === void 0 ? void 0 : _getBrand2.background) !== null && _getBrand$background2 !== void 0 ? _getBrand$background2 : bgSubtlestPressedMap[color];
123
127
  }
124
128
  function bgSubtler(color) {
125
- return bgSubtlerMap[color];
129
+ var _getBrand$border, _getBrand3;
130
+ return (_getBrand$border = (_getBrand3 = getBrand(color)) === null || _getBrand3 === void 0 ? void 0 : _getBrand3.border) !== null && _getBrand$border !== void 0 ? _getBrand$border : bgSubtlerMap[color];
126
131
  }
127
132
  function bgBolder(color) {
128
- return bgBolderMap[color];
133
+ var _getBrand$bold, _getBrand4;
134
+ return (_getBrand$bold = (_getBrand4 = getBrand(color)) === null || _getBrand4 === void 0 ? void 0 : _getBrand4.bold) !== null && _getBrand$bold !== void 0 ? _getBrand$bold : bgBolderMap[color];
129
135
  }
130
136
  function bgSubtlerPressed(color) {
131
- return bgSubtlerPressedMap[color];
137
+ var _getBrand$background3, _getBrand5;
138
+ return (_getBrand$background3 = (_getBrand5 = getBrand(color)) === null || _getBrand5 === void 0 ? void 0 : _getBrand5.background) !== null && _getBrand$background3 !== void 0 ? _getBrand$background3 : bgSubtlerPressedMap[color];
132
139
  }
133
140
  function borderAccent(color) {
134
- return borderAccentMap[color];
141
+ var _getBrand$border2, _getBrand6;
142
+ return (_getBrand$border2 = (_getBrand6 = getBrand(color)) === null || _getBrand6 === void 0 ? void 0 : _getBrand6.border) !== null && _getBrand$border2 !== void 0 ? _getBrand$border2 : borderAccentMap[color];
135
143
  }
136
144
  function textAccent(color) {
137
- return textAccentMap[color];
145
+ var _getBrand$accentText, _getBrand7;
146
+ return (_getBrand$accentText = (_getBrand7 = getBrand(color)) === null || _getBrand7 === void 0 ? void 0 : _getBrand7.accentText) !== null && _getBrand$accentText !== void 0 ? _getBrand$accentText : textAccentMap[color];
147
+ }
148
+ function deletedBackground(color) {
149
+ var _getBrand$emphasisBac, _getBrand8;
150
+ return (_getBrand$emphasisBac = (_getBrand8 = getBrand(color)) === null || _getBrand8 === void 0 ? void 0 : _getBrand8.emphasisBackground) !== null && _getBrand$emphasisBac !== void 0 ? _getBrand$emphasisBac : bgSubtlest(color);
138
151
  }
139
152
 
140
- /**
141
- * Presentation tokens for a contributor tag in one diff colour.
142
- *
143
- * A `bolder` fill rather than the `subtlest` tint the highlight uses: the tag is a label on the
144
- * change, and at tag size a tint reads as a second, weaker highlight sitting above the real one.
145
- * `inverse` is the only text tone that reads on a bolder fill, and it needs no per-colour map —
146
- * which is also why the tag carries no border: the fill alone bounds it.
147
- */
153
+ /** Contributor tags use a bold fill with a contrasting foreground, including branded schemes. */
148
154
  export function getAccentTokens(color) {
155
+ var _getBrand$boldText, _getBrand9;
149
156
  return {
150
157
  background: bgBolder(color),
151
- text: "var(--ds-text-inverse, #FFFFFF)"
158
+ text: (_getBrand$boldText = (_getBrand9 = getBrand(color)) === null || _getBrand9 === void 0 ? void 0 : _getBrand9.boldText) !== null && _getBrand$boldText !== void 0 ? _getBrand$boldText : "var(--ds-text-inverse, #FFFFFF)"
152
159
  };
153
160
  }
154
161
 
@@ -170,13 +177,18 @@ function deletedCellOutline(colors) {
170
177
  // --- Inserted content styles ---
171
178
 
172
179
  function insertedInlineBorderColor(colors) {
173
- return colors.insertedInlineBorderTone === 'backgroundHovered' ? bgSubtlestHoveredMap[colors.insertColor] : borderAccent(colors.insertColor);
180
+ var _getBrand$border3, _getBrand0;
181
+ return colors.insertedInlineBorderTone === 'backgroundHovered' ? (_getBrand$border3 = (_getBrand0 = getBrand(colors.insertColor)) === null || _getBrand0 === void 0 ? void 0 : _getBrand0.border) !== null && _getBrand$border3 !== void 0 ? _getBrand$border3 : bgSubtlestHoveredMap[colors.insertColor] : borderAccent(colors.insertColor);
174
182
  }
175
183
 
176
184
  /** Inline inserted content — default state. */
177
185
  export function buildInsertStyle(colors) {
186
+ const brand = getBrand(colors.insertColor);
178
187
  return convertToInlineCss({
179
188
  background: bgSubtlest(colors.insertColor),
189
+ ...(brand ? {
190
+ color: brand.text
191
+ } : {}),
180
192
  textDecoration: 'underline',
181
193
  textDecorationStyle: colors.insertUnderlineStyle,
182
194
  textDecorationThickness: "var(--ds-space-025, 2px)",
@@ -196,8 +208,12 @@ export function buildInsertStyleInBlock(colors) {
196
208
 
197
209
  /** Extended insert style — background + border-bottom underline. */
198
210
  export function buildInsertStyleExtended(colors) {
211
+ const brand = getBrand(colors.insertColor);
199
212
  return convertToInlineCss({
200
213
  background: bgSubtlest(colors.insertColor),
214
+ ...(brand ? {
215
+ color: brand.text
216
+ } : {}),
201
217
  borderBottom: `2px solid ${insertedInlineBorderColor(colors)}`,
202
218
  padding: `1px 0 2px`
203
219
  });
@@ -213,8 +229,12 @@ export function buildInsertStyleInBlockExtended(colors) {
213
229
 
214
230
  /** Active state of the extended insert style. */
215
231
  export function buildInsertStyleExtendedActive(colors) {
232
+ const brand = getBrand(colors.insertColor);
216
233
  return convertToInlineCss({
217
234
  background: bgSubtlerPressed(colors.insertActiveColor),
235
+ ...(brand ? {
236
+ color: brand.text
237
+ } : {}),
218
238
  borderBottom: `2px solid ${borderAccent(colors.insertColor)}`,
219
239
  padding: `1px 0 2px`
220
240
  });
@@ -222,16 +242,24 @@ export function buildInsertStyleExtendedActive(colors) {
222
242
 
223
243
  /** Extended insert style without underline — background + padding only. */
224
244
  export function buildInsertStyleExtendedNoUnderline(colors) {
245
+ const brand = getBrand(colors.insertColor);
225
246
  return convertToInlineCss({
226
247
  background: bgSubtlest(colors.insertColor),
248
+ ...(brand ? {
249
+ color: brand.text
250
+ } : {}),
227
251
  padding: `1px 0 2px`
228
252
  });
229
253
  }
230
254
 
231
255
  /** Active state of the no-underline extended insert style. */
232
256
  export function buildInsertStyleExtendedNoUnderlineActive(colors) {
257
+ const brand = getBrand(colors.insertColor);
233
258
  return convertToInlineCss({
234
259
  background: bgSubtlerPressed(colors.insertActiveColor),
260
+ ...(brand ? {
261
+ color: brand.text
262
+ } : {}),
235
263
  padding: `1px 0 2px`
236
264
  });
237
265
  }
@@ -245,8 +273,12 @@ export function buildInsertStyleInBlockExtendedNoUnderline(_colors) {
245
273
 
246
274
  /** Inline inserted content — active/focused state. */
247
275
  export function buildInsertStyleActive(colors) {
276
+ const brand = getBrand(colors.insertColor);
248
277
  return convertToInlineCss({
249
278
  background: bgSubtlerPressed(colors.insertActiveColor),
279
+ ...(brand ? {
280
+ color: brand.text
281
+ } : {}),
250
282
  textDecoration: 'underline',
251
283
  textDecorationStyle: colors.insertUnderlineStyle,
252
284
  textDecorationThickness: "var(--ds-space-025, 2px)",
@@ -610,7 +642,7 @@ export function buildDeletedInlineContentStyleExtended(colors, isActive = false)
610
642
  // Under 'underline' emphasis the highlight stays on its resting tint — the same one hover paints —
611
643
  // so only the border carries the emphasis.
612
644
  const isUnderlineEmphasis = isActive && usesUnderlineEmphasis(colors);
613
- const backgroundColor = isActive && !isUnderlineEmphasis && colors.deletedInlineBorderTone === 'background' ? bgSubtlerPressed(colors.deleteActiveColor) : bgSubtlest(colors.deleteColor);
645
+ const backgroundColor = isActive && !isUnderlineEmphasis && colors.deletedInlineBorderTone === 'background' ? bgSubtlerPressed(colors.deleteActiveColor) : deletedBackground(colors.deleteColor);
614
646
  const borderColor = isUnderlineEmphasis ? borderAccent(colors.deleteActiveColor) : colors.deletedInlineBorderTone === 'background' ? backgroundColor : borderAccent(colors.deleteColor);
615
647
  return convertToInlineCss({
616
648
  backgroundColor,
@@ -822,7 +854,7 @@ export function getDeletedInlineRevealColors(colors) {
822
854
  * paints at rest, so hovering restores the appearance rather than introducing a third one.
823
855
  */
824
856
  export function getDeletedHighlightHoverColor(colors) {
825
- return bgSubtlest(colors.deleteColor);
857
+ return deletedBackground(colors.deleteColor);
826
858
  }
827
859
 
828
860
  /**
@@ -1,3 +1,4 @@
1
+ import { agentBrandColorSchemes } from '@atlaskit/agent-color/agent-brand-color-schemes';
1
2
  import { PARTICIPANT_COLOR_SCHEMES } from './types';
2
3
 
3
4
  /** Green insertions, red deletions. */
@@ -74,10 +75,12 @@ const createAttributionScheme = color => {
74
75
  };
75
76
  };
76
77
  const attributionColorSchemes = Object.fromEntries(PARTICIPANT_COLOR_SCHEMES.map(color => [color, createAttributionScheme(color)]));
78
+ const brandSchemes = Object.fromEntries(Object.keys(agentBrandColorSchemes).map(scheme => [scheme, createAttributionScheme(scheme)]));
77
79
 
78
80
  /** Maps public schemes and attribution-only participant slots to their data objects. */
79
81
  export const colorSchemeRegistry = {
80
82
  ...attributionColorSchemes,
83
+ ...brandSchemes,
81
84
  traditional: traditionalScheme,
82
85
  standard: standardScheme
83
86
  };
@@ -5,8 +5,6 @@ export const PARTICIPANT_COLOR_SCHEMES = ['red', 'blue', 'green', 'yellow', 'pur
5
5
 
6
6
  /** ADS accent hues used by the public and participant colour schemes. */
7
7
 
8
- /** A public scheme or an attribution participant slot resolved internally for one change. */
9
-
10
8
  /**
11
9
  * A show-diff colour scheme. Colour fields are ADS hues that `factory.ts` expands into token paths;
12
10
  * the rest encode structural differences no colour can express. Ring widths, padding, radius and