@atlaskit/editor-plugin-show-diff 10.1.23 → 10.3.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.
- package/CHANGELOG.md +63 -0
- package/calculate-diff/package.json +8 -0
- package/dist/cjs/entry-points/calculate-diff.js +12 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +132 -31
- package/dist/cjs/pm-plugins/calculateDiff/computeDiffChanges.js +112 -0
- package/dist/cjs/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -2
- package/dist/cjs/pm-plugins/calculateDiff/smart/classifySmartChanges.js +841 -0
- package/dist/cjs/pm-plugins/calculateDiff/smart/helpers.js +135 -0
- package/dist/cjs/pm-plugins/calculateDiff/smart/segmentText.js +331 -0
- package/dist/cjs/pm-plugins/calculateDiff/smart/thresholds.js +46 -0
- package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
- package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
- package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +30 -17
- package/dist/cjs/pm-plugins/decorations/decorationKeys.js +8 -5
- package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -30
- package/dist/cjs/pm-plugins/getScrollableDecorations.js +6 -6
- package/dist/cjs/pm-plugins/isExtendedEnabled.js +20 -0
- package/dist/cjs/pm-plugins/main.js +21 -16
- package/dist/es2019/entry-points/calculate-diff.js +2 -0
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +119 -29
- package/dist/es2019/pm-plugins/calculateDiff/computeDiffChanges.js +94 -0
- package/dist/es2019/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -3
- package/dist/es2019/pm-plugins/calculateDiff/smart/classifySmartChanges.js +689 -0
- package/dist/es2019/pm-plugins/calculateDiff/smart/helpers.js +106 -0
- package/dist/es2019/pm-plugins/calculateDiff/smart/segmentText.js +283 -0
- package/dist/es2019/pm-plugins/calculateDiff/smart/thresholds.js +45 -0
- package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
- package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
- package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +31 -16
- package/dist/es2019/pm-plugins/decorations/decorationKeys.js +9 -5
- package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -34
- package/dist/es2019/pm-plugins/getScrollableDecorations.js +6 -6
- package/dist/es2019/pm-plugins/isExtendedEnabled.js +12 -0
- package/dist/es2019/pm-plugins/main.js +21 -16
- package/dist/esm/entry-points/calculate-diff.js +2 -0
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +132 -31
- package/dist/esm/pm-plugins/calculateDiff/computeDiffChanges.js +107 -0
- package/dist/esm/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -3
- package/dist/esm/pm-plugins/calculateDiff/smart/classifySmartChanges.js +834 -0
- package/dist/esm/pm-plugins/calculateDiff/smart/helpers.js +128 -0
- package/dist/esm/pm-plugins/calculateDiff/smart/segmentText.js +325 -0
- package/dist/esm/pm-plugins/calculateDiff/smart/thresholds.js +39 -0
- package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
- package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
- package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +30 -17
- package/dist/esm/pm-plugins/decorations/decorationKeys.js +9 -5
- package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -30
- package/dist/esm/pm-plugins/getScrollableDecorations.js +6 -6
- package/dist/esm/pm-plugins/isExtendedEnabled.js +14 -0
- package/dist/esm/pm-plugins/main.js +21 -16
- package/dist/types/entry-points/calculate-diff.d.ts +2 -0
- package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
- package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +3 -1
- package/dist/types/pm-plugins/calculateDiff/computeDiffChanges.d.ts +46 -0
- package/dist/types/pm-plugins/calculateDiff/groupChangesByBlock.d.ts +9 -0
- package/dist/types/pm-plugins/calculateDiff/smart/classifySmartChanges.d.ts +26 -0
- package/dist/types/pm-plugins/calculateDiff/smart/helpers.d.ts +44 -0
- package/dist/types/pm-plugins/calculateDiff/smart/segmentText.d.ts +32 -0
- package/dist/types/pm-plugins/calculateDiff/smart/thresholds.d.ts +44 -0
- package/dist/types/pm-plugins/decorations/createBlockChangedDecoration.d.ts +3 -2
- package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +3 -2
- package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +3 -2
- package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +4 -2
- package/dist/types/pm-plugins/decorations/decorationKeys.d.ts +5 -3
- package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +6 -4
- package/dist/types/pm-plugins/getScrollableDecorations.d.ts +2 -1
- package/dist/types/pm-plugins/isExtendedEnabled.d.ts +11 -0
- package/dist/types/pm-plugins/main.d.ts +11 -1
- package/dist/types/showDiffPluginType.d.ts +20 -1
- package/docs/smart-diff-design.md +240 -0
- package/package.json +8 -3
|
@@ -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
|
+
};
|