@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
128 lines (127 loc) • 5.31 kB
JavaScript
import { simplifyChanges, ChangeSet } from 'prosemirror-changeset';
import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
import { optimizeChanges } from './optimizeChanges';
const mapPosition = (mapping, pos) => mapping.map(pos);
const createMapping = maps => {
const mapping = new Mapping();
for (const map of maps) {
mapping.appendMap(map);
}
return mapping;
};
const createSpans = length => length > 0 ? [{
length,
data: null
}] : [];
const mergeOverlappingByNewDocRange = changes => {
if (changes.length <= 1) {
return changes;
}
const sortedChanges = [...changes].sort((left, right) => left.fromB - right.fromB);
const merged = [];
let current = {
...sortedChanges[0]
};
for (let i = 1; i < sortedChanges.length; i++) {
const next = sortedChanges[i];
const isOverlapping = next.fromB <= current.toB;
if (isOverlapping) {
current = {
fromA: Math.min(current.fromA, next.fromA),
toA: Math.max(current.toA, next.toA),
fromB: Math.min(current.fromB, next.fromB),
toB: Math.max(current.toB, next.toB),
deleted: [...current.deleted, ...next.deleted],
inserted: [...current.inserted, ...next.inserted]
};
} else {
merged.push(current);
current = {
...next
};
}
}
merged.push(current);
return merged;
};
const isReplaceStepForTextBlockNode = (step, before, from, to) => {
var _replacedSlice$conten, _replacingSlice$conte, _replacedSlice$conten2;
if (!(step instanceof ReplaceStep)) {
return false;
}
if (step.slice.openStart !== 0 || step.slice.openEnd !== 0) {
return false;
}
const replacedSlice = before.slice(from, to);
const replacingSlice = step.slice;
return Boolean(replacedSlice.openStart === 0 && replacedSlice.openEnd === 0 && replacedSlice.content.childCount === 1 && replacingSlice.content.childCount === 1 && ((_replacedSlice$conten = replacedSlice.content.firstChild) === null || _replacedSlice$conten === void 0 ? void 0 : _replacedSlice$conten.type.name) === ((_replacingSlice$conte = replacingSlice.content.firstChild) === null || _replacingSlice$conte === void 0 ? void 0 : _replacingSlice$conte.type.name) && ((_replacedSlice$conten2 = replacedSlice.content.firstChild) === null || _replacedSlice$conten2 === void 0 ? void 0 : _replacedSlice$conten2.type.isTextblock));
};
export const diffBySteps = (originalDoc, steps) => {
const changes = [];
let currentDoc = originalDoc;
const successfulStepMaps = [];
const rangedSteps = [];
for (const step of steps) {
const before = currentDoc;
const result = step.apply(currentDoc);
if (result.failed !== null || !result.doc) {
continue;
}
const stepMap = step.getMap();
const rangeStep = step;
if (typeof rangeStep.from === 'number' && typeof rangeStep.to === 'number') {
rangedSteps.push({
before,
doc: result.doc,
from: rangeStep.from,
to: rangeStep.to,
mapIndex: successfulStepMaps.length,
step,
stepMap
});
}
successfulStepMaps.push(stepMap);
currentDoc = result.doc;
}
for (const rangedStep of rangedSteps) {
// Mapping from original -> doc before this step.
const originalToBeforeStep = createMapping(successfulStepMaps.slice(0, rangedStep.mapIndex));
const beforeStepToOriginal = originalToBeforeStep.invert();
const fromA = mapPosition(beforeStepToOriginal, rangedStep.from);
const toA = mapPosition(beforeStepToOriginal, rangedStep.to);
// Map the step range into final steppedDoc coordinates.
const fromAfterStep = rangedStep.stepMap.map(rangedStep.from, -1);
const toAfterStep = rangedStep.stepMap.map(rangedStep.to, 1);
const afterStepToFinal = createMapping(successfulStepMaps.slice(rangedStep.mapIndex + 1));
const fromB = mapPosition(afterStepToFinal, fromAfterStep);
const toB = mapPosition(afterStepToFinal, toAfterStep);
if (isReplaceStepForTextBlockNode(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
const granularStepChanges = ChangeSet.create(rangedStep.before).addSteps(rangedStep.doc, [rangedStep.stepMap], null);
const optimizedGranularStepChanges = optimizeChanges(simplifyChanges(granularStepChanges.changes, granularStepChanges.startDoc));
for (const granularChange of optimizedGranularStepChanges) {
const granularFromA = mapPosition(beforeStepToOriginal, granularChange.fromA);
const granularToA = mapPosition(beforeStepToOriginal, granularChange.toA);
const granularFromB = mapPosition(afterStepToFinal, granularChange.fromB);
const granularToB = mapPosition(afterStepToFinal, granularChange.toB);
changes.push({
fromA: granularFromA,
toA: granularToA,
fromB: granularFromB,
toB: granularToB,
deleted: createSpans(Math.max(0, granularToA - granularFromA)),
inserted: createSpans(Math.max(0, granularToB - granularFromB))
});
}
continue;
}
changes.push({
fromA,
toA,
fromB,
toB,
deleted: createSpans(Math.max(0, toA - fromA)),
inserted: createSpans(Math.max(0, toB - fromB))
});
}
return mergeOverlappingByNewDocRange(changes);
};