You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
4.4 KiB
JavaScript
145 lines
4.4 KiB
JavaScript
9 years ago
|
define(function () {
|
||
|
|
||
9 years ago
|
/* diff takes two strings, the old content, and the desired content
|
||
|
it returns the difference between these two strings in the form
|
||
|
of an 'Operation' (as defined in chainpad.js).
|
||
9 years ago
|
|
||
9 years ago
|
diff is purely functional.
|
||
9 years ago
|
*/
|
||
9 years ago
|
var diff = function (oldval, newval) {
|
||
9 years ago
|
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
|
||
|
if (oldval === newval) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var commonStart = 0;
|
||
|
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
|
||
|
commonStart++;
|
||
|
}
|
||
|
|
||
|
var commonEnd = 0;
|
||
|
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
|
||
|
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
|
||
|
commonEnd++;
|
||
|
}
|
||
|
|
||
9 years ago
|
var toRemove = 0;
|
||
|
var toInsert = '';
|
||
9 years ago
|
|
||
9 years ago
|
/* throw some assertions in here before dropping patches into the realtime */
|
||
9 years ago
|
if (oldval.length !== commonStart + commonEnd) {
|
||
9 years ago
|
toRemove = oldval.length - commonStart - commonEnd;
|
||
9 years ago
|
}
|
||
|
if (newval.length !== commonStart + commonEnd) {
|
||
9 years ago
|
toInsert = newval.slice(commonStart, newval.length - commonEnd);
|
||
9 years ago
|
}
|
||
|
|
||
9 years ago
|
return {
|
||
|
type: 'Operation',
|
||
|
offset: commonStart,
|
||
|
toInsert: toInsert,
|
||
|
toRemove: toRemove
|
||
|
};
|
||
9 years ago
|
};
|
||
9 years ago
|
|
||
|
/* patch accepts a realtime facade and an operation (which might be falsey)
|
||
|
it applies the operation to the realtime as components (remove/insert)
|
||
|
|
||
|
patch has no return value, and operates solely through side effects on
|
||
|
the realtime facade.
|
||
|
*/
|
||
|
var patch = function (ctx, op) {
|
||
|
if (!op) { return; }
|
||
|
if (op.toRemove) { ctx.remove(op.offset, op.toRemove); }
|
||
|
if (op.toInsert) { ctx.insert(op.offset, op.toInsert); }
|
||
|
};
|
||
|
|
||
9 years ago
|
/* format has the same signature as log, but doesn't log to the console
|
||
|
use it to get the pretty version of a diff */
|
||
|
var format = function (text, op) {
|
||
|
return op?{
|
||
|
insert: op.toInsert,
|
||
|
remove: text.slice(op.offset, op.offset + op.toRemove)
|
||
|
}: { insert: '', remove: '' };
|
||
|
};
|
||
|
|
||
9 years ago
|
/* log accepts a string and an operation, and prints an object to the console
|
||
|
the object will display the content which is to be removed, and the content
|
||
|
which will be inserted in its place.
|
||
|
|
||
|
log is useful for debugging, but can otherwise be disabled.
|
||
|
*/
|
||
|
var log = function (text, op) {
|
||
|
if (!op) { return; }
|
||
9 years ago
|
console.log(format(text, op));
|
||
9 years ago
|
};
|
||
|
|
||
|
/* applyChange takes:
|
||
|
ctx: the context (aka the realtime)
|
||
|
oldval: the old value
|
||
|
newval: the new value
|
||
|
|
||
|
it performs a diff on the two values, and generates patches
|
||
|
which are then passed into `ctx.remove` and `ctx.insert`.
|
||
|
|
||
|
Due to its reliance on patch, applyChange has side effects on the supplied
|
||
|
realtime facade.
|
||
|
*/
|
||
9 years ago
|
var applyChange = function(ctx, oldval, newval, logging) {
|
||
9 years ago
|
var op = diff(oldval, newval);
|
||
9 years ago
|
if (logging) { log(oldval, op); }
|
||
9 years ago
|
patch(ctx, op);
|
||
9 years ago
|
};
|
||
|
|
||
9 years ago
|
var transformCursor = function (cursor, op) {
|
||
9 years ago
|
if (!op) { return cursor; }
|
||
9 years ago
|
var pos = op.offset;
|
||
|
var remove = op.toRemove;
|
||
|
var insert = op.toInsert.length;
|
||
|
if (typeof cursor === 'undefined') { return; }
|
||
|
if (typeof remove === 'number' && pos < cursor) {
|
||
|
cursor -= Math.min(remove, cursor - pos);
|
||
|
}
|
||
|
if (typeof insert === 'number' && pos < cursor) {
|
||
|
cursor += insert;
|
||
|
}
|
||
|
return cursor;
|
||
|
};
|
||
|
|
||
9 years ago
|
var create = function(config) {
|
||
9 years ago
|
var ctx = config.realtime;
|
||
9 years ago
|
var logging = config.logging;
|
||
9 years ago
|
|
||
|
// initial state will always fail the !== check in genop.
|
||
|
// because nothing will equal this object
|
||
|
var content = {};
|
||
|
|
||
|
// *** remote -> local changes
|
||
9 years ago
|
ctx.onPatch(function(pos, length) {
|
||
9 years ago
|
content = ctx.getUserDoc();
|
||
9 years ago
|
});
|
||
|
|
||
9 years ago
|
// propogate()
|
||
9 years ago
|
return function (newContent, force) {
|
||
|
if (newContent !== content || force) {
|
||
9 years ago
|
applyChange(ctx, ctx.getUserDoc(), newContent, logging);
|
||
9 years ago
|
if (ctx.getUserDoc() !== newContent) {
|
||
|
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
|
||
|
}
|
||
9 years ago
|
return true;
|
||
9 years ago
|
}
|
||
9 years ago
|
return false;
|
||
9 years ago
|
};
|
||
|
};
|
||
|
|
||
9 years ago
|
return {
|
||
|
create: create, // create a TextPatcher object
|
||
|
diff: diff, // diff two strings
|
||
|
patch: patch, // apply an operation to a chainpad's realtime facade
|
||
9 years ago
|
format: format,
|
||
9 years ago
|
log: log, // print the components of an operation
|
||
9 years ago
|
transformCursor: transformCursor, // transform the position of a cursor
|
||
9 years ago
|
applyChange: applyChange, // a convenient wrapper around diff/log/patch
|
||
9 years ago
|
};
|
||
9 years ago
|
});
|