Anakunda / libTextDiff

// ==UserScript==
// ==UserLibrary==
// @name         libTextDiff
// @namespace    https://openuserjs.org/users/Anakunda
// @version      1.02
// @license      GPL-3.0-or-later
// @copyright    2023, Anakunda (https://openuserjs.org/users/Anakunda)
// @description  Text diff implementation
// @exclude      *
// ==/UserScript==
// ==/UserLibrary==

const DIFF_DELETE = -1;
const DIFF_INSERT = 1;
const DIFF_EQUAL = 0;

function TextDiff(options) {
	if (!options) options = { };
	this.Timeout = options.timeout || 1000;
	this.EditCost = options.editCost || 4;
}

TextDiff.prototype.main = function(text1, text2, opt_checklines, opt_deadline) {
	let deadline = Date.now() + (opt_deadline > 0 ? opt_deadline : this.Timeout);
	if (text1 == null || text2 == null) throw new Error('Null input. (diff_main)');
	if (text1 == text2) {
		if (text1) return [[DIFF_EQUAL, text1]];
		return [ ];
	}
	if (typeof opt_checklines == 'undefined') opt_checklines = true;
	let checklines = opt_checklines;
	let commonlength = this.commonPrefix(text1, text2);
	let commonprefix = text1.substring(0, commonlength);
	text1 = text1.substring(commonlength);
	text2 = text2.substring(commonlength);
	commonlength = this.commonSuffix(text1, text2);
	let commonsuffix = text1.substring(text1.length - commonlength);
	text1 = text1.substring(0, text1.length - commonlength);
	text2 = text2.substring(0, text2.length - commonlength);
	let diffs = this.compute_(text1, text2, checklines, deadline);
	if (commonprefix) diffs.unshift([DIFF_EQUAL, commonprefix]);
	if (commonsuffix) diffs.push([DIFF_EQUAL, commonsuffix]);
	this.cleanupMerge(diffs);
	return diffs;
};

TextDiff.prototype.compute_ = function(text1, text2, checklines, deadline) {
	if (!text1) return [[DIFF_INSERT, text2]];
	if (!text2) return [[DIFF_DELETE, text1]];
	let longtext = text1.length > text2.length ? text1 : text2;
	let shorttext = text1.length > text2.length ? text2 : text1;
	let i = longtext.indexOf(shorttext), diffs;
	if (i != -1) {
		diffs = [[DIFF_INSERT, longtext.substring(0, i)],
						 [DIFF_EQUAL, shorttext],
						 [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
		if (text1.length > text2.length) diffs[0][0] = diffs[2][0] = DIFF_DELETE;
		return diffs;
	}
	if (shorttext.length == 1) return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
	let hm = this.halfMatch_(text1, text2);
	if (hm) {
		let text1_a = hm[0], text1_b = hm[1], text2_a = hm[2], text2_b = hm[3];
		let mid_common = hm[4];
		let diffs_a = this.main(text1_a, text2_a, checklines, deadline);
		let diffs_b = this.main(text1_b, text2_b, checklines, deadline);
		return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
	}
	if (checklines && text1.length > 100 && text2.length > 100)
		return this.lineMode_(text1, text2, deadline);
	return this.bisect_(text1, text2, deadline);
};

TextDiff.prototype.lineMode_ = function(text1, text2, deadline) {
	let a = this.linesToChars_(text1, text2);
	text1 = a.chars1; text2 = a.chars2;
	let linearray = a.lineArray;
	let diffs = this.main(text1, text2, false, deadline);
	this.charsToLines_(diffs, linearray);
	this.cleanupSemantic(diffs);
	diffs.push([DIFF_EQUAL, '']);
	let pointer = 0;
	let count_delete = 0, count_insert = 0;
	let text_delete = '', text_insert = '';
	while (pointer < diffs.length) {
		switch (diffs[pointer][0]) {
			case DIFF_INSERT:
				++count_insert;
				text_insert += diffs[pointer][1];
				break;
			case DIFF_DELETE:
				++count_delete;
				text_delete += diffs[pointer][1];
				break;
			case DIFF_EQUAL:
				if (count_delete >= 1 && count_insert >= 1) {
					diffs.splice(pointer - count_delete - count_insert, count_delete + count_insert);
					pointer = pointer - count_delete - count_insert;
					a = this.main(text_delete, text_insert, false, deadline);
					for (var j = a.length - 1; j >= 0; j--) diffs.splice(pointer, 0, a[j]);
					pointer = pointer + a.length;
				}
				count_insert = 0; count_delete = 0;
				text_delete = ''; text_insert = '';
				break;
		}
		++pointer;
	}
	diffs.pop();
	return diffs;
};

TextDiff.prototype.bisect_ = function(text1, text2, deadline) {
	let text1_length = text1.length, text2_length = text2.length;
	let max_d = Math.ceil((text1_length + text2_length) / 2);
	let v_offset = max_d, v_length = 2 * max_d;
	let v1 = new Array(v_length), v2 = new Array(v_length);
	for (var x = 0; x < v_length; x++) { v1[x] = -1; v2[x] = -1; }
	v1[v_offset + 1] = 0; v2[v_offset + 1] = 0;
	let delta = text1_length - text2_length, front = (delta % 2 != 0);
	let k1start = 0, k1end = 0;
	let k2start = 0, k2end = 0;
	for (let d = 0; d < max_d; ++d) {
		if (deadline > 0 && Date.now() > deadline) break;
		for (let k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
			let k1_offset = v_offset + k1;
			let x1 = k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) ?
    		    v1[k1_offset + 1] : v1[k1_offset - 1] + 1;
			let y1 = x1 - k1;
			while (x1 < text1_length && y1 < text2_length && text1.charAt(x1) == text2.charAt(y1)) {
				++x1;
				++y1;
			}
			v1[k1_offset] = x1;
			if (x1 > text1_length) k1end += 2;
			else if (y1 > text2_length) k1start += 2;
			else if (front) {
				let k2_offset = v_offset + delta - k1;
				if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
					let x2 = text1_length - v2[k2_offset];
					if (x1 >= x2) return this.bisectSplit_(text1, text2, x1, y1, deadline);
				}
			}
		}

		for (let k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
			let k2_offset = v_offset + k2;
			let x2 = k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) ?
			    v2[k2_offset + 1] : v2[k2_offset - 1] + 1;
			let y2 = x2 - k2;
			while (x2 < text1_length && y2 < text2_length &&
						 text1.charAt(text1_length - x2 - 1) ==
						 text2.charAt(text2_length - y2 - 1)) {
				++x2;
				++y2;
			}
			v2[k2_offset] = x2;
			if (x2 > text1_length) k2end += 2;
			else if (y2 > text2_length) k2start += 2;
			else if (!front) {
				let k1_offset = v_offset + delta - k2;
				if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
					x1 = v1[k1_offset];
					y1 = v_offset + x1 - k1_offset;
					x2 = text1_length - x2;
					if (x1 >= x2) return this.bisectSplit_(text1, text2, x1, y1, deadline);
				}
			}
		}
	}
	return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
};

TextDiff.prototype.bisectSplit_ = function(text1, text2, x, y, deadline) {
	let text1a = text1.substring(0, x), text2a = text2.substring(0, y);
	let text1b = text1.substring(x), text2b = text2.substring(y);
	let diffs = this.main(text1a, text2a, false, deadline);
	let diffsb = this.main(text1b, text2b, false, deadline);
	return diffs.concat(diffsb);
};

TextDiff.prototype.linesToChars_ = function(text1, text2) {
	function diff_linesToCharsMunge_(text) {
		let chars = '', lineStart = 0, lineEnd = -1;
		letlineArrayLength = lineArray.length;
		while (lineEnd < text.length - 1) {
			lineEnd = text.indexOf('\n', lineStart);
			if (lineEnd == -1) lineEnd = text.length - 1;
			var line = text.substring(lineStart, lineEnd + 1);
			lineStart = lineEnd + 1;
			if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : (lineHash[line] !== undefined)) {
				chars += String.fromCharCode(lineHash[line]);
			} else {
				chars += String.fromCharCode(lineArrayLength);
				lineHash[line] = lineArrayLength;
				lineArray[lineArrayLength++] = line;
			}
		}
		return chars;
	}

	let lineArray = [ ], lineHash = { };
	lineArray[0] = '';
    return {
       chars1: diff_linesToCharsMunge_(text1),
       chars2: diff_linesToCharsMunge_(text2),
       lineArray: lineArray,
    };
};

TextDiff.prototype.charsToLines_ = function(diffs, lineArray) {
	for (let x = 0; x < diffs.length; ++x) {
		let chars = diffs[x][1], text = [ ];
		for (let y = 0; y < chars.length; ++y) text[y] = lineArray[chars.charCodeAt(y)];
		diffs[x][1] = text.join('');
	}
};

TextDiff.prototype.commonPrefix = function(text1, text2) {
	if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) return 0;
	let pointermin = 0, pointermax = Math.min(text1.length, text2.length);
	let pointermid = pointermax, pointerstart = 0;
	while (pointermin < pointermid) {
		if (text1.substring(pointerstart, pointermid) == text2.substring(pointerstart, pointermid)) {
			pointermin = pointermid;
			pointerstart = pointermin;
		} else pointermax = pointermid;
		pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
	}
	return pointermid;
};

TextDiff.prototype.commonSuffix = function(text1, text2) {
	if (!text1 || !text2 || text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1))
		return 0;
	let pointermin = 0, pointermax = Math.min(text1.length, text2.length);
	let pointermid = pointermax, pointerend = 0;
	while (pointermin < pointermid) {
		if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
				text2.substring(text2.length - pointermid, text2.length - pointerend)) {
			pointermin = pointermid;
			pointerend = pointermin;
		} else pointermax = pointermid;
		pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
	}
	return pointermid;
};

TextDiff.prototype.commonOverlap_ = function(text1, text2) {
	let text1_length = text1.length, text2_length = text2.length;
	if (text1_length == 0 || text2_length == 0) return 0;
	if (text1_length > text2_length)
		text1 = text1.substring(text1_length - text2_length);
	else if (text1_length < text2_length)
		text2 = text2.substring(0, text1_length);
	let text_length = Math.min(text1_length, text2_length);
	if (text1 == text2) return text_length;
	let best = 0, length = 1;
	while (true) {
		let pattern = text1.substring(text_length - length);
		let found = text2.indexOf(pattern);
		if (found == -1) return best;
		length += found;
		if (found == 0 || text1.substring(text_length - length) ==
				text2.substring(0, length)) {
			best = length;
			length++;
		}
	}
};

TextDiff.prototype.halfMatch_ = function(text1, text2) {
	function diff_halfMatchI_(longtext, shorttext, i) {
		let seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
		let j = -1, best_common = '';
		let best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
		while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
			let prefixLength = dmp.commonPrefix(longtext.substring(i), shorttext.substring(j));
			let suffixLength = dmp.commonSuffix(longtext.substring(0, i), shorttext.substring(0, j));
			if (best_common.length < suffixLength + prefixLength) {
				best_common = shorttext.substring(j - suffixLength, j) +
					shorttext.substring(j, j + prefixLength);
				best_longtext_a = longtext.substring(0, i - suffixLength);
				best_longtext_b = longtext.substring(i + prefixLength);
				best_shorttext_a = shorttext.substring(0, j - suffixLength);
				best_shorttext_b = shorttext.substring(j + prefixLength);
			}
		}
		return best_common.length * 2 >= longtext.length ? [
			best_longtext_a,
			best_longtext_b,
			best_shorttext_a,
			best_shorttext_b,
			best_common,
		] : null;
	}

	if (this.Timeout <= 0) return null;
	let longtext = text1.length > text2.length ? text1 : text2;
	let shorttext = text1.length > text2.length ? text2 : text1;
	if (longtext.length < 4 || shorttext.length * 2 < longtext.length) return null;
	let dmp = this, hm;
	let hm1 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 4));
	let hm2 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 2));
	if (!hm1 && !hm2) return null;
	if (!hm2) hm = hm1; else if (!hm1) hm = hm2;
	else hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
	let text1_a, text1_b, text2_a, text2_b;
	if (text1.length > text2.length) {
		text1_a = hm[0]; text1_b = hm[1];
		text2_a = hm[2]; text2_b = hm[3];
	} else {
		text2_a = hm[0]; text2_b = hm[1];
		text1_a = hm[2]; text1_b = hm[3];
	}
	return [text1_a, text1_b, text2_a, text2_b, hm[4]];
};

TextDiff.prototype.cleanupSemantic = function(diffs) {
	let changes = false, equalities = [ ], equalitiesLength = 0;
	let lastequality = null, pointer = 0;
	let length_insertions1 = 0, length_deletions1 = 0;
	let length_insertions2 = 0, length_deletions2 = 0;
	while (pointer < diffs.length) {
		if (diffs[pointer][0] == DIFF_EQUAL) {
			equalities[equalitiesLength++] = pointer;
			length_insertions1 = length_insertions2;
			length_deletions1 = length_deletions2;
			length_insertions2 = 0;
			length_deletions2 = 0;
			lastequality = diffs[pointer][1];
		} else {
			if (diffs[pointer][0] == DIFF_INSERT) {
				length_insertions2 += diffs[pointer][1].length;
			} else length_deletions2 += diffs[pointer][1].length;
			if (lastequality && (lastequality.length <=
					Math.max(length_insertions1, length_deletions1)) &&
					(lastequality.length <= Math.max(length_insertions2, length_deletions2))) {
				diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]);
				diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
				--equalitiesLength;
				--equalitiesLength;
				pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
				length_insertions1 = 0;
				length_deletions1 = 0;
				length_insertions2 = 0;
				length_deletions2 = 0;
				lastequality = null;
				changes = true;
			}
		}
		++pointer;
	}
	if (changes) this.cleanupMerge(diffs);
	this.cleanupSemanticLossless(diffs);
	pointer = 1;
	while (pointer < diffs.length) {
		if (diffs[pointer - 1][0] == DIFF_DELETE && diffs[pointer][0] == DIFF_INSERT) {
			let deletion = diffs[pointer - 1][1], insertion = diffs[pointer][1];
			let overlap_length1 = this.commonOverlap_(deletion, insertion);
			let overlap_length2 = this.commonOverlap_(insertion, deletion);
			if (overlap_length1 >= overlap_length2) {
				if (overlap_length1 >= deletion.length / 2 ||
						overlap_length1 >= insertion.length / 2) {
					diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlap_length1)]);
					diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlap_length1);
					diffs[pointer + 1][1] = insertion.substring(overlap_length1);
					++pointer;
				}
			} else {
				if (overlap_length2 >= deletion.length / 2 || overlap_length2 >= insertion.length / 2) {
					diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlap_length2)]);
					diffs[pointer - 1][0] = DIFF_INSERT;
					diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlap_length2);
					diffs[pointer + 1][0] = DIFF_DELETE;
					diffs[pointer + 1][1] = deletion.substring(overlap_length2);
					++pointer;
				}
			}
			++pointer;
		}
		++pointer;
	}
};

TextDiff.prototype.cleanupSemanticLossless = function(diffs) {
	function diff_cleanupSemanticScore_(one, two) {
		if (!one || !two) return 6;
		let char1 = one.charAt(one.length - 1), char2 = two.charAt(0);
		let nonAlphaNumeric1 = char1.match(TextDiff.nonAlphaNumericRegex_);
		let nonAlphaNumeric2 = char2.match(TextDiff.nonAlphaNumericRegex_);
		let whitespace1 = nonAlphaNumeric1 && char1.match(TextDiff.whitespaceRegex_);
		let whitespace2 = nonAlphaNumeric2 && char2.match(TextDiff.whitespaceRegex_);
		let lineBreak1 = whitespace1 && char1.match(TextDiff.linebreakRegex_);
		let lineBreak2 = whitespace2 && char2.match(TextDiff.linebreakRegex_);
		let blankLine1 = lineBreak1 && one.match(TextDiff.blanklineEndRegex_);
		let blankLine2 = lineBreak2 && two.match(TextDiff.blanklineStartRegex_);
		if (blankLine1 || blankLine2) return 5;
		if (lineBreak1 || lineBreak2) return 4;
		if (nonAlphaNumeric1 && !whitespace1 && whitespace2) return 3;
		if (whitespace1 || whitespace2) return 2;
		if (nonAlphaNumeric1 || nonAlphaNumeric2) return 1;
		return 0;
	}

	let pointer = 1;
	while (pointer < diffs.length - 1) {
		if (diffs[pointer - 1][0] == DIFF_EQUAL && diffs[pointer + 1][0] == DIFF_EQUAL) {
			let equality1 = diffs[pointer - 1][1], equality2 = diffs[pointer + 1][1];
			let edit = diffs[pointer][1];
			let commonOffset = this.commonSuffix(equality1, edit);
			if (commonOffset) {
				var commonString = edit.substring(edit.length - commonOffset);
				equality1 = equality1.substring(0, equality1.length - commonOffset);
				edit = commonString + edit.substring(0, edit.length - commonOffset);
				equality2 = commonString + equality2;
			}

			let bestEquality1 = equality1, bestEquality2 = equality2;
			let bestEdit = edit;
			let bestScore = diff_cleanupSemanticScore_(equality1, edit) +
				diff_cleanupSemanticScore_(edit, equality2);
			while (edit.charAt(0) === equality2.charAt(0)) {
				equality1 += edit.charAt(0);
				edit = edit.substring(1) + equality2.charAt(0);
				equality2 = equality2.substring(1);
				let score = diff_cleanupSemanticScore_(equality1, edit) +
					diff_cleanupSemanticScore_(edit, equality2);
				if (score >= bestScore) {
					bestScore = score;
					bestEquality1 = equality1;
					bestEdit = edit;
					bestEquality2 = equality2;
				}
			}
			if (diffs[pointer - 1][1] != bestEquality1) {
				if (bestEquality1) diffs[pointer - 1][1] = bestEquality1; else {
					diffs.splice(pointer - 1, 1);
					--pointer;
				}
				diffs[pointer][1] = bestEdit;
				if (bestEquality2) diffs[pointer + 1][1] = bestEquality2; else {
					diffs.splice(pointer + 1, 1);
					--pointer;
				}
			}
		}
		++pointer;
	}
};

TextDiff.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/;
TextDiff.whitespaceRegex_ = /\s/;
TextDiff.linebreakRegex_ = /[\r\n]/;
TextDiff.blanklineEndRegex_ = /\n\r?\n$/;
TextDiff.blanklineStartRegex_ = /^\r?\n\r?\n/;

TextDiff.prototype.cleanupEfficiency = function(diffs) {
	let changes = false, equalities = [ ], equalitiesLength = 0;
	let lastequality = null, pointer = 0, pre_ins = false, pre_del = false;
	let post_ins = false, post_del = false;
	while (pointer < diffs.length) {
		if (diffs[pointer][0] == DIFF_EQUAL) {
			if (diffs[pointer][1].length < this.EditCost && (post_ins || post_del)) {
				equalities[equalitiesLength++] = pointer;
				pre_ins = post_ins; pre_del = post_del;
				lastequality = diffs[pointer][1];
			} else {
				equalitiesLength = 0;
				lastequality = null;
			}
			post_ins = post_del = false;
		} else {
			if (diffs[pointer][0] == DIFF_DELETE) post_del = true; else post_ins = true;
			if (lastequality && ((pre_ins && pre_del && post_ins && post_del)
					|| ((lastequality.length < this.EditCost / 2)
					&& (pre_ins + pre_del + post_ins + post_del) == 3))) {
				diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]);
				diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
				--equalitiesLength;
				lastequality = null;
				if (pre_ins && pre_del) {
					post_ins = post_del = true;
					equalitiesLength = 0;
				} else {
					--equalitiesLength;
					pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
					post_ins = post_del = false;
				}
				changes = true;
			}
		}
		++pointer;
	}
	if (changes) this.cleanupMerge(diffs);
};

TextDiff.prototype.cleanupMerge = function(diffs) {
	diffs.push([DIFF_EQUAL, '']);
	let pointer = 0, count_delete = 0, count_insert = 0;
	let text_delete = '', text_insert = '', commonlength;
	while (pointer < diffs.length) {
		switch (diffs[pointer][0]) {
			case DIFF_INSERT:
				++count_insert;
				text_insert += diffs[pointer++][1];
				break;
			case DIFF_DELETE:
				++count_delete;
				text_delete += diffs[pointer++][1];
				break;
			case DIFF_EQUAL:
				if (count_delete + count_insert > 1) {
					if (count_delete !== 0 && count_insert !== 0) {
						commonlength = this.commonPrefix(text_insert, text_delete);
						if (commonlength !== 0) {
							if ((pointer - count_delete - count_insert) > 0 &&
									diffs[pointer - count_delete - count_insert - 1][0] ==
									DIFF_EQUAL) {
								diffs[pointer - count_delete - count_insert - 1][1] +=
										text_insert.substring(0, commonlength);
							} else {
								diffs.splice(0, 0, [DIFF_EQUAL, text_insert.substring(0, commonlength)]);
								++pointer;
							}
							text_insert = text_insert.substring(commonlength);
							text_delete = text_delete.substring(commonlength);
						}
						commonlength = this.commonSuffix(text_insert, text_delete);
						if (commonlength !== 0) {
							diffs[pointer][1] = text_insert.substring(text_insert.length -
									commonlength) + diffs[pointer][1];
							text_insert = text_insert.substring(0, text_insert.length -
									commonlength);
							text_delete = text_delete.substring(0, text_delete.length -
									commonlength);
						}
					}
					if (count_delete === 0) {
						diffs.splice(pointer - count_insert,
								count_delete + count_insert, [DIFF_INSERT, text_insert]);
					} else if (count_insert === 0) {
						diffs.splice(pointer - count_delete,
								count_delete + count_insert, [DIFF_DELETE, text_delete]);
					} else {
						diffs.splice(pointer - count_delete - count_insert,
								count_delete + count_insert, [DIFF_DELETE, text_delete],
								[DIFF_INSERT, text_insert]);
					}
					pointer = pointer - count_delete - count_insert +
						(count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
				} else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
					diffs[pointer - 1][1] += diffs[pointer][1];
					diffs.splice(pointer, 1);
				} else ++pointer;
				count_insert = 0; count_delete = 0;
				text_delete = ''; text_insert = '';
				break;
		}
	}
	if (diffs[diffs.length - 1][1] === '') diffs.pop();
	let changes = false;
	pointer = 1;
	while (pointer < diffs.length - 1) {
		if (diffs[pointer - 1][0] == DIFF_EQUAL && diffs[pointer + 1][0] == DIFF_EQUAL) {
			if (diffs[pointer][1].substring(diffs[pointer][1].length -
					diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
				diffs[pointer][1] = diffs[pointer - 1][1] +
					diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length);
				diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
				diffs.splice(pointer - 1, 1);
				changes = true;
			} else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == diffs[pointer + 1][1]) {
				diffs[pointer - 1][1] += diffs[pointer + 1][1];
				diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
					diffs[pointer + 1][1];
				diffs.splice(pointer + 1, 1);
				changes = true;
			}
		}
		++pointer;
	}
	if (changes) this.cleanupMerge(diffs);
};

TextDiff.prototype.xIndex = function(diffs, loc) {
	let chars1 = 0, chars2 = 0, last_chars1 = 0, last_chars2 = 0;
	for (var x = 0; x < diffs.length; ++x) {
		if (diffs[x][0] !== DIFF_INSERT) chars1 += diffs[x][1].length;
		if (diffs[x][0] !== DIFF_DELETE) chars2 += diffs[x][1].length;
		if (chars1 > loc) break;
		last_chars1 = chars1; last_chars2 = chars2;
	}
	if (diffs.length != x && diffs[x][0] === DIFF_DELETE) return last_chars2;
	return last_chars2 + (loc - last_chars1);
};

TextDiff.prototype.prettyHtml = function(diffs) {
	let html = [ ];
	let pattern_amp = /&/g, pattern_br = /\n/g;
	let pattern_lt = /</g, pattern_gt = />/g;
	for (let x = 0; x < diffs.length; x++) {
		let op = diffs[x][0], data = diffs[x][1];
		let text = data.replace(pattern_amp, '&amp;').replace(pattern_lt, '&lt;')
			.replace(pattern_gt, '&gt;').replace(pattern_br, '<br/>');
		switch (op) {
			case DIFF_INSERT: html[x] = '<ins>' + text + '</ins>'; break;
			case DIFF_DELETE: html[x] = '<del>' + text + '</del>'; break;
			case DIFF_EQUAL: html[x] = '<span>' + text + '</span>'; break;
		}
	}
	return html.join('');
};

TextDiff.prototype.text1 = function(diffs) {
	let text = [ ];
	for (let x = 0; x < diffs.length; ++x)
		if (diffs[x][0] !== DIFF_INSERT) text[x] = diffs[x][1];
	return text.join('');
};

TextDiff.prototype.text2 = function(diffs) {
	let text = [ ];
	for (let x = 0; x < diffs.length; ++x)
		if (diffs[x][0] !== DIFF_DELETE) text[x] = diffs[x][1];
	return text.join('');
};

TextDiff.prototype.levenshtein = function(diffs) {
	let levenshtein = 0, insertions = 0, deletions = 0;
	for (let x = 0; x < diffs.length; ++x) {
		let op = diffs[x][0], data = diffs[x][1];
		switch (op) {
			case DIFF_INSERT: insertions += data.length; break;
			case DIFF_DELETE: deletions += data.length; break;
			case DIFF_EQUAL:
				levenshtein += Math.max(insertions, deletions);
				insertions = 0; deletions = 0;
				break;
		}
	}
	return (levenshtein += Math.max(insertions, deletions));
};

TextDiff.prototype.toDelta = function(diffs) {
	let text = [ ];
	for (let x = 0; x < diffs.length; ++x) {
		switch (diffs[x][0]) {
			case DIFF_INSERT: text[x] = '+' + encodeURI(diffs[x][1]); break;
			case DIFF_DELETE: text[x] = '-' + diffs[x][1].length; break;
			case DIFF_EQUAL: text[x] = '=' + diffs[x][1].length; break;
		}
	}
	return text.join('\t').replace(/%20/g, ' ');
};

TextDiff.prototype.fromDelta = function(text1, delta) {
	let diffs = [ ], diffsLength = 0, pointer = 0, tokens = delta.split(/\t/g);
	for (let x = 0; x < tokens.length; ++x) {
		let param = tokens[x].substring(1);
		switch (tokens[x].charAt(0)) {
			case '+':
				try {
					diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)];
				} catch (ex) {
					throw new Error('Illegal escape in diff_fromDelta: ' + param);
				}
				break;
			case '-':
			case '=': {
				let n = parseInt(param, 10);
				if (isNaN(n) || n < 0) throw new Error('Invalid number in diff_fromDelta: ' + param);
				let text = text1.substring(pointer, pointer += n);
				if (tokens[x].charAt(0) == '=') diffs[diffsLength++] = [DIFF_EQUAL, text];
				else diffs[diffsLength++] = [DIFF_DELETE, text];
				break;
			}
			default:
				if (tokens[x]) throw new Error('Invalid TextDiff operation in diff_fromDelta: ' + tokens[x]);
		}
	}
	if (pointer != text1.length) throw new Error('Delta length (' + pointer +
		') does not equal source text length (' + text1.length + ').');
	return diffs;
};