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 + ''; } return content; }; Provider.prototype.parseContent = function(content) { var discussionList; var discussionListJSON = '{}'; var discussionExtractor = /$/.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; });