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],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (discussion.offset0 === discussion.offset1) {
|
|
|
|
// Remove discussion offsets if markers are at the same position
|
|
|
|
discussion.offset0 = undefined;
|
|
|
|
discussion.offset1 = undefined;
|
|
|
|
} else {
|
|
|
|
addMarker('offset0');
|
|
|
|
addMarker('offset1');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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) {
|
|
|
|
const len = content.text.length;
|
|
|
|
const maxIdx = markerKeys.length;
|
|
|
|
for (let i = 0; i < len; i += 1) {
|
|
|
|
const idx = content.text.charCodeAt(i) - 0xe000;
|
|
|
|
if (idx >= 0 && idx < maxIdx) {
|
|
|
|
const markerKey = markerKeys[idx];
|
|
|
|
content.text = content.text.slice(0, i) + content.text.slice(i + 1);
|
|
|
|
const discussion = content.discussions[markerKey.id];
|
|
|
|
if (discussion) {
|
|
|
|
discussion[markerKey.offsetName] = i;
|
|
|
|
}
|
|
|
|
// We just removed the current character, we may have multiple markers with same offset
|
|
|
|
i -= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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(serverClientDiffs);
|
|
|
|
// 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;
|
|
|
|
const isTextSynchronized = serverText === clientText;
|
2017-08-19 10:24:08 +00:00
|
|
|
|
|
|
|
const result = {
|
|
|
|
text: isTextSynchronized || !isServerTextChanges
|
2017-08-25 10:37:46 +00:00
|
|
|
? clientText
|
|
|
|
: mergeText(serverText, clientText, lastMergedText),
|
|
|
|
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,
|
|
|
|
};
|