@atlaskit/editor-plugin-show-diff 10.1.23 → 10.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 (65) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +132 -31
  3. package/dist/cjs/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -2
  4. package/dist/cjs/pm-plugins/calculateDiff/smart/classifySmartChanges.js +841 -0
  5. package/dist/cjs/pm-plugins/calculateDiff/smart/helpers.js +135 -0
  6. package/dist/cjs/pm-plugins/calculateDiff/smart/segmentText.js +331 -0
  7. package/dist/cjs/pm-plugins/calculateDiff/smart/thresholds.js +46 -0
  8. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
  9. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
  10. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
  11. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +30 -17
  12. package/dist/cjs/pm-plugins/decorations/decorationKeys.js +8 -5
  13. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -30
  14. package/dist/cjs/pm-plugins/getScrollableDecorations.js +6 -6
  15. package/dist/cjs/pm-plugins/isExtendedEnabled.js +20 -0
  16. package/dist/cjs/pm-plugins/main.js +21 -16
  17. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +119 -29
  18. package/dist/es2019/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -3
  19. package/dist/es2019/pm-plugins/calculateDiff/smart/classifySmartChanges.js +689 -0
  20. package/dist/es2019/pm-plugins/calculateDiff/smart/helpers.js +106 -0
  21. package/dist/es2019/pm-plugins/calculateDiff/smart/segmentText.js +283 -0
  22. package/dist/es2019/pm-plugins/calculateDiff/smart/thresholds.js +45 -0
  23. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
  24. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
  25. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
  26. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +31 -16
  27. package/dist/es2019/pm-plugins/decorations/decorationKeys.js +9 -5
  28. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -34
  29. package/dist/es2019/pm-plugins/getScrollableDecorations.js +6 -6
  30. package/dist/es2019/pm-plugins/isExtendedEnabled.js +12 -0
  31. package/dist/es2019/pm-plugins/main.js +21 -16
  32. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +132 -31
  33. package/dist/esm/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -3
  34. package/dist/esm/pm-plugins/calculateDiff/smart/classifySmartChanges.js +834 -0
  35. package/dist/esm/pm-plugins/calculateDiff/smart/helpers.js +128 -0
  36. package/dist/esm/pm-plugins/calculateDiff/smart/segmentText.js +325 -0
  37. package/dist/esm/pm-plugins/calculateDiff/smart/thresholds.js +39 -0
  38. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
  39. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
  40. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
  41. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +30 -17
  42. package/dist/esm/pm-plugins/decorations/decorationKeys.js +9 -5
  43. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -30
  44. package/dist/esm/pm-plugins/getScrollableDecorations.js +6 -6
  45. package/dist/esm/pm-plugins/isExtendedEnabled.js +14 -0
  46. package/dist/esm/pm-plugins/main.js +21 -16
  47. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  48. package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +3 -1
  49. package/dist/types/pm-plugins/calculateDiff/groupChangesByBlock.d.ts +9 -0
  50. package/dist/types/pm-plugins/calculateDiff/smart/classifySmartChanges.d.ts +26 -0
  51. package/dist/types/pm-plugins/calculateDiff/smart/helpers.d.ts +44 -0
  52. package/dist/types/pm-plugins/calculateDiff/smart/segmentText.d.ts +32 -0
  53. package/dist/types/pm-plugins/calculateDiff/smart/thresholds.d.ts +44 -0
  54. package/dist/types/pm-plugins/decorations/createBlockChangedDecoration.d.ts +3 -2
  55. package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +3 -2
  56. package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +3 -2
  57. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +4 -2
  58. package/dist/types/pm-plugins/decorations/decorationKeys.d.ts +5 -3
  59. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +6 -4
  60. package/dist/types/pm-plugins/getScrollableDecorations.d.ts +2 -1
  61. package/dist/types/pm-plugins/isExtendedEnabled.d.ts +11 -0
  62. package/dist/types/pm-plugins/main.d.ts +11 -1
  63. package/dist/types/showDiffPluginType.d.ts +20 -1
  64. package/docs/smart-diff-design.md +240 -0
  65. package/package.json +7 -2
@@ -0,0 +1,834 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
4
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
5
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
6
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
7
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
8
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
9
+ import { makePromotedChange, mergeOverlappingByNewDocRange, rangesOverlap } from './helpers';
10
+ import { buildCharsByOffset, countWords, segmentSentences, segmentWordSpans } from './segmentText';
11
+ import { resolveThresholds } from './thresholds';
12
+ /**
13
+ * Block-first `smart` classifier.
14
+ *
15
+ * Groups changes by the top-level block they touch — like the `block` diff type — then
16
+ * classifies WITHIN each block group:
17
+ *
18
+ * 1. structural / node-type change (blockA.type !== blockB.type) → whole block
19
+ * 2. text-bearing block (paragraph/heading) → sentence / paragraph / inline
20
+ * 3. container block (list/table/layout/panel/...) → recurse into children,
21
+ * promoting the whole container when changed-child density ≥ node.ratio (with the
22
+ * rigid-child escalation: cell → row → table, column → section, item → list).
23
+ *
24
+ * Grouping on real top-level block boundaries prevents the "empty structural shell" family
25
+ * of bugs (e.g. bulletList → table rendering empty bullets).
26
+ */
27
+ export var classifySmartChanges = function classifySmartChanges(_ref) {
28
+ var changes = _ref.changes,
29
+ originalDoc = _ref.originalDoc,
30
+ newDoc = _ref.newDoc,
31
+ locale = _ref.locale,
32
+ overrides = _ref.thresholds;
33
+ if (changes.length === 0) {
34
+ return changes;
35
+ }
36
+ var thresholds = resolveThresholds(overrides);
37
+ var groups = groupByTopLevelBlock(changes, originalDoc, newDoc);
38
+ var result = [];
39
+ for (var _i = 0, _groups = groups; _i < _groups.length; _i++) {
40
+ var group = _groups[_i];
41
+ result.push.apply(result, _toConsumableArray(classifyBlockGroup(group, originalDoc, newDoc, locale, thresholds)));
42
+ }
43
+
44
+ // Clamp to valid bounds (defensive) and coalesce overlaps.
45
+ var maxA = originalDoc.content.size;
46
+ var maxB = newDoc.content.size;
47
+ var clamped = result.map(function (change) {
48
+ return clampChange(change, maxA, maxB);
49
+ }).filter(function (change) {
50
+ return change !== null;
51
+ });
52
+ return mergeOverlappingByNewDocRange(clamped);
53
+ };
54
+
55
+ /**
56
+ * A group of raw changes that all fall within the same top-level block, plus the resolved
57
+ * block node on each side. `blockA`/`blockB` are null for pure insertions/deletions where
58
+ * one side has no corresponding block.
59
+ */
60
+
61
+ /** A resolved block: the node plus its OUTER bounds (before open token / after close token). */
62
+
63
+ /**
64
+ * Group changes by the top-level block (direct child of doc) they touch, aligning the A-side
65
+ * and B-side blocks a change covers. A single change can span MULTIPLE top-level blocks (e.g.
66
+ * a ReplaceStep whose slice contains several nodes); we enumerate every block it overlaps on
67
+ * both sides (`topLevelBlocksInRange`) and create one group per aligned block, so added/removed
68
+ * blocks in a multi-node replacement are never dropped. Unlike `groupChangesByBlock`, we KEEP
69
+ * each group's constituent raw changes so intra-block density can be measured.
70
+ */
71
+ var groupByTopLevelBlock = function groupByTopLevelBlock(changes, docA, docB) {
72
+ var groups = new Map();
73
+ var ensureGroup = function ensureGroup(blockA, blockB, anchors) {
74
+ if (!blockA && !blockB) {
75
+ return null;
76
+ }
77
+ // Key by both sides so an added block (blockA=null) on the B side and a deleted block
78
+ // (blockB=null) on the A side each get their own group.
79
+ var key = "".concat(blockB ? blockB.from : 'x', ":").concat(blockA ? blockA.from : 'x');
80
+ var group = groups.get(key);
81
+ if (!group) {
82
+ group = _objectSpread({
83
+ blockA: blockA,
84
+ blockB: blockB,
85
+ changes: []
86
+ }, anchors);
87
+ groups.set(key, group);
88
+ }
89
+ return group;
90
+ };
91
+ var _iterator = _createForOfIteratorHelper(changes),
92
+ _step;
93
+ try {
94
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
95
+ var change = _step.value;
96
+ // A single change (e.g. a ReplaceStep whose slice spans several nodes) can cover MULTIPLE
97
+ // top-level blocks on either side. Resolving only the block at `fromB` would silently drop
98
+ // the extra blocks (e.g. an added table after a replaced paragraph). So we enumerate every
99
+ // top-level block the change overlaps on BOTH sides and create a group per aligned block.
100
+ var blocksB = topLevelBlocksInRange(docB, change.fromB, change.toB);
101
+ var blocksA = topLevelBlocksInRange(docA, change.fromA, change.toA);
102
+
103
+ // Simple, common case: exactly one block on each side (or one side empty).
104
+ if (blocksB.length <= 1 && blocksA.length <= 1) {
105
+ var _blocksA$, _blocksB$;
106
+ var group = ensureGroup((_blocksA$ = blocksA[0]) !== null && _blocksA$ !== void 0 ? _blocksA$ : null, (_blocksB$ = blocksB[0]) !== null && _blocksB$ !== void 0 ? _blocksB$ : null);
107
+ group === null || group === void 0 || group.changes.push(change);
108
+ continue;
109
+ }
110
+
111
+ // Multi-block span: align blocks positionally by index. The first `min(len)` blocks are
112
+ // REPLACEMENTS (paired A↔B). Extra B-blocks are PURE INSERTIONS and extra A-blocks are
113
+ // PURE DELETIONS — and, crucially, these must NOT reuse the raw change's full A/B range
114
+ // (that range covers the paired blocks too, so an added block would claim the original
115
+ // content already owned by a paired replacement, rendering it deleted twice). Instead we
116
+ // anchor a pure insertion's A side (and a pure deletion's B side) as ZERO-WIDTH at the
117
+ // end of the last paired block on the opposite side.
118
+ var paired = Math.min(blocksA.length, blocksB.length);
119
+ for (var i = 0; i < paired; i++) {
120
+ var _group = ensureGroup(blocksA[i], blocksB[i]);
121
+ _group === null || _group === void 0 || _group.changes.push(change);
122
+ }
123
+ // Anchor for extras = end of the last paired block on the opposite side (or the start of
124
+ // the span if there were no paired blocks).
125
+ var anchorA = paired > 0 ? blocksA[paired - 1].to : change.fromA;
126
+ var anchorB = paired > 0 ? blocksB[paired - 1].to : change.fromB;
127
+ for (var _i2 = paired; _i2 < blocksB.length; _i2++) {
128
+ var _group2 = ensureGroup(null, blocksB[_i2], {
129
+ anchorA: anchorA
130
+ });
131
+ _group2 === null || _group2 === void 0 || _group2.changes.push(change);
132
+ }
133
+ for (var _i3 = paired; _i3 < blocksA.length; _i3++) {
134
+ var _group3 = ensureGroup(blocksA[_i3], null, {
135
+ anchorB: anchorB
136
+ });
137
+ _group3 === null || _group3 === void 0 || _group3.changes.push(change);
138
+ }
139
+ }
140
+ } catch (err) {
141
+ _iterator.e(err);
142
+ } finally {
143
+ _iterator.f();
144
+ }
145
+ return Array.from(groups.values()).sort(function (a, b) {
146
+ var _ref2, _a$blockB$from, _a$blockB, _a$blockA, _ref3, _b$blockB$from, _b$blockB, _b$blockA;
147
+ return ((_ref2 = (_a$blockB$from = (_a$blockB = a.blockB) === null || _a$blockB === void 0 ? void 0 : _a$blockB.from) !== null && _a$blockB$from !== void 0 ? _a$blockB$from : (_a$blockA = a.blockA) === null || _a$blockA === void 0 ? void 0 : _a$blockA.from) !== null && _ref2 !== void 0 ? _ref2 : 0) - ((_ref3 = (_b$blockB$from = (_b$blockB = b.blockB) === null || _b$blockB === void 0 ? void 0 : _b$blockB.from) !== null && _b$blockB$from !== void 0 ? _b$blockB$from : (_b$blockA = b.blockA) === null || _b$blockA === void 0 ? void 0 : _b$blockA.from) !== null && _ref3 !== void 0 ? _ref3 : 0);
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Enumerate the top-level blocks (direct children of doc) whose outer range overlaps
153
+ * `[from, to)`. Returns each as a BlockRef. Used to split a change that spans several blocks.
154
+ */
155
+ var topLevelBlocksInRange = function topLevelBlocksInRange(doc, from, to) {
156
+ var lo = Math.min(Math.max(from, 0), doc.content.size);
157
+ var hi = Math.min(Math.max(to, lo), doc.content.size);
158
+ var refs = [];
159
+ var offset = 0;
160
+ for (var i = 0; i < doc.childCount; i++) {
161
+ var node = doc.child(i);
162
+ var blockFrom = offset;
163
+ var blockTo = offset + node.nodeSize;
164
+ // Overlap test; include a zero-width touch at the block start (pure insertion point).
165
+ if (blockFrom < hi && blockTo > lo) {
166
+ refs.push({
167
+ node: node,
168
+ from: blockFrom,
169
+ to: blockTo
170
+ });
171
+ } else if (blockFrom === lo && blockFrom === hi) {
172
+ refs.push({
173
+ node: node,
174
+ from: blockFrom,
175
+ to: blockTo
176
+ });
177
+ }
178
+ offset = blockTo;
179
+ }
180
+ return refs;
181
+ };
182
+ var clampChange = function clampChange(change, maxA, maxB) {
183
+ var fromA = Math.max(0, Math.min(change.fromA, maxA));
184
+ var toA = Math.max(fromA, Math.min(change.toA, maxA));
185
+ var fromB = Math.max(0, Math.min(change.fromB, maxB));
186
+ var toB = Math.max(fromB, Math.min(change.toB, maxB));
187
+ if (toA === fromA && toB === fromB) {
188
+ return null;
189
+ }
190
+ return _objectSpread(_objectSpread({}, change), {}, {
191
+ fromA: fromA,
192
+ toA: toA,
193
+ fromB: fromB,
194
+ toB: toB
195
+ });
196
+ };
197
+ var TEXT_BLOCK_TYPES = new Set(['paragraph', 'heading']);
198
+
199
+ /**
200
+ * Rigid children cannot be individually deleted+re-inserted without breaking their parent's
201
+ * structure — promoting one escalates to its structural unit (see classifyContainer):
202
+ * layoutColumn → layoutSection, tableCell/tableHeader → tableRow → table.
203
+ */
204
+ var RIGID_CHILD_TYPES = new Set(['layoutColumn', 'tableCell', 'tableHeader']);
205
+ var TABLE_TYPE = 'table';
206
+
207
+ /**
208
+ * Emit a single whole-block (node-level) change covering both sides of a group. Used for
209
+ * structural / node-type replacements and for containers dense enough to replace wholesale.
210
+ */
211
+ var wholeBlockChange = function wholeBlockChange(blockA, blockB, changes, anchors) {
212
+ var _anchors$anchorA, _anchors$anchorA2, _anchors$anchorB, _anchors$anchorB2;
213
+ // Pure insertion (blockA === null): the A side must be a ZERO-WIDTH anchor, never the raw
214
+ // change's A range (which spans sibling blocks that were separately replaced). Same for a
215
+ // pure deletion's B side. Falling back to the change coords is the last-resort path when no
216
+ // anchor was supplied (single-block insert/delete, where the coords are already zero-width).
217
+ var fromA = blockA ? blockA.from : (_anchors$anchorA = anchors === null || anchors === void 0 ? void 0 : anchors.anchorA) !== null && _anchors$anchorA !== void 0 ? _anchors$anchorA : changes[0].fromA;
218
+ var toA = blockA ? blockA.to : (_anchors$anchorA2 = anchors === null || anchors === void 0 ? void 0 : anchors.anchorA) !== null && _anchors$anchorA2 !== void 0 ? _anchors$anchorA2 : changes[changes.length - 1].toA;
219
+ var fromB = blockB ? blockB.from : (_anchors$anchorB = anchors === null || anchors === void 0 ? void 0 : anchors.anchorB) !== null && _anchors$anchorB !== void 0 ? _anchors$anchorB : changes[0].fromB;
220
+ var toB = blockB ? blockB.to : (_anchors$anchorB2 = anchors === null || anchors === void 0 ? void 0 : anchors.anchorB) !== null && _anchors$anchorB2 !== void 0 ? _anchors$anchorB2 : changes[changes.length - 1].toB;
221
+ return makePromotedChange(fromA, toA, fromB, toB, 'node');
222
+ };
223
+
224
+ /**
225
+ * Classify one block group. This is the single recursive decision point:
226
+ * - one side missing (pure insert/delete) → whole block
227
+ * - node type changed (para→panel, list→table, heading→para, …) → whole block
228
+ * - text-bearing block → sentence / paragraph / inline
229
+ * - container → recurse into children (with rigid escalation)
230
+ */
231
+ var classifyBlockGroup = function classifyBlockGroup(group, originalDoc, newDoc, locale, thresholds) {
232
+ var blockA = group.blockA,
233
+ blockB = group.blockB,
234
+ changes = group.changes,
235
+ anchorA = group.anchorA,
236
+ anchorB = group.anchorB;
237
+
238
+ // Pure insertion or deletion of a whole block.
239
+ if (!blockA || !blockB) {
240
+ return [wholeBlockChange(blockA, blockB, changes, {
241
+ anchorA: anchorA,
242
+ anchorB: anchorB
243
+ })];
244
+ }
245
+
246
+ // Node-type / structural change: the whole block was replaced. No further analysis — this
247
+ // is what makes list→table, paragraph→panel, heading→paragraph "just work".
248
+ if (blockA.node.type.name !== blockB.node.type.name) {
249
+ return [wholeBlockChange(blockA, blockB, changes)];
250
+ }
251
+
252
+ // Text-bearing block → sentence / paragraph / inline.
253
+ if (TEXT_BLOCK_TYPES.has(blockB.node.type.name)) {
254
+ return classifyTextblock(blockA, blockB, changes, locale, thresholds);
255
+ }
256
+
257
+ // Container block → measure changed-child density, promote whole container or recurse.
258
+ return classifyContainer(blockA, blockB, changes, originalDoc, newDoc, locale, thresholds);
259
+ };
260
+
261
+ /**
262
+ * Sentence- and paragraph-level classification for a single text-bearing block.
263
+ */
264
+ var classifyTextblock = function classifyTextblock(blockA, blockB, changes, locale, thresholds) {
265
+ var charsB = buildCharsByOffset(blockB.node);
266
+ var sentencesB = segmentSentences(charsB, locale);
267
+ var charsA = buildCharsByOffset(blockA.node);
268
+ var sentencesA = segmentSentences(charsA, locale);
269
+
270
+ // Block content starts one position after the block's outer start (open token).
271
+ var contentStartB = blockB.from + 1;
272
+
273
+ // Map each change to the sentence indices (new-doc offset space) it overlaps.
274
+ var changedSentenceIdx = new Set();
275
+ var perSentenceChanges = new Map();
276
+ var _iterator2 = _createForOfIteratorHelper(changes),
277
+ _step2;
278
+ try {
279
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
280
+ var change = _step2.value;
281
+ var fromOff = change.fromB - contentStartB;
282
+ var toOff = change.toB - contentStartB;
283
+ for (var s = 0; s < sentencesB.length; s++) {
284
+ var sentence = sentencesB[s];
285
+ if (rangesOverlap(fromOff, Math.max(toOff, fromOff + 1), sentence.from, sentence.to)) {
286
+ changedSentenceIdx.add(s);
287
+ var list = perSentenceChanges.get(s);
288
+ if (!list) {
289
+ list = [];
290
+ perSentenceChanges.set(s, list);
291
+ }
292
+ list.push(change);
293
+ }
294
+ }
295
+ }
296
+
297
+ // Level 2: paragraph promotion.
298
+ } catch (err) {
299
+ _iterator2.e(err);
300
+ } finally {
301
+ _iterator2.f();
302
+ }
303
+ var sentenceDenom = Math.max(sentencesA.length, sentencesB.length, 1);
304
+ var sentencesChanged = changedSentenceIdx.size;
305
+ if (sentencesChanged >= thresholds.paragraph.minChanged && sentencesChanged / sentenceDenom >= thresholds.paragraph.ratio) {
306
+ return [makePromotedChange(blockA.from, blockA.to, blockB.from, blockB.to, 'paragraph')];
307
+ }
308
+
309
+ // Level 1: per-sentence promotion (else keep inline changes).
310
+ var contentStartA = blockA.from + 1;
311
+ var out = [];
312
+ var _iterator3 = _createForOfIteratorHelper(perSentenceChanges.entries()),
313
+ _step3;
314
+ try {
315
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
316
+ var _step3$value = _slicedToArray(_step3.value, 2),
317
+ sIdx = _step3$value[0],
318
+ sentenceChanges = _step3$value[1];
319
+ var _sentence = sentencesB[sIdx];
320
+ var wordsNew = countWords(charsB, _sentence, locale);
321
+ var sentA = sentencesA[sIdx];
322
+ var wordsOld = sentA ? countWords(charsA, sentA, locale) : 0;
323
+ var wordSpans = segmentWordSpans(charsB, _sentence, locale);
324
+ var wordsChanged = 0;
325
+ var _iterator4 = _createForOfIteratorHelper(wordSpans),
326
+ _step4;
327
+ try {
328
+ var _loop = function _loop() {
329
+ var w = _step4.value;
330
+ var overlaps = sentenceChanges.some(function (c) {
331
+ return rangesOverlap(c.fromB - contentStartB, Math.max(c.toB - contentStartB, c.fromB - contentStartB + 1), w.from, w.to);
332
+ });
333
+ if (overlaps) {
334
+ wordsChanged++;
335
+ }
336
+ };
337
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
338
+ _loop();
339
+ }
340
+ } catch (err) {
341
+ _iterator4.e(err);
342
+ } finally {
343
+ _iterator4.f();
344
+ }
345
+ var wordDenom = Math.max(wordsOld, wordsNew, 1);
346
+ if (wordsChanged >= thresholds.sentence.minChanged && wordsChanged / wordDenom >= thresholds.sentence.ratio) {
347
+ var fromB = contentStartB + _sentence.from;
348
+ var toB = contentStartB + _sentence.to;
349
+ var fromA = sentA ? contentStartA + sentA.from : sentenceChanges[0].fromA;
350
+ var toA = sentA ? contentStartA + sentA.to : sentenceChanges[0].toA;
351
+ out.push(makePromotedChange(fromA, toA, fromB, toB, 'sentence'));
352
+ } else {
353
+ out.push.apply(out, _toConsumableArray(sentenceChanges));
354
+ }
355
+ }
356
+ } catch (err) {
357
+ _iterator3.e(err);
358
+ } finally {
359
+ _iterator3.f();
360
+ }
361
+ return out;
362
+ };
363
+
364
+ /** A resolved direct child of a container, with its outer bounds and index. */
365
+
366
+ /** Resolve the direct children of a container node (whose OUTER start is `blockFrom`). */
367
+ var childRefs = function childRefs(block) {
368
+ var refs = [];
369
+ var offset = block.from + 1; // content starts after the container's open token
370
+ block.node.forEach(function (child, _, index) {
371
+ refs.push({
372
+ node: child,
373
+ from: offset,
374
+ to: offset + child.nodeSize,
375
+ index: index
376
+ });
377
+ offset += child.nodeSize;
378
+ });
379
+ return refs;
380
+ };
381
+
382
+ /** Which direct-child indices of `block` are touched by any of `changes` (new-doc coords). */
383
+ var changedChildIndices = function changedChildIndices(block, children, changes) {
384
+ var changed = new Set();
385
+ var _iterator5 = _createForOfIteratorHelper(children),
386
+ _step5;
387
+ try {
388
+ var _loop2 = function _loop2() {
389
+ var child = _step5.value;
390
+ var touched = changes.some(function (c) {
391
+ return rangesOverlap(c.fromB, Math.max(c.toB, c.fromB + 1), child.from, child.to);
392
+ });
393
+ if (touched) {
394
+ changed.add(child.index);
395
+ }
396
+ };
397
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
398
+ _loop2();
399
+ }
400
+ } catch (err) {
401
+ _iterator5.e(err);
402
+ } finally {
403
+ _iterator5.f();
404
+ }
405
+ return changed;
406
+ };
407
+
408
+ /**
409
+ * A single entry in an aligned child list: a matched A/B pair, a pure insertion (a=null), or a
410
+ * pure deletion (b=null).
411
+ */
412
+
413
+ /**
414
+ * Align a container's A-side and B-side direct children via an LCS over their serialized
415
+ * content. This is essential because index alignment (`childrenA[childB.index]`) breaks the
416
+ * moment a child is inserted or removed: every child after the insertion/deletion point would
417
+ * be mis-paired, causing added items to be classified against the wrong (or a B-side) original
418
+ * child — which corrupts the A-side coordinates and makes one list's deletions surface under a
419
+ * different block. The LCS pairs identical children as "matched" (unchanged, skipped later),
420
+ * leaving genuinely added children as B-only and removed children as A-only.
421
+ */
422
+ var alignChildren = function alignChildren(childrenA, childrenB) {
423
+ var keyA = childrenA.map(function (c) {
424
+ return JSON.stringify(c.node.toJSON());
425
+ });
426
+ var keyB = childrenB.map(function (c) {
427
+ return JSON.stringify(c.node.toJSON());
428
+ });
429
+ var n = childrenA.length;
430
+ var m = childrenB.length;
431
+ // LCS length table.
432
+ var lcs = Array.from({
433
+ length: n + 1
434
+ }, function () {
435
+ return Array.from({
436
+ length: m + 1
437
+ }, function () {
438
+ return 0;
439
+ });
440
+ });
441
+ for (var _i4 = n - 1; _i4 >= 0; _i4--) {
442
+ for (var _j = m - 1; _j >= 0; _j--) {
443
+ lcs[_i4][_j] = keyA[_i4] === keyB[_j] ? lcs[_i4 + 1][_j + 1] + 1 : Math.max(lcs[_i4 + 1][_j], lcs[_i4][_j + 1]);
444
+ }
445
+ }
446
+ // Backtrack. Identical children become matched anchors. Runs of non-identical children
447
+ // between anchors are "zipped" positionally into modified pairs (a & b), with any leftover
448
+ // B children as pure insertions and leftover A children as pure deletions. Zipping avoids
449
+ // treating a MODIFIED child (whose content merely differs) as a delete+insert — that pairing
450
+ // lets the recursion diff inside the child (inline/sentence) instead of replacing it whole.
451
+ var out = [];
452
+ var i = 0;
453
+ var j = 0;
454
+ // Pending runs of unmatched children on each side, flushed (zipped) at each anchor / at end.
455
+ var runA = [];
456
+ var runB = [];
457
+ var flushRuns = function flushRuns() {
458
+ var shared = Math.min(runA.length, runB.length);
459
+ for (var k = 0; k < shared; k++) {
460
+ out.push({
461
+ a: runA[k],
462
+ b: runB[k]
463
+ });
464
+ }
465
+ for (var _k = shared; _k < runA.length; _k++) {
466
+ out.push({
467
+ a: runA[_k],
468
+ b: null
469
+ });
470
+ }
471
+ for (var _k2 = shared; _k2 < runB.length; _k2++) {
472
+ out.push({
473
+ a: null,
474
+ b: runB[_k2]
475
+ });
476
+ }
477
+ runA = [];
478
+ runB = [];
479
+ };
480
+ while (i < n && j < m) {
481
+ if (keyA[i] === keyB[j]) {
482
+ flushRuns();
483
+ out.push({
484
+ a: childrenA[i],
485
+ b: childrenB[j]
486
+ });
487
+ i++;
488
+ j++;
489
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
490
+ runA.push(childrenA[i]);
491
+ i++;
492
+ } else {
493
+ runB.push(childrenB[j]);
494
+ j++;
495
+ }
496
+ }
497
+ while (i < n) {
498
+ runA.push(childrenA[i]);
499
+ i++;
500
+ }
501
+ while (j < m) {
502
+ runB.push(childrenB[j]);
503
+ j++;
504
+ }
505
+ flushRuns();
506
+ return out;
507
+ };
508
+
509
+ /**
510
+ * Container classification with the rigid-child escalation rules:
511
+ * - list: replace whole list if changed items / items ≥ node.ratio, else recurse
512
+ * into each changed listItem's content.
513
+ * - table: replace whole table if changed cells / cells ≥ node.ratio; else, for each
514
+ * changed ROW, replace the row if changed cells / row-cells ≥ node.ratio,
515
+ * else recurse into each changed cell's content.
516
+ * - layout: replace whole section if changed columns / columns ≥ node.ratio, else
517
+ * recurse into each changed column's content.
518
+ * - generic (panel/expand/quote/...): replace whole block if changed children / children ≥
519
+ * node.ratio, else recurse into each changed child.
520
+ */
521
+ var classifyContainer = function classifyContainer(blockA, blockB, changes, originalDoc, newDoc, locale, thresholds) {
522
+ var typeName = blockB.node.type.name;
523
+
524
+ // Tables need cell-level counting across rows; handle them specially.
525
+ if (typeName === TABLE_TYPE) {
526
+ return classifyTable(blockA, blockB, changes, originalDoc, newDoc, locale, thresholds);
527
+ }
528
+ var childrenB = childRefs(blockB);
529
+ var childrenA = childRefs(blockA);
530
+ var changed = changedChildIndices(blockB, childrenB, changes);
531
+ var denom = Math.max(childrenA.length, childrenB.length, 1);
532
+ if (changed.size / denom >= thresholds.node.ratio) {
533
+ return [wholeBlockChange(blockA, blockB, changes)];
534
+ }
535
+
536
+ // Below threshold → recurse per child, using an LCS alignment so inserted/removed children
537
+ // do not mis-pair (which previously corrupted A-side coordinates and leaked one block's
538
+ // deletions into another). Each alignment entry is one of:
539
+ // - matched (a & b): recurse to classify any intra-child changes (skipped if identical);
540
+ // - added (b only): a pure insertion, with a zero-width A anchor near its position;
541
+ // - removed (a only): a pure deletion, with a zero-width B anchor near its position.
542
+ var alignment = alignChildren(childrenA, childrenB);
543
+ var out = [];
544
+ // Running A/B anchors from the last matched pair, so pure insert/delete get sensible
545
+ // zero-width coordinates on the opposite side.
546
+ var lastMatchedAEnd = blockA.from + 1;
547
+ var lastMatchedBEnd = blockB.from + 1;
548
+ var _iterator6 = _createForOfIteratorHelper(alignment),
549
+ _step6;
550
+ try {
551
+ var _loop3 = function _loop3() {
552
+ var _step6$value = _step6.value,
553
+ a = _step6$value.a,
554
+ b = _step6$value.b;
555
+ if (a && b) {
556
+ lastMatchedAEnd = a.to;
557
+ lastMatchedBEnd = b.to;
558
+ // Identical content is left as-is by the LCS; if a change still overlaps this pair
559
+ // (e.g. marks), recurse to classify it.
560
+ var childChanges = changes.filter(function (c) {
561
+ return rangesOverlap(c.fromB, Math.max(c.toB, c.fromB + 1), b.from, b.to);
562
+ });
563
+ if (childChanges.length === 0) {
564
+ return 0; // continue
565
+ }
566
+ out.push.apply(out, _toConsumableArray(_classifyChild(a, b, childChanges, originalDoc, newDoc, locale, thresholds)));
567
+ return 0; // continue
568
+ }
569
+ if (b && !a) {
570
+ // Added child: pure insertion. Anchor the (empty) A side at the last matched A end.
571
+ out.push(makePromotedChange(lastMatchedAEnd, lastMatchedAEnd, b.from, b.to, 'node'));
572
+ lastMatchedBEnd = b.to;
573
+ return 0; // continue
574
+ }
575
+ if (a && !b) {
576
+ // Removed child: pure deletion. Anchor the (empty) B side at the last matched B end.
577
+ out.push(makePromotedChange(a.from, a.to, lastMatchedBEnd, lastMatchedBEnd, 'node'));
578
+ lastMatchedAEnd = a.to;
579
+ }
580
+ },
581
+ _ret;
582
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
583
+ _ret = _loop3();
584
+ if (_ret === 0) continue;
585
+ }
586
+ } catch (err) {
587
+ _iterator6.e(err);
588
+ } finally {
589
+ _iterator6.f();
590
+ }
591
+ return out;
592
+ };
593
+
594
+ /**
595
+ * Classify a table: replace the whole table when changed-cells/total-cells ≥ node.ratio;
596
+ * otherwise for each changed row, replace the row when its own changed-cells/row-cells ≥
597
+ * node.ratio, else recurse into each changed cell.
598
+ */
599
+ var classifyTable = function classifyTable(blockA, blockB, changes, originalDoc, newDoc, locale, thresholds) {
600
+ var rowsB = childRefs(blockB);
601
+ var rowsA = childRefs(blockA);
602
+
603
+ // Total cell counts across the whole table.
604
+ var totalCellsB = 0;
605
+ var changedCellsB = 0;
606
+ var perRowChangedCells = new Map();
607
+ var _iterator7 = _createForOfIteratorHelper(rowsB),
608
+ _step7;
609
+ try {
610
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
611
+ var row = _step7.value;
612
+ var cells = childRefs(row);
613
+ totalCellsB += cells.length;
614
+ var changedCells = changedChildIndices(row, cells, changes);
615
+ if (changedCells.size > 0) {
616
+ perRowChangedCells.set(row.index, changedCells);
617
+ changedCellsB += changedCells.size;
618
+ }
619
+ }
620
+ } catch (err) {
621
+ _iterator7.e(err);
622
+ } finally {
623
+ _iterator7.f();
624
+ }
625
+ var totalCellsA = 0;
626
+ var _iterator8 = _createForOfIteratorHelper(rowsA),
627
+ _step8;
628
+ try {
629
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
630
+ var _row = _step8.value;
631
+ totalCellsA += _row.node.childCount;
632
+ }
633
+ } catch (err) {
634
+ _iterator8.e(err);
635
+ } finally {
636
+ _iterator8.f();
637
+ }
638
+ var cellDenom = Math.max(totalCellsA, totalCellsB, 1);
639
+
640
+ // Whole-table replacement.
641
+ if (changedCellsB / cellDenom >= thresholds.node.ratio) {
642
+ return [wholeBlockChange(blockA, blockB, changes)];
643
+ }
644
+ var out = [];
645
+ // LCS-align rows (mirrors classifyContainer) so an inserted/deleted row does not mis-pair
646
+ // every subsequent row by index (which would corrupt the row-level A coordinates).
647
+ var rowAlignment = alignChildren(rowsA, rowsB);
648
+ var lastMatchedRowAEnd = blockA.from + 1;
649
+ var lastMatchedRowBEnd = blockB.from + 1;
650
+ var _iterator9 = _createForOfIteratorHelper(rowAlignment),
651
+ _step9;
652
+ try {
653
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
654
+ var _step9$value = _step9.value,
655
+ rowA = _step9$value.a,
656
+ rowB = _step9$value.b;
657
+ // Added row: pure insertion (only if it actually carries changes).
658
+ if (rowB && !rowA) {
659
+ var _perRowChangedCells$g, _perRowChangedCells$g2;
660
+ if (((_perRowChangedCells$g = (_perRowChangedCells$g2 = perRowChangedCells.get(rowB.index)) === null || _perRowChangedCells$g2 === void 0 ? void 0 : _perRowChangedCells$g2.size) !== null && _perRowChangedCells$g !== void 0 ? _perRowChangedCells$g : 0) > 0) {
661
+ out.push(makePromotedChange(lastMatchedRowAEnd, lastMatchedRowAEnd, rowB.from, rowB.to, 'node'));
662
+ }
663
+ lastMatchedRowBEnd = rowB.to;
664
+ continue;
665
+ }
666
+ // Removed row: pure deletion.
667
+ if (rowA && !rowB) {
668
+ out.push(makePromotedChange(rowA.from, rowA.to, lastMatchedRowBEnd, lastMatchedRowBEnd, 'node'));
669
+ lastMatchedRowAEnd = rowA.to;
670
+ continue;
671
+ }
672
+ if (!rowA || !rowB) {
673
+ continue;
674
+ }
675
+ lastMatchedRowAEnd = rowA.to;
676
+ lastMatchedRowBEnd = rowB.to;
677
+ var _changedCells = perRowChangedCells.get(rowB.index);
678
+ if (!_changedCells || _changedCells.size === 0) {
679
+ continue;
680
+ }
681
+ var cellsB = childRefs(rowB);
682
+ // Row-level replacement.
683
+ if (_changedCells.size / Math.max(rowA.node.childCount, cellsB.length, 1) >= thresholds.node.ratio) {
684
+ out.push(makePromotedChange(rowA.from, rowA.to, rowB.from, rowB.to, 'node'));
685
+ continue;
686
+ }
687
+ // Else recurse into each changed cell's content (cell is never replaced alone). Cells are
688
+ // positionally aligned within a matched row (table columns are fixed, so a cell at index i
689
+ // on the B side corresponds to index i on the A side).
690
+ var cellsA = childRefs(rowA);
691
+ var _iterator0 = _createForOfIteratorHelper(cellsB),
692
+ _step0;
693
+ try {
694
+ var _loop4 = function _loop4() {
695
+ var _cellsA$cellB$index;
696
+ var cellB = _step0.value;
697
+ if (!_changedCells.has(cellB.index)) {
698
+ return 1; // continue
699
+ }
700
+ var cellA = (_cellsA$cellB$index = cellsA[cellB.index]) !== null && _cellsA$cellB$index !== void 0 ? _cellsA$cellB$index : null;
701
+ var cellChanges = changes.filter(function (c) {
702
+ return rangesOverlap(c.fromB, Math.max(c.toB, c.fromB + 1), cellB.from, cellB.to);
703
+ });
704
+ out.push.apply(out, _toConsumableArray(_classifyChild(cellA, cellB, cellChanges, originalDoc, newDoc, locale, thresholds)));
705
+ };
706
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
707
+ if (_loop4()) continue;
708
+ }
709
+ } catch (err) {
710
+ _iterator0.e(err);
711
+ } finally {
712
+ _iterator0.f();
713
+ }
714
+ }
715
+ } catch (err) {
716
+ _iterator9.e(err);
717
+ } finally {
718
+ _iterator9.f();
719
+ }
720
+ return out;
721
+ };
722
+
723
+ /**
724
+ * Recurse into a rigid/structural child (listItem, layoutColumn, tableCell/tableHeader) or a
725
+ * plain child block. Rigid children are containers of blocks: we recurse into THEIR children
726
+ * (paragraphs, nested lists, …) so we never delete+replace the rigid child itself. A plain
727
+ * text-bearing or container child is classified directly.
728
+ */
729
+ var _classifyChild = function classifyChild(childA, childB, changes, originalDoc, newDoc, locale, thresholds) {
730
+ if (changes.length === 0) {
731
+ return [];
732
+ }
733
+ var typeName = childB.node.type.name;
734
+ var asBlock = function asBlock(ref) {
735
+ return ref ? {
736
+ node: ref.node,
737
+ from: ref.from,
738
+ to: ref.to
739
+ } : null;
740
+ };
741
+
742
+ // Structurally-rigid wrapper (layoutColumn / tableCell / tableHeader): the wrapper itself is
743
+ // NEVER a whole-block result — deleting+re-inserting a single column or cell would break the
744
+ // parent layout/table structure. We only reach here because the parent already decided NOT
745
+ // to promote wholesale, so we bypass `classifyContainer` (which could promote the whole
746
+ // wrapper when it is internally dense) and classify EACH of the wrapper's direct children on
747
+ // its own. This keeps the diff strictly inside the wrapper (inline / sentence / paragraph,
748
+ // or a nested container such as a list inside a table cell).
749
+ if (RIGID_CHILD_TYPES.has(typeName)) {
750
+ var blockB = asBlock(childB);
751
+ if (!blockB) {
752
+ return [];
753
+ }
754
+ var wrapperA = asBlock(childA);
755
+ var childrenB = childRefs(blockB);
756
+ var childrenA = wrapperA ? childRefs(wrapperA) : [];
757
+ var out = [];
758
+ // LCS-align inner children (mirrors classifyContainer) so a paragraph inserted/deleted
759
+ // inside the cell/column does not mis-pair every subsequent inner child by index. We never
760
+ // promote the wrapper itself here — we only classify each inner child.
761
+ var alignment = alignChildren(childrenA, childrenB);
762
+ var lastMatchedAEnd = wrapperA ? wrapperA.from + 1 : blockB.from + 1;
763
+ var lastMatchedBEnd = blockB.from + 1;
764
+ var _iterator1 = _createForOfIteratorHelper(alignment),
765
+ _step1;
766
+ try {
767
+ var _loop5 = function _loop5() {
768
+ var _step1$value = _step1.value,
769
+ a = _step1$value.a,
770
+ b = _step1$value.b;
771
+ if (a && b) {
772
+ lastMatchedAEnd = a.to;
773
+ lastMatchedBEnd = b.to;
774
+ var innerChanges = changes.filter(function (c) {
775
+ return rangesOverlap(c.fromB, Math.max(c.toB, c.fromB + 1), b.from, b.to);
776
+ });
777
+ if (innerChanges.length === 0) {
778
+ return 0; // continue
779
+ }
780
+ out.push.apply(out, _toConsumableArray(_classifyChild(a, b, innerChanges, originalDoc, newDoc, locale, thresholds)));
781
+ return 0; // continue
782
+ }
783
+ if (b && !a) {
784
+ // Added inner child: pure insertion, zero-width A anchor at the last matched A end.
785
+ out.push(makePromotedChange(lastMatchedAEnd, lastMatchedAEnd, b.from, b.to, 'node'));
786
+ lastMatchedBEnd = b.to;
787
+ return 0; // continue
788
+ }
789
+ if (a && !b) {
790
+ // Removed inner child: pure deletion, zero-width B anchor at the last matched B end.
791
+ out.push(makePromotedChange(a.from, a.to, lastMatchedBEnd, lastMatchedBEnd, 'node'));
792
+ lastMatchedAEnd = a.to;
793
+ }
794
+ },
795
+ _ret2;
796
+ for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
797
+ _ret2 = _loop5();
798
+ if (_ret2 === 0) continue;
799
+ }
800
+ } catch (err) {
801
+ _iterator1.e(err);
802
+ } finally {
803
+ _iterator1.f();
804
+ }
805
+ return out;
806
+ }
807
+
808
+ // A `listItem` CAN be replaced wholesale (delete + re-insert at the same position does not
809
+ // break the list's structure), so per the spec it uses the normal container rule: promote
810
+ // the whole item when its own content is dense enough, else recurse into its children.
811
+ if (typeName === 'listItem') {
812
+ var _blockB = asBlock(childB);
813
+ var blockAItem = asBlock(childA);
814
+ // Pure deletion of the item: emit the whole A-side item as deleted (zero-width B anchor at
815
+ // the item's own B-less position — anchored at the A start for lack of parent context).
816
+ if (!_blockB) {
817
+ return blockAItem ? [makePromotedChange(blockAItem.from, blockAItem.to, blockAItem.from, blockAItem.from, 'node')] : [];
818
+ }
819
+ // Pure insertion of the item: emit the whole B-side item as inserted with a ZERO-WIDTH A
820
+ // anchor. (Using `blockB` as the A side would make the LCS compare the item against itself
821
+ // and drop the insertion.)
822
+ if (!blockAItem) {
823
+ return [makePromotedChange(_blockB.from, _blockB.from, _blockB.from, _blockB.to, 'node')];
824
+ }
825
+ return classifyContainer(blockAItem, _blockB, changes, originalDoc, newDoc, locale, thresholds);
826
+ }
827
+
828
+ // Plain child block: classify as its own group (text or nested container).
829
+ return classifyBlockGroup({
830
+ blockA: asBlock(childA),
831
+ blockB: asBlock(childB),
832
+ changes: changes
833
+ }, originalDoc, newDoc, locale, thresholds);
834
+ };