UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

381 lines (368 loc) • 17.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.simplifyChangesWithAttribution = exports.collapseOverlappingChanges = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _prosemirrorChangeset = require("prosemirror-changeset"); var _attributions = require("../decorations/colorSchemes/attributions"); var _optimizeChanges = require("./optimizeChanges"); 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) { (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; } /** * 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((0, _toConsumableArray2.default)(change.deleted), (0, _toConsumableArray2.default)(change.inserted)); _i < _arr.length; _i++) { var span = _arr[_i]; identities.add((0, _attributions.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((0, _toConsumableArray2.default)(change.deleted), (0, _toConsumableArray2.default)(change.inserted)).some(function (span) { return (0, _attributions.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 (0, _toConsumableArray2.default)(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((0, _toConsumableArray2.default)(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 (0, _attributions.getAttributionKey)(span.data); })); var firstInsertedKey = (0, _attributions.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 = (0, _attributions.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, (0, _toConsumableArray2.default)(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((0, _toConsumableArray2.default)(left.blockNodeRangesB), (0, _toConsumableArray2.default)(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((0, _toConsumableArray2.default)(current.deleted), (0, _toConsumableArray2.default)(change.deleted)), inserted: [].concat((0, _toConsumableArray2.default)(current.inserted), (0, _toConsumableArray2.default)(change.inserted)) }, blockNodeRangesB ? { blockNodeRangesB: blockNodeRangesB } : {}); } else { collapsed.push(_objectSpread({}, change)); } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } return [].concat(collapsed, (0, _toConsumableArray2.default)(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`. */ var collapseOverlappingChanges = exports.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. */ var simplifyChangesWithAttribution = exports.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, (0, _toConsumableArray2.default)((0, _optimizeChanges.optimizeChanges)((0, _prosemirrorChangeset.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; };