forked from ivirabyan/jquery-mentions
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
No more zero-width spaces inside textarea.
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
Showing
3 changed files
with
537 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) }; | ||
} |
Oops, something went wrong.