Merge branch 'diffdom' into netflux
Simple cleanup and unit tests
This commit is contained in:
commit
b37dab1f49
9 changed files with 723 additions and 93 deletions
|
@ -2,13 +2,13 @@ define([
|
|||
'/api/config?cb=' + Math.random().toString(16).substring(2),
|
||||
'/common/messages.js',
|
||||
'/common/crypto.js',
|
||||
'/_socket/realtime-input.js',
|
||||
'/common/RealtimeTextSocket.js',
|
||||
'/common/hyperjson.js',
|
||||
'/common/hyperscript.js',
|
||||
'/_socket/toolbar.js',
|
||||
'/common/cursor.js',
|
||||
'/common/json-ot.js',
|
||||
'/_socket/typingTest.js',
|
||||
'/common/TypingTests.js',
|
||||
'/bower_components/diff-dom/diffDOM.js',
|
||||
'/bower_components/jquery/dist/jquery.min.js',
|
||||
'/customize/pad.js'
|
||||
|
|
|
@ -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 };
|
||||
});
|
124
www/assert/hyperjson.js
Normal file
124
www/assert/hyperjson.js
Normal file
|
@ -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
|
||||
};
|
||||
});
|
400
www/assert/hyperscript.js
Normal file
400
www/assert/hyperscript.js
Normal file
|
@ -0,0 +1,400 @@
|
|||
define([], function () {
|
||||
var Hyperscript;
|
||||
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
var split = require('browser-split')
|
||||
var ClassList = require('class-list')
|
||||
require('html-element')
|
||||
|
||||
function context () {
|
||||
|
||||
var cleanupFuncs = []
|
||||
|
||||
function h() {
|
||||
var args = [].slice.call(arguments), e = null
|
||||
function item (l) {
|
||||
var r
|
||||
function parseClass (string) {
|
||||
// Our minimal parser doesn’t understand escaping CSS special
|
||||
// characters like `#`. Don’t use them. More reading:
|
||||
// https://mathiasbynens.be/notes/css-escapes .
|
||||
|
||||
var m = split(string, /([\.#]?[^\s#.]+)/)
|
||||
if(/^\.|#/.test(m[1]))
|
||||
e = document.createElement('div')
|
||||
forEach(m, function (v) {
|
||||
var s = v.substring(1,v.length)
|
||||
if(!v) return
|
||||
if(!e)
|
||||
e = document.createElement(v)
|
||||
else if (v[0] === '.')
|
||||
ClassList(e).add(s)
|
||||
else if (v[0] === '#')
|
||||
e.setAttribute('id', s)
|
||||
})
|
||||
}
|
||||
|
||||
if(l == null)
|
||||
;
|
||||
else if('string' === typeof l) {
|
||||
if(!e)
|
||||
parseClass(l)
|
||||
else
|
||||
e.appendChild(r = document.createTextNode(l))
|
||||
}
|
||||
else if('number' === typeof l
|
||||
|| 'boolean' === typeof l
|
||||
|| l instanceof Date
|
||||
|| l instanceof RegExp ) {
|
||||
e.appendChild(r = document.createTextNode(l.toString()))
|
||||
}
|
||||
//there might be a better way to handle this...
|
||||
else if (isArray(l))
|
||||
forEach(l, item)
|
||||
else if(isNode(l))
|
||||
e.appendChild(r = l)
|
||||
else if(l instanceof Text)
|
||||
e.appendChild(r = l)
|
||||
else if ('object' === typeof l) {
|
||||
for (var k in l) {
|
||||
if('function' === typeof l[k]) {
|
||||
if(/^on\w+/.test(k)) {
|
||||
(function (k, l) { // capture k, l in the closure
|
||||
if (e.addEventListener){
|
||||
e.addEventListener(k.substring(2), l[k], false)
|
||||
cleanupFuncs.push(function(){
|
||||
e.removeEventListener(k.substring(2), l[k], false)
|
||||
})
|
||||
}else{
|
||||
e.attachEvent(k, l[k])
|
||||
cleanupFuncs.push(function(){
|
||||
e.detachEvent(k, l[k])
|
||||
})
|
||||
}
|
||||
})(k, l)
|
||||
} else {
|
||||
// observable
|
||||
e[k] = l[k]()
|
||||
cleanupFuncs.push(l[k](function (v) {
|
||||
e[k] = v
|
||||
}))
|
||||
}
|
||||
}
|
||||
else if(k === 'style') {
|
||||
if('string' === typeof l[k]) {
|
||||
e.style.cssText = l[k]
|
||||
}else{
|
||||
for (var s in l[k]) (function(s, v) {
|
||||
if('function' === typeof v) {
|
||||
// observable
|
||||
e.style.setProperty(s, v())
|
||||
cleanupFuncs.push(v(function (val) {
|
||||
e.style.setProperty(s, val)
|
||||
}))
|
||||
} else
|
||||
e.style.setProperty(s, l[k][s])
|
||||
})(s, l[k][s])
|
||||
}
|
||||
} else if (k.substr(0, 5) === "data-") {
|
||||
e.setAttribute(k, l[k])
|
||||
} else {
|
||||
e.setAttribute(k, l[k])
|
||||
if (e.getAttribute(k) !== l[k]) {
|
||||
e[k] = l[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ('function' === typeof l) {
|
||||
//assume it's an observable!
|
||||
var v = l()
|
||||
e.appendChild(r = isNode(v) ? v : document.createTextNode(v))
|
||||
|
||||
cleanupFuncs.push(l(function (v) {
|
||||
if(isNode(v) && r.parentElement)
|
||||
r.parentElement.replaceChild(v, r), r = v
|
||||
else
|
||||
r.textContent = v
|
||||
}))
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
while(args.length)
|
||||
item(args.shift())
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
h.cleanup = function () {
|
||||
for (var i = 0; i < cleanupFuncs.length; i++){
|
||||
cleanupFuncs[i]()
|
||||
}
|
||||
cleanupFuncs.length = 0
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
var h = module.exports = context()
|
||||
h.context = context
|
||||
|
||||
Hyperscript = h;
|
||||
|
||||
function isNode (el) {
|
||||
return el && el.nodeName && el.nodeType
|
||||
}
|
||||
|
||||
function forEach (arr, fn) {
|
||||
if (arr.forEach) return arr.forEach(fn)
|
||||
for (var i = 0; i < arr.length; i++) fn(arr[i], i)
|
||||
}
|
||||
|
||||
function isArray (arr) {
|
||||
return Object.prototype.toString.call(arr) == '[object Array]'
|
||||
}
|
||||
|
||||
},{"browser-split":2,"class-list":3,"html-element":6}],2:[function(require,module,exports){
|
||||
/*!
|
||||
* Cross-Browser Split 1.1.1
|
||||
* Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
|
||||
* Available under the MIT License
|
||||
* ECMAScript compliant, uniform cross-browser split method
|
||||
*/
|
||||
|
||||
/**
|
||||
* Splits a string into an array of strings using a regex or string separator. Matches of the
|
||||
* separator are not included in the result array. However, if `separator` is a regex that contains
|
||||
* capturing groups, backreferences are spliced into the result each time `separator` is matched.
|
||||
* Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
|
||||
* cross-browser.
|
||||
* @param {String} str String to split.
|
||||
* @param {RegExp|String} separator Regex or string to use for separating the string.
|
||||
* @param {Number} [limit] Maximum number of items to include in the result array.
|
||||
* @returns {Array} Array of substrings.
|
||||
* @example
|
||||
*
|
||||
* // Basic use
|
||||
* split('a b c d', ' ');
|
||||
* // -> ['a', 'b', 'c', 'd']
|
||||
*
|
||||
* // With limit
|
||||
* split('a b c d', ' ', 2);
|
||||
* // -> ['a', 'b']
|
||||
*
|
||||
* // Backreferences in result array
|
||||
* split('..word1 word2..', /([a-z]+)(\d+)/i);
|
||||
* // -> ['..', 'word', '1', ' ', 'word', '2', '..']
|
||||
*/
|
||||
module.exports = (function split(undef) {
|
||||
|
||||
var nativeSplit = String.prototype.split,
|
||||
compliantExecNpcg = /()??/.exec("")[1] === undef,
|
||||
// NPCG: nonparticipating capturing group
|
||||
self;
|
||||
|
||||
self = function(str, separator, limit) {
|
||||
// If `separator` is not a regex, use `nativeSplit`
|
||||
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
|
||||
return nativeSplit.call(str, separator, limit);
|
||||
}
|
||||
var output = [],
|
||||
flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6
|
||||
(separator.sticky ? "y" : ""),
|
||||
// Firefox 3+
|
||||
lastLastIndex = 0,
|
||||
// Make `global` and avoid `lastIndex` issues by working with a copy
|
||||
separator = new RegExp(separator.source, flags + "g"),
|
||||
separator2, match, lastIndex, lastLength;
|
||||
str += ""; // Type-convert
|
||||
if (!compliantExecNpcg) {
|
||||
// Doesn't need flags gy, but they don't hurt
|
||||
separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
|
||||
}
|
||||
/* Values for `limit`, per the spec:
|
||||
* If undefined: 4294967295 // Math.pow(2, 32) - 1
|
||||
* If 0, Infinity, or NaN: 0
|
||||
* If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
|
||||
* If negative number: 4294967296 - Math.floor(Math.abs(limit))
|
||||
* If other: Type-convert, then use the above rules
|
||||
*/
|
||||
limit = limit === undef ? -1 >>> 0 : // Math.pow(2, 32) - 1
|
||||
limit >>> 0; // ToUint32(limit)
|
||||
while (match = separator.exec(str)) {
|
||||
// `separator.lastIndex` is not reliable cross-browser
|
||||
lastIndex = match.index + match[0].length;
|
||||
if (lastIndex > lastLastIndex) {
|
||||
output.push(str.slice(lastLastIndex, match.index));
|
||||
// Fix browsers whose `exec` methods don't consistently return `undefined` for
|
||||
// nonparticipating capturing groups
|
||||
if (!compliantExecNpcg && match.length > 1) {
|
||||
match[0].replace(separator2, function() {
|
||||
for (var i = 1; i < arguments.length - 2; i++) {
|
||||
if (arguments[i] === undef) {
|
||||
match[i] = undef;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (match.length > 1 && match.index < str.length) {
|
||||
Array.prototype.push.apply(output, match.slice(1));
|
||||
}
|
||||
lastLength = match[0].length;
|
||||
lastLastIndex = lastIndex;
|
||||
if (output.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (separator.lastIndex === match.index) {
|
||||
separator.lastIndex++; // Avoid an infinite loop
|
||||
}
|
||||
}
|
||||
if (lastLastIndex === str.length) {
|
||||
if (lastLength || !separator.test("")) {
|
||||
output.push("");
|
||||
}
|
||||
} else {
|
||||
output.push(str.slice(lastLastIndex));
|
||||
}
|
||||
return output.length > limit ? output.slice(0, limit) : output;
|
||||
};
|
||||
|
||||
return self;
|
||||
})();
|
||||
|
||||
},{}],3:[function(require,module,exports){
|
||||
// contains, add, remove, toggle
|
||||
var indexof = require('indexof')
|
||||
|
||||
module.exports = ClassList
|
||||
|
||||
function ClassList(elem) {
|
||||
var cl = elem.classList
|
||||
|
||||
if (cl) {
|
||||
return cl
|
||||
}
|
||||
|
||||
var classList = {
|
||||
add: add
|
||||
, remove: remove
|
||||
, contains: contains
|
||||
, toggle: toggle
|
||||
, toString: $toString
|
||||
, length: 0
|
||||
, item: item
|
||||
}
|
||||
|
||||
return classList
|
||||
|
||||
function add(token) {
|
||||
var list = getTokens()
|
||||
if (indexof(list, token) > -1) {
|
||||
return
|
||||
}
|
||||
list.push(token)
|
||||
setTokens(list)
|
||||
}
|
||||
|
||||
function remove(token) {
|
||||
var list = getTokens()
|
||||
, index = indexof(list, token)
|
||||
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
list.splice(index, 1)
|
||||
setTokens(list)
|
||||
}
|
||||
|
||||
function contains(token) {
|
||||
return indexof(getTokens(), token) > -1
|
||||
}
|
||||
|
||||
function toggle(token) {
|
||||
if (contains(token)) {
|
||||
remove(token)
|
||||
return false
|
||||
} else {
|
||||
add(token)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function $toString() {
|
||||
return elem.className
|
||||
}
|
||||
|
||||
function item(index) {
|
||||
var tokens = getTokens()
|
||||
return tokens[index] || null
|
||||
}
|
||||
|
||||
function getTokens() {
|
||||
var className = elem.className
|
||||
|
||||
return filter(className.split(" "), isTruthy)
|
||||
}
|
||||
|
||||
function setTokens(list) {
|
||||
var length = list.length
|
||||
|
||||
elem.className = list.join(" ")
|
||||
classList.length = length
|
||||
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
classList[i] = list[i]
|
||||
}
|
||||
|
||||
delete list[length]
|
||||
}
|
||||
}
|
||||
|
||||
function filter (arr, fn) {
|
||||
var ret = []
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (fn(arr[i])) ret.push(arr[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function isTruthy(value) {
|
||||
return !!value
|
||||
}
|
||||
|
||||
},{"indexof":4}],4:[function(require,module,exports){
|
||||
|
||||
var indexOf = [].indexOf;
|
||||
|
||||
module.exports = function(arr, obj){
|
||||
if (indexOf) return arr.indexOf(obj);
|
||||
for (var i = 0; i < arr.length; ++i) {
|
||||
if (arr[i] === obj) return i;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
},{}],5:[function(require,module,exports){
|
||||
var h = require("./index.js");
|
||||
|
||||
module.exports = h;
|
||||
|
||||
/*
|
||||
$(function () {
|
||||
|
||||
var newDoc = h('p',
|
||||
|
||||
h('ul', 'bang bang bang'.split(/\s/).map(function (word) {
|
||||
return h('li', word);
|
||||
}))
|
||||
);
|
||||
$('body').html(newDoc.outerHTML);
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
},{"./index.js":1}],6:[function(require,module,exports){
|
||||
|
||||
},{}]},{},[5]);
|
||||
|
||||
return Hyperscript;
|
||||
});
|
25
www/assert/index.html
Normal file
25
www/assert/index.html
Normal file
|
@ -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>
|
50
www/assert/main.js
Normal file
50
www/assert/main.js
Normal file
|
@ -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');
|
||||
});
|
|
@ -18,11 +18,10 @@ define([
|
|||
'/common/messages.js',
|
||||
'/bower_components/reconnectingWebsocket/reconnecting-websocket.js',
|
||||
'/common/crypto.js',
|
||||
'/_socket/toolbar.js',
|
||||
'/_socket/text-patcher.js',
|
||||
'/common/TextPatcher.js',
|
||||
'/common/chainpad.js',
|
||||
'/bower_components/jquery/dist/jquery.min.js',
|
||||
], function (Messages,/*FIXME*/ ReconnectingWebSocket, Crypto, Toolbar, TextPatcher) {
|
||||
], function (Messages, ReconnectingWebSocket, Crypto, TextPatcher) {
|
||||
var $ = window.jQuery;
|
||||
var ChainPad = window.ChainPad;
|
||||
var PARANOIA = true;
|
120
www/common/TextPatcher.js
Normal file
120
www/common/TextPatcher.js
Normal file
|
@ -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 a new issue