UNPKG

phaser4-rex-plugins

Version:
236 lines (206 loc) 8.07 kB
export default function Diff() {} Diff.prototype = { diff(oldString, newString, options = {}) { let callback = options.callback; if (typeof options === 'function') { callback = options; options = {}; } this.options = options; let self = this; function done(value) { if (callback) { setTimeout(function() { callback(undefined, value); }, 0); return true; } else { return value; } } // Allow subclasses to massage the input prior to running oldString = this.castInput(oldString); newString = this.castInput(newString); oldString = this.removeEmpty(this.tokenize(oldString)); newString = this.removeEmpty(this.tokenize(newString)); let newLen = newString.length, oldLen = oldString.length; let editLength = 1; let maxEditLength = newLen + oldLen; if(options.maxEditLength) { maxEditLength = Math.min(maxEditLength, options.maxEditLength); } let bestPath = [{ newPos: -1, components: [] }]; // Seed editLength = 0, i.e. the content starts with the same values let oldPos = this.extractCommon(bestPath[0], newString, oldString, 0); if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) { // Identity per the equality and tokenizer return done([{value: this.join(newString), count: newString.length}]); } // Main worker method. checks all permutations of a given edit length for acceptance. function execEditLength() { for (let diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) { let basePath; let addPath = bestPath[diagonalPath - 1], removePath = bestPath[diagonalPath + 1], oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; if (addPath) { // No one else is going to attempt to use this value, clear it bestPath[diagonalPath - 1] = undefined; } let canAdd = addPath && addPath.newPos + 1 < newLen, canRemove = removePath && 0 <= oldPos && oldPos < oldLen; if (!canAdd && !canRemove) { // If this path is a terminal then prune bestPath[diagonalPath] = undefined; continue; } // Select the diagonal that we want to branch from. We select the prior // path whose position in the new string is the farthest from the origin // and does not pass the bounds of the diff graph if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) { basePath = clonePath(removePath); self.pushComponent(basePath.components, undefined, true); } else { basePath = addPath; // No need to clone, we've pulled it from the list basePath.newPos++; self.pushComponent(basePath.components, true, undefined); } oldPos = self.extractCommon(basePath, newString, oldString, diagonalPath); // If we have hit the end of both strings, then we are done if (basePath.newPos + 1 >= newLen && oldPos + 1 >= oldLen) { return done(buildValues(self, basePath.components, newString, oldString, self.useLongestToken)); } else { // Otherwise track this path as a potential candidate and continue. bestPath[diagonalPath] = basePath; } } editLength++; } // Performs the length of edit iteration. Is a bit fugly as this has to support the // sync and async mode which is never fun. Loops over execEditLength until a value // is produced, or until the edit length exceeds options.maxEditLength (if given), // in which case it will return undefined. if (callback) { (function exec() { setTimeout(function() { if (editLength > maxEditLength) { return callback(); } if (!execEditLength()) { exec(); } }, 0); }()); } else { while (editLength <= maxEditLength) { let ret = execEditLength(); if (ret) { return ret; } } } }, pushComponent(components, added, removed) { let last = components[components.length - 1]; if (last && last.added === added && last.removed === removed) { // We need to clone here as the component clone operation is just // as shallow array clone components[components.length - 1] = {count: last.count + 1, added: added, removed: removed }; } else { components.push({count: 1, added: added, removed: removed }); } }, extractCommon(basePath, newString, oldString, diagonalPath) { let newLen = newString.length, oldLen = oldString.length, newPos = basePath.newPos, oldPos = newPos - diagonalPath, commonCount = 0; while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) { newPos++; oldPos++; commonCount++; } if (commonCount) { basePath.components.push({count: commonCount}); } basePath.newPos = newPos; return oldPos; }, equals(left, right) { if (this.options.comparator) { return this.options.comparator(left, right); } else { return left === right || (this.options.ignoreCase && left.toLowerCase() === right.toLowerCase()); } }, removeEmpty(array) { let ret = []; for (let i = 0; i < array.length; i++) { if (array[i]) { ret.push(array[i]); } } return ret; }, castInput(value) { return value; }, tokenize(value) { return value.split(''); }, join(chars) { return chars.join(''); } }; function buildValues(diff, components, newString, oldString, useLongestToken) { let componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0; for (; componentPos < componentLen; componentPos++) { let component = components[componentPos]; if (!component.removed) { if (!component.added && useLongestToken) { let value = newString.slice(newPos, newPos + component.count); value = value.map(function(value, i) { let oldValue = oldString[oldPos + i]; return oldValue.length > value.length ? oldValue : value; }); component.value = diff.join(value); } else { component.value = diff.join(newString.slice(newPos, newPos + component.count)); } newPos += component.count; // Common case if (!component.added) { oldPos += component.count; } } else { component.value = diff.join(oldString.slice(oldPos, oldPos + component.count)); oldPos += component.count; // Reverse add and remove so removes are output first to match common convention // The diffing algorithm is tied to add then remove output and this is the simplest // route to get the desired output with minimal overhead. if (componentPos && components[componentPos - 1].added) { let tmp = components[componentPos - 1]; components[componentPos - 1] = components[componentPos]; components[componentPos] = tmp; } } } // Special case handle for when one terminal is ignored (i.e. whitespace). // For this case we merge the terminal into the prior string and drop the change. // This is only available for string mode. let lastComponent = components[componentLen - 1]; if (componentLen > 1 && typeof lastComponent.value === 'string' && (lastComponent.added || lastComponent.removed) && diff.equals('', lastComponent.value)) { components[componentLen - 2].value += lastComponent.value; components.pop(); } return components; } function clonePath(path) { return { newPos: path.newPos, components: path.components.slice(0) }; }