Stackedit/src/services/diffUtils.js

200 lines
6.5 KiB
JavaScript
Raw Normal View History

2017-08-19 10:24:08 +00:00
import DiffMatchPatch from 'diff-match-patch';
import utils from './utils';
const diffMatchPatch = new DiffMatchPatch();
2017-08-25 10:37:46 +00:00
diffMatchPatch.Match_Distance = 10000;
2017-08-19 10:24:08 +00:00
function makePatchableText(content, markerKeys, markerIdxMap) {
2017-08-25 10:37:46 +00:00
if (!content || !content.discussions) {
return null;
}
2017-08-19 10:24:08 +00:00
const markers = [];
// Sort keys to have predictable marker positions in case of same offset
const discussionKeys = Object.keys(content.discussions).sort();
discussionKeys.forEach((discussionId) => {
const discussion = content.discussions[discussionId];
function addMarker(offsetName) {
const markerKey = discussionId + offsetName;
if (discussion[offsetName] !== undefined) {
let idx = markerIdxMap[markerKey];
if (idx === undefined) {
idx = markerKeys.length;
markerIdxMap[markerKey] = idx;
markerKeys.push({
id: discussionId,
offsetName,
});
}
markers.push({
idx,
offset: discussion[offsetName],
});
}
}
addMarker('start');
addMarker('end');
2017-08-19 10:24:08 +00:00
});
let lastOffset = 0;
let result = '';
markers
.sort((marker1, marker2) => marker1.offset - marker2.offset)
.forEach((marker) => {
result +=
content.text.slice(lastOffset, marker.offset) +
String.fromCharCode(0xe000 + marker.idx); // Use a character from the private use area
lastOffset = marker.offset;
});
return result + content.text.slice(lastOffset);
}
function stripDiscussionOffsets(objectMap) {
2017-08-25 10:37:46 +00:00
if (objectMap == null) {
return objectMap;
}
2017-08-19 10:24:08 +00:00
const result = {};
Object.keys(objectMap).forEach((id) => {
result[id] = {
text: objectMap[id].text,
};
});
return result;
}
function restoreDiscussionOffsets(content, markerKeys) {
if (markerKeys.length) {
// Go through markers
let count = 0;
content.text = content.text.replace(
new RegExp(`[\ue000-${String.fromCharCode((0xe000 + markerKeys.length) - 1)}]`, 'g'),
(match, offset) => {
const idx = match.charCodeAt(0) - 0xe000;
const markerKey = markerKeys[idx];
const discussion = content.discussions[markerKey.id];
if (discussion) {
discussion[markerKey.offsetName] = offset - count;
}
count += 1;
return '';
2018-05-06 00:46:33 +00:00
},
);
// Sanitize offsets
Object.keys(content.discussions).forEach((discussionId) => {
const discussion = content.discussions[discussionId];
if (discussion.start === undefined) {
discussion.start = discussion.end || 0;
}
if (discussion.end === undefined || discussion.end < discussion.start) {
discussion.end = discussion.start;
}
});
}
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
function mergeText(serverText, clientText, lastMergedText) {
const serverClientDiffs = diffMatchPatch.diff_main(serverText, clientText);
diffMatchPatch.diff_cleanupSemantic(serverClientDiffs);
// Fusion text is a mix of both server and client contents
const fusionText = serverClientDiffs.map(diff => diff[1]).join('');
if (!lastMergedText) {
return fusionText;
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
// Let's try to find out what text has to be removed from fusion
const intersectionText = serverClientDiffs
// Keep only equalities
.filter(diff => diff[0] === DiffMatchPatch.DIFF_EQUAL)
.map(diff => diff[1]).join('');
const lastMergedTextDiffs = diffMatchPatch.diff_main(lastMergedText, intersectionText)
// Keep only equalities and deletions
.filter(diff => diff[0] !== DiffMatchPatch.DIFF_INSERT);
diffMatchPatch.diff_cleanupSemantic(lastMergedTextDiffs);
2017-08-25 10:37:46 +00:00
// Make a patch with deletions only
const patches = diffMatchPatch.patch_make(lastMergedText, lastMergedTextDiffs);
// Apply patch to fusion text
return diffMatchPatch.patch_apply(patches, fusionText)[0];
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
function mergeValues(serverValue, clientValue, lastMergedValue) {
if (!lastMergedValue) {
return serverValue || clientValue; // Take the server value in priority
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
const newSerializedValue = utils.serializeObject(clientValue);
const serverSerializedValue = utils.serializeObject(serverValue);
2017-08-19 10:24:08 +00:00
if (newSerializedValue === serverSerializedValue) {
return serverValue; // no conflict
}
2017-08-25 10:37:46 +00:00
const oldSerializedValue = utils.serializeObject(lastMergedValue);
2017-08-19 10:24:08 +00:00
if (oldSerializedValue !== newSerializedValue && !serverValue) {
2017-08-25 10:37:46 +00:00
return clientValue; // Removed on server but changed on client
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
if (oldSerializedValue !== serverSerializedValue && !clientValue) {
2017-08-19 10:24:08 +00:00
return serverValue; // Removed on client but changed on server
}
if (oldSerializedValue !== newSerializedValue && oldSerializedValue === serverSerializedValue) {
2017-08-25 10:37:46 +00:00
return clientValue; // Take the client value
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
return serverValue; // Take the server value
2017-08-19 10:24:08 +00:00
}
2017-08-25 10:37:46 +00:00
function mergeObjects(serverObject, clientObject, lastMergedObject = {}) {
2017-08-19 10:24:08 +00:00
const mergedObject = {};
Object.keys({
2017-08-25 10:37:46 +00:00
...clientObject,
2017-08-19 10:24:08 +00:00
...serverObject,
}).forEach((key) => {
2017-08-25 10:37:46 +00:00
const mergedValue = mergeValues(serverObject[key], clientObject[key], lastMergedObject[key]);
2017-08-19 10:24:08 +00:00
if (mergedValue != null) {
mergedObject[key] = mergedValue;
}
});
return utils.deepCopy(mergedObject);
}
2017-08-25 10:37:46 +00:00
function mergeContent(serverContent, clientContent, lastMergedContent = {}) {
2017-08-19 10:24:08 +00:00
const markerKeys = [];
const markerIdxMap = Object.create(null);
2017-08-25 10:37:46 +00:00
const lastMergedText = makePatchableText(lastMergedContent, markerKeys, markerIdxMap);
2017-08-19 10:24:08 +00:00
const serverText = makePatchableText(serverContent, markerKeys, markerIdxMap);
2017-08-25 10:37:46 +00:00
const clientText = makePatchableText(clientContent, markerKeys, markerIdxMap);
const isServerTextChanges = lastMergedText !== serverText;
2018-01-04 20:19:10 +00:00
const isClientTextChanges = lastMergedText !== clientText;
2017-08-25 10:37:46 +00:00
const isTextSynchronized = serverText === clientText;
2018-01-04 20:19:10 +00:00
let text = clientText;
if (!isTextSynchronized && isServerTextChanges) {
text = serverText;
if (isClientTextChanges) {
text = mergeText(serverText, clientText, lastMergedText);
}
}
2017-08-19 10:24:08 +00:00
const result = {
2018-01-04 20:19:10 +00:00
text,
2017-08-25 10:37:46 +00:00
properties: mergeValues(
2017-08-19 10:24:08 +00:00
serverContent.properties,
2017-08-25 10:37:46 +00:00
clientContent.properties,
lastMergedContent.properties,
2017-08-19 10:24:08 +00:00
),
discussions: mergeObjects(
stripDiscussionOffsets(serverContent.discussions),
2017-08-25 10:37:46 +00:00
stripDiscussionOffsets(clientContent.discussions),
stripDiscussionOffsets(lastMergedContent.discussions),
2017-08-19 10:24:08 +00:00
),
comments: mergeObjects(
serverContent.comments,
2017-08-25 10:37:46 +00:00
clientContent.comments,
lastMergedContent.comments,
2017-08-19 10:24:08 +00:00
),
};
restoreDiscussionOffsets(result, markerKeys);
2017-08-25 10:37:46 +00:00
return result;
2017-08-19 10:24:08 +00:00
}
export default {
makePatchableText,
restoreDiscussionOffsets,
2017-09-26 22:54:26 +00:00
mergeObjects,
2017-08-19 10:24:08 +00:00
mergeContent,
};