305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
/*
|
|
This file is part of BBB-Notes.
|
|
|
|
Copyright (c) Islam El-Ashi. All rights reserved.
|
|
|
|
BBB-Notes is free software: you can redistribute it and/or modify
|
|
it under the terms of the Lesser GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
any later version.
|
|
|
|
BBB-Notes is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
Lesser GNU General Public License for more details.
|
|
|
|
You should have received a copy of the Lesser GNU General Public License
|
|
along with BBB-Notes. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
Author: Islam El-Ashi <ielashi@gmail.com>, <http://www.ielashi.com>
|
|
*/
|
|
var dmp = new diff_match_patch();
|
|
var debug = false;
|
|
|
|
function diff(text1, text2) {
|
|
return dmp.patch_toText(dmp.patch_make(dmp.diff_main(unescape(text1),unescape(text2))));
|
|
}
|
|
|
|
function patch(patch, text) {
|
|
return dmp.patch_apply(dmp.patch_fromText(patch), unescape(text))[0];
|
|
}
|
|
|
|
function unpatch(patch, text) {
|
|
return dmp.patch_apply_reverse(dmp.patch_fromText(patch), unescape(text))[0];
|
|
}
|
|
|
|
|
|
/**
|
|
* Helper Methods
|
|
*/
|
|
|
|
/**
|
|
* MobWrite - Real-time Synchronization and Collaboration Service
|
|
*
|
|
* Copyright 2006 Google Inc.
|
|
* http://code.google.com/p/google-mobwrite/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* Modify the user's plaintext by applying a series of patches against it.
|
|
* @param {Array.<patch_obj>} patches Array of Patch objects.
|
|
* @param {String} client text
|
|
*/
|
|
function patchClientText(patches, text, selectionStart, selectionEnd) {
|
|
text = unescape(text)
|
|
// Set some constants which tweak the matching behaviour.
|
|
// Maximum distance to search from expected location.
|
|
dmp.Match_Distance = 1000;
|
|
// At what point is no match declared (0.0 = perfection, 1.0 = very loose)
|
|
dmp.Match_Threshold = 0.6;
|
|
|
|
var oldClientText = text
|
|
var cursor = captureCursor_(oldClientText, selectionStart, selectionEnd);
|
|
// Pack the cursor offsets into an array to be adjusted.
|
|
// See http://neil.fraser.name/writing/cursor/
|
|
var offsets = [];
|
|
if (cursor) {
|
|
offsets[0] = cursor.startOffset;
|
|
if ('endOffset' in cursor) {
|
|
offsets[1] = cursor.endOffset;
|
|
}
|
|
}
|
|
var newClientText = patch_apply_(patches, oldClientText, offsets);
|
|
// Set the new text only if there is a change to be made.
|
|
if (oldClientText != newClientText) {
|
|
//this.setClientText(newClientText);
|
|
if (cursor) {
|
|
// Unpack the offset array.
|
|
cursor.startOffset = offsets[0];
|
|
if (offsets.length > 1) {
|
|
cursor.endOffset = offsets[1];
|
|
if (cursor.startOffset >= cursor.endOffset) {
|
|
cursor.collapsed = true;
|
|
}
|
|
}
|
|
return [restoreCursor_(cursor, newClientText), newClientText];
|
|
// this.restoreCursor_(cursor);
|
|
}
|
|
}
|
|
// no change in client text
|
|
return [[selectionStart, selectionEnd], newClientText];
|
|
}
|
|
|
|
/**
|
|
* Merge a set of patches onto the text. Return a patched text.
|
|
* @param {Array.<patch_obj>} patches Array of patch objects.
|
|
* @param {string} text Old text.
|
|
* @param {Array.<number>} offsets Offset indices to adjust.
|
|
* @return {string} New text.
|
|
*/
|
|
function patch_apply_(patchText, text, offsets) {
|
|
var patches = dmp.patch_fromText(patchText);
|
|
if (patches.length == 0) {
|
|
return text;
|
|
}
|
|
|
|
// Deep copy the patches so that no changes are made to originals.
|
|
patches = dmp.patch_deepCopy(patches);
|
|
var nullPadding = dmp.patch_addPadding(patches);
|
|
text = nullPadding + text + nullPadding;
|
|
|
|
dmp.patch_splitMax(patches);
|
|
// delta keeps track of the offset between the expected and actual location
|
|
// of the previous patch. If there are patches expected at positions 10 and
|
|
// 20, but the first patch was found at 12, delta is 2 and the second patch
|
|
// has an effective expected position of 22.
|
|
var delta = 0;
|
|
for (var x = 0; x < patches.length; x++) {
|
|
var expected_loc = patches[x].start2 + delta;
|
|
var text1 = dmp.diff_text1(patches[x].diffs);
|
|
var start_loc;
|
|
var end_loc = -1;
|
|
if (text1.length > dmp.Match_MaxBits) {
|
|
// patch_splitMax will only provide an oversized pattern in the case of
|
|
// a monster delete.
|
|
start_loc = dmp.match_main(text, text1.substring(0, dmp.Match_MaxBits), expected_loc);
|
|
if (start_loc != -1) {
|
|
end_loc = dmp.match_main(text, text1.substring(text1.length - dmp.Match_MaxBits),
|
|
expected_loc + text1.length - dmp.Match_MaxBits);
|
|
if (end_loc == -1 || start_loc >= end_loc) {
|
|
// Can't find valid trailing context. Drop this patch.
|
|
start_loc = -1;
|
|
}
|
|
}
|
|
} else {
|
|
start_loc = dmp.match_main(text, text1, expected_loc);
|
|
}
|
|
if (start_loc == -1) {
|
|
// No match found. :(
|
|
if (debug) {
|
|
window.console.warn('Patch failed: ' + patches[x]);
|
|
}
|
|
// Subtract the delta for this failed patch from subsequent patches.
|
|
delta -= patches[x].length2 - patches[x].length1;
|
|
} else {
|
|
// Found a match. :)
|
|
if (debug) {
|
|
window.console.info('Patch OK.');
|
|
}
|
|
delta = start_loc - expected_loc;
|
|
var text2;
|
|
if (end_loc == -1) {
|
|
text2 = text.substring(start_loc, start_loc + text1.length);
|
|
} else {
|
|
text2 = text.substring(start_loc, end_loc + dmp.Match_MaxBits);
|
|
}
|
|
// Run a diff to get a framework of equivalent indices.
|
|
var diffs = dmp.diff_main(text1, text2, false);
|
|
if (text1.length > dmp.Match_MaxBits &&
|
|
dmp.diff_levenshtein(diffs) / text1.length >
|
|
dmp.Patch_DeleteThreshold) {
|
|
// The end points match, but the content is unacceptably bad.
|
|
if (debug) {
|
|
window.console.warn('Patch contents mismatch: ' + patches[x]);
|
|
}
|
|
} else {
|
|
var index1 = 0;
|
|
var index2;
|
|
for (var y = 0; y < patches[x].diffs.length; y++) {
|
|
var mod = patches[x].diffs[y];
|
|
if (mod[0] !== DIFF_EQUAL) {
|
|
index2 = dmp.diff_xIndex(diffs, index1);
|
|
}
|
|
if (mod[0] === DIFF_INSERT) { // Insertion
|
|
text = text.substring(0, start_loc + index2) + mod[1] +
|
|
text.substring(start_loc + index2);
|
|
for (var i = 0; i < offsets.length; i++) {
|
|
if (offsets[i] + nullPadding.length > start_loc + index2) {
|
|
offsets[i] += mod[1].length;
|
|
}
|
|
}
|
|
} else if (mod[0] === DIFF_DELETE) { // Deletion
|
|
var del_start = start_loc + index2;
|
|
var del_end = start_loc + dmp.diff_xIndex(diffs,
|
|
index1 + mod[1].length);
|
|
text = text.substring(0, del_start) + text.substring(del_end);
|
|
for (var i = 0; i < offsets.length; i++) {
|
|
if (offsets[i] + nullPadding.length > del_start) {
|
|
if (offsets[i] + nullPadding.length < del_end) {
|
|
offsets[i] = del_start - nullPadding.length;
|
|
} else {
|
|
offsets[i] -= del_end - del_start;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (mod[0] !== DIFF_DELETE) {
|
|
index1 += mod[1].length;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Strip the padding off.
|
|
text = text.substring(nullPadding.length, text.length - nullPadding.length);
|
|
return text;
|
|
}
|
|
|
|
|
|
/**
|
|
* Record information regarding the current cursor.
|
|
* @return {Object?} Context information of the cursor.
|
|
* @private
|
|
*/
|
|
function captureCursor_(text, selectionStart, selectionEnd) {
|
|
var padLength = dmp.Match_MaxBits / 2; // Normally 16.
|
|
var cursor = {};
|
|
|
|
cursor.startPrefix = text.substring(selectionStart - padLength, selectionStart);
|
|
cursor.startSuffix = text.substring(selectionStart, selectionStart + padLength);
|
|
cursor.startOffset = selectionStart;
|
|
cursor.collapsed = (selectionStart == selectionEnd);
|
|
if (!cursor.collapsed) {
|
|
cursor.endPrefix = text.substring(selectionEnd - padLength, selectionEnd);
|
|
cursor.endSuffix = text.substring(selectionEnd, selectionEnd + padLength);
|
|
cursor.endOffset = selectionEnd;
|
|
}
|
|
|
|
return cursor;
|
|
}
|
|
|
|
/**
|
|
* Attempt to restore the cursor's location.
|
|
* @param {Object} cursor Context information of the cursor.
|
|
* @private
|
|
*/
|
|
function restoreCursor_(cursor, text) {
|
|
// Set some constants which tweak the matching behaviour.
|
|
// Maximum distance to search from expected location.
|
|
dmp.Match_Distance = 1000;
|
|
// At what point is no match declared (0.0 = perfection, 1.0 = very loose)
|
|
dmp.Match_Threshold = 0.9;
|
|
|
|
var padLength = dmp.Match_MaxBits / 2; // Normally 16.
|
|
var newText = text;
|
|
|
|
// Find the start of the selection in the new text.
|
|
var pattern1 = cursor.startPrefix + cursor.startSuffix;
|
|
var pattern2, diff;
|
|
var cursorStartPoint = dmp.match_main(newText, pattern1,
|
|
cursor.startOffset - padLength);
|
|
if (cursorStartPoint !== null) {
|
|
pattern2 = newText.substring(cursorStartPoint,
|
|
cursorStartPoint + pattern1.length);
|
|
//alert(pattern1 + '\nvs\n' + pattern2);
|
|
// Run a diff to get a framework of equivalent indicies.
|
|
diff = dmp.diff_main(pattern1, pattern2, false);
|
|
cursorStartPoint += dmp.diff_xIndex(diff, cursor.startPrefix.length);
|
|
}
|
|
|
|
var cursorEndPoint = null;
|
|
if (!cursor.collapsed) {
|
|
// Find the end of the selection in the new text.
|
|
pattern1 = cursor.endPrefix + cursor.endSuffix;
|
|
cursorEndPoint = dmp.match_main(newText, pattern1,
|
|
cursor.endOffset - padLength);
|
|
if (cursorEndPoint !== null) {
|
|
pattern2 = newText.substring(cursorEndPoint,
|
|
cursorEndPoint + pattern1.length);
|
|
//alert(pattern1 + '\nvs\n' + pattern2);
|
|
// Run a diff to get a framework of equivalent indicies.
|
|
diff = dmp.diff_main(pattern1, pattern2, false);
|
|
cursorEndPoint += dmp.diff_xIndex(diff, cursor.endPrefix.length);
|
|
}
|
|
}
|
|
|
|
// Deal with loose ends
|
|
if (cursorStartPoint === null && cursorEndPoint !== null) {
|
|
// Lost the start point of the selection, but we have the end point.
|
|
// Collapse to end point.
|
|
cursorStartPoint = cursorEndPoint;
|
|
} else if (cursorStartPoint === null && cursorEndPoint === null) {
|
|
// Lost both start and end points.
|
|
// Jump to the offset of start.
|
|
cursorStartPoint = cursor.startOffset;
|
|
}
|
|
if (cursorEndPoint === null) {
|
|
// End not known, collapse to start.
|
|
cursorEndPoint = cursorStartPoint;
|
|
}
|
|
|
|
return [cursorStartPoint, cursorEndPoint];
|
|
}
|