379 lines
16 KiB
JavaScript
379 lines
16 KiB
JavaScript
define([
|
||
'underscore',
|
||
'utils',
|
||
'settings',
|
||
'eventMgr',
|
||
'fileMgr',
|
||
'editor',
|
||
'diff_match_patch_uncompressed',
|
||
'jsondiffpatch'
|
||
], function(_, utils, settings, eventMgr, fileMgr, editor, diff_match_patch, jsondiffpatch) {
|
||
|
||
function Provider(providerId, providerName) {
|
||
this.providerId = providerId;
|
||
this.providerName = providerName;
|
||
this.isPublishEnabled = true;
|
||
}
|
||
|
||
// Parse and check a JSON discussion list
|
||
Provider.prototype.parseDiscussionList = function(discussionListJSON) {
|
||
try {
|
||
var discussionList = JSON.parse(discussionListJSON);
|
||
_.each(discussionList, function(discussion, discussionIndex) {
|
||
if(
|
||
(discussion.discussionIndex != discussionIndex) ||
|
||
(!_.isNumber(discussion.selectionStart)) ||
|
||
(!_.isNumber(discussion.selectionEnd))
|
||
) {
|
||
throw 'invalid';
|
||
}
|
||
discussion.commentList && discussion.commentList.forEach(function(comment) {
|
||
if(
|
||
(!(!comment.author || _.isString(comment.author))) ||
|
||
(!_.isString(comment.content))
|
||
) {
|
||
throw 'invalid';
|
||
}
|
||
});
|
||
});
|
||
return discussionList;
|
||
}
|
||
catch(e) {
|
||
}
|
||
};
|
||
|
||
Provider.prototype.serializeContent = function(content, discussionList) {
|
||
if(discussionList.length > 2) { // Serialized JSON
|
||
return content + '<!--se_discussion_list:' + discussionList + '-->';
|
||
}
|
||
return content;
|
||
};
|
||
|
||
Provider.prototype.parseContent = function(content) {
|
||
var discussionList;
|
||
var discussionListJSON = '{}';
|
||
var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content);
|
||
if(discussionExtractor && (discussionList = this.parseDiscussionList(discussionExtractor[1]))) {
|
||
content = content.substring(0, discussionExtractor.index);
|
||
discussionListJSON = discussionExtractor[1];
|
||
}
|
||
return {
|
||
content: content,
|
||
discussionList: discussionList || {},
|
||
discussionListJSON: discussionListJSON
|
||
};
|
||
};
|
||
|
||
var diffMatchPatch = new diff_match_patch();
|
||
diffMatchPatch.Match_Threshold = 0;
|
||
diffMatchPatch.Patch_DeleteThreshold = 0;
|
||
var jsonDiffPatch = jsondiffpatch.create({
|
||
objectHash: function(obj) {
|
||
return JSON.stringify(obj);
|
||
},
|
||
textDiff: {
|
||
minLength: 9999999
|
||
}
|
||
});
|
||
|
||
var merge = settings.conflictMode == 'merge';
|
||
Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON) {
|
||
|
||
function cleanupDiffs(diffs) {
|
||
var result = [];
|
||
var removeDiff = [-1, ''];
|
||
var addDiff = [1, ''];
|
||
var distance = 20;
|
||
function pushDiff() {
|
||
if(!removeDiff[1] && !addDiff[1]) {
|
||
return;
|
||
}
|
||
if(!removeDiff[1] || !addDiff[1]) {
|
||
result.push([0, removeDiff[1] + addDiff[1]]);
|
||
}
|
||
else {
|
||
removeDiff[1] = '⧸⧸' + removeDiff[1] + '⧸⧸';
|
||
addDiff[1] += '⧸⧸';
|
||
result.push(removeDiff);
|
||
result.push(addDiff);
|
||
}
|
||
removeDiff = [-1, ''];
|
||
addDiff = [1, ''];
|
||
}
|
||
diffs.forEach(function(diff, index) {
|
||
function firstOrLast() {
|
||
return index === 0 || index === diffs.length - 1;
|
||
}
|
||
var diffType = diff[0];
|
||
var diffText = diff[1];
|
||
if(diffType === 0) {
|
||
if(firstOrLast() || diffText.length > distance) {
|
||
if(removeDiff[1] || addDiff[1]) {
|
||
var match = /\s/.exec(diffText);
|
||
if(match) {
|
||
var prefixOffset = match.index;
|
||
var prefix = diffText.substring(0, prefixOffset);
|
||
diffText = diffText.substring(prefixOffset);
|
||
removeDiff[1] += prefix;
|
||
addDiff[1] += prefix;
|
||
}
|
||
}
|
||
if(diffText) {
|
||
var suffixOffset = diffText.length;
|
||
while(suffixOffset && /\S/.test(diffText[suffixOffset - 1])) {
|
||
suffixOffset--;
|
||
}
|
||
var suffix = diffText.substring(suffixOffset);
|
||
diffText = diffText.substring(0, suffixOffset);
|
||
if(firstOrLast() || diffText.length > distance) {
|
||
pushDiff();
|
||
result.push([0, diffText]);
|
||
}
|
||
else {
|
||
removeDiff[1] += diffText;
|
||
addDiff[1] += diffText;
|
||
}
|
||
removeDiff[1] += suffix;
|
||
addDiff[1] += suffix;
|
||
}
|
||
}
|
||
else {
|
||
removeDiff[1] += diffText;
|
||
addDiff[1] += diffText;
|
||
}
|
||
}
|
||
else if(diffType === -1) {
|
||
removeDiff[1] += diffText;
|
||
}
|
||
else if(diffType === 1) {
|
||
addDiff[1] += diffText;
|
||
}
|
||
});
|
||
if(removeDiff[1] == addDiff[1]) {
|
||
result.push([0, addDiff[1]]);
|
||
}
|
||
else {
|
||
pushDiff();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
var localContent = fileDesc.content;
|
||
var localTitle = fileDesc.title;
|
||
var localDiscussionListJSON = fileDesc.discussionListJSON;
|
||
var localDiscussionList = fileDesc.discussionList;
|
||
|
||
// Local/Remote CRCs
|
||
var localContentCRC = utils.crc32(localContent);
|
||
var localTitleCRC = utils.crc32(localTitle);
|
||
var localDiscussionListCRC = utils.crc32(localDiscussionListJSON);
|
||
var remoteContentCRC = utils.crc32(remoteContent);
|
||
var remoteTitleCRC = utils.crc32(remoteTitle);
|
||
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON);
|
||
|
||
// Check content
|
||
var localContentChanged = syncAttributes.contentCRC != localContentCRC;
|
||
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
|
||
var contentChanged = localContent != remoteContent && remoteContentChanged;
|
||
var contentConflict = contentChanged && localContentChanged;
|
||
|
||
// Check title
|
||
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
|
||
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
|
||
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
|
||
var titleChanged = localTitle != remoteTitle && remoteTitleChanged;
|
||
var titleConflict = titleChanged && localTitleChanged;
|
||
|
||
// Check discussionList
|
||
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
|
||
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
|
||
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON && remoteDiscussionListChanged;
|
||
var discussionListConflict = discussionListChanged && localDiscussionListChanged;
|
||
|
||
var conflictList = [];
|
||
var newContent = remoteContent;
|
||
var newTitle = remoteTitle;
|
||
var newDiscussionList = remoteDiscussionList;
|
||
var adjustLocalDiscussionList = false;
|
||
var adjustRemoteDiscussionList = false;
|
||
var mergeDiscussionList = false;
|
||
var diffs, patch;
|
||
if(
|
||
(!merge && (contentConflict || titleConflict || discussionListConflict)) ||
|
||
(contentConflict && syncAttributes.content === undefined) ||
|
||
(titleConflict && syncAttributes.title === undefined) ||
|
||
(discussionListConflict && syncAttributes.discussionList === undefined)
|
||
) {
|
||
fileMgr.createFile(localTitle + " (backup)", localContent, localDiscussionListJSON);
|
||
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
|
||
}
|
||
else {
|
||
if(contentConflict) {
|
||
// Patch content
|
||
var oldContent = syncAttributes.content;
|
||
diffs = diffMatchPatch.diff_main(oldContent, localContent);
|
||
diffMatchPatch.diff_cleanupSemantic(diffs);
|
||
patch = diffMatchPatch.patch_make(oldContent, diffs);
|
||
var patchResult = diffMatchPatch.patch_apply(patch, remoteContent);
|
||
newContent = patchResult[0];
|
||
if(!patchResult[1].every(_.identity)) {
|
||
// Remaining conflicts
|
||
diffs = diffMatchPatch.diff_main(localContent, newContent);
|
||
diffs = cleanupDiffs(diffs);
|
||
|
||
newContent = '';
|
||
var conflict;
|
||
diffs.forEach(function(diff) {
|
||
var diffType = diff[0];
|
||
var diffText = diff[1];
|
||
if(diffType !== 0 && !conflict) {
|
||
conflict = {
|
||
selectionStart: newContent.length,
|
||
type: 'conflict'
|
||
};
|
||
}
|
||
else if(diffType === 0 && conflict) {
|
||
conflict.selectionEnd = newContent.length;
|
||
conflictList.push(conflict);
|
||
conflict = undefined;
|
||
}
|
||
newContent += diffText;
|
||
});
|
||
if(conflict) {
|
||
conflict.selectionEnd = newContent.length;
|
||
conflictList.push(conflict);
|
||
}
|
||
}
|
||
}
|
||
|
||
if(contentChanged) {
|
||
if(localDiscussionListChanged) {
|
||
adjustLocalDiscussionList = true;
|
||
}
|
||
if(remoteDiscussionListChanged) {
|
||
adjustRemoteDiscussionList = true;
|
||
}
|
||
else {
|
||
adjustLocalDiscussionList = true;
|
||
newDiscussionList = localDiscussionList;
|
||
}
|
||
}
|
||
|
||
if(discussionListConflict) {
|
||
mergeDiscussionList = true;
|
||
}
|
||
|
||
if(titleConflict) {
|
||
// Patch title
|
||
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
|
||
newTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0];
|
||
}
|
||
}
|
||
|
||
// Adjust local discussions offsets
|
||
var editorSelection;
|
||
if(contentChanged) {
|
||
var localDiscussionArray = [];
|
||
// Adjust editor's cursor position and local discussions at the same time
|
||
if(fileMgr.currentFile === fileDesc) {
|
||
editorSelection = {
|
||
selectionStart: editor.selectionMgr.selectionStart,
|
||
selectionEnd: editor.selectionMgr.selectionEnd
|
||
};
|
||
localDiscussionArray.push(editorSelection);
|
||
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
|
||
}
|
||
if(adjustLocalDiscussionList) {
|
||
localDiscussionArray = localDiscussionArray.concat(_.values(localDiscussionList));
|
||
}
|
||
discussionListChanged |= editor.adjustCommentOffsets(localContent, newContent, localDiscussionArray);
|
||
}
|
||
|
||
// Adjust remote discussions offsets
|
||
if(adjustRemoteDiscussionList) {
|
||
var remoteDiscussionArray = _.values(remoteDiscussionList);
|
||
editor.adjustCommentOffsets(remoteContent, newContent, remoteDiscussionArray);
|
||
}
|
||
|
||
// Patch remote discussionList with local modifications
|
||
if(mergeDiscussionList) {
|
||
var oldDiscussionList = JSON.parse(syncAttributes.discussionList);
|
||
diffs = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
|
||
jsonDiffPatch.patch(remoteDiscussionList, diffs);
|
||
_.each(remoteDiscussionList, function(discussion, discussionIndex) {
|
||
if(!discussion) {
|
||
delete remoteDiscussionList[discussionIndex];
|
||
}
|
||
});
|
||
}
|
||
|
||
if(conflictList.length) {
|
||
discussionListChanged = true;
|
||
// Add conflicts to discussionList
|
||
conflictList.forEach(function(conflict) {
|
||
// Create discussion index
|
||
var discussionIndex;
|
||
do {
|
||
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
|
||
} while(_.has(newDiscussionList, discussionIndex));
|
||
conflict.discussionIndex = discussionIndex;
|
||
newDiscussionList[discussionIndex] = conflict;
|
||
});
|
||
}
|
||
|
||
if(titleChanged) {
|
||
fileDesc.title = newTitle;
|
||
eventMgr.onTitleChanged(fileDesc);
|
||
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + newTitle + '" on ' + this.providerName + '.');
|
||
}
|
||
|
||
if(contentChanged || discussionListChanged) {
|
||
editor.watcher.noWatch(_.bind(function() {
|
||
if(contentChanged) {
|
||
if(fileMgr.currentFile === fileDesc) {
|
||
editor.setValueNoWatch(newContent);
|
||
editorSelection && editor.selectionMgr.setSelectionStartEnd(
|
||
editorSelection.selectionStart,
|
||
editorSelection.selectionEnd
|
||
);
|
||
}
|
||
fileDesc.content = newContent;
|
||
eventMgr.onContentChanged(fileDesc, newContent);
|
||
}
|
||
if(discussionListChanged) {
|
||
fileDesc.discussionList = newDiscussionList;
|
||
var diff = jsonDiffPatch.diff(localDiscussionList, newDiscussionList);
|
||
var commentsChanged = false;
|
||
_.each(diff, function(discussionDiff, discussionIndex) {
|
||
if(!_.isArray(discussionDiff)) {
|
||
commentsChanged = true;
|
||
}
|
||
else if(discussionDiff.length === 1) {
|
||
eventMgr.onDiscussionCreated(fileDesc, newDiscussionList[discussionIndex]);
|
||
}
|
||
else {
|
||
eventMgr.onDiscussionRemoved(fileDesc, localDiscussionList[discussionIndex]);
|
||
}
|
||
});
|
||
commentsChanged && eventMgr.onCommentsChanged(fileDesc);
|
||
}
|
||
editor.undoMgr.currentMode = 'sync';
|
||
editor.undoMgr.saveState();
|
||
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + this.providerName + '.');
|
||
if(conflictList.length) {
|
||
eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.');
|
||
}
|
||
}, this));
|
||
}
|
||
|
||
// Return remote CRCs
|
||
return {
|
||
contentCRC: remoteContentCRC,
|
||
titleCRC: remoteTitleCRC,
|
||
discussionListCRC: remoteDiscussionListCRC
|
||
};
|
||
};
|
||
|
||
return Provider;
|
||
});
|