commit
b37dab1f49
@ -1,88 +0,0 @@
|
|||||||
define(function () {
|
|
||||||
|
|
||||||
/* 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`
|
|
||||||
*/
|
|
||||||
var applyChange = function(ctx, oldval, newval) {
|
|
||||||
// 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++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result;
|
|
||||||
|
|
||||||
/* throw some assertions in here before dropping patches into the realtime
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (oldval.length !== commonStart + commonEnd) {
|
|
||||||
if (ctx.localChange) { ctx.localChange(true); }
|
|
||||||
result = oldval.length - commonStart - commonEnd;
|
|
||||||
ctx.remove(commonStart, result);
|
|
||||||
console.log('removal at position: %s, length: %s', commonStart, result);
|
|
||||||
console.log("remove: [" + oldval.slice(commonStart, commonStart + result ) + ']');
|
|
||||||
}
|
|
||||||
if (newval.length !== commonStart + commonEnd) {
|
|
||||||
if (ctx.localChange) { ctx.localChange(true); }
|
|
||||||
result = newval.slice(commonStart, newval.length - commonEnd);
|
|
||||||
ctx.insert(commonStart, result);
|
|
||||||
console.log("insert: [" + result + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
var userDoc;
|
|
||||||
try {
|
|
||||||
var userDoc = ctx.getUserDoc();
|
|
||||||
JSON.parse(userDoc);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[textPatcherParseErr]');
|
|
||||||
console.error(err);
|
|
||||||
window.REALTIME_MODULE.textPatcher_parseError = {
|
|
||||||
error: err,
|
|
||||||
userDoc: userDoc
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var create = function(config) {
|
|
||||||
var ctx = config.realtime;
|
|
||||||
|
|
||||||
// initial state will always fail the !== check in genop.
|
|
||||||
// because nothing will equal this object
|
|
||||||
var content = {};
|
|
||||||
|
|
||||||
// *** remote -> local changes
|
|
||||||
ctx.onPatch(function(pos, length) {
|
|
||||||
content = ctx.getUserDoc()
|
|
||||||
});
|
|
||||||
|
|
||||||
// propogate()
|
|
||||||
return function (newContent) {
|
|
||||||
if (newContent !== content) {
|
|
||||||
applyChange(ctx, ctx.getUserDoc(), newContent);
|
|
||||||
if (ctx.getUserDoc() !== newContent) {
|
|
||||||
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return { create: create };
|
|
||||||
});
|
|
@ -0,0 +1,124 @@
|
|||||||
|
define([], function () {
|
||||||
|
// this makes recursing a lot simpler
|
||||||
|
var isArray = function (A) {
|
||||||
|
return Object.prototype.toString.call(A)==='[object Array]';
|
||||||
|
};
|
||||||
|
|
||||||
|
var parseStyle = function(el){
|
||||||
|
var style = el.style;
|
||||||
|
var output = {};
|
||||||
|
for (var i = 0; i < style.length; ++i) {
|
||||||
|
var item = style.item(i);
|
||||||
|
output[item] = style[item];
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
var callOnHyperJSON = function (hj, cb) {
|
||||||
|
var children;
|
||||||
|
|
||||||
|
if (hj && hj[2]) {
|
||||||
|
children = hj[2].map(function (child) {
|
||||||
|
if (isArray(child)) {
|
||||||
|
// if the child is an array, recurse
|
||||||
|
return callOnHyperJSON(child, cb);
|
||||||
|
} else if (typeof (child) === 'string') {
|
||||||
|
// string nodes have leading and trailing quotes
|
||||||
|
return child.replace(/(^"|"$)/g,"");
|
||||||
|
} else {
|
||||||
|
// the above branches should cover all methods
|
||||||
|
// if we hit this, there is a problem
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
children = [];
|
||||||
|
}
|
||||||
|
// this should return the top level element of your new DOM
|
||||||
|
return cb(hj[0], hj[1], children);
|
||||||
|
};
|
||||||
|
|
||||||
|
var classify = function (token) {
|
||||||
|
return '.' + token.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
var isValidClass = function (x) {
|
||||||
|
if (x && /\S/.test(x)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var isTruthy = function (x) {
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
var DOM2HyperJSON = function(el, predicate, filter){
|
||||||
|
if(!el.tagName && el.nodeType === Node.TEXT_NODE){
|
||||||
|
return el.textContent;
|
||||||
|
}
|
||||||
|
if(!el.attributes){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (predicate) {
|
||||||
|
if (!predicate(el)) {
|
||||||
|
// shortcircuit
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributes = {};
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
for(;i < el.attributes.length; i++){
|
||||||
|
var attr = el.attributes[i];
|
||||||
|
if(attr.name && attr.value){
|
||||||
|
if(attr.name === "style"){
|
||||||
|
attributes.style = parseStyle(el);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
attributes[attr.name] = attr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should never be longer than three elements
|
||||||
|
var result = [];
|
||||||
|
|
||||||
|
// get the element type, id, and classes of the element
|
||||||
|
// and push them to the result array
|
||||||
|
var sel = el.tagName;
|
||||||
|
|
||||||
|
if(attributes.id){
|
||||||
|
// we don't have to do much to validate IDs because the browser
|
||||||
|
// will only permit one id to exist
|
||||||
|
// unless we come across a strange browser in the wild
|
||||||
|
sel = sel +'#'+ attributes.id;
|
||||||
|
delete attributes.id;
|
||||||
|
}
|
||||||
|
result.push(sel);
|
||||||
|
|
||||||
|
// second element of the array is the element attributes
|
||||||
|
result.push(attributes);
|
||||||
|
|
||||||
|
// third element of the array is an array of child nodes
|
||||||
|
var children = [];
|
||||||
|
|
||||||
|
// js hint complains if we use 'var' here
|
||||||
|
i = 0;
|
||||||
|
for(; i < el.childNodes.length; i++){
|
||||||
|
children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter));
|
||||||
|
}
|
||||||
|
result.push(children.filter(isTruthy));
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
return filter(result);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fromDOM: DOM2HyperJSON,
|
||||||
|
callOn: callOnHyperJSON
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Serialization tests</h1>
|
||||||
|
|
||||||
|
<h2>Test 1</h2>
|
||||||
|
<h3>class strings</h3>
|
||||||
|
<!-- put in weird HTML that might cause problems -->
|
||||||
|
<div id="target"><p class=" alice bob charlie has.dot" id="bang">pewpewpew</p></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Test 2</h2>
|
||||||
|
<h3>XWiki Macros</h3>
|
||||||
|
|
||||||
|
<!-- Can we serialize XWiki Macros? -->
|
||||||
|
<div id="widget"><div data-cke-widget-id="0" tabindex="-1" data-cke-widget-wrapper="1" data-cke-filter="off" class="cke_widget_wrapper cke_widget_block" data-cke-display-name="macro:velocity" contenteditable="false"><div class="macro cke_widget_element" data-macro="startmacro:velocity|-||-|Here is a macro" data-cke-widget-data="%7B%22classes%22%3A%7B%22macro%22%3A1%7D%7D" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="xwiki-macro"><p>Here is a macro</p></div><span style='background: rgba(220, 220, 220, 0.5) url("/common/cryptofist.png") repeat scroll 0% 0%; top: -15px; left: 0px; display: block;' class="cke_reset cke_widget_drag_handler_container"><img title="Click and drag to move" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" data-cke-widget-drag-handler="1" class="cke_reset cke_widget_drag_handler" height="15" width="15"></span></div></div>
|
||||||
|
|
||||||
|
<hr>
|
@ -0,0 +1,50 @@
|
|||||||
|
define([
|
||||||
|
'/bower_components/jquery/dist/jquery.min.js',
|
||||||
|
'/assert/hyperjson.js', // serializing classes as an attribute
|
||||||
|
'/assert/hyperscript.js', // using setAttribute
|
||||||
|
'/common/TextPatcher.js'
|
||||||
|
], function (jQuery, Hyperjson, Hyperscript, TextPatcher) {
|
||||||
|
var $ = window.jQuery;
|
||||||
|
window.Hyperjson = Hyperjson;
|
||||||
|
window.Hyperscript = Hyperscript;
|
||||||
|
window.TextPatcher = TextPatcher;
|
||||||
|
|
||||||
|
var assertions = 0;
|
||||||
|
|
||||||
|
var assert = function (test, msg) {
|
||||||
|
if (test()) {
|
||||||
|
assertions++;
|
||||||
|
} else {
|
||||||
|
throw new Error(msg || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var $body = $('body');
|
||||||
|
|
||||||
|
var roundTrip = function (target) {
|
||||||
|
assert(function () {
|
||||||
|
var hjson = Hyperjson.fromDOM(target);
|
||||||
|
var cloned = Hyperjson.callOn(hjson, Hyperscript);
|
||||||
|
|
||||||
|
var success = cloned.outerHTML === target.outerHTML;
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
window.DEBUG = {
|
||||||
|
error: "Expected equality between A and B",
|
||||||
|
A: target.outerHTML,
|
||||||
|
B: cloned.outerHTML,
|
||||||
|
target: target,
|
||||||
|
diff: TextPatcher.diff(target.outerHTML, cloned.outerHTML)
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(window.DEBUG, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}, "Round trip serialization introduced artifacts.");
|
||||||
|
};
|
||||||
|
|
||||||
|
roundTrip($('#target')[0]);
|
||||||
|
roundTrip($('#widget')[0]);
|
||||||
|
|
||||||
|
console.log("%s test%s passed", assertions, assertions === 1? '':'s');
|
||||||
|
});
|
@ -0,0 +1,120 @@
|
|||||||
|
define(function () {
|
||||||
|
|
||||||
|
/* 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).
|
||||||
|
|
||||||
|
diff is purely functional.
|
||||||
|
*/
|
||||||
|
var diff = function (oldval, newval) {
|
||||||
|
// 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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toRemove;
|
||||||
|
var toInsert;
|
||||||
|
|
||||||
|
/* throw some assertions in here before dropping patches into the realtime */
|
||||||
|
if (oldval.length !== commonStart + commonEnd) {
|
||||||
|
toRemove = oldval.length - commonStart - commonEnd;
|
||||||
|
}
|
||||||
|
if (newval.length !== commonStart + commonEnd) {
|
||||||
|
toInsert = newval.slice(commonStart, newval.length - commonEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Operation',
|
||||||
|
offset: commonStart,
|
||||||
|
toInsert: toInsert,
|
||||||
|
toRemove: toRemove
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
console.log({
|
||||||
|
insert: op.toInsert,
|
||||||
|
remove: text.slice(op.offset, op.offset + op.toRemove)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
var applyChange = function(ctx, oldval, newval) {
|
||||||
|
var op = diff(oldval, newval);
|
||||||
|
// log(oldval, op)
|
||||||
|
patch(ctx, op);
|
||||||
|
};
|
||||||
|
|
||||||
|
var create = function(config) {
|
||||||
|
var ctx = config.realtime;
|
||||||
|
|
||||||
|
// initial state will always fail the !== check in genop.
|
||||||
|
// because nothing will equal this object
|
||||||
|
var content = {};
|
||||||
|
|
||||||
|
// *** remote -> local changes
|
||||||
|
ctx.onPatch(function(pos, length) {
|
||||||
|
content = ctx.getUserDoc()
|
||||||
|
});
|
||||||
|
|
||||||
|
// propogate()
|
||||||
|
return function (newContent) {
|
||||||
|
if (newContent !== content) {
|
||||||
|
applyChange(ctx, ctx.getUserDoc(), newContent);
|
||||||
|
if (ctx.getUserDoc() !== newContent) {
|
||||||
|
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: create, // create a TextPatcher object
|
||||||
|
diff: diff, // diff two strings
|
||||||
|
patch: patch, // apply an operation to a chainpad's realtime facade
|
||||||
|
log: log, // print the components of an operation
|
||||||
|
applyChange: applyChange // a convenient wrapper around diff/log/patch
|
||||||
|
};
|
||||||
|
});
|
Loading…
Reference in New Issue