Stackedit/public/res/classes/Provider.js

390 lines
16 KiB
JavaScript
Raw Normal View History

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-04-06 00:59:32 +00:00
discussion.commentList && discussion.commentList.forEach(function(comment) {
2014-03-28 00:49:49 +00:00
if(
2014-04-06 00:59:32 +00:00
(!(!comment.author || _.isString(comment.author))) ||
2014-03-28 00:49:49 +00:00
(!_.isString(comment.content))
) {
throw 'invalid';
}
});
});
return discussionList;
}
catch(e) {
}
};
Provider.prototype.serializeContent = function(content, discussionList) {
2014-04-06 00:59:32 +00:00
if(discussionList.length > 2) { // Serialized JSON
2014-03-28 00:49:49 +00:00
return content + '<!--se_discussion_list:' + discussionList + '-->';
}
return content;
};
Provider.prototype.parseContent = function(content) {
if(!_.isString(content)) {
// Real time content is already an object
return {
content: content.content,
discussionList: content.discussionList,
discussionListJSON: JSON.stringify(content.discussionList)
};
}
var discussionList;
2014-04-07 23:19:47 +00:00
var discussionListJSON = '{}';
2014-03-28 00:49:49 +00:00
var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content);
if(discussionExtractor && (discussionList = this.parseDiscussionList(discussionExtractor[1]))) {
2014-03-28 00:49:49 +00:00
content = content.substring(0, discussionExtractor.index);
2014-04-07 23:19:47 +00:00
discussionListJSON = discussionExtractor[1];
2014-03-28 00:49:49 +00:00
}
return {
content: content,
discussionList: discussionList || {},
2014-04-07 23:19:47 +00:00
discussionListJSON: discussionListJSON
2014-03-28 00:49:49 +00:00
};
};
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);
},
textDiff: {
minLength: 9999999
}
});
var merge = settings.conflictMode == 'merge';
2014-04-07 23:19:47 +00:00
Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON) {
2014-03-29 01:22:24 +00:00
2014-03-31 00:10:28 +00:00
function cleanupDiffs(diffs) {
var result = [];
var removeDiff = [-1, ''];
var addDiff = [1, ''];
var distance = 20;
2014-04-02 23:35:07 +00:00
function pushDiff() {
2014-04-06 00:59:32 +00:00
if(!removeDiff[1] && !addDiff[1]) {
return;
}
if(!removeDiff[1] || !addDiff[1]) {
result.push([0, removeDiff[1] + addDiff[1]]);
2014-04-02 23:35:07 +00:00
}
2014-04-06 00:59:32 +00:00
else {
2014-04-06 23:39:24 +00:00
removeDiff[1] = '//' + removeDiff[1] + '//';
addDiff[1] += '//';
2014-04-06 00:59:32 +00:00
result.push(removeDiff);
2014-04-02 23:35:07 +00:00
result.push(addDiff);
}
removeDiff = [-1, ''];
addDiff = [1, ''];
}
2014-04-06 00:59:32 +00:00
diffs.forEach(function(diff, index) {
function firstOrLast() {
return index === 0 || index === diffs.length - 1;
}
2014-03-31 00:10:28 +00:00
var diffType = diff[0];
var diffText = diff[1];
if(diffType === 0) {
2014-04-06 00:59:32 +00:00
if(firstOrLast() || diffText.length > distance) {
2014-03-31 00:10:28 +00:00
if(removeDiff[1] || addDiff[1]) {
2014-04-02 23:35:07 +00:00
var match = /\s/.exec(diffText);
2014-03-31 00:10:28 +00:00
if(match) {
2014-04-02 23:35:07 +00:00
var prefixOffset = match.index;
2014-03-31 00:10:28 +00:00
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);
2014-04-06 00:59:32 +00:00
if(firstOrLast() || diffText.length > distance) {
2014-04-02 23:35:07 +00:00
pushDiff();
2014-03-31 00:10:28 +00:00
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;
2014-03-29 01:22:24 +00:00
}
});
2014-03-31 00:10:28 +00:00
if(removeDiff[1] == addDiff[1]) {
result.push([0, addDiff[1]]);
}
else {
2014-04-02 23:35:07 +00:00
pushDiff();
2014-03-31 00:10:28 +00:00
}
return result;
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;
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 localContentChanged = syncAttributes.contentCRC != localContentCRC;
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
2014-03-31 00:10:28 +00:00
var contentChanged = localContent != remoteContent && remoteContentChanged;
var contentConflict = contentChanged && localContentChanged;
2014-03-28 00:49:49 +00:00
// Check title
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
2014-03-31 00:10:28 +00:00
var titleChanged = localTitle != remoteTitle && remoteTitleChanged;
var titleConflict = titleChanged && localTitleChanged;
2014-03-28 00:49:49 +00:00
// Check discussionList
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
2014-03-31 00:10:28 +00:00
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON && remoteDiscussionListChanged;
var discussionListConflict = discussionListChanged && localDiscussionListChanged;
2014-03-28 00:49:49 +00:00
2014-03-30 01:44:51 +00:00
var conflictList = [];
2014-03-31 00:10:28 +00:00
var newContent = remoteContent;
var newTitle = remoteTitle;
var newDiscussionList = remoteDiscussionList;
var adjustLocalDiscussionList = false;
var adjustRemoteDiscussionList = false;
var mergeDiscussionList = false;
var diffs, patch;
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
if(contentConflict) {
2014-03-31 00:10:28 +00:00
// Patch content
2014-03-30 01:44:51 +00:00
var oldContent = syncAttributes.content;
2014-03-31 00:10:28 +00:00
diffs = diffMatchPatch.diff_main(oldContent, localContent);
diffMatchPatch.diff_cleanupSemantic(diffs);
patch = diffMatchPatch.patch_make(oldContent, diffs);
2014-03-30 01:44:51 +00:00
var patchResult = diffMatchPatch.patch_apply(patch, remoteContent);
2014-03-31 00:10:28 +00:00
newContent = patchResult[0];
2014-04-08 23:20:48 +00:00
if(!patchResult[1].every(_.identity)) {
2014-03-31 00:10:28 +00:00
// Remaining conflicts
diffs = diffMatchPatch.diff_main(localContent, newContent);
diffs = cleanupDiffs(diffs);
2014-03-30 01:44:51 +00:00
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-31 00:10:28 +00:00
}
2014-03-29 01:22:24 +00:00
2014-03-31 00:10:28 +00:00
if(contentChanged) {
if(localDiscussionListChanged) {
adjustLocalDiscussionList = true;
2014-03-29 01:22:24 +00:00
}
if(remoteDiscussionListChanged) {
2014-03-31 00:10:28 +00:00
adjustRemoteDiscussionList = true;
2014-03-29 01:22:24 +00:00
}
else {
2014-03-31 00:10:28 +00:00
adjustLocalDiscussionList = true;
newDiscussionList = localDiscussionList;
2014-03-29 01:22:24 +00:00
}
}
2014-03-31 00:10:28 +00:00
if(discussionListConflict) {
mergeDiscussionList = true;
2014-03-29 01:22:24 +00:00
}
2014-03-31 00:10:28 +00:00
2014-03-29 01:22:24 +00:00
if(titleConflict) {
// Patch title
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
2014-03-31 00:10:28 +00:00
newTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0];
2014-03-28 00:49:49 +00:00
}
}
2014-04-06 00:59:32 +00:00
// Adjust local discussions offsets
2014-03-31 00:10:28 +00:00
var editorSelection;
if(contentChanged) {
var localDiscussionArray = [];
// Adjust editor's cursor position and local discussions at the same time
if(fileMgr.currentFile === fileDesc) {
editorSelection = {
2014-04-05 00:54:06 +00:00
selectionStart: editor.selectionMgr.selectionStart,
selectionEnd: editor.selectionMgr.selectionEnd
2014-03-31 00:10:28 +00:00
};
localDiscussionArray.push(editorSelection);
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
}
if(adjustLocalDiscussionList) {
localDiscussionArray = localDiscussionArray.concat(_.values(localDiscussionList));
}
2014-04-06 00:59:32 +00:00
discussionListChanged |= editor.adjustCommentOffsets(localContent, newContent, localDiscussionArray);
2014-03-31 00:10:28 +00:00
}
2014-04-06 00:59:32 +00:00
// Adjust remote discussions offsets
2014-03-31 00:10:28 +00:00
if(adjustRemoteDiscussionList) {
var remoteDiscussionArray = _.values(remoteDiscussionList);
2014-04-06 00:59:32 +00:00
editor.adjustCommentOffsets(remoteContent, newContent, remoteDiscussionArray);
2014-03-31 00:10:28 +00:00
}
// 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;
});
}
2014-03-30 01:44:51 +00:00
if(titleChanged) {
2014-03-31 00:10:28 +00:00
fileDesc.title = newTitle;
2014-03-29 01:22:24 +00:00
eventMgr.onTitleChanged(fileDesc);
2014-03-31 00:10:28 +00:00
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + newTitle + '" on ' + this.providerName + '.');
2014-03-29 01:22:24 +00:00
}
2014-03-30 01:44:51 +00:00
if(contentChanged || discussionListChanged) {
2014-04-08 23:20:48 +00:00
editor.watcher.noWatch(_.bind(function() {
2014-03-30 01:44:51 +00:00
if(contentChanged) {
2014-03-31 00:10:28 +00:00
if(!/\n$/.test(newContent)) {
newContent += '\n';
2014-03-30 01:44:51 +00:00
}
if(fileMgr.currentFile === fileDesc) {
2014-03-31 00:10:28 +00:00
editor.setValueNoWatch(newContent);
2014-04-05 00:54:06 +00:00
editorSelection && editor.selectionMgr.setSelectionStartEnd(
2014-03-31 00:10:28 +00:00
editorSelection.selectionStart,
editorSelection.selectionEnd
);
2014-03-30 01:44:51 +00:00
}
2014-03-31 00:10:28 +00:00
fileDesc.content = newContent;
eventMgr.onContentChanged(fileDesc, newContent);
2014-03-30 01:44:51 +00:00
}
if(discussionListChanged) {
2014-04-02 23:35:07 +00:00
fileDesc.discussionList = newDiscussionList;
var diff = jsonDiffPatch.diff(localDiscussionList, newDiscussionList);
2014-03-30 01:44:51 +00:00
var commentsChanged = false;
_.each(diff, function(discussionDiff, discussionIndex) {
if(!_.isArray(discussionDiff)) {
commentsChanged = true;
}
else if(discussionDiff.length === 1) {
2014-04-02 23:35:07 +00:00
eventMgr.onDiscussionCreated(fileDesc, newDiscussionList[discussionIndex]);
2014-03-30 01:44:51 +00:00
}
else {
eventMgr.onDiscussionRemoved(fileDesc, localDiscussionList[discussionIndex]);
}
});
commentsChanged && eventMgr.onCommentsChanged(fileDesc);
}
2014-04-05 00:54:06 +00:00
editor.undoMgr.currentMode = 'sync';
editor.undoMgr.saveState();
2014-04-08 23:20:48 +00:00
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + this.providerName + '.');
2014-03-30 01:44:51 +00:00
if(conflictList.length) {
2014-03-31 00:10:28 +00:00
eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.');
2014-03-30 01:44:51 +00:00
}
2014-04-08 23:20:48 +00:00
}), this);
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
});