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