Stackedit/public/res/classes/Provider.js
2014-04-21 02:12:57 +01:00

379 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
});