2014-03-28 00:49:49 +00:00
|
|
|
define([
|
|
|
|
'underscore',
|
|
|
|
'utils',
|
|
|
|
'settings',
|
|
|
|
'eventMgr',
|
|
|
|
'fileMgr',
|
2014-03-30 01:44:51 +00:00
|
|
|
'editor',
|
2014-03-28 00:49:49 +00:00
|
|
|
'diff_match_patch_uncompressed',
|
|
|
|
'jsondiffpatch',
|
2014-03-30 01:44:51 +00:00
|
|
|
], function(_, utils, settings, eventMgr, fileMgr, editor, diff_match_patch, jsondiffpatch) {
|
2014-03-28 00:49:49 +00:00
|
|
|
|
2013-06-22 23:48:57 +00:00
|
|
|
function Provider(providerId, providerName) {
|
|
|
|
this.providerId = providerId;
|
|
|
|
this.providerName = providerName;
|
2013-12-23 22:33:33 +00:00
|
|
|
this.isPublishEnabled = true;
|
2013-06-22 23:48:57 +00:00
|
|
|
}
|
2014-03-28 00:49:49 +00:00
|
|
|
|
|
|
|
// 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';
|
|
|
|
}
|
2014-03-30 01:44:51 +00:00
|
|
|
if(discussion.type == 'conflict') {
|
|
|
|
return;
|
|
|
|
}
|
2014-03-28 00:49:49 +00:00
|
|
|
discussion.commentList.forEach(function(comment) {
|
|
|
|
if(
|
|
|
|
(!_.isString(comment.author)) ||
|
|
|
|
(!_.isString(comment.content))
|
|
|
|
) {
|
|
|
|
throw 'invalid';
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return discussionList;
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Provider.prototype.serializeContent = function(content, discussionList) {
|
2014-03-30 01:44:51 +00:00
|
|
|
if(discussionList.length > 2) { // It's a serialized JSON
|
2014-03-28 00:49:49 +00:00
|
|
|
return content + '<!--se_discussion_list:' + discussionList + '-->';
|
|
|
|
}
|
|
|
|
return content;
|
|
|
|
};
|
|
|
|
|
|
|
|
Provider.prototype.parseSerializedContent = function(content) {
|
|
|
|
var discussionList = '{}';
|
|
|
|
var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content);
|
|
|
|
if(discussionExtractor && this.parseDiscussionList(discussionExtractor[1])) {
|
|
|
|
content = content.substring(0, discussionExtractor.index);
|
|
|
|
discussionList = discussionExtractor[1];
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
content: content,
|
|
|
|
discussionList: discussionList
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
var diffMatchPatch = new diff_match_patch();
|
2014-03-30 01:44:51 +00:00
|
|
|
diffMatchPatch.Match_Threshold = 0;
|
|
|
|
diffMatchPatch.Patch_DeleteThreshold = 0;
|
2014-03-28 00:49:49 +00:00
|
|
|
var jsonDiffPatch = jsondiffpatch.create({
|
|
|
|
objectHash: function(obj) {
|
|
|
|
return JSON.stringify(obj);
|
|
|
|
},
|
|
|
|
arrays: {
|
|
|
|
detectMove: false,
|
|
|
|
},
|
|
|
|
textDiff: {
|
|
|
|
minLength: 9999999
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var merge = settings.conflictMode == 'merge';
|
2014-03-29 01:22:24 +00:00
|
|
|
Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) {
|
|
|
|
var lineArray = [];
|
|
|
|
var lineHash = {};
|
|
|
|
|
|
|
|
function linesToChars(text) {
|
|
|
|
var chars = '';
|
|
|
|
var lineArrayLength = lineArray.length;
|
|
|
|
text.split('\n').forEach(function(line) {
|
|
|
|
if(lineHash.hasOwnProperty(line)) {
|
|
|
|
chars += String.fromCharCode(lineHash[line]);
|
|
|
|
} else {
|
|
|
|
chars += String.fromCharCode(lineArrayLength);
|
|
|
|
lineHash[line] = lineArrayLength;
|
|
|
|
lineArray[lineArrayLength++] = line;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return chars;
|
|
|
|
}
|
|
|
|
|
|
|
|
function moveComments(oldTextContent, newTextContent, discussionList) {
|
|
|
|
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
|
2014-03-30 01:44:51 +00:00
|
|
|
var changed = false;
|
2014-03-29 01:22:24 +00:00
|
|
|
var startOffset = 0;
|
|
|
|
changes.forEach(function(change) {
|
|
|
|
var changeType = change[0];
|
|
|
|
var changeText = change[1];
|
|
|
|
if(changeType === 0) {
|
|
|
|
startOffset += changeText.length;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var endOffset = startOffset;
|
|
|
|
var diffOffset = changeText.length;
|
|
|
|
if(changeType === -1) {
|
|
|
|
endOffset += diffOffset;
|
|
|
|
diffOffset = -diffOffset;
|
|
|
|
}
|
|
|
|
discussionList.forEach(function(discussion) {
|
|
|
|
// selectionEnd
|
|
|
|
if(discussion.selectionEnd >= endOffset) {
|
|
|
|
discussion.selectionEnd += diffOffset;
|
2014-03-30 01:44:51 +00:00
|
|
|
changed = true;
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
|
|
|
else if(discussion.selectionEnd > startOffset) {
|
|
|
|
discussion.selectionEnd = startOffset;
|
2014-03-30 01:44:51 +00:00
|
|
|
changed = true;
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
|
|
|
// selectionStart
|
|
|
|
if(discussion.selectionStart >= endOffset) {
|
|
|
|
discussion.selectionStart += diffOffset;
|
2014-03-30 01:44:51 +00:00
|
|
|
changed = true;
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
|
|
|
else if(discussion.selectionStart > startOffset) {
|
|
|
|
discussion.selectionStart = startOffset;
|
2014-03-30 01:44:51 +00:00
|
|
|
changed = true;
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
startOffset = endOffset;
|
|
|
|
});
|
2014-03-30 01:44:51 +00:00
|
|
|
return changed;
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var localContent = fileDesc.content;
|
|
|
|
var localTitle = fileDesc.title;
|
|
|
|
var localDiscussionListJSON = fileDesc.discussionListJSON;
|
2014-03-30 01:44:51 +00:00
|
|
|
var localDiscussionList = fileDesc.discussionList;
|
|
|
|
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
|
2014-03-28 00:49:49 +00:00
|
|
|
|
|
|
|
// Local/Remote CRCs
|
|
|
|
var localContentCRC = utils.crc32(localContent);
|
|
|
|
var localTitleCRC = utils.crc32(localTitle);
|
2014-03-29 01:22:24 +00:00
|
|
|
var localDiscussionListCRC = utils.crc32(localDiscussionListJSON);
|
2014-03-28 00:49:49 +00:00
|
|
|
var remoteContentCRC = utils.crc32(remoteContent);
|
|
|
|
var remoteTitleCRC = utils.crc32(remoteTitle);
|
2014-03-29 01:22:24 +00:00
|
|
|
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON);
|
2014-03-28 00:49:49 +00:00
|
|
|
|
|
|
|
// Check content
|
|
|
|
var contentChanged = localContent != remoteContent;
|
|
|
|
var localContentChanged = syncAttributes.contentCRC != localContentCRC;
|
|
|
|
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
|
|
|
|
var contentConflict = contentChanged && localContentChanged && remoteContentChanged;
|
2014-03-30 01:44:51 +00:00
|
|
|
contentChanged = contentChanged && remoteContentChanged;
|
2014-03-28 00:49:49 +00:00
|
|
|
|
|
|
|
// Check title
|
|
|
|
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
|
|
|
|
var titleChanged = localTitle != remoteTitle;
|
|
|
|
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
|
|
|
|
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
|
|
|
|
var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged;
|
2014-03-30 01:44:51 +00:00
|
|
|
titleChanged = titleChanged && remoteTitleChanged;
|
2014-03-28 00:49:49 +00:00
|
|
|
|
|
|
|
// Check discussionList
|
2014-03-29 01:22:24 +00:00
|
|
|
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON;
|
2014-03-28 00:49:49 +00:00
|
|
|
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
|
|
|
|
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
|
|
|
|
var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged;
|
2014-03-30 01:44:51 +00:00
|
|
|
discussionListChanged = discussionListChanged && remoteDiscussionListChanged;
|
2014-03-28 00:49:49 +00:00
|
|
|
|
2014-03-30 01:44:51 +00:00
|
|
|
var conflictList = [];
|
2014-03-28 00:49:49 +00:00
|
|
|
if(
|
|
|
|
(!merge && (contentConflict || titleConflict || discussionListConflict)) ||
|
|
|
|
(contentConflict && syncAttributes.content === undefined) ||
|
|
|
|
(titleConflict && syncAttributes.title === undefined) ||
|
|
|
|
(discussionListConflict && syncAttributes.discussionList === undefined)
|
|
|
|
) {
|
2014-03-29 01:22:24 +00:00
|
|
|
fileMgr.createFile(localTitle + " (backup)", localContent, localDiscussionListJSON);
|
2014-03-28 00:49:49 +00:00
|
|
|
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
|
|
|
|
}
|
|
|
|
else {
|
2014-03-29 01:22:24 +00:00
|
|
|
var oldDiscussionList;
|
|
|
|
var patch, delta;
|
|
|
|
if(contentConflict) {
|
|
|
|
// Patch content (line mode)
|
2014-03-30 01:44:51 +00:00
|
|
|
var oldContent = syncAttributes.content;
|
|
|
|
/*
|
2014-03-29 01:22:24 +00:00
|
|
|
var oldContentLines = linesToChars(syncAttributes.content);
|
|
|
|
var localContentLines = linesToChars(localContent);
|
|
|
|
var remoteContentLines = linesToChars(remoteContent);
|
2014-03-30 01:44:51 +00:00
|
|
|
*/
|
|
|
|
patch = diffMatchPatch.patch_make(oldContent, localContent);
|
|
|
|
var patchResult = diffMatchPatch.patch_apply(patch, remoteContent);
|
|
|
|
var newContent = patchResult[0];
|
|
|
|
if(patchResult[1].some(function(patchSuccess) {
|
|
|
|
return !patchSuccess;
|
|
|
|
})) {
|
|
|
|
// Conflicts (some modifications have not been applied properly)
|
|
|
|
var diffs = diffMatchPatch.diff_main(localContent, newContent);
|
|
|
|
diffMatchPatch.diff_cleanupSemantic(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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
2014-03-29 01:22:24 +00:00
|
|
|
remoteContentLines = diffMatchPatch.patch_apply(patch, remoteContentLines)[0];
|
|
|
|
var newContent = remoteContentLines.split('').map(function(char) {
|
|
|
|
return lineArray[char.charCodeAt(0)];
|
|
|
|
}).join('\n');
|
2014-03-30 01:44:51 +00:00
|
|
|
*/
|
2014-03-29 01:22:24 +00:00
|
|
|
|
|
|
|
// Whether we take the local discussionList into account
|
|
|
|
if(localDiscussionListChanged || !remoteDiscussionListChanged) {
|
|
|
|
// Move local discussion according to content patch
|
|
|
|
var localDiscussionArray = _.values(localDiscussionList);
|
|
|
|
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
|
2014-03-30 01:44:51 +00:00
|
|
|
discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray);
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if(remoteDiscussionListChanged) {
|
|
|
|
// Move remote discussion according to content patch
|
|
|
|
var remoteDiscussionArray = _.values(remoteDiscussionList);
|
|
|
|
moveComments(remoteContent, newContent, remoteDiscussionArray);
|
|
|
|
|
|
|
|
if(localDiscussionListChanged) {
|
|
|
|
// Patch remote discussionList with local modifications
|
|
|
|
oldDiscussionList = JSON.parse(syncAttributes.discussionList);
|
|
|
|
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
|
|
|
|
jsonDiffPatch.patch(remoteDiscussionList, delta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
remoteDiscussionList = localDiscussionList;
|
|
|
|
}
|
2014-03-30 01:44:51 +00:00
|
|
|
|
|
|
|
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(remoteDiscussionList, discussionIndex));
|
|
|
|
conflict.discussionIndex = discussionIndex;
|
|
|
|
remoteDiscussionList[discussionIndex] = conflict;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-03-29 01:22:24 +00:00
|
|
|
remoteContent = newContent;
|
|
|
|
}
|
|
|
|
else if(discussionListConflict) {
|
|
|
|
// Patch remote discussionList with local modifications
|
|
|
|
oldDiscussionList = JSON.parse(syncAttributes.discussionList);
|
|
|
|
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
|
|
|
|
jsonDiffPatch.patch(remoteDiscussionList, delta);
|
|
|
|
}
|
|
|
|
if(titleConflict) {
|
|
|
|
// Patch title
|
|
|
|
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
|
|
|
|
remoteTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0];
|
2014-03-28 00:49:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-30 01:44:51 +00:00
|
|
|
if(titleChanged) {
|
2014-03-29 01:22:24 +00:00
|
|
|
fileDesc.title = remoteTitle;
|
|
|
|
eventMgr.onTitleChanged(fileDesc);
|
|
|
|
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.');
|
|
|
|
}
|
2014-03-30 01:44:51 +00:00
|
|
|
|
|
|
|
if(contentChanged || discussionListChanged) {
|
|
|
|
var self = this;
|
|
|
|
editor.watcher.noWatch(function() {
|
|
|
|
if(contentChanged) {
|
|
|
|
if(!/\n$/.test(remoteContent)) {
|
|
|
|
remoteContent += '\n';
|
|
|
|
}
|
|
|
|
if(fileMgr.currentFile === fileDesc) {
|
|
|
|
editor.setValueNoWatch(remoteContent);
|
|
|
|
}
|
|
|
|
fileDesc.content = remoteContent;
|
|
|
|
eventMgr.onContentChanged(fileDesc, remoteContent);
|
|
|
|
}
|
|
|
|
if(discussionListChanged) {
|
|
|
|
fileDesc.discussionList = remoteDiscussionList;
|
|
|
|
var diff = jsonDiffPatch.diff(localDiscussionList, remoteDiscussionList);
|
|
|
|
var commentsChanged = false;
|
|
|
|
_.each(diff, function(discussionDiff, discussionIndex) {
|
|
|
|
if(!_.isArray(discussionDiff)) {
|
|
|
|
commentsChanged = true;
|
|
|
|
}
|
|
|
|
else if(discussionDiff.length === 1) {
|
|
|
|
eventMgr.onDiscussionCreated(fileDesc, remoteDiscussionList[discussionIndex]);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
eventMgr.onDiscussionRemoved(fileDesc, localDiscussionList[discussionIndex]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
commentsChanged && eventMgr.onCommentsChanged(fileDesc);
|
|
|
|
}
|
|
|
|
editor.undoManager.currentMode = 'sync';
|
|
|
|
editor.undoManager.saveState();
|
|
|
|
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.');
|
|
|
|
if(conflictList.length) {
|
|
|
|
eventMgr.onMessage('"' + remoteTitle + '" contains conflicts you need to review.');
|
|
|
|
}
|
|
|
|
});
|
2014-03-29 01:22:24 +00:00
|
|
|
}
|
2014-03-30 01:44:51 +00:00
|
|
|
|
|
|
|
// Return remote CRCs
|
|
|
|
return {
|
|
|
|
contentCRC: remoteContentCRC,
|
|
|
|
titleCRC: remoteTitleCRC,
|
|
|
|
discussionListCRC: remoteDiscussionListCRC
|
|
|
|
};
|
2014-03-28 00:49:49 +00:00
|
|
|
};
|
|
|
|
|
2013-06-22 23:48:57 +00:00
|
|
|
return Provider;
|
2014-03-28 00:49:49 +00:00
|
|
|
});
|