Skip to content

Commit

Permalink
No more zero-width spaces inside textarea.
Browse files Browse the repository at this point in the history
We used zero-width spaces to keep track of mentions within textarea. It works good, but many fonts print this as an ordinary space, so we had to refuse this approach.  Now we use diff algorithm to keep track of mentions.
  • Loading branch information
Ivan Virabyan committed May 5, 2015
1 parent 18ed214 commit 5b12ab2
Show file tree
Hide file tree
Showing 3 changed files with 537 additions and 121 deletions.
142 changes: 142 additions & 0 deletions diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
function diffChars(oldString, newString) {
// Handle the identity case (this is due to unrolling editLength == 0
if (newString === oldString) {
return [{ value: newString }];
}
if (!newString) {
return [{ value: oldString, removed: true }];
}
if (!oldString) {
return [{ value: newString, added: true }];
}

var newLen = newString.length, oldLen = oldString.length;
var maxEditLength = newLen + oldLen;
var bestPath = [{ newPos: -1, components: [] }];

// Seed editLength = 0, i.e. the content starts with the same values
var oldPos = extractCommon(bestPath[0], newString, oldString, 0);
if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) {
// Identity per the equality and tokenizer
return [{value: newString}];
}

// Main worker method. checks all permutations of a given edit length for acceptance.
function execEditLength() {
for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) {
var basePath;
var 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;
}

var canAdd = addPath && addPath.newPos+1 < newLen;
var 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);
pushComponent(basePath.components, undefined, true);
} else {
basePath = addPath; // No need to clone, we've pulled it from the list
basePath.newPos++;
pushComponent(basePath.components, true, undefined);
}

var oldPos = 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 buildValues(basePath.components, newString, oldString);
} 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.
var editLength = 1;
while(editLength <= maxEditLength) {
var ret = execEditLength();
if (ret) {
return ret;
}
}
}

function buildValues(components, newString, oldString) {
var componentPos = 0,
componentLen = components.length,
newPos = 0,
oldPos = 0;

for (; componentPos < componentLen; componentPos++) {
var component = components[componentPos];
if (!component.removed) {
component.value = newString.slice(newPos, newPos + component.count);
newPos += component.count;

// Common case
if (!component.added) {
oldPos += component.count;
}
} else {
component.value = oldString.slice(oldPos, oldPos + component.count);
oldPos += component.count;
}
}

return components;
}


function pushComponent(components, added, removed) {
var 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 });
}
}

function extractCommon(basePath, newString, oldString, diagonalPath) {
var newLen = newString.length,
oldLen = oldString.length,
newPos = basePath.newPos,
oldPos = newPos - diagonalPath,

commonCount = 0;
while (newPos+1 < newLen && oldPos+1 < oldLen && newString[newPos+1] == oldString[oldPos+1]) {
newPos++;
oldPos++;
commonCount++;
}

if (commonCount) {
basePath.components.push({count: commonCount});
}

basePath.newPos = newPos;
return oldPos;
}

function clonePath(path) {
return { newPos: path.newPos, components: path.components.slice(0) };
}
Loading

0 comments on commit 5b12ab2

Please sign in to comment.