@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
375 lines (362 loc) • 16.4 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
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; } } }; }
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; } }
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; }
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; }
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; }
import { simplifyChanges } from 'prosemirror-changeset';
import { getAttributionKey } from '../decorations/colorSchemes/attributions';
import { optimizeChanges } from './optimizeChanges';
/**
* A change, plus the block nodes it owns when it is one piece of a split — a piece's own range no
* longer covers the container it opened. Absent on an unsplit change, which decorates the blocks
* inside its own range as before.
*/
var getAttributionIdentity = function getAttributionIdentity(change) {
var identities = new Set();
for (var _i = 0, _arr = [].concat(_toConsumableArray(change.deleted), _toConsumableArray(change.inserted)); _i < _arr.length; _i++) {
var span = _arr[_i];
identities.add(getAttributionKey(span.data));
}
if (identities.size > 1) {
return {
key: undefined,
mergeable: false
};
}
return {
key: identities.values().next().value,
mergeable: true
};
};
var hasContributor = function hasContributor(change) {
return [].concat(_toConsumableArray(change.deleted), _toConsumableArray(change.inserted)).some(function (span) {
return getAttributionKey(span.data) !== undefined;
});
};
var byNewDocRange = function byNewDocRange(left, right) {
return left.fromB - right.fromB || left.toB - right.toB;
};
/** Union of the given ranges, with overlapping and touching ones merged. */
var mergeRanges = function mergeRanges(ranges) {
return _toConsumableArray(ranges).sort(function (left, right) {
return left.from - right.from;
}).reduce(function (merged, range) {
var last = merged[merged.length - 1];
if (last && range.from <= last.to) {
last.to = Math.max(last.to, range.to);
return merged;
}
return [].concat(_toConsumableArray(merged), [_objectSpread({}, range)]);
}, []);
};
/** Whether `inner` sits wholly inside `outer` and covers less of the document. */
var isContained = function isContained(outer, inner) {
return inner.toB > inner.fromB && outer.fromB <= inner.fromB && inner.toB <= outer.toB && inner.toB - inner.fromB < outer.toB - outer.fromB;
};
/**
* The block nodes in `change` whose opening position `ownsPosition` claims. Ownership is by opening
* position so a block another contributor added inside a container is not claimed by whoever opened
* the container. `undefined` for a range this document cannot resolve, which falls the piece back
* to decorating its own range.
*/
var blockNodeRanges = function blockNodeRanges(doc, change, ownsPosition) {
if (!doc || change.from >= change.to || change.to > doc.content.size) {
return undefined;
}
var ranges = [];
doc.nodesBetween(change.from, change.to, function (node, pos) {
if (node.isBlock && ownsPosition(pos) && pos + node.nodeSize <= change.to) {
ranges.push({
from: pos,
to: pos + node.nodeSize
});
}
return true;
});
return ranges;
};
/**
* Splits a change whose spans name more than one contributor into one change per run of consecutive
* spans sharing a key.
*
* `prosemirror-changeset` merges contiguous inserted content into one change while keeping per-span
* data, so a panel inserted by one contributor and edited inside by another arrives as a single
* change holding every span — and the latest-writer rule credited all of it, panel included, to
* whoever wrote last (EDITOR-8932).
*/
var splitMixedIdentityChanges = function splitMixedIdentityChanges(changes, doc) {
return changes.flatMap(function (change) {
var _change$inserted$;
// A replacement can carry an anonymous deletion and an authored insertion. The deleted side
// needs its own zero-width change or the insertion's actor claims the deletion too.
var deletedKeys = new Set(change.deleted.map(function (span) {
return getAttributionKey(span.data);
}));
var firstInsertedKey = getAttributionKey((_change$inserted$ = change.inserted[0]) === null || _change$inserted$ === void 0 ? void 0 : _change$inserted$.data);
var detachDeleted = change.deleted.length > 0 && change.inserted.length > 0 && deletedKeys.size === 1 && !deletedKeys.has(firstInsertedKey) && (deletedKeys.has(undefined) || firstInsertedKey === undefined);
var deletedChange = detachDeleted ? [_objectSpread(_objectSpread({}, change), {}, {
toB: change.fromB,
inserted: []
})] : [];
var contentChange = detachDeleted ? _objectSpread(_objectSpread({}, change), {}, {
fromA: change.toA,
deleted: []
}) : change;
if (getAttributionIdentity(contentChange).mergeable) {
return [].concat(deletedChange, [contentChange]);
}
var runs = [];
var offset = contentChange.fromB;
var _iterator = _createForOfIteratorHelper(contentChange.inserted),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var span = _step.value;
var key = getAttributionKey(span.data);
var last = runs[runs.length - 1];
if (last && last.key === key) {
last.to = offset + span.length;
last.spans.push(span);
} else {
runs.push({
key: key,
from: offset,
to: offset + span.length,
spans: [span]
});
}
offset += span.length;
}
// The offsets above are only positions if the spans partition the inserted range.
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
if (runs.length < 2 || offset !== contentChange.toB) {
return [].concat(deletedChange, [contentChange]);
}
return [].concat(deletedChange, _toConsumableArray(runs.map(function (run, index) {
var owned = blockNodeRanges(doc, {
from: contentChange.fromB,
to: contentChange.toB
}, function (pos) {
return pos >= run.from && pos < run.to;
});
return _objectSpread(_objectSpread({}, contentChange), {}, {
fromA: index === 0 ? contentChange.fromA : contentChange.toA,
fromB: run.from,
toB: run.to,
deleted: index === 0 ? contentChange.deleted : [],
inserted: run.spans
}, owned ? {
blockNodeRangesB: owned
} : {});
})));
});
};
/**
* Subtracts every differently attributed contained change from a change's range, including when
* one has no contributor. Applies when the changes arrive separately rather than as spans of one —
* the `step` diff type. A contained change by the same author, and one that only partially
* overlaps, are left for the collapse below.
*
* The leading fragment keeps the deleted side, so one deletion is not drawn once per fragment.
*/
var splitContainedChanges = function splitContainedChanges(changes, doc) {
return changes.flatMap(function (change) {
var _getAttributionIdenti = getAttributionIdentity(change),
key = _getAttributionIdenti.key,
mergeable = _getAttributionIdenti.mergeable;
var inner = mergeRanges(changes.filter(function (other) {
var otherIdentity = getAttributionIdentity(other);
return other !== change && mergeable && otherIdentity.mergeable && otherIdentity.key !== key && isContained(change, other);
}).map(function (_ref) {
var fromB = _ref.fromB,
toB = _ref.toB;
return {
from: fromB,
to: toB
};
}));
if (inner.length === 0) {
return [change];
}
var gaps = [];
var cursor = change.fromB;
var _iterator2 = _createForOfIteratorHelper(inner),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _step2$value = _step2.value,
from = _step2$value.from,
to = _step2$value.to;
if (from > cursor) {
gaps.push({
from: cursor,
to: from
});
}
cursor = Math.max(cursor, to);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (change.toB > cursor) {
gaps.push({
from: cursor,
to: change.toB
});
}
// Subtracted end to end, so nothing is left to highlight — but the blocks are still owned.
var fragments = gaps.length > 0 ? gaps : [{
from: change.toB,
to: change.toB
}];
// The outer author's: every block in their range that no inner change opened.
var owned = blockNodeRanges(doc, {
from: change.fromB,
to: change.toB
}, function (pos) {
return !inner.some(function (_ref2) {
var from = _ref2.from,
to = _ref2.to;
return pos >= from && pos < to;
});
});
return fragments.map(function (fragment, index) {
return _objectSpread(_objectSpread({}, change), {}, {
fromA: index === 0 ? change.fromA : change.toA,
fromB: fragment.from,
toB: fragment.to,
deleted: index === 0 ? change.deleted : []
}, owned ? {
blockNodeRangesB: index === 0 ? owned : []
} : {});
});
});
};
/**
* The blocks a merged change owns. Both sides must name theirs, or the merge falls back to its own
* range — narrowing it could drop a block altogether.
*/
var mergeBlockNodeRanges = function mergeBlockNodeRanges(left, right) {
return left.blockNodeRangesB && right.blockNodeRangesB ? [].concat(_toConsumableArray(left.blockNodeRangesB), _toConsumableArray(right.blockNodeRangesB)) : undefined;
};
/** Unions overlapping changes, except when only one side names a contributor. */
var collapse = function collapse(changes) {
// A zero-length new-document range paints no inline decoration, so it is kept aside rather than
// folded into a neighbouring insertion.
var emptyRanges = changes.filter(function (change) {
return change.toB <= change.fromB;
});
var collapsed = [];
var _iterator3 = _createForOfIteratorHelper(changes.filter(function (change) {
return change.toB > change.fromB;
}).sort(byNewDocRange)),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var change = _step3.value;
var current = collapsed[collapsed.length - 1];
var currentHasContributor = current && hasContributor(current);
var changeHasContributor = hasContributor(change);
if (current && change.fromB < current.toB && currentHasContributor === changeHasContributor) {
var blockNodeRangesB = mergeBlockNodeRanges(current, change);
collapsed[collapsed.length - 1] = _objectSpread({
fromA: Math.min(current.fromA, change.fromA),
toA: Math.max(current.toA, change.toA),
fromB: Math.min(current.fromB, change.fromB),
toB: Math.max(current.toB, change.toB),
deleted: [].concat(_toConsumableArray(current.deleted), _toConsumableArray(change.deleted)),
inserted: [].concat(_toConsumableArray(current.inserted), _toConsumableArray(change.inserted))
}, blockNodeRangesB ? {
blockNodeRangesB: blockNodeRangesB
} : {});
} else {
collapsed.push(_objectSpread({}, change));
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
return [].concat(collapsed, _toConsumableArray(emptyRanges)).sort(byNewDocRange);
};
/**
* Collapses changes whose new-document ranges strictly overlap. Attribution runs are simplified
* independently, so runs either side of an unattributed step can come back covering the same
* characters — which stacks two inline decorations and renders one contributor tag per copy.
*
* Merging keeps every span from both sides, so the latest-writer rule still decides whose tag is
* shown among attributed edits. An unattributed change never merges with an attributed one.
* Ranges that merely touch are left alone.
*
* Contributors are separated first, by `splitMixedIdentityChanges` and `splitContainedChanges`.
* Both leave ranges that only touch, so they survive the collapse.
*
* Carries no gate of its own: the only caller reaches it through the attributed changeset, which
* already requires `confluence_ncs_step_diffing_version_history`.
*/
export var collapseOverlappingChanges = function collapseOverlappingChanges(changes, doc) {
return collapse(splitContainedChanges(splitMixedIdentityChanges(changes, doc), doc));
};
/**
* Simplifies and optimizes consecutive changes without crossing an attribution boundary.
* A change which already contains more than one identity is deliberately kept isolated.
*/
export var simplifyChangesWithAttribution = function simplifyChangesWithAttribution(changes, doc) {
var result = [];
var run = [];
var runIdentity;
var flush = function flush() {
if (run.length > 0) {
// Some show-diff producers use structurally compatible Change objects rather than the
// library class. `simplifyChanges` reads runtime `lenA`/`lenB` getters even though they
// are intentionally omitted from its public declarations, so add equivalent getters
// without calling the library's internal constructor.
var normalizedRun = run.map(function (change) {
return _objectSpread(_objectSpread({}, change), {}, {
get lenA() {
return change.toA - change.fromA;
},
get lenB() {
return change.toB - change.fromB;
}
});
});
result.push.apply(result, _toConsumableArray(optimizeChanges(simplifyChanges(normalizedRun, doc))));
}
run = [];
runIdentity = undefined;
};
var _iterator4 = _createForOfIteratorHelper(changes),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var _runIdentity;
var change = _step4.value;
var identity = getAttributionIdentity(change);
var canJoinRun = ((_runIdentity = runIdentity) === null || _runIdentity === void 0 ? void 0 : _runIdentity.mergeable) === true && identity.mergeable && runIdentity.key === identity.key;
if (run.length > 0 && !canJoinRun) {
flush();
}
run.push(change);
runIdentity = identity;
if (!identity.mergeable) {
flush();
}
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
flush();
return result;
};