commit 1508c7ba71f5de5e51f061fbef45bc1f18493832 Author: Caleb James DeLisle Date: Fri Oct 31 16:42:58 2014 +0100 and so it begins diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 000000000..67b62e774 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory" : "www/bower" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..7a7368acf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +www/bower/* +node_modules diff --git a/ChainPadSrv.js b/ChainPadSrv.js new file mode 100644 index 000000000..1054670a8 --- /dev/null +++ b/ChainPadSrv.js @@ -0,0 +1,189 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var WebSocket = require('ws'); + +var REGISTER = 0; +var REGISTER_ACK = 1; +var PATCH = 2; +var DISCONNECT = 3; +var PING = 4; +var PONG = 5; + +var parseMessage = function (msg) { + var passLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(passLen.length+1); + var pass = msg.substring(0,Number(passLen)); + msg = msg.substring(pass.length); + + var unameLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(unameLen.length+1); + var userName = msg.substring(0,Number(unameLen)); + msg = msg.substring(userName.length); + + var channelIdLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(channelIdLen.length+1); + var channelId = msg.substring(0,Number(channelIdLen)); + msg = msg.substring(channelId.length); + + var contentStrLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(contentStrLen.length+1); + var contentStr = msg.substring(0,Number(contentStrLen)); + + return { + user: userName, + pass: pass, + channelId: channelId, + content: JSON.parse(contentStr) + }; +}; + +// get the password off the message before sending it to other clients. +var popPassword = function (msg) { + var passLen = msg.substring(0,msg.indexOf(':')); + return msg.substring(passLen.length+1 + Number(passLen)); +}; + +var sendMsg = function (msg, socket) { + socket.send(msg); +}; + +var sendChannelMessage = function (ctx, channel, msg, cb) { + ctx.store.message(channel.name, msg, function () { + channel.forEach(function (user) { + try { + sendMsg(msg, user.socket); + } catch (e) { + console.log(e.stack); + dropClient(ctx, userPass); + } + }); + cb && cb(); + }); +}; + +var mkMessage = function (user, channel, content) { + content = JSON.stringify(content); + return user.length + ':' + user + + channel.length + ':' + channel + + content.length + ':' + content; +}; + +var dropClient = function (ctx, userpass) { + var client = ctx.registeredClients[userpass]; + if (client.socket.readyState !== WebSocket.CLOSING + && client.socket.readyState !== WebSocket.CLOSED) + { + try { + client.socket.close(); + } catch (e) { + console.log("Failed to disconnect ["+client.userName+"], attempting to terminate"); + try { + client.socket.terminate(); + } catch (ee) { + console.log("Failed to terminate ["+client.userName+"] *shrug*"); + } + } + } + + for (var i = 0; i < client.channels.length; i++) { + var chanName = client.channels[i]; + var chan = ctx.channels[chanName]; + var idx = chan.indexOf(client); + if (idx < 0) { throw new Error(); } + console.log("Removing ["+client.userName+"] from channel ["+chanName+"]"); + chan.splice(idx, 1); + if (chan.length === 0) { + console.log("Removing empty channel ["+chanName+"]"); + delete ctx.channels[chanName]; + } else { + sendChannelMessage(ctx, chan, mkMessage(client.userName, chanName, [DISCONNECT,0])); + } + } + delete ctx.registeredClients[userpass]; +}; + +var handleMessage = function (ctx, socket, msg) { + var parsed = parseMessage(msg); + var userPass = parsed.user + ':' + parsed.pass; + msg = popPassword(msg); + + if (parsed.content[0] === REGISTER) { +console.log("[" + userPass + "] registered"); + var client = ctx.registeredClients[userPass] = ctx.registeredClients[userPass] || { + channels: [parsed.channelId], + userName: parsed.user + }; + if (client.socket && client.socket !== socket) { client.socket.close(); } + client.socket = socket; + + var chan = ctx.channels[parsed.channelId] = ctx.channels[parsed.channelId] || []; + chan.name = parsed.channelId; + chan.push(client); + + // we send a register ack right away but then we fallthrough + // to let other users know that we were registered. + sendMsg(mkMessage('', parsed.channelId, [1,0]), socket); + sendChannelMessage(ctx, chan, msg, function () { + ctx.store.getMessages(chan.name, function (msg) { + sendMsg(msg, socket); + }); + }); + } + + if (parsed.content[0] === PING) { + // 31:xwiki:XWiki.Admin-141475016907510:RWJ5xF2+SL17:[5,1414752676547] + // 1:y31:xwiki:XWiki.Admin-141475016907510:RWJ5xF2+SL17:[4,1414752676547] + sendMsg(mkMessage(parsed.user, parsed.channelId, [ PONG, parsed.content[1] ]), socket); + return; + } + + var client = ctx.registeredClients[userPass]; + if (typeof(client) === 'undefined') { throw new Error('unregistered'); } + + var channel = ctx.channels[parsed.channelId]; + if (typeof(channel) === 'undefined') { throw new Error('no such channel'); } + + if (channel.indexOf(client) === -1) { throw new Error('client not in channel'); } + + sendChannelMessage(ctx, channel, msg); +}; + +var create = module.exports.create = function (socketServer, store) { + var ctx = { + registeredClients: {}, + channels: {}, + store: store + }; + + socketServer.on('connection', function(socket) { + socket.on('message', function(message) { + try { + handleMessage(ctx, socket, message); + } catch (e) { + console.log(e.stack); + socket.close(); + } + }); + socket.on('close', function (evt) { + for (client in ctx.registeredClients) { + if (ctx.registeredClients[client].socket === socket) { + dropClient(ctx, client); + } + } + }); + }); +}; diff --git a/Storage.js b/Storage.js new file mode 100644 index 000000000..91cf2bd87 --- /dev/null +++ b/Storage.js @@ -0,0 +1,55 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var MongoClient = require('mongodb').MongoClient; + +var MONGO_URI = "mongodb://demo_user:demo_password@ds027769.mongolab.com:27769/demo_database"; +var COLLECTION_NAME = 'cryptpad'; + +var insert = function (coll, channelName, content, cb) { + var val = {chan: channelName, msg:content, time: (new Date()).getTime()}; + coll.insertOne(val, {}, function (err, r) { + if (err || (r.insertedCount !== 1)) { + console.log('failed to insert ' + err); + return; + } + cb(); + }); +}; + +var getMessages = function (coll, channelName, cb) { + coll.find({chan:channelName}).forEach(function (doc) { + cb(doc.msg); + }, function (err) { + if (!err) { return; } + console.log('error ' + err); + }); +}; + +module.exports.create = function (conf, cb) { + MongoClient.connect(conf.mongoUri, function(err, db) { + var coll = db.collection(conf.mongoCollectionName); + if (err) { throw err; } + cb({ + message: function (channelName, content, cb) { + insert(coll, channelName, content, cb); + }, + getMessages: function (channelName, msgHandler) { + getMessages(coll, channelName, msgHandler); + } + }); + }); +}; diff --git a/and_so_it_begins.png b/and_so_it_begins.png new file mode 100644 index 000000000..556d55a18 Binary files /dev/null and b/and_so_it_begins.png differ diff --git a/bower.json b/bower.json new file mode 100644 index 000000000..c5da7135c --- /dev/null +++ b/bower.json @@ -0,0 +1,26 @@ +{ + "name": "cryptpad", + "version": "0.1.0", + "authors": [ + "Caleb James DeLisle " + ], + "description": "realtime collaborative visual editor with zero knowlege server", + "main": "www/index.html", + "moduleType": [ + "node" + ], + "license": "AGPLv3", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "jquery": "~2.1.1", + "tweetnacl": "~0.12.2", + "ckeditor": "~4.4.5", + "requirejs": "~2.1.15" + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..bb2f0893d --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "cryptpad", + "description": "realtime collaborative visual editor with zero knowlege server", + "dependencies": { + "express": "~4.10.1", + "ws": "~0.4.32", + "mongodb": "~2.0.5" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 000000000..20034bab7 --- /dev/null +++ b/readme.md @@ -0,0 +1,17 @@ +# CryptPad + +Unity is Strength - Collaboration is Key + +![and_so_it_begins.png](https://github.com/cjdelisle/cryptpad/raw/master/and_so_it_begins.png "We are the 99%") + +CryptPad is a **zero knowledge** realtime collaborative editor. +Encryption carried out in your web browser protects the data from the server, the cloud +and the NSA. This project uses the [CKEdit] Visual Editor and the [ChainPad] realtime +engine. The secret key is stored in the URL [fragment identifier] which is never sent to +the server but is available to javascript so by sharing the URL, you give authorization + + +Realtime Collaboration with + + +[fragment identifier]: http://en.wikipedia.org/wiki/Fragment_identifier diff --git a/server.js b/server.js new file mode 100644 index 000000000..e794811d2 --- /dev/null +++ b/server.js @@ -0,0 +1,24 @@ +var Express = require('express'); +var Http = require('http'); +var WebSocketServer = require('ws').Server; +var ChainPadSrv = require('./ChainPadSrv'); +var Storage = require('./Storage'); + +var config = { + httpPort: 3000, + mongoUri: "mongodb://demo_user:demo_password@ds027769.mongolab.com:27769/demo_database", + mongoCollectionName: 'cryptpad' +}; + +var app = Express(); +app.use(Express.static(__dirname + '/www')); + +var httpServer = Http.createServer(app); +httpServer.listen(config.httpPort); +console.log('listening on port ' + config.httpPort); + +var wsSrv = new WebSocketServer({server: httpServer}); +Storage.create(config, function (store) { + console.log('DB connected'); + ChainPadSrv.create(wsSrv, store); +}); diff --git a/www/chainpad.js b/www/chainpad.js new file mode 100644 index 000000000..f6d5b59f8 --- /dev/null +++ b/www/chainpad.js @@ -0,0 +1,1434 @@ +(function(){ +var r=function(){var e="function"==typeof require&&require,r=function(i,o,u){o||(o=0);var n=r.resolve(i,o),t=r.m[o][n];if(!t&&e){if(t=e(n))return t}else if(t&&t.c&&(o=t.c,n=t.m,t=r.m[o][t.m],!t))throw new Error('failed to require "'+n+'" from '+o);if(!t)throw new Error('failed to require "'+i+'" from '+u);return t.exports||(t.exports={},t.call(t.exports,t,t.exports,r.relative(n,o))),t.exports};return r.resolve=function(e,n){var i=e,t=e+".js",o=e+"/index.js";return r.m[n][t]&&t?t:r.m[n][o]&&o?o:i},r.relative=function(e,t){return function(n){if("."!=n.charAt(0))return r(n,t,e);var o=e.split("/"),f=n.split("/");o.pop();for(var i=0;i. + */ +var Common = require('./Common'); +var Operation = require('./Operation'); +var Sha = require('./SHA256'); + +var Patch = module.exports; + +var create = Patch.create = function (parentHash) { + return { + type: 'Patch', + operations: [], + parentHash: parentHash + }; +}; + +var check = Patch.check = function (patch, docLength_opt) { + Common.assert(patch.type === 'Patch'); + Common.assert(Array.isArray(patch.operations)); + Common.assert(/^[0-9a-f]{64}$/.test(patch.parentHash)); + for (var i = patch.operations.length - 1; i >= 0; i--) { + Operation.check(patch.operations[i], docLength_opt); + if (i > 0) { + Common.assert(!Operation.shouldMerge(patch.operations[i], patch.operations[i-1])); + } + if (typeof(docLength_opt) === 'number') { + docLength_opt += Operation.lengthChange(patch.operations[i]); + } + } +}; + +var toObj = Patch.toObj = function (patch) { + if (Common.PARANOIA) { check(patch); } + var out = new Array(patch.operations.length+1); + var i; + for (i = 0; i < patch.operations.length; i++) { + out[i] = Operation.toObj(patch.operations[i]); + } + out[i] = patch.parentHash; + return out; +}; + +var fromObj = Patch.fromObj = function (obj) { + Common.assert(Array.isArray(obj) && obj.length > 0); + var patch = create(); + var i; + for (i = 0; i < obj.length-1; i++) { + patch.operations[i] = Operation.fromObj(obj[i]); + } + patch.parentHash = obj[i]; + if (Common.PARANOIA) { check(patch); } + return patch; +}; + +var hash = function (text) { + return Sha.hex_sha256(text); +}; + +var addOperation = Patch.addOperation = function (patch, op) { + if (Common.PARANOIA) { + check(patch); + Operation.check(op); + } + for (var i = 0; i < patch.operations.length; i++) { + if (Operation.shouldMerge(patch.operations[i], op)) { + op = Operation.merge(patch.operations[i], op); + patch.operations.splice(i,1); + if (op === null) { + //console.log("operations cancelled eachother"); + return; + } + i--; + } else { + var out = Operation.rebase(patch.operations[i], op); + if (out === op) { + // op could not be rebased further, insert it here to keep the list ordered. + patch.operations.splice(i,0,op); + return; + } else { + op = out; + // op was rebased, try rebasing it against the next operation. + } + } + } + patch.operations.push(op); + if (Common.PARANOIA) { check(patch); } +}; + +var clone = Patch.clone = function (patch) { + if (Common.PARANOIA) { check(patch); } + var out = create(); + out.parentHash = patch.parentHash; + for (var i = 0; i < patch.operations.length; i++) { + out.operations[i] = Operation.clone(patch.operations[i]); + } + return out; +}; + +var merge = Patch.merge = function (oldPatch, newPatch) { + if (Common.PARANOIA) { + check(oldPatch); + check(newPatch); + } + oldPatch = clone(oldPatch); + for (var i = newPatch.operations.length-1; i >= 0; i--) { + addOperation(oldPatch, newPatch.operations[i]); + } + return oldPatch; +}; + +var apply = Patch.apply = function (patch, doc) +{ + if (Common.PARANOIA) { + check(patch); + Common.assert(typeof(doc) === 'string'); + Common.assert(Sha.hex_sha256(doc) === patch.parentHash); + } + var newDoc = doc; + for (var i = patch.operations.length-1; i >= 0; i--) { + newDoc = Operation.apply(patch.operations[i], newDoc); + } + return newDoc; +}; + +var lengthChange = Patch.lengthChange = function (patch) +{ + if (Common.PARANOIA) { check(patch); } + var out = 0; + for (var i = 0; i < patch.operations.length; i++) { + out += Operation.lengthChange(patch.operations[i]); + } + return out; +}; + +var invert = Patch.invert = function (patch, doc) +{ + if (Common.PARANOIA) { + check(patch); + Common.assert(typeof(doc) === 'string'); + Common.assert(Sha.hex_sha256(doc) === patch.parentHash); + } + var rpatch = create(); + var newDoc = doc; + for (var i = patch.operations.length-1; i >= 0; i--) { + rpatch.operations[i] = Operation.invert(patch.operations[i], newDoc); + newDoc = Operation.apply(patch.operations[i], newDoc); + } + for (var i = rpatch.operations.length-1; i >= 0; i--) { + for (var j = i - 1; j >= 0; j--) { + rpatch.operations[i].offset += rpatch.operations[j].toRemove; + rpatch.operations[i].offset -= rpatch.operations[j].toInsert.length; + } + } + rpatch.parentHash = Sha.hex_sha256(newDoc); + if (Common.PARANOIA) { check(rpatch); } + return rpatch; +}; + +var simplify = Patch.simplify = function (patch, doc, operationSimplify) +{ + if (Common.PARANOIA) { + check(patch); + Common.assert(typeof(doc) === 'string'); + Common.assert(Sha.hex_sha256(doc) === patch.parentHash); + } + operationSimplify = operationSimplify || Operation.simplify; + var spatch = create(patch.parentHash); + var newDoc = doc; + var outOps = []; + var j = 0; + for (var i = patch.operations.length-1; i >= 0; i--) { + outOps[j] = operationSimplify(patch.operations[i], newDoc, Operation.simplify); + if (outOps[j]) { + newDoc = Operation.apply(outOps[j], newDoc); + j++; + } + } + spatch.operations = outOps.reverse(); + if (!spatch.operations[0]) { + spatch.operations.shift(); + } + if (Common.PARANOIA) { + check(spatch); + } + return spatch; +}; + +var equals = Patch.equals = function (patchA, patchB) { + if (patchA.operations.length !== patchB.operations.length) { return false; } + for (var i = 0; i < patchA.operations.length; i++) { + if (!Operation.equals(patchA.operations[i], patchB.operations[i])) { return false; } + } + return true; +}; + +var transform = Patch.transform = function (origToTransform, transformBy, doc, transformFunction) { + if (Common.PARANOIA) { + check(origToTransform, doc.length); + check(transformBy, doc.length); + Common.assert(Sha.hex_sha256(doc) === origToTransform.parentHash); + } + Common.assert(origToTransform.parentHash === transformBy.parentHash); + var resultOfTransformBy = apply(transformBy, doc); + + toTransform = clone(origToTransform); + var text = doc; + for (var i = toTransform.operations.length-1; i >= 0; i--) { + text = Operation.apply(toTransform.operations[i], text); + for (var j = transformBy.operations.length-1; j >= 0; j--) { + toTransform.operations[i] = Operation.transform(text, + toTransform.operations[i], + transformBy.operations[j], + transformFunction); + if (!toTransform.operations[i]) { + break; + } + } + if (Common.PARANOIA && toTransform.operations[i]) { + Operation.check(toTransform.operations[i], resultOfTransformBy.length); + } + } + var out = create(transformBy.parentHash); + for (var i = toTransform.operations.length-1; i >= 0; i--) { + if (toTransform.operations[i]) { + addOperation(out, toTransform.operations[i]); + } + } + + out.parentHash = Sha.hex_sha256(resultOfTransformBy); + + if (Common.PARANOIA) { + check(out, resultOfTransformBy.length); + } + return out; +}; + +var random = Patch.random = function (doc, opCount) { + Common.assert(typeof(doc) === 'string'); + opCount = opCount || (Math.floor(Math.random() * 30) + 1); + var patch = create(Sha.hex_sha256(doc)); + var docLength = doc.length; + while (opCount-- > 0) { + var op = Operation.random(docLength); + docLength += Operation.lengthChange(op); + addOperation(patch, op); + } + check(patch); + return patch; +}; + +}, +"SHA256.js": function(module, exports, require){ +/* A JavaScript implementation of the Secure Hash Algorithm, SHA-256 + * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ + * Distributed under the BSD License + * Some bits taken from Paul Johnston's SHA-1 implementation + */ +(function () { + var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + function safe_add (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + function S (X, n) {return ( X >>> n ) | (X << (32 - n));} + function R (X, n) {return ( X >>> n );} + function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} + function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} + function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} + function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} + function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} + function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} + function newArray (n) { + var a = []; + for (;n>0;n--) { + a.push(undefined); + } + return a; + } + function core_sha256 (m, l) { + var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2]; + var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; + var W = newArray(64); + var a, b, c, d, e, f, g, h, i, j; + var T1, T2; + /* append padding */ + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >> 9) << 4) + 15] = l; + for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); + return bin; + } + function binb2hex (binarray) { + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for (var i = 0; i < binarray.length * 4; i++) { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; + } + function hex_sha256(s){ + return binb2hex(core_sha256(str2binb(s),s.length * chrsz)); + } + module.exports.hex_sha256 = hex_sha256; +}()); + +}, +"Common.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +var PARANOIA = module.exports.PARANOIA = false; + +/* throw errors over non-compliant messages which would otherwise be treated as invalid */ +var TESTING = module.exports.TESTING = true; + +var assert = module.exports.assert = function (expr) { + if (!expr) { throw new Error("Failed assertion"); } +}; + +var isUint = module.exports.isUint = function (integer) { + return (typeof(integer) === 'number') && + (Math.floor(integer) === integer) && + (integer >= 0); +}; + +var randomASCII = module.exports.randomASCII = function (length) { + var content = []; + for (var i = 0; i < length; i++) { + content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 ); + } + return content.join(''); +}; + +var strcmp = module.exports.strcmp = function (a, b) { + if (PARANOIA && typeof(a) !== 'string') { throw new Error(); } + if (PARANOIA && typeof(b) !== 'string') { throw new Error(); } + return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) ); +} + +}, +"Message.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var Common = require('./Common'); +var Operation = require('./Operation'); +var Patch = require('./Patch'); +var Sha = require('./SHA256'); + +var Message = module.exports; + +var REGISTER = Message.REGISTER = 0; +var REGISTER_ACK = Message.REGISTER_ACK = 1; +var PATCH = Message.PATCH = 2; +var DISCONNECT = Message.DISCONNECT = 3; +var PING = Message.PING = 4; +var PONG = Message.PONG = 5; + +var check = Message.check = function(msg) { + Common.assert(msg.type === 'Message'); + Common.assert(typeof(msg.userName) === 'string'); + Common.assert(typeof(msg.authToken) === 'string'); + Common.assert(typeof(msg.channelId) === 'string'); + + if (msg.messageType === PATCH) { + Patch.check(msg.content); + Common.assert(typeof(msg.lastMsgHash) === 'string'); + } else if (msg.messageType === PING || msg.messageType === PONG) { + Common.assert(typeof(msg.lastMsgHash) === 'undefined'); + Common.assert(typeof(msg.content) === 'number'); + } else if (msg.messageType === REGISTER + || msg.messageType === REGISTER_ACK + || msg.messageType === DISCONNECT) + { + Common.assert(typeof(msg.lastMsgHash) === 'undefined'); + Common.assert(typeof(msg.content) === 'undefined'); + } else { + throw new Error("invalid message type [" + msg.messageType + "]"); + } +}; + +var create = Message.create = function (userName, authToken, channelId, type, content, lastMsgHash) { + var msg = { + type: 'Message', + userName: userName, + authToken: authToken, + channelId: channelId, + messageType: type, + content: content, + lastMsgHash: lastMsgHash + }; + if (Common.PARANOIA) { check(msg); } + return msg; +}; + +var toString = Message.toString = function (msg) { + if (Common.PARANOIA) { check(msg); } + var prefix = msg.messageType + ':'; + var content = ''; + if (msg.messageType === REGISTER) { + content = JSON.stringify([REGISTER]); + } else if (msg.messageType === PING || msg.messageType === PONG) { + content = JSON.stringify([msg.messageType, msg.content]); + } else if (msg.messageType === PATCH) { + content = JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); + } + return msg.authToken.length + ":" + msg.authToken + + msg.userName.length + ":" + msg.userName + + msg.channelId.length + ":" + msg.channelId + + content.length + ':' + content; +}; + +var fromString = Message.fromString = function (str) { + var msg = str; + + var unameLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(unameLen.length+1); + var userName = msg.substring(0,Number(unameLen)); + msg = msg.substring(userName.length); + + var channelIdLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(channelIdLen.length+1); + var channelId = msg.substring(0,Number(channelIdLen)); + msg = msg.substring(channelId.length); + + var contentStrLen = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(contentStrLen.length+1); + var contentStr = msg.substring(0,Number(contentStrLen)); + + Common.assert(contentStr.length === Number(contentStrLen)); + + var content = JSON.parse(contentStr); + var message; + if (content[0] === PATCH) { + message = create(userName, '', channelId, PATCH, Patch.fromObj(content[1]), content[2]); + } else if (content[0] === PING || content[0] === PONG) { + message = create(userName, '', channelId, content[0], content[1]); + } else { + message = create(userName, '', channelId, content[0]); + } + + // This check validates every operation in the patch. + check(message); + + return message +}; + +var hashOf = Message.hashOf = function (msg) { + if (Common.PARANOIA) { check(msg); } + var authToken = msg.authToken; + msg.authToken = ''; + var hash = Sha.hex_sha256(toString(msg)); + msg.authToken = authToken; + return hash; +}; + +}, +"ChainPad.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var Common = require('./Common'); +var Operation = require('./Operation'); +var Patch = require('./Patch'); +var Message = require('./Message'); +var Sha = require('./SHA256'); + +var ChainPad = {}; + +// hex_sha256('') +var EMPTY_STR_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; +var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; + +var enterChainPad = function (realtime, func) { + return function () { + if (realtime.failed) { return; } + func.apply(null, arguments); + }; +}; + +var debug = function (realtime, msg) { + console.log("[" + realtime.userName + "] " + msg); +}; + +var schedule = function (realtime, func, timeout) { + if (!timeout) { + timeout = Math.floor(Math.random() * 2 * realtime.avgSyncTime); + } + var to = setTimeout(enterChainPad(realtime, function () { + realtime.schedules.splice(realtime.schedules.indexOf(to), 1); + func(); + }), timeout); + realtime.schedules.push(to); + return to; +}; + +var unschedule = function (realtime, schedule) { + var index = realtime.schedules.indexOf(schedule); + if (index > -1) { + realtime.schedules.splice(index, 1); + } + clearTimeout(schedule); +}; + +var sync = function (realtime) { + if (Common.PARANOIA) { check(realtime); } + if (realtime.syncSchedule) { + unschedule(realtime, realtime.syncSchedule); + realtime.syncSchedule = null; + } else { + // we're currently waiting on something from the server. + return; + } + + realtime.uncommitted = Patch.simplify( + realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify); + + if (realtime.uncommitted.operations.length === 0) { + //debug(realtime, "No data to sync to the server, sleeping"); + realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); + return; + } + + var msg; + if (realtime.best === realtime.initialMessage) { + msg = realtime.initialMessage; + } else { + msg = Message.create(realtime.userName, + realtime.authToken, + realtime.channelId, + Message.PATCH, + realtime.uncommitted, + realtime.best.hashOf); + } + + var strMsg = Message.toString(msg); + + realtime.onMessage(strMsg, function (err) { + if (err) { + debug(realtime, "Posting to server failed [" + err + "]"); + } + }); + + var hash = Message.hashOf(msg); + + var timeout = schedule(realtime, function () { + debug(realtime, "Failed to send message ["+hash+"] to server"); + sync(realtime); + }, 10000 + (Math.random() * 5000)); + realtime.pending = { + hash: hash, + callback: function () { + if (realtime.initialMessage && realtime.initialMessage.hashOf === hash) { + debug(realtime, "initial Ack received ["+hash+"]"); + realtime.initialMessage = null; + } + unschedule(realtime, timeout); + realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); + } + }; + if (Common.PARANOIA) { check(realtime); } +}; + +var getMessages = function (realtime) { + if (realtime.registered === true) { return; } + realtime.registered = true; + /*var to = schedule(realtime, function () { + throw new Error("failed to connect to the server"); + }, 5000);*/ + var msg = Message.create(realtime.userName, + realtime.authToken, + realtime.channelId, + Message.REGISTER); + realtime.onMessage(Message.toString(msg), function (err) { + if (err) { throw err; } + }); +}; + +var sendPing = function (realtime) { + realtime.pingSchedule = undefined; + realtime.lastPingTime = (new Date()).getTime(); + var msg = Message.create(realtime.userName, + realtime.authToken, + realtime.channelId, + Message.PING, + realtime.lastPingTime); + realtime.onMessage(Message.toString(msg), function (err) { + if (err) { throw err; } + }); +}; + +var onPong = function (realtime, msg) { + if (Common.PARANOIA) { + Common.assert(realtime.lastPingTime === Number(msg.content)); + } + realtime.lastPingLag = (new Date()).getTime() - Number(msg.content); + realtime.lastPingTime = 0; + realtime.pingSchedule = + schedule(realtime, function () { sendPing(realtime); }, realtime.pingCycle); +}; + +var create = ChainPad.create = function (userName, authToken, channelId, initialState, config) { + + var realtime = { + type: 'ChainPad', + + authDoc: '', + + config: config || {}, + + userName: userName, + authToken: authToken, + channelId: channelId, + + /** A patch representing all uncommitted work. */ + uncommitted: null, + + uncommittedDocLength: initialState.length, + + patchHandlers: [], + opHandlers: [], + + onMessage: function (message, callback) { + callback("no onMessage() handler registered"); + }, + + schedules: [], + + syncSchedule: null, + + registered: false, + + avgSyncTime: 100, + + // this is only used if PARANOIA is enabled. + userInterfaceContent: undefined, + + failed: false, + + // hash and callback for previously send patch, currently in flight. + pending: null, + + messages: {}, + messagesByParent: {}, + + rootMessage: null, + + /** + * Set to the message which sets the initialState if applicable. + * Reset to null after the initial message has been successfully broadcasted. + */ + initialMessage: null, + + userListChangeHandlers: [], + userList: [], + + /** The schedule() for sending pings. */ + pingSchedule: undefined, + + lastPingLag: 0, + lastPingTime: 0, + + /** Average number of milliseconds between pings. */ + pingCycle: 5000 + }; + + if (Common.PARANOIA) { + realtime.userInterfaceContent = initialState; + } + + var zeroPatch = Patch.create(EMPTY_STR_HASH); + zeroPatch.inverseOf = Patch.invert(zeroPatch, ''); + zeroPatch.inverseOf.inverseOf = zeroPatch; + var zeroMsg = Message.create('', '', channelId, Message.PATCH, zeroPatch, ZERO); + zeroMsg.hashOf = Message.hashOf(zeroMsg); + zeroMsg.parentCount = 0; + realtime.messages[zeroMsg.hashOf] = zeroMsg; + (realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); + realtime.rootMessage = zeroMsg; + realtime.best = zeroMsg; + + if (initialState === '') { + realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); + return realtime; + } + + var initialOp = Operation.create(0, 0, initialState); + var initialStatePatch = Patch.create(zeroPatch.inverseOf.parentHash); + Patch.addOperation(initialStatePatch, initialOp); + initialStatePatch.inverseOf = Patch.invert(initialStatePatch, ''); + initialStatePatch.inverseOf.inverseOf = initialStatePatch; + + // flag this patch so it can be handled specially. + // Specifically, we never treat an initialStatePatch as our own, + // we let it be reverted to prevent duplication of data. + initialStatePatch.isInitialStatePatch = true; + initialStatePatch.inverseOf.isInitialStatePatch = true; + + realtime.authDoc = initialState; + if (Common.PARANOIA) { + realtime.userInterfaceContent = initialState; + } + initialMessage = Message.create(realtime.userName, + realtime.authToken, + realtime.channelId, + Message.PATCH, + initialStatePatch, + zeroMsg.hashOf); + initialMessage.hashOf = Message.hashOf(initialMessage); + initialMessage.parentCount = 1; + + realtime.messages[initialMessage.hashOf] = initialMessage; + (realtime.messagesByParent[initialMessage.lastMessageHash] || []).push(initialMessage); + + realtime.best = initialMessage; + realtime.uncommitted = Patch.create(initialStatePatch.inverseOf.parentHash); + realtime.initialMessage = initialMessage; + + return realtime; +}; + +var getParent = function (realtime, message) { + return message.parent = message.parent || realtime.messages[message.lastMsgHash]; +}; + +var check = ChainPad.check = function(realtime) { + Common.assert(realtime.type === 'ChainPad'); + Common.assert(typeof(realtime.authDoc) === 'string'); + + Patch.check(realtime.uncommitted, realtime.authDoc.length); + + var uiDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); + if (uiDoc.length !== realtime.uncommittedDocLength) { + Common.assert(0); + } + if (realtime.userInterfaceContent !== '') { + Common.assert(uiDoc === realtime.userInterfaceContent); + } + + var doc = realtime.authDoc; + var patchMsg = realtime.best; + Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash); + var patches = []; + do { + patches.push(patchMsg); + doc = Patch.apply(patchMsg.content.inverseOf, doc); + } while ((patchMsg = getParent(realtime, patchMsg))); + Common.assert(doc === ''); + while ((patchMsg = patches.pop())) { + doc = Patch.apply(patchMsg.content, doc); + } + Common.assert(doc === realtime.authDoc); +}; + +var doOperation = ChainPad.doOperation = function (realtime, op) { + if (Common.PARANOIA) { + check(realtime); + realtime.userInterfaceContent = Operation.apply(op, realtime.userInterfaceContent); + } + Operation.check(op, realtime.uncommittedDocLength); + Patch.addOperation(realtime.uncommitted, op); + realtime.uncommittedDocLength += Operation.lengthChange(op); +}; + +var isAncestorOf = function (realtime, ancestor, decendent) { + if (!decendent || !ancestor) { return false; } + if (ancestor === decendent) { return true; } + return isAncestorOf(realtime, ancestor, getParent(realtime, decendent)); +}; + +var parentCount = function (realtime, message) { + if (typeof(message.parentCount) !== 'number') { + message.parentCount = parentCount(realtime, getParent(realtime, message)) + 1; + } + return message.parentCount; +}; + +var applyPatch = function (realtime, author, patch) { + if (author === realtime.userName && !patch.isInitialStatePatch) { + var inverseOldUncommitted = Patch.invert(realtime.uncommitted, realtime.authDoc); + var userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); + if (Common.PARANOIA) { + Common.assert(userInterfaceContent === realtime.userInterfaceContent); + } + realtime.uncommitted = Patch.merge(inverseOldUncommitted, patch); + realtime.uncommitted = Patch.invert(realtime.uncommitted, userInterfaceContent); + + } else { + realtime.uncommitted = + Patch.transform( + realtime.uncommitted, patch, realtime.authDoc, realtime.config.transformFunction); + } + realtime.uncommitted.parentHash = patch.inverseOf.parentHash; + + realtime.authDoc = Patch.apply(patch, realtime.authDoc); + + if (Common.PARANOIA) { + realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); + } +}; + +var revertPatch = function (realtime, author, patch) { + applyPatch(realtime, author, patch.inverseOf); +}; + +var getBestChild = function (realtime, msg) { + var best = msg; + (realtime.messagesByParent[msg.hashOf] || []).forEach(function (child) { + Common.assert(child.lastMsgHash === msg.hashOf); + child = getBestChild(realtime, child); + if (parentCount(realtime, child) > parentCount(realtime, best)) { best = child; } + }); + return best; +}; + +var userListChange = function (realtime) { + for (var i = 0; i < realtime.userListChangeHandlers.length; i++) { + var list = []; + list.push.apply(list, realtime.userList); + realtime.userListChangeHandlers[i](list); + } +}; + +var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { + + if (Common.PARANOIA) { check(realtime); } + var msg = Message.fromString(msgStr); + Common.assert(msg.channelId === realtime.channelId); + + if (msg.messageType === Message.REGISTER_ACK) { + debug(realtime, "registered"); + realtime.registered = true; + sendPing(realtime); + return; + } + + if (msg.messageType === Message.REGISTER) { + realtime.userList.push(msg.userName); + userListChange(realtime); + return; + } + + if (msg.messageType === Message.PONG) { + onPong(realtime, msg); + return; + } + + if (msg.messageType === Message.DISCONNECT) { + var idx = realtime.userList.indexOf(msg.userName); + if (Common.PARANOIA) { Common.assert(idx > -1); } + if (idx > -1) { + realtime.userList.splice(idx, 1); + userListChange(realtime); + } + return; + } + + // otherwise it's a disconnect. + if (msg.messageType !== Message.PATCH) { return; } + + msg.hashOf = Message.hashOf(msg); + + if (realtime.pending && realtime.pending.hash === msg.hashOf) { + realtime.pending.callback(); + realtime.pending = null; + } + + if (realtime.messages[msg.hashOf]) { + debug(realtime, "Patch [" + msg.hashOf + "] is already known"); + if (Common.PARANOIA) { check(realtime); } + return; + } + + realtime.messages[msg.hashOf] = msg; + (realtime.messagesByParent[msg.lastMsgHash] = + realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); + + if (!isAncestorOf(realtime, realtime.rootMessage, msg)) { + // we'll probably find the missing parent later. + debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); + if (Common.PARANOIA) { check(realtime); } + return; + } + + // of this message fills in a hole in the chain which makes another patch better, swap to the + // best child of this patch since longest chain always wins. + msg = getBestChild(realtime, msg); + var patch = msg.content; + + // Find the ancestor of this patch which is in the main chain, reverting as necessary + var toRevert = []; + var commonAncestor = realtime.best; + if (!isAncestorOf(realtime, realtime.best, msg)) { + var pcBest = parentCount(realtime, realtime.best); + var pcMsg = parentCount(realtime, msg); + if (pcBest < pcMsg + || (pcBest === pcMsg + && Common.strcmp(realtime.best.hashOf, msg.hashOf) > 0)) + { + // switch chains + while (commonAncestor && !isAncestorOf(realtime, commonAncestor, msg)) { + toRevert.push(commonAncestor); + commonAncestor = getParent(realtime, commonAncestor); + } + Common.assert(commonAncestor); + } else { + debug(realtime, "Patch [" + msg.hashOf + "] chain is ["+pcMsg+"] best chain is ["+pcBest+"]"); + if (Common.PARANOIA) { check(realtime); } + return; + } + } + + // Find the parents of this patch which are not in the main chain. + var toApply = []; + var current = msg; + do { + toApply.unshift(current); + current = getParent(realtime, current); + Common.assert(current); + } while (current !== commonAncestor); + + + var authDocAtTimeOfPatch = realtime.authDoc; + + for (var i = 0; i < toRevert.length; i++) { + authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch); + } + + // toApply.length-1 because we do not want to apply the new patch. + for (var i = 0; i < toApply.length-1; i++) { + if (typeof(toApply[i].content.inverseOf) === 'undefined') { + toApply[i].content.inverseOf = Patch.invert(toApply[i].content, authDocAtTimeOfPatch); + toApply[i].content.inverseOf.inverseOf = toApply[i].content; + } + authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); + } + + if (Sha.hex_sha256(authDocAtTimeOfPatch) !== patch.parentHash) { + debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; + } + + var simplePatch = + Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); + if (!Patch.equals(simplePatch, patch)) { + debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; + } + + patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch); + patch.inverseOf.inverseOf = patch; + + realtime.uncommitted = Patch.simplify( + realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify); + var oldUserInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); + if (Common.PARANOIA) { + Common.assert(oldUserInterfaceContent === realtime.userInterfaceContent); + } + + // Derive the patch for the user's uncommitted work + var uncommittedPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); + + for (var i = 0; i < toRevert.length; i++) { + debug(realtime, "reverting [" + toRevert[i].hashOf + "]"); + uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf); + revertPatch(realtime, toRevert[i].userName, toRevert[i].content); + } + + for (var i = 0; i < toApply.length; i++) { + debug(realtime, "applying [" + toApply[i].hashOf + "]"); + uncommittedPatch = Patch.merge(uncommittedPatch, toApply[i].content); + applyPatch(realtime, toApply[i].userName, toApply[i].content); + } + + uncommittedPatch = Patch.merge(uncommittedPatch, realtime.uncommitted); + uncommittedPatch = Patch.simplify( + uncommittedPatch, oldUserInterfaceContent, realtime.config.operationSimplify); + + realtime.uncommittedDocLength += Patch.lengthChange(uncommittedPatch); + realtime.best = msg; + + if (Common.PARANOIA) { + // apply the uncommittedPatch to the userInterface content. + var newUserInterfaceContent = Patch.apply(uncommittedPatch, oldUserInterfaceContent); + Common.assert(realtime.userInterfaceContent.length === realtime.uncommittedDocLength); + Common.assert(newUserInterfaceContent === realtime.userInterfaceContent); + } + + // push the uncommittedPatch out to the user interface. + for (var i = 0; i < realtime.patchHandlers.length; i++) { + realtime.patchHandlers[i](uncommittedPatch); + } + if (realtime.opHandlers.length) { + for (var i = uncommittedPatch.operations.length-1; i >= 0; i--) { + for (var j = 0; j < realtime.opHandlers.length; j++) { + realtime.opHandlers[j](uncommittedPatch.operations[i]); + } + } + } + if (Common.PARANOIA) { check(realtime); } +}; + +module.exports.create = function (userName, authToken, channelId, initialState, conf) { + Common.assert(typeof(userName) === 'string'); + Common.assert(typeof(authToken) === 'string'); + Common.assert(typeof(channelId) === 'string'); + Common.assert(typeof(initialState) === 'string'); + var realtime = ChainPad.create(userName, authToken, channelId, initialState, conf); + return { + onPatch: enterChainPad(realtime, function (handler) { + Common.assert(typeof(handler) === 'function'); + realtime.patchHandlers.push(handler); + }), + onRemove: enterChainPad(realtime, function (handler) { + Common.assert(typeof(handler) === 'function'); + realtime.opHandlers.unshift(function (op) { + if (op.toRemove > 0) { handler(op.offset, op.toRemove); } + }); + }), + onInsert: enterChainPad(realtime, function (handler) { + Common.assert(typeof(handler) === 'function'); + realtime.opHandlers.push(function (op) { + if (op.toInsert.length > 0) { handler(op.offset, op.toInsert); } + }); + }), + remove: enterChainPad(realtime, function (offset, numChars) { + doOperation(realtime, Operation.create(offset, numChars, '')); + }), + insert: enterChainPad(realtime, function (offset, str) { + doOperation(realtime, Operation.create(offset, 0, str)); + }), + onMessage: enterChainPad(realtime, function (handler) { + realtime.onMessage = handler; + }), + message: enterChainPad(realtime, function (message) { + handleMessage(realtime, message); + }), + start: enterChainPad(realtime, function () { + getMessages(realtime); + realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); + }), + abort: enterChainPad(realtime, function () { + realtime.schedules.forEach(function (s) { clearTimeout(s) }); + }), + sync: enterChainPad(realtime, function () { + sync(realtime); + }), + getAuthDoc: function () { return realtime.authDoc; }, + getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, + onUserListChange: enterChainPad(realtime, function (handler) { + Common.assert(typeof(handler) === 'function'); + realtime.userListChangeHandlers.push(handler); + }), + getLag: function () { + if (realtime.lastPingTime) { + return { waiting:1, lag: (new Date()).getTime() - realtime.lastPingTime }; + } + return { waiting:0, lag: realtime.lastPingLag }; + } + }; +}; + +}, +"Operation.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var Common = require('./Common'); + +var Operation = module.exports; + +var check = Operation.check = function (op, docLength_opt) { + Common.assert(op.type === 'Operation'); + Common.assert(Common.isUint(op.offset)); + Common.assert(Common.isUint(op.toRemove)); + Common.assert(typeof(op.toInsert) === 'string'); + Common.assert(op.toRemove > 0 || op.toInsert.length > 0); + Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt); +}; + +var create = Operation.create = function (offset, toRemove, toInsert) { + var out = { + type: 'Operation', + offset: offset || 0, + toRemove: toRemove || 0, + toInsert: toInsert || '', + }; + if (Common.PARANOIA) { check(out); } + return out; +}; + +var toObj = Operation.toObj = function (op) { + if (Common.PARANOIA) { check(op); } + return [op.offset,op.toRemove,op.toInsert]; +}; + +var fromObj = Operation.fromObj = function (obj) { + Common.assert(Array.isArray(obj) && obj.length === 3); + return create(obj[0], obj[1], obj[2]); +}; + +var clone = Operation.clone = function (op) { + return create(op.offset, op.toRemove, op.toInsert); +}; + +/** + * @param op the operation to apply. + * @param doc the content to apply the operation on + */ +var apply = Operation.apply = function (op, doc) +{ + if (Common.PARANOIA) { + check(op); + Common.assert(typeof(doc) === 'string'); + Common.assert(op.offset + op.toRemove <= doc.length); + } + return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove); +}; + +var invert = Operation.invert = function (op, doc) { + if (Common.PARANOIA) { + check(op); + Common.assert(typeof(doc) === 'string'); + Common.assert(op.offset + op.toRemove <= doc.length); + } + var rop = clone(op); + rop.toInsert = doc.substring(op.offset, op.offset + op.toRemove); + rop.toRemove = op.toInsert.length; + return rop; +}; + +var simplify = Operation.simplify = function (op, doc) { + if (Common.PARANOIA) { + check(op); + Common.assert(typeof(doc) === 'string'); + Common.assert(op.offset + op.toRemove <= doc.length); + } + var rop = invert(op, doc); + op = clone(op); + + var minLen = Math.min(op.toInsert.length, rop.toInsert.length); + var i; + for (i = 0; i < minLen && rop.toInsert[i] === op.toInsert[i]; i++) ; + op.offset += i; + op.toRemove -= i; + op.toInsert = op.toInsert.substring(i); + rop.toInsert = rop.toInsert.substring(i); + + if (rop.toInsert.length === op.toInsert.length) { + for (i = rop.toInsert.length-1; i >= 0 && rop.toInsert[i] === op.toInsert[i]; i--) ; + op.toInsert = op.toInsert.substring(0, i+1); + op.toRemove = i+1; + } + + if (op.toRemove === 0 && op.toInsert.length === 0) { return null; } + return op; +}; + +var equals = Operation.equals = function (opA, opB) { + return (opA.toRemove === opB.toRemove + && opA.toInsert === opB.toInsert + && opA.offset === opB.offset); +}; + +var lengthChange = Operation.lengthChange = function (op) +{ + if (Common.PARANOIA) { check(op); } + return op.toInsert.length - op.toRemove; +}; + +/* + * @return the merged operation OR null if the result of the merger is a noop. + */ +var merge = Operation.merge = function (oldOpOrig, newOpOrig) { + if (Common.PARANOIA) { + check(newOpOrig); + check(oldOpOrig); + } + + var newOp = clone(newOpOrig); + var oldOp = clone(oldOpOrig); + var offsetDiff = newOp.offset - oldOp.offset; + + if (newOp.toRemove > 0) { + var origOldInsert = oldOp.toInsert; + oldOp.toInsert = ( + oldOp.toInsert.substring(0,offsetDiff) + + oldOp.toInsert.substring(offsetDiff + newOp.toRemove) + ); + newOp.toRemove -= (origOldInsert.length - oldOp.toInsert.length); + if (newOp.toRemove < 0) { newOp.toRemove = 0; } + + oldOp.toRemove += newOp.toRemove; + newOp.toRemove = 0; + } + + if (offsetDiff < 0) { + oldOp.offset += offsetDiff; + oldOp.toInsert = newOp.toInsert + oldOp.toInsert; + + } else if (oldOp.toInsert.length === offsetDiff) { + oldOp.toInsert = oldOp.toInsert + newOp.toInsert; + + } else if (oldOp.toInsert.length > offsetDiff) { + oldOp.toInsert = ( + oldOp.toInsert.substring(0,offsetDiff) + + newOp.toInsert + + oldOp.toInsert.substring(offsetDiff) + ); + } else { + throw new Error("should never happen\n" + + JSON.stringify([oldOpOrig,newOpOrig], null, ' ')); + } + + if (oldOp.toInsert === '' && oldOp.toRemove === 0) { + return null; + } + if (Common.PARANOIA) { check(oldOp); } + + return oldOp; +}; + +/** + * If the new operation deletes what the old op inserted or inserts content in the middle of + * the old op's content or if they abbut one another, they should be merged. + */ +var shouldMerge = Operation.shouldMerge = function (oldOp, newOp) { + if (Common.PARANOIA) { + check(oldOp); + check(newOp); + } + if (newOp.offset < oldOp.offset) { + return (oldOp.offset <= (newOp.offset + newOp.toRemove)); + } else { + return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length)); + } +}; + +/** + * Rebase newOp against oldOp. + * + * @param oldOp the eariler operation to have happened. + * @param newOp the later operation to have happened (in time). + * @return either the untouched newOp if it need not be rebased, + * the rebased clone of newOp if it needs rebasing, or + * null if newOp and oldOp must be merged. + */ +var rebase = Operation.rebase = function (oldOp, newOp) { + if (Common.PARANOIA) { + check(oldOp); + check(newOp); + } + if (newOp.offset < oldOp.offset) { return newOp; } + newOp = clone(newOp); + newOp.offset += oldOp.toRemove; + newOp.offset -= oldOp.toInsert.length; + return newOp; +}; + +/** + * this is a lossy and dirty algorithm, everything else is nice but transformation + * has to be lossy because both operations have the same base and they diverge. + * This could be made nicer and/or tailored to a specific data type. + * + * @param toTransform the operation which is converted *MUTATED*. + * @param transformBy an existing operation which also has the same base. + * @return toTransform *or* null if the result is a no-op. + */ +var transform0 = Operation.transform0 = function (text, toTransform, transformBy) { + if (toTransform.offset > transformBy.offset) { + if (toTransform.offset > transformBy.offset + transformBy.toRemove) { + // simple rebase + toTransform.offset -= transformBy.toRemove; + toTransform.offset += transformBy.toInsert.length; + return toTransform; + } + // goto the end, anything you deleted that they also deleted should be skipped. + var newOffset = transformBy.offset + transformBy.toInsert.length; + toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); + if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } + toTransform.offset = newOffset; + if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { + return null; + } + return toTransform; + } + if (toTransform.offset + toTransform.toRemove < transformBy.offset) { + return toTransform; + } + toTransform.toRemove = transformBy.offset - toTransform.offset; + if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { + return null; + } + return toTransform; +}; + +/** + * @param toTransform the operation which is converted + * @param transformBy an existing operation which also has the same base. + * @return a modified clone of toTransform *or* toTransform itself if no change was made. + */ +var transform = Operation.transform = function (text, toTransform, transformBy, transformFunction) { + if (Common.PARANOIA) { + check(toTransform); + check(transformBy); + } + transformFunction = transformFunction || transform0; + toTransform = clone(toTransform); + var result = transformFunction(text, toTransform, transformBy); + if (Common.PARANOIA && result) { check(result); } + return result; +}; + +/** Used for testing. */ +var random = Operation.random = function (docLength) { + Common.assert(Common.isUint(docLength)); + var offset = Math.floor(Math.random() * 100000000 % docLength) || 0; + var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0; + var toInsert = ''; + do { + var toInsert = Common.randomASCII(Math.floor(Math.random() * 20)); + } while (toRemove === 0 && toInsert === ''); + return create(offset, toRemove, toInsert); +}; + +} +}; +ChainPad = r("ChainPad.js");}()); diff --git a/www/html-patcher.js b/www/html-patcher.js new file mode 100644 index 000000000..9d80eff65 --- /dev/null +++ b/www/html-patcher.js @@ -0,0 +1,483 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +define([ + 'bower/jquery/dist/jquery.min', + 'otaml' +], function () { + + var $ = jQuery; + var Otaml = window.Otaml; + var module = { exports: {} }; + var PARANOIA = true; + + var debug = function (x) { }; + debug = function (x) { console.log(x); }; + + var getNextSiblingDeep = function (node, parent) + { + if (node.firstChild) { return node.firstChild; } + do { + if (node.nextSibling) { return node.nextSibling; } + node = node.parentNode; + } while (node && node !== parent); + }; + + var getOuterHTML = function (node) + { + var html = node.outerHTML; + if (html) { return html; } + if (node.parentNode && node.parentNode.childNodes.length === 1) { + return node.parentNode.innerHTML; + } + var div = document.createElement('div'); + div.appendChild(node.cloneNode(true)); + return div.innerHTML; + }; + + var nodeFromHTML = function (html) + { + var e = document.createElement('div'); + e.innerHTML = html; + return e.childNodes[0]; + }; + + var getInnerHTML = function (node) + { + var html = node.innerHTML; + if (html) { return html; } + var outerHTML = getOuterHTML(node); + var tw = Otaml.tagWidth(outerHTML); + if (!tw) { return outerHTML; } + return outerHTML.substring(tw, outerHTML.lastIndexOf(''; + if (PARANOIA && spanHTML !== span.outerHTML) { throw new Error(); } + + node.parentNode.insertBefore(span, node); + var newDocText = getInnerHTML(dom); + idx = newDocText.lastIndexOf(spanHTML); + if (idx === -1 || idx !== newDocText.indexOf(spanHTML)) { throw new Error(); } + node.parentNode.removeChild(span); + + if (PARANOIA && getInnerHTML(dom) !== docText) { throw new Error(); } + } + + if (PARANOIA && docText.indexOf(content, idx) !== idx) { throw new Error() } + return idx; + }; + + var patchString = module.exports.patchString = function (oldString, offset, toRemove, toInsert) + { + return oldString.substring(0, offset) + toInsert + oldString.substring(offset + toRemove); + }; + + var getNodeAtOffset = function (docText, offset, dom) + { + if (PARANOIA && dom.childNodes.length && docText !== dom.innerHTML) { throw new Error(); } + if (offset < 0) { throw new Error(); } + + var idx = 0; + for (var i = 0; i < dom.childNodes.length; i++) { + var childOuterHTML = getOuterHTML(dom.childNodes[i]); + if (PARANOIA && docText.indexOf(childOuterHTML, idx) !== idx) { throw new Error(); } + if (i === 0 && idx >= offset) { + return { node: dom, pos: 0 }; + } + if (idx + childOuterHTML.length > offset) { + var childInnerHTML = childOuterHTML; + var tw = Otaml.tagWidth(childOuterHTML); + if (tw) { + childInnerHTML = childOuterHTML.substring(tw, childOuterHTML.lastIndexOf(' docText.length) { throw new Error(); } + var beforeOffset = docText.substring(0, offset); + if (beforeOffset.indexOf('&') > -1) { + var tn = nodeFromHTML(beforeOffset); + offset = tn.data.length; + } + } else { + offset = 0; + } + + return { node: dom, pos: offset }; + }; + + var relocatedPositionInNode = function (newNode, oldNode, offset) + { + if (newNode.nodeName !== '#text' || oldNode.nodeName !== '#text' || offset === 0) { + offset = 0; + } else if (oldNode.data === newNode.data) { + // fallthrough + } else if (offset > newNode.length) { + offset = newNode.length; + } else if (oldNode.data.substring(0, offset) === newNode.data.substring(0, offset)) { + // keep same offset and fall through + } else { + var rOffset = oldNode.length - offset; + if (oldNode.data.substring(offset) === + newNode.data.substring(newNode.length - rOffset)) + { + offset = newNode.length - rOffset; + } else { + offset = 0; + } + } + return { node: newNode, pos: offset }; + }; + + var pushNode = function (list, node) { + if (node.nodeName === '#text') { + list.push.apply(list, node.data.split('')); + } else { + list.push('#' + node.nodeName); + } + }; + + var getChildPath = function (parent) { + var out = []; + for (var next = parent; next; next = getNextSiblingDeep(next, parent)) { + pushNode(out, next); + } + return out; + }; + + var tryFromBeginning = function (oldPath, newPath) { + for (var i = 0; i < oldPath.length; i++) { + if (oldPath[i] !== newPath[i]) { return i; } + } + return oldPath.length; + }; + + var tryFromEnd = function (oldPath, newPath) { + for (var i = 1; i <= oldPath.length; i++) { + if (oldPath[oldPath.length - i] !== newPath[newPath.length - i]) { + return false; + } + } + return true; + }; + + /** + * returns 2 arrays (before and after). + * before is string representations (see nodeId()) of all nodes before the target + * node and after is representations of all nodes which follow. + */ + var getNodePaths = function (parent, node) { + var before = []; + var next = parent; + for (; next && next !== node; next = getNextSiblingDeep(next, parent)) { + pushNode(before, next); + } + + if (next !== node) { throw new Error(); } + + var after = []; + next = getNextSiblingDeep(next, parent); + for (; next; next = getNextSiblingDeep(next, parent)) { + pushNode(after, next); + } + + return { before: before, after: after }; + }; + + var nodeAtIndex = function (parent, idx) { + var node = parent; + for (var i = 0; i < idx; i++) { + if (node.nodeName === '#text') { + if (i + node.data.length > idx) { return node; } + i += node.data.length - 1; + } + node = getNextSiblingDeep(node); + } + return node; + }; + + var getRelocatedPosition = function (newParent, oldParent, oldNode, oldOffset, origText, op) + { + var newPath = getChildPath(newParent); + if (newPath.length === 1) { + return { node: null, pos: 0 }; + } + var oldPaths = getNodePaths(oldParent, oldNode); + + var idx = -1; + var fromBeginning = tryFromBeginning(oldPaths.before, newPath); + if (fromBeginning === oldPaths.before.length) { + idx = oldPaths.before.length; + } else if (tryFromEnd(oldPaths.after, newPath)) { + idx = (newPath.length - oldPaths.after.length - 1); + } else { + idx = fromBeginning; + var id = 'relocate-' + String(Math.random()).substring(2); + $(document.body).append(''); + $('#'+id).val(JSON.stringify([origText, op, newPath, getChildPath(oldParent), oldPaths])); + } + + var out = nodeAtIndex(newParent, idx); + return relocatedPositionInNode(out, oldNode, oldOffset); + }; + + // We can't create a real range until the new parent is installed in the document + // but we need the old range to be in the document so we can do comparisons + // so create a "pseudo" range instead. + var getRelocatedPseudoRange = function (newParent, oldParent, range, origText, op) + { + if (!range.startContainer) { + throw new Error(); + } + if (!newParent) { throw new Error(); } + + // Copy because tinkering in the dom messes up the original range. + var startContainer = range.startContainer; + var startOffset = range.startOffset; + var endContainer = range.endContainer; + var endOffset = range.endOffset; + + var newStart = + getRelocatedPosition(newParent, oldParent, startContainer, startOffset, origText, op); + + if (!newStart.node) { + // there is probably nothing left of the document so just clear the selection. + endContainer = null; + } + + var newEnd = { node: newStart.node, pos: newStart.pos }; + if (endContainer) { + if (endContainer !== startContainer) { + newEnd = getRelocatedPosition(newParent, oldParent, endContainer, endOffset, origText, op); + } else if (endOffset !== startOffset) { + newEnd = { + node: newStart.node, + pos: relocatedPositionInNode(newStart.node, endContainer, endOffset).pos + }; + } else { + newEnd = { node: newStart.node, pos: newStart.pos }; + } + } + + return { start: newStart, end: newEnd }; + }; + + var replaceAllChildren = function (parent, newParent) + { + var c; + while ((c = parent.firstChild)) { + parent.removeChild(c); + } + while ((c = newParent.firstChild)) { + newParent.removeChild(c); + parent.appendChild(c); + } + }; + + var isAncestorOf = function (maybeDecendent, maybeAncestor) { + while ((maybeDecendent = maybeDecendent.parentNode)) { + if (maybeDecendent === maybeAncestor) { return true; } + } + return false; + }; + + var getSelectedRange = function (rangy, ifrWindow, selection) { + selection = selection || rangy.getSelection(ifrWindow); + if (selection.rangeCount === 0) { + return; + } + var range = selection.getRangeAt(0); + range.backward = (selection.rangeCount === 1 && selection.isBackward()); + if (!range.startContainer) { + throw new Error(); + } + + // Occasionally, some browsers *cough* firefox *cough* will attach the range to something + // which has been used in the past but is nolonger part of the dom... + if (range.startContainer && + isAncestorOf(range.startContainer, ifrWindow.document)) + { + return range; + } + + return; + }; + + var applyHTMLOp = function (docText, op, dom, rangy, ifrWindow) + { + var parent = getNodeAtOffset(docText, op.offset, dom).node; + var htmlToRemove = docText.substring(op.offset, op.offset + op.toRemove); + + var parentInnerHTML; + var indexOfInnerHTML; + var localOffset; + for (;;) { + for (;;) { + parentInnerHTML = parent.innerHTML; + if (typeof(parentInnerHTML) !== 'undefined' + && parentInnerHTML.indexOf(htmlToRemove) !== -1) + { + break; + } + if (parent === dom || !(parent = parent.parentNode)) { throw new Error(); } + } + + var indexOfOuterHTML = 0; + var tw = 0; + if (parent !== dom) { + indexOfOuterHTML = offsetOfNodeOuterHTML(docText, parent, dom, ifrWindow); + tw = Otaml.tagWidth(docText.substring(indexOfOuterHTML)); + } + indexOfInnerHTML = indexOfOuterHTML + tw; + + localOffset = op.offset - indexOfInnerHTML; + + if (localOffset >= 0 && localOffset + op.toRemove <= parentInnerHTML.length) { + break; + } + + parent = parent.parentNode; + if (!parent) { throw new Error(); } + } + + if (PARANOIA && + docText.substr(indexOfInnerHTML, parentInnerHTML.length) !== parentInnerHTML) + { + throw new Error(); + } + + var newParentInnerHTML = + patchString(parentInnerHTML, localOffset, op.toRemove, op.toInsert); + + // Create a temp container for holding the children of the parent node. + // Once we've identified the new range, we'll return the nodes to the + // original parent. This is because parent might be the and we + // don't want to destroy all of our event listeners. + var babysitter = ifrWindow.document.createElement('div'); + // give it a uid so that we can prove later that it's not in the document, + // see getSelectedRange() + babysitter.setAttribute('id', uniqueId()); + babysitter.innerHTML = newParentInnerHTML; + + var range = getSelectedRange(rangy, ifrWindow); + + // doesn't intersect at all + if (!range || !range.containsNode(parent, true)) { + replaceAllChildren(parent, babysitter); + return; + } + + var pseudoRange = getRelocatedPseudoRange(babysitter, parent, range, rangy); + range.detach(); + replaceAllChildren(parent, babysitter); + if (pseudoRange.start.node) { + var selection = rangy.getSelection(ifrWindow); + var newRange = rangy.createRange(); + newRange.setStart(pseudoRange.start.node, pseudoRange.start.pos); + newRange.setEnd(pseudoRange.end.node, pseudoRange.end.pos); + selection.setSingleRange(newRange); + } + return; + }; + + var applyHTMLOpHammer = function (docText, op, dom, rangy, ifrWindow) + { + var newDocText = patchString(docText, op.offset, op.toRemove, op.toInsert); + var babysitter = ifrWindow.document.createElement('body'); + // give it a uid so that we can prove later that it's not in the document, + // see getSelectedRange() + babysitter.setAttribute('id', uniqueId()); + babysitter.innerHTML = newDocText; + + var range = getSelectedRange(rangy, ifrWindow); + + // doesn't intersect at all + if (!range) { + replaceAllChildren(dom, babysitter); + return; + } + + var pseudoRange = getRelocatedPseudoRange(babysitter, dom, range, docText, op); + range.detach(); + replaceAllChildren(dom, babysitter); + if (pseudoRange.start.node) { + var selection = rangy.getSelection(ifrWindow); + var newRange = rangy.createRange(); + newRange.setStart(pseudoRange.start.node, pseudoRange.start.pos); + newRange.setEnd(pseudoRange.end.node, pseudoRange.end.pos); + selection.setSingleRange(newRange); + } + return; + }; + + /* Return whether the selection range has been "dirtied" and needs to be reloaded. */ + var applyOp = module.exports.applyOp = function (docText, op, dom, rangy, ifrWindow) + { + if (PARANOIA && docText !== getInnerHTML(dom)) { throw new Error(); } + + if (op.offset + op.toRemove > docText.length) { + throw new Error(); + } + try { + applyHTMLOp(docText, op, dom, rangy, ifrWindow); + var result = patchString(docText, op.offset, op.toRemove, op.toInsert); + var innerHTML = getInnerHTML(dom); + if (result !== innerHTML) { + $(document.body).append(''); + $(document.body).append(''); + var SEP = '\n\n\n\n\n\n\n\n\n\n'; + $('#statebox').val(docText + SEP + result + SEP + innerHTML); + var diff = Otaml.makeTextOperation(result, innerHTML); + $('#errorbox').val(JSON.stringify(op) + '\n' + JSON.stringify(diff)); + throw new Error(); + } + } catch (err) { + if (PARANOIA) { console.log(err.stack); } + // The big hammer + dom.innerHTML = patchString(docText, op.offset, op.toRemove, op.toInsert); + } + }; + + return module.exports; +}); diff --git a/www/index.html b/www/index.html new file mode 100644 index 000000000..da13ffe09 --- /dev/null +++ b/www/index.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ +
+ + + diff --git a/www/main.js b/www/main.js new file mode 100644 index 000000000..5808c41e9 --- /dev/null +++ b/www/main.js @@ -0,0 +1,52 @@ +define([ + 'realtime-wysiwyg', + 'bower/jquery/dist/jquery.min', + 'bower/ckeditor/ckeditor', + 'bower/tweetnacl/nacl-fast.min' +], function (RTWysiwyg) { + var Ckeditor = window.CKEDITOR; + var Nacl = window.nacl; + var $ = jQuery; + + var module = { exports: {} }; + + var parseKey = function (str) { + var array = Nacl.util.decodeBase64(str); + var hash = Nacl.hash(array); + return { lookupKey: hash.subarray(32), cryptKey: hash.subarray(0,32) }; + }; + + var genKey = function () { + return Nacl.util.encodeBase64(Nacl.randomBytes(18)); + }; + + var userName = function () { + return Nacl.util.encodeBase64(Nacl.randomBytes(8)); + }; + + $(function () { + if (window.location.href.indexOf('#') === -1) { + window.location.href = window.location.href + '#' + genKey(); + } + $(window).on('hashchange', function() { + window.location.reload(); + }); + var key = parseKey(window.location.hash.substring(1)); + var editor = Ckeditor.replace('editor1', { + removeButtons: 'Source,Maximize', + }); + editor.on('instanceReady', function () { + //editor.execCommand('maximize'); + var ifr = window.ifr = $('iframe')[0]; + ifr.contentDocument.body.innerHTML = '

It works!

'; + + var rtw = + RTWysiwyg.start(window.location.href.replace(/#.*$/, '').replace(/^http/, 'ws'), + userName(), + {}, + Nacl.util.encodeBase64(key.lookupKey).substring(0,10), + key.cryptKey); + editor.on('change', function () { rtw.onEvent(); }); + }); + }); +}); diff --git a/www/otaml.js b/www/otaml.js new file mode 100644 index 000000000..5ea64aef7 --- /dev/null +++ b/www/otaml.js @@ -0,0 +1,1003 @@ +(function(){ +var r=function(){var e="function"==typeof require&&require,r=function(i,o,u){o||(o=0);var n=r.resolve(i,o),t=r.m[o][n];if(!t&&e){if(t=e(n))return t}else if(t&&t.c&&(o=t.c,n=t.m,t=r.m[o][t.m],!t))throw new Error('failed to require "'+n+'" from '+o);if(!t)throw new Error('failed to require "'+i+'" from '+u);return t.exports||(t.exports={},t.call(t.exports,t,t.exports,r.relative(n,o))),t.exports};return r.resolve=function(e,n){var i=e,t=e+".js",o=e+"/index.js";return r.m[n][t]&&t?t:r.m[n][o]&&o?o:i},r.relative=function(e,t){return function(n){if("."!=n.charAt(0))return r(n,t,e);var o=e.split("/"),f=n.split("/");o.pop();for(var i=0;i. + */ + +var Common = require('./Common'); +var HtmlParse = require('./HtmlParse'); +var Operation = require('./Operation'); +var Sha = require('./SHA256'); + +var makeTextOperation = module.exports.makeTextOperation = function(oldval, newval) +{ + if (oldval === newval) { return; } + + var begin = 0; + for (; oldval[begin] === newval[begin]; begin++) ; + + var end = 0; + for (var oldI = oldval.length, newI = newval.length; + oldval[--oldI] === newval[--newI]; + end++) ; + + if (end >= oldval.length - begin) { end = oldval.length - begin; } + if (end >= newval.length - begin) { end = newval.length - begin; } + + return { + offset: begin, + toRemove: oldval.length - begin - end, + toInsert: newval.slice(begin, newval.length - end), + }; +}; + +var VOID_TAG_REGEX = new RegExp('^(' + [ + 'area', + 'base', + 'br', + 'col', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'command', + 'keygen', + 'source', +].join('|') + ')$'); + +// Get the offset of the previous open/close/void tag. +// returns the offset of the opening angle bracket. +var getPreviousTagIdx = function (data, idx) +{ + if (idx === 0) { return -1; } + idx = data.lastIndexOf('>', idx); + // The html tag from hell: + // < abc def="g" k='lm"nopw>"qrstu" + for (;;) { + var mch = data.substring(0,idx).match(/[<"'][^<'"]*$/); + if (!mch) { return -1; } + if (mch[0][0] === '<') { return mch.index; } + idx = data.lastIndexOf(mch[0][0], mch.index-1); + } +}; + +/** + * Get the name of an HTML tag with leading / if the tag is an end tag. + * + * @param data the html text + * @param offset the index of the < bracket. + * @return the tag name with possible leading slash. + */ +var getTagName = function (data, offset) +{ + if (data[offset] !== '<') { throw new Error(); } + // Match ugly tags like < / xxx> + // or < xxx y="z" > + var m = data.substring(offset).match(/^(<[\s\/]*)([a-zA-Z0-9_-]+)/); + if (!m) { throw new Error("could not get tag name"); } + if (m[1].indexOf('/') !== -1) { return '/'+m[2]; } + return m[2]; +}; + +/** + * Get the previous non-void opening tag. + * + * @param data the document html + * @param ctx an empty map for the first call, the same element thereafter. + * @return an array containing the offset of the open bracket for the begin tag and the + * the offset of the open bracket for the matching end tag. + */ +var getPreviousNonVoidTag = function (data, ctx) +{ + for (;;) { + if (typeof(ctx.offsets) === 'undefined') { + // ' ' is an invalid html element name so it will never match anything. + ctx.offsets = [ { idx: data.length, name: ' ' } ]; + ctx.idx = data.length; + } + + var prev = ctx.idx = getPreviousTagIdx(data, ctx.idx); + if (prev === -1) { + if (ctx.offsets.length > 1) { throw new Error(); } + return [ 0, data.length ]; + } + var prevTagName = getTagName(data, prev); + + if (prevTagName[0] === '/') { + ctx.offsets.push({ idx: prev, name: prevTagName.substring(1) }); + } else if (prevTagName === ctx.offsets[ctx.offsets.length-1].name) { + var os = ctx.offsets.pop(); + return [ prev, os.idx ]; + } else if (!VOID_TAG_REGEX.test(prevTagName)) { + throw new Error(); + } + } +}; + +var indexOfSkipQuoted = function (haystack, needle) +{ + var os = 0; + for (;;) { + var dqi = haystack.indexOf('"'); + var sqi = haystack.indexOf("'"); + var needlei = haystack.indexOf(needle); + if (needlei === -1) { return -1; } + if (dqi > -1 && dqi < sqi && dqi < needlei) { + dqi = haystack.indexOf('"', dqi+1); + if (dqi === -1) { throw new Error(); } + haystack = haystack.substring(dqi+1); + os += dqi+1; + } else if (sqi > -1 && sqi < needlei) { + sqi = haystack.indexOf('"', sqi+1); + if (sqi === -1) { throw new Error(); } + haystack = haystack.substring(sqi+1); + os += sqi+1; + } else { + return needlei + os; + } + } +}; + +var tagWidth = module.exports.tagWidth = function (nodeOuterHTML) +{ + if (nodeOuterHTML.length < 2 || nodeOuterHTML[1] === '!' || nodeOuterHTML[0] !== '<') { + return 0; + } + return indexOfSkipQuoted(nodeOuterHTML, '>') + 1; +}; + +var makeHTMLOperation = module.exports.makeHTMLOperation = function (oldval, newval) +{ + var op = makeTextOperation(oldval, newval); + if (!op) { return; } + + var end = op.offset + op.toRemove; + var lastTag; + var tag; + var ctx = {}; + do { + lastTag = tag; + tag = getPreviousNonVoidTag(oldval, ctx); + } while (tag[0] > op.offset || tag[1] < end); + + if (lastTag + && end < lastTag[0] + && op.offset > tag[0] + tagWidth(oldval.substring(tag[0]))) + { + // plain old text operation. + if (op.toRemove && oldval.substr(op.offset, op.toRemove).indexOf('<') !== -1) { + throw new Error(); + } + return op; + } + + op.offset = tag[0]; + op.toRemove = tag[1] - tag[0]; + op.toInsert = newval.slice(tag[0], newval.length - (oldval.length - tag[1])); + + return op; +}; + +/** + * Expand an operation to cover enough HTML that any naive transformation + * will result in correct HTML. + */ +var expandOp = module.exports.expandOp = function (html, op) { +return op; + if (Common.PARANOIA && typeof(html) !== 'string') { throw new Error(); } + var ctx = {}; + for (;;) { + var elem = HtmlParse.getPreviousElement(html, ctx); + // reached the end, this should not happen... + if (!elem) { throw new Error(JSON.stringify(op)); } + if (elem.openTagIndex <= op.offset) { + var endIndex = html.indexOf('>', elem.closeTagIndex) + 1; + if (!endIndex) { throw new Error(); } + if (endIndex >= op.offset + op.toRemove) { + var newHtml = Operation.apply(op, html); + var newEndIndex = endIndex - op.toRemove + op.toInsert.length; + var out = Operation.create(elem.openTagIndex, + endIndex - elem.openTagIndex, + newHtml.substring(elem.openTagIndex, newEndIndex)); + if (Common.PARANOIA) { + var test = Operation.apply(out, html); + if (test !== newHtml) { + throw new Error(test + '\n\n\n' + newHtml + '\n\n' + elem.openTagIndex + '\n\n' + newEndIndex); + } + if (out.toInsert[0] !== '<') { throw new Error(); } + if (out.toInsert[out.toInsert.length - 1] !== '>') { throw new Error(); } + } + return out; + } + } + //console.log(elem); + } +}; + +var transformB = function (html, toTransform, transformBy) { + + var transformByEndOffset = transformBy.offset + transformBy.toRemove; + if (toTransform.offset > transformByEndOffset) { + // simple rebase + toTransform.offset -= transformBy.toRemove; + toTransform.offset += transformBy.toInsert.length; + return toTransform; + } + + var toTransformEndOffset = toTransform.offset + toTransform.toRemove; + + if (transformBy.offset > toTransformEndOffset) { + // we're before them, no transformation needed. + return toTransform; + } + + // so we overlap, we're just going to revert one and apply the other. + // The one which affects more content should probably be applied. + var toRevert = toTransform; + var toApply = transformBy; + var swap = function () { + var x = toRevert; + toRevert = toApply; + toApply = x; + }; + + if (toTransform.toInsert.length > transformBy.toInsert.length) { + swap(); + } else if (toTransform.toInsert.length < transformBy.toInsert.length) { + // fall through + } else if (toTransform.toRemove > transformBy.toRemove) { + swap(); + } else if (toTransform.toRemove < transformBy.toRemove) { + // fall through + } else { + if (Operation.equals(toTransform, transformBy)) { return null; } + // tie-breaker: we just strcmp the JSON. + if (Common.strcmp(JSON.stringify(toTransform), JSON.stringify(transformBy)) < 0) { swap(); } + } + + var inverse = Operation.invert(toRevert, html); + if (Common.PARANOIA) { + var afterToRevert = Operation.apply(toRevert, html); + + } + if (Common.PARANOIA && !Operation.shouldMerge(inverse, toApply)) { throw new Error(); } + var out = Operation.merge(inverse, toApply); +}; + +var transform = module.exports.transform = function (html, toTransform, transformBy) { + + return transformB(html, toTransform, transformBy); +/* + toTransform = Operation.clone(toTransform); + toTransform = expandOp(html, toTransform); + + transformBy = Operation.clone(transformBy); + transformBy = expandOp(html, transformBy); + + if (toTransform.offset >= transformBy.offset) { + if (toTransform.offset >= transformBy.offset + transformBy.toRemove) { + // simple rebase + toTransform.offset -= transformBy.toRemove; + toTransform.offset += transformBy.toInsert.length; + return toTransform; + } + + // They deleted our begin offset... + + var toTransformEndOffset = toTransform.offset + toTransform.toRemove; + var transformByEndOffset = transformBy.offset + transformBy.toRemove; + if (transformByEndOffset >= toTransformEndOffset) { + // They also deleted our end offset, lets forget we wrote anything because + // whatever it was, they deleted it's context. + return null; + } + + // goto the end, anything you deleted that they also deleted should be skipped. + var newOffset = transformBy.offset + transformBy.toInsert.length; + toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); + if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } + toTransform.offset = newOffset; + if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { + return null; + } + return toTransform; + } + if (toTransform.offset + toTransform.toRemove < transformBy.offset) { + return toTransform; + } + toTransform.toRemove = transformBy.offset - toTransform.offset; + if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { + return null; + } + return toTransform; +*/ +}; + +}, +"SHA256.js": function(module, exports, require){ +/* A JavaScript implementation of the Secure Hash Algorithm, SHA-256 + * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ + * Distributed under the BSD License + * Some bits taken from Paul Johnston's SHA-1 implementation + */ +(function () { + var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + function safe_add (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + function S (X, n) {return ( X >>> n ) | (X << (32 - n));} + function R (X, n) {return ( X >>> n );} + function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} + function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} + function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} + function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} + function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} + function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} + function newArray (n) { + var a = []; + for (;n>0;n--) { + a.push(undefined); + } + return a; + } + function core_sha256 (m, l) { + var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2]; + var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; + var W = newArray(64); + var a, b, c, d, e, f, g, h, i, j; + var T1, T2; + /* append padding */ + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >> 9) << 4) + 15] = l; + for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); + return bin; + } + function binb2hex (binarray) { + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for (var i = 0; i < binarray.length * 4; i++) { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; + } + function hex_sha256(s){ + return binb2hex(core_sha256(str2binb(s),s.length * chrsz)); + } + module.exports.hex_sha256 = hex_sha256; +}()); + +}, +"Common.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +var PARANOIA = module.exports.PARANOIA = false; + +/* throw errors over non-compliant messages which would otherwise be treated as invalid */ +var TESTING = module.exports.TESTING = true; + +var assert = module.exports.assert = function (expr) { + if (!expr) { throw new Error("Failed assertion"); } +}; + +var isUint = module.exports.isUint = function (integer) { + return (typeof(integer) === 'number') && + (Math.floor(integer) === integer) && + (integer >= 0); +}; + +var randomASCII = module.exports.randomASCII = function (length) { + var content = []; + for (var i = 0; i < length; i++) { + content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 ); + } + return content.join(''); +}; + +var strcmp = module.exports.strcmp = function (a, b) { + if (PARANOIA && typeof(a) !== 'string') { throw new Error(); } + if (PARANOIA && typeof(b) !== 'string') { throw new Error(); } + return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) ); +} + +}, +"Operation.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var Common = require('./Common'); + +var Operation = module.exports; + +var check = Operation.check = function (op, docLength_opt) { + Common.assert(op.type === 'Operation'); + Common.assert(Common.isUint(op.offset)); + Common.assert(Common.isUint(op.toRemove)); + Common.assert(typeof(op.toInsert) === 'string'); + Common.assert(op.toRemove > 0 || op.toInsert.length > 0); + Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt); +}; + +var create = Operation.create = function (offset, toRemove, toInsert) { + var out = { + type: 'Operation', + offset: offset || 0, + toRemove: toRemove || 0, + toInsert: toInsert || '', + }; + if (Common.PARANOIA) { check(out); } + return out; +}; + +var toObj = Operation.toObj = function (op) { + if (Common.PARANOIA) { check(op); } + return [op.offset,op.toRemove,op.toInsert]; +}; + +var fromObj = Operation.fromObj = function (obj) { + Common.assert(Array.isArray(obj) && obj.length === 3); + return create(obj[0], obj[1], obj[2]); +}; + +var clone = Operation.clone = function (op) { + return create(op.offset, op.toRemove, op.toInsert); +}; + +/** + * @param op the operation to apply. + * @param doc the content to apply the operation on + */ +var apply = Operation.apply = function (op, doc) +{ + if (Common.PARANOIA) { + check(op); + Common.assert(typeof(doc) === 'string'); + Common.assert(op.offset + op.toRemove <= doc.length); + } + return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove); +}; + +var invert = Operation.invert = function (op, doc) { + if (Common.PARANOIA) { + check(op); + Common.assert(typeof(doc) === 'string'); + Common.assert(op.offset + op.toRemove <= doc.length); + } + var rop = clone(op); + rop.toInsert = doc.substring(op.offset, op.offset + op.toRemove); + rop.toRemove = op.toInsert.length; + return rop; +}; + +var simplify = Operation.simplify = function (op, doc) { + if (Common.PARANOIA) { + check(op); + Common.assert(typeof(doc) === 'string'); + Common.assert(op.offset + op.toRemove <= doc.length); + } + var rop = invert(op, doc); + op = clone(op); + + var minLen = Math.min(op.toInsert.length, rop.toInsert.length); + var i; + for (i = 0; i < minLen && rop.toInsert[i] === op.toInsert[i]; i++) ; + op.offset += i; + op.toRemove -= i; + op.toInsert = op.toInsert.substring(i); + rop.toInsert = rop.toInsert.substring(i); + + if (rop.toInsert.length === op.toInsert.length) { + for (i = rop.toInsert.length-1; i >= 0 && rop.toInsert[i] === op.toInsert[i]; i--) ; + op.toInsert = op.toInsert.substring(0, i+1); + op.toRemove = i+1; + } + + if (op.toRemove === 0 && op.toInsert.length === 0) { return null; } + return op; +}; + +var equals = Operation.equals = function (opA, opB) { + return (opA.toRemove === opB.toRemove + && opA.toInsert === opB.toInsert + && opA.offset === opB.offset); +}; + +var lengthChange = Operation.lengthChange = function (op) +{ + if (Common.PARANOIA) { check(op); } + return op.toInsert.length - op.toRemove; +}; + +/* + * @return the merged operation OR null if the result of the merger is a noop. + */ +var merge = Operation.merge = function (oldOpOrig, newOpOrig) { + if (Common.PARANOIA) { + check(newOpOrig); + check(oldOpOrig); + } + + var newOp = clone(newOpOrig); + var oldOp = clone(oldOpOrig); + var offsetDiff = newOp.offset - oldOp.offset; + + if (newOp.toRemove > 0) { + var origOldInsert = oldOp.toInsert; + oldOp.toInsert = ( + oldOp.toInsert.substring(0,offsetDiff) + + oldOp.toInsert.substring(offsetDiff + newOp.toRemove) + ); + newOp.toRemove -= (origOldInsert.length - oldOp.toInsert.length); + if (newOp.toRemove < 0) { newOp.toRemove = 0; } + + oldOp.toRemove += newOp.toRemove; + newOp.toRemove = 0; + } + + if (offsetDiff < 0) { + oldOp.offset += offsetDiff; + oldOp.toInsert = newOp.toInsert + oldOp.toInsert; + + } else if (oldOp.toInsert.length === offsetDiff) { + oldOp.toInsert = oldOp.toInsert + newOp.toInsert; + + } else if (oldOp.toInsert.length > offsetDiff) { + oldOp.toInsert = ( + oldOp.toInsert.substring(0,offsetDiff) + + newOp.toInsert + + oldOp.toInsert.substring(offsetDiff) + ); + } else { + throw new Error("should never happen\n" + + JSON.stringify([oldOpOrig,newOpOrig], null, ' ')); + } + + if (oldOp.toInsert === '' && oldOp.toRemove === 0) { + return null; + } + if (Common.PARANOIA) { check(oldOp); } + + return oldOp; +}; + +/** + * If the new operation deletes what the old op inserted or inserts content in the middle of + * the old op's content or if they abbut one another, they should be merged. + */ +var shouldMerge = Operation.shouldMerge = function (oldOp, newOp) { + if (Common.PARANOIA) { + check(oldOp); + check(newOp); + } + if (newOp.offset < oldOp.offset) { + return (oldOp.offset <= (newOp.offset + newOp.toRemove)); + } else { + return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length)); + } +}; + +/** + * Rebase newOp against oldOp. + * + * @param oldOp the eariler operation to have happened. + * @param newOp the later operation to have happened (in time). + * @return either the untouched newOp if it need not be rebased, + * the rebased clone of newOp if it needs rebasing, or + * null if newOp and oldOp must be merged. + */ +var rebase = Operation.rebase = function (oldOp, newOp) { + if (Common.PARANOIA) { + check(oldOp); + check(newOp); + } + if (newOp.offset < oldOp.offset) { return newOp; } + newOp = clone(newOp); + newOp.offset += oldOp.toRemove; + newOp.offset -= oldOp.toInsert.length; + return newOp; +}; + +/** + * this is a lossy and dirty algorithm, everything else is nice but transformation + * has to be lossy because both operations have the same base and they diverge. + * This could be made nicer and/or tailored to a specific data type. + * + * @param toTransform the operation which is converted *MUTATED*. + * @param transformBy an existing operation which also has the same base. + * @return toTransform *or* null if the result is a no-op. + */ +var transform0 = Operation.transform0 = function (text, toTransform, transformBy) { + if (toTransform.offset > transformBy.offset) { + if (toTransform.offset > transformBy.offset + transformBy.toRemove) { + // simple rebase + toTransform.offset -= transformBy.toRemove; + toTransform.offset += transformBy.toInsert.length; + return toTransform; + } + // goto the end, anything you deleted that they also deleted should be skipped. + var newOffset = transformBy.offset + transformBy.toInsert.length; + toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); + if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } + toTransform.offset = newOffset; + if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { + return null; + } + return toTransform; + } + if (toTransform.offset + toTransform.toRemove < transformBy.offset) { + return toTransform; + } + toTransform.toRemove = transformBy.offset - toTransform.offset; + if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { + return null; + } + return toTransform; +}; + +/** + * @param toTransform the operation which is converted + * @param transformBy an existing operation which also has the same base. + * @return a modified clone of toTransform *or* toTransform itself if no change was made. + */ +var transform = Operation.transform = function (text, toTransform, transformBy, transformFunction) { + if (Common.PARANOIA) { + check(toTransform); + check(transformBy); + } + transformFunction = transformFunction || transform0; + toTransform = clone(toTransform); + var result = transformFunction(text, toTransform, transformBy); + if (Common.PARANOIA && result) { check(result); } + return result; +}; + +/** Used for testing. */ +var random = Operation.random = function (docLength) { + Common.assert(Common.isUint(docLength)); + var offset = Math.floor(Math.random() * 100000000 % docLength) || 0; + var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0; + var toInsert = ''; + do { + var toInsert = Common.randomASCII(Math.floor(Math.random() * 20)); + } while (toRemove === 0 && toInsert === ''); + return create(offset, toRemove, toInsert); +}; + +}, +"HtmlParse.js": function(module, exports, require){ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +var VOID_TAG_REGEX = module.exports.VOID_TAG_REGEX = new RegExp('^(' + [ + 'area', + 'base', + 'br', + 'col', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'command', + 'keygen', + 'source', +].join('|') + ')$'); + +/** + * Get the offset of the previous open/close/void tag. + * returns the offset of the opening angle bracket. + */ +var getPreviousTagIdx = module.exports.getPreviousTagIdx = function (data, idx) { + if (idx === 0) { return -1; } + idx = data.lastIndexOf('>', idx); + // The html tag from hell: + // < abc def="g" k='lm"nopw>"qrstu" + for (;;) { + var mch = data.substring(0,idx).match(/[<"'][^<'"]*$/); + if (!mch) { return -1; } + if (mch[0][0] === '<') { return mch.index; } + idx = data.lastIndexOf(mch[0][0], mch.index-1); + } +}; + +/** + * Get the name of an HTML tag with leading / if the tag is an end tag. + * + * @param data the html text + * @param offset the index of the < bracket. + * @return the tag name with possible leading slash. + */ +var getTagName = module.exports.getTagName = function (data, offset) { + if (data[offset] !== '<') { throw new Error(); } + // Match ugly tags like < / xxx> + // or < xxx y="z" > + var m = data.substring(offset).match(/^(<[\s\/]*)([a-zA-Z0-9_-]+)/); + if (!m) { throw new Error("could not get tag name"); } + if (m[1].indexOf('/') !== -1) { return '/'+m[2]; } + return m[2]; +}; + +/** + * Get the previous void or opening tag. + * + * @param data the document html + * @param ctx an empty map for the first call, the same element thereafter. + * @return an object containing openTagIndex: the offset of the < bracket for the begin tag, + * closeTagIndex: the the offset of the < bracket for the matching end tag, and + * nodeName: the element name. + * If the element is a void element, the second value in the array will be -1. + */ +var getPreviousElement = module.exports.getPreviousElement = function (data, ctx) { + for (;;) { + if (typeof(ctx.offsets) === 'undefined') { + // ' ' is an invalid html element name so it will never match anything. + ctx.offsets = [ { idx: data.length, name: ' ' } ]; + ctx.idx = data.length; + } + + var prev = ctx.idx = getPreviousTagIdx(data, ctx.idx); + if (prev === -1) { + if (ctx.offsets.length > 1) { throw new Error(); } + return null; + } + var prevTagName = getTagName(data, prev); + + if (prevTagName[0] === '/') { + ctx.offsets.push({ idx: prev, name: prevTagName.substring(1) }); + } else if (prevTagName === ctx.offsets[ctx.offsets.length-1].name) { + var os = ctx.offsets.pop(); + return { openTagIndex: prev, closeTagIndex: os.idx, nodeName: prevTagName }; + } else if (!VOID_TAG_REGEX.test(prevTagName)) { + throw new Error("unmatched tag [" + prevTagName + "] which is not a void tag"); + } else { + return { openTagIndex: prev, closeTagIndex: -1, nodeName: prevTagName }; + } + } +}; + +/** + * Given a piece of HTML text which begins at the < of a non-close tag, + * give the index within that content which contains the matching > + * character skipping > characters contained within attributes. + */ +var getEndOfTag = module.exports.getEndOfTag = function (html) { + var arr = html.match(/['">][^"'>]*/g); + var q = null; + var idx = html.indexOf(arr[0]); + for (var i = 0; i < arr.length; i++) { + if (!q) { + q = arr[i][0]; + if (q === '>') { return idx; } + } else if (q === arr[i][0]) { + q = null; + } + idx += arr[i].length; + } + throw new Error("Could not find end of tag"); +}; + + +var ParseTagState = { + OUTSIDE: 0, + NAME: 1, + VALUE: 2, + SQUOTE: 3, + DQUOTE: 4, +}; + +var parseTag = module.exports.parseTag = function (html) { + if (html[0] !== '<') { throw new Error("Must be the beginning of a tag"); } + + var out = { + nodeName: null, + attributes: [], + endIndex: -1, + trailingSlash: false + }; + + if (html.indexOf('>') < html.indexOf(' ') || html.indexOf(' ') === -1) { + out.endIndex = html.indexOf('>'); + out.nodeName = html.substring(1, out.endIndex); + return out; + } + + out.nodeName = html.substring(1, html.indexOf(' ')); + + if (html.indexOf('<' + out.nodeName + ' ') !== 0) { + throw new Error("Nonstandard beginning of tag [" + + html.substring(0, 30) + '] for nodeName [' + out.nodeName + ']'); + } + var i = 1 + out.nodeName.length + 1; + + var state = ParseTagState.OUTSIDE; + var name = []; + var value = []; + var pushAttribute = function () { + out.attributes.push([name.join(''), value.join('')]); + name = []; + value = []; + }; + for (; i < html.length; i++) { + var chr = html[i]; + switch (state) { + case ParseTagState.OUTSIDE: { + if (chr === '/') { + out.trailingSlash = true; + } else if (chr.match(/[a-zA-Z0-9_-]/)) { + state = ParseTagState.NAME; + if (name.length > 0) { throw new Error(); } + name.push(chr); + } else if (chr === '>') { + out.endIndex = i; + return out; + } else if (chr === ' ') { + // fall through + } else { + throw new Error(); + } + continue; + } + case ParseTagState.NAME: { + if (chr.match(/[a-zA-Z0-9_-]/)) { + name.push(chr); + } else if (chr === '=') { + state = ParseTagState.VALUE; + } else if (chr === '/' || chr === ' ') { + if (chr === '/') { + out.trailingSlash = true; + } + out.attributes.push([name.join(''), null]); + name = []; + state = ParseTagState.OUTSIDE; + } else if (chr === '>') { + out.attributes.push([name.join(''), null]); + name = []; + out.endIndex = i; + return out; + } else { + throw new Error("bad character [" + chr + "] in name [" + name.join('') + "]"); + } + continue; + } + case ParseTagState.VALUE: { + value.push(chr); + if (chr === '"') { + state = ParseTagState.DQUOTE; + } else if (chr === "'") { + state = ParseTagState.SQUOTE; + } else { + throw new Error(); + } + continue; + } + case ParseTagState.SQUOTE: { + value.push(chr); + if (chr === "'") { + pushAttribute(); + state = ParseTagState.OUTSIDE; + } + continue; + } + case ParseTagState.DQUOTE: { + value.push(chr); + if (chr === '"') { + pushAttribute(); + state = ParseTagState.OUTSIDE; + } + continue; + } + } + } + + throw new Error("reached end of file while parsing"); +}; + +var serializeTag = module.exports.serializeTag = function (tag) { + var out = ['<', tag.nodeName]; + for (var i = 0; i < tag.attributes.length; i++) { + var att = tag.attributes[i]; + if (att[1] === null) { + out.push(' ', att[0]); + } else { + out.push(' ', att[0], '=', att[1]); + } + } + if (tag.trailingSlash) { + out.push(' /'); + } + out.push('>'); + return out.join(''); +}; + +} +}; +Otaml = r("Otaml.js");}()); diff --git a/www/rangy.js b/www/rangy.js new file mode 100644 index 000000000..513046e89 --- /dev/null +++ b/www/rangy.js @@ -0,0 +1,3738 @@ +/** + * Rangy, a cross-browser JavaScript range and selection library + * http://code.google.com/p/rangy/ + * + * Copyright 2013, Tim Down + * Licensed under the MIT license. + * Version: 1.3alpha.804 + * Build date: 8 December 2013 + */ + +(function(global) { + var amdSupported = (typeof global.define == "function" && global.define.amd); + + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; + + // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START + // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. + var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + // Minimal set of methods required for DOM Level 2 Range compliance + var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", + "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", + "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; + + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; + + // Subset of TextRange's full set of methods that we're interested in + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", + "setEndPoint", "getBoundingClientRect"]; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Trio of functions taken from Peter Michaux's article: + // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting + function isHostMethod(o, p) { + var t = typeof o[p]; + return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; + } + + function isHostObject(o, p) { + return !!(typeof o[p] == OBJECT && o[p]); + } + + function isHostProperty(o, p) { + return typeof o[p] != UNDEFINED; + } + + // Creates a convenience function to save verbose repeated calls to tests functions + function createMultiplePropertyTest(testFunc) { + return function(o, props) { + var i = props.length; + while (i--) { + if (!testFunc(o, props[i])) { + return false; + } + } + return true; + }; + } + + // Next trio of functions are a convenience to save verbose repeated calls to previous two functions + var areHostMethods = createMultiplePropertyTest(isHostMethod); + var areHostObjects = createMultiplePropertyTest(isHostObject); + var areHostProperties = createMultiplePropertyTest(isHostProperty); + + function isTextRange(range) { + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); + } + + function getBody(doc) { + return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; + } + + var modules = {}; + + var api = { + version: "1.3alpha.804", + initialized: false, + supported: true, + + util: { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange, + getBody: getBody + }, + + features: {}, + + modules: modules, + config: { + alertOnFail: true, + alertOnWarn: false, + preferTextRange: false + } + }; + + function consoleLog(msg) { + if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { + window.console.log(msg); + } + } + + function alertOrLog(msg, shouldAlert) { + if (shouldAlert) { + window.alert(msg); + } else { + consoleLog(msg); + } + } + + function fail(reason) { + api.initialized = true; + api.supported = false; + alertOrLog("Rangy is not supported on this page in your browser. Reason: " + reason, api.config.alertOnFail); + } + + api.fail = fail; + + function warn(msg) { + alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); + } + + api.warn = warn; + + // Add utility extend() method + if ({}.hasOwnProperty) { + api.util.extend = function(obj, props, deep) { + var o, p; + for (var i in props) { + if (props.hasOwnProperty(i)) { + o = obj[i]; + p = props[i]; + //if (deep) alert([o !== null, typeof o == "object", p !== null, typeof p == "object"]) + if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { + api.util.extend(o, p, true); + } + obj[i] = p; + } + } + return obj; + }; + } else { + fail("hasOwnProperty not supported"); + } + + // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not + (function() { + var el = document.createElement("div"); + el.appendChild(document.createElement("span")); + var slice = [].slice; + var toArray; + try { + if (slice.call(el.childNodes, 0)[0].nodeType == 1) { + toArray = function(arrayLike) { + return slice.call(arrayLike, 0); + }; + } + } catch (e) {} + + if (!toArray) { + toArray = function(arrayLike) { + var arr = []; + for (var i = 0, len = arrayLike.length; i < len; ++i) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + api.util.toArray = toArray; + })(); + + + // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or + // normalization of event properties + var addListener; + if (isHostMethod(document, "addEventListener")) { + addListener = function(obj, eventType, listener) { + obj.addEventListener(eventType, listener, false); + }; + } else if (isHostMethod(document, "attachEvent")) { + addListener = function(obj, eventType, listener) { + obj.attachEvent("on" + eventType, listener); + }; + } else { + fail("Document does not have required addEventListener or attachEvent method"); + } + + api.util.addListener = addListener; + + var initListeners = []; + + function getErrorDesc(ex) { + return ex.message || ex.description || String(ex); + } + + // Initialization + function init() { + if (api.initialized) { + return; + } + var testRange; + var implementsDomRange = false, implementsTextRange = false; + + // First, perform basic feature tests + + if (isHostMethod(document, "createRange")) { + testRange = document.createRange(); + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { + implementsDomRange = true; + } + testRange.detach(); + } + + var body = getBody(document); + if (!body || body.nodeName.toLowerCase() != "body") { + fail("No body element found"); + return; + } + + if (body && isHostMethod(body, "createTextRange")) { + testRange = body.createTextRange(); + if (isTextRange(testRange)) { + implementsTextRange = true; + } + } + + if (!implementsDomRange && !implementsTextRange) { + fail("Neither Range nor TextRange are available"); + return; + } + + api.initialized = true; + api.features = { + implementsDomRange: implementsDomRange, + implementsTextRange: implementsTextRange + }; + + // Initialize modules + var module, errorMessage; + for (var moduleName in modules) { + if ( (module = modules[moduleName]) instanceof Module ) { + module.init(module, api); + } + } + + // Call init listeners + for (var i = 0, len = initListeners.length; i < len; ++i) { + try { + initListeners[i](api); + } catch (ex) { + errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); + consoleLog(errorMessage); + } + } + } + + // Allow external scripts to initialize this library in case it's loaded after the document has loaded + api.init = init; + + // Execute listener immediately if already initialized + api.addInitListener = function(listener) { + if (api.initialized) { + listener(api); + } else { + initListeners.push(listener); + } + }; + + var createMissingNativeApiListeners = []; + + api.addCreateMissingNativeApiListener = function(listener) { + createMissingNativeApiListeners.push(listener); + }; + + function createMissingNativeApi(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { + createMissingNativeApiListeners[i](win); + } + } + + api.createMissingNativeApi = createMissingNativeApi; + + function Module(name, dependencies, initializer) { + this.name = name; + this.dependencies = dependencies; + this.initialized = false; + this.supported = false; + this.initializer = initializer; + } + + Module.prototype = { + init: function(api) { + var requiredModuleNames = this.dependencies || []; + for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { + moduleName = requiredModuleNames[i]; + + requiredModule = modules[moduleName]; + if (!requiredModule || !(requiredModule instanceof Module)) { + throw new Error("required module '" + moduleName + "' not found"); + } + + requiredModule.init(); + + if (!requiredModule.supported) { + throw new Error("required module '" + moduleName + "' not supported"); + } + } + + // Now run initializer + this.initializer(this) + }, + + fail: function(reason) { + this.initialized = true; + this.supported = false; + throw new Error("Module '" + this.name + "' failed to load: " + reason); + }, + + warn: function(msg) { + api.warn("Module " + this.name + ": " + msg); + }, + + deprecationNotice: function(deprecated, replacement) { + api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " + + replacement + " instead"); + }, + + createError: function(msg) { + return new Error("Error in Rangy " + this.name + " module: " + msg); + } + }; + + function createModule(isCore, name, dependencies, initFunc) { + var newModule = new Module(name, dependencies, function(module) { + if (!module.initialized) { + module.initialized = true; + try { + initFunc(api, module); + module.supported = true; + } catch (ex) { + var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); + consoleLog(errorMessage); + } + } + }); + modules[name] = newModule; + +/* + // Add module AMD support + if (!isCore && amdSupported) { + global.define(["rangy-core"], function(rangy) { + + }); + } +*/ + } + + api.createModule = function(name) { + // Allow 2 or 3 arguments (second argument is an optional array of dependencies) + var initFunc, dependencies; + if (arguments.length == 2) { + initFunc = arguments[1]; + dependencies = []; + } else { + initFunc = arguments[2]; + dependencies = arguments[1]; + } + createModule(false, name, dependencies, initFunc); + }; + + api.createCoreModule = function(name, dependencies, initFunc) { + createModule(true, name, dependencies, initFunc); + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately + + function RangePrototype() {} + api.RangePrototype = RangePrototype; + api.rangePrototype = new RangePrototype(); + + function SelectionPrototype() {} + api.selectionPrototype = new SelectionPrototype(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wait for document to load before running tests + + var docReady = false; + + var loadHandler = function(e) { + if (!docReady) { + docReady = true; + if (!api.initialized) { + init(); + } + } + }; + + // Test whether we have window and document objects that we will need + if (typeof window == UNDEFINED) { + fail("No window found"); + return; + } + if (typeof document == UNDEFINED) { + fail("No document found"); + return; + } + + if (isHostMethod(document, "addEventListener")) { + document.addEventListener("DOMContentLoaded", loadHandler, false); + } + + // Add a fallback in case the DOMContentLoaded event isn't supported + addListener(window, "load", loadHandler); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // AMD, for those who like this kind of thing + + if (amdSupported) { + // AMD. Register as an anonymous module. + global.define(function() { + api.amd = true; + return api; + }); + } + + // Create a "rangy" property of the global object in any case. Other Rangy modules (which use Rangy's own simple + // module system) rely on the existence of this global property + global.rangy = api; +})(this); + +rangy.createCoreModule("DomUtil", [], function(api, module) { + var UNDEF = "undefined"; + var util = api.util; + + // Perform feature tests + if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { + module.fail("document missing a Node creation method"); + } + + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); + } + + var el = document.createElement("div"); + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { + module.fail("Incomplete Element implementation"); + } + + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } + + var textNode = document.createTextNode("test"); + if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || + !util.areHostProperties(textNode, ["data"]))) { + module.fail("Incomplete Text Node implementation"); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been + // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that + // contains just the document as a single element and the value searched for is the document. + var arrayContains = /*Array.prototype.indexOf ? + function(arr, val) { + return arr.indexOf(val) > -1; + }:*/ + + function(arr, val) { + var i = arr.length; + while (i--) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI + function isHtmlNamespace(node) { + var ns; + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); + } + + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } + + function getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + ++i; + } + return i; + } + + function getNodeLength(node) { + switch (node.nodeType) { + case 7: + case 10: + return 0; + case 3: + case 8: + return node.length; + default: + return node.childNodes.length; + } + } + + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); + } + + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; + } + } + + return null; + } + + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; + } + } + return false; + } + + function isOrIsAncestorOf(ancestor, descendant) { + return isAncestorOf(ancestor, descendant, true); + } + + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; + } + n = p; + } + return null; + } + + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + } + + function isTextOrCommentNode(node) { + if (!node) { + return false; + } + var t = node.nodeType; + return t == 3 || t == 8 ; // Text or Comment + } + + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } + + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index, positionsToPreserve) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + + // Preserve positions + if (positionsToPreserve) { + for (var i = 0, position; position = positionsToPreserve[i++]; ) { + // Handle case where position was inside the portion of node after the split point + if (position.node == node && position.offset > index) { + position.node = newNode; + position.offset -= index; + } + // Handle the case where the position is a node offset within node's parent + else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { + ++position.offset; + } + } + } + return newNode; + } + + function getDocument(node) { + if (node.nodeType == 9) { + return node; + } else if (typeof node.ownerDocument != UNDEF) { + return node.ownerDocument; + } else if (typeof node.document != UNDEF) { + return node.document; + } else if (node.parentNode) { + return getDocument(node.parentNode); + } else { + throw module.createError("getDocument: no document found for node"); + } + } + + function getWindow(node) { + var doc = getDocument(node); + if (typeof doc.defaultView != UNDEF) { + return doc.defaultView; + } else if (typeof doc.parentWindow != UNDEF) { + return doc.parentWindow; + } else { + throw module.createError("Cannot get a window object for node"); + } + } + + function getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw module.createError("getIframeDocument: No Document object found for iframe element"); + } + } + + function getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw module.createError("getIframeWindow: No Window object found for iframe element"); + } + } + + // This looks bad. Is it worth it? + function isWindow(obj) { + return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); + } + + function getContentDocument(obj, module, methodName) { + var doc; + + if (!obj) { + doc = document; + } + + // Test if a DOM node has been passed and obtain a document object for it if so + else if (util.isHostProperty(obj, "nodeType")) { + doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") + ? getIframeDocument(obj) : getDocument(obj); + } + + // Test if the doc parameter appears to be a Window object + else if (isWindow(obj)) { + doc = obj.document; + } + + if (!doc) { + throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); + } + + return doc; + } + + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + + function comparePoints(nodeA, offsetA, nodeB, offsetB) { + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing + var nodeC, root, childA, childB, n; + if (nodeA == nodeB) { + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { + // Case 2: node C (container B or an ancestor) is a child node of A + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { + root = getCommonAncestor(nodeA, nodeB); + if (!root) { + throw new Error("comparePoints error: nodes have no common ancestor"); + } + + // Case 4: containers are siblings or descendants of siblings + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible + throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); + } else { + n = root.firstChild; + while (n) { + if (n === childA) { + return -1; + } else if (n === childB) { + return 1; + } + n = n.nextSibling; + } + } + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried + var crashyTextNodes = false; + + function isBrokenNode(node) { + try { + node.parentNode; + return false; + } catch (e) { + return true; + } + } + + (function() { + var el = document.createElement("b"); + el.innerHTML = "1"; + var textNode = el.firstChild; + el.innerHTML = "
"; + crashyTextNodes = isBrokenNode(textNode); + + api.features.crashyTextNodes = crashyTextNodes; + })(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (crashyTextNodes && isBrokenNode(node)) { + return "[Broken node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } + if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[" + getNodeIndex(node) + "][" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; + } + return node.nodeName; + } + + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + + var getComputedStyleProperty; + if (typeof window.getComputedStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return getWindow(el).getComputedStyle(el, null)[propName]; + }; + } else if (typeof document.documentElement.currentStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return el.currentStyle[propName]; + }; + } else { + module.fail("No means of obtaining computed style properties found"); + } + + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + var n = this._current = this._next; + var child, next; + if (this._current) { + child = n.firstChild; + if (child) { + this._next = child; + } else { + next = null; + while ((n !== this.root) && !(next = n.nextSibling)) { + n = n.parentNode; + } + this._next = next; + } + } + return this._current; + }, + + detach: function() { + this._current = this._next = this.root = null; + } + }; + + function createIterator(root) { + return new NodeIterator(root); + } + + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } + + DomPosition.prototype = { + equals: function(pos) { + return !!pos && this.node === pos.node && this.offset == pos.offset; + }, + + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; + }, + + toString: function() { + return this.inspect(); + } + }; + + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } + + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11 + }; + + DOMException.prototype.toString = function() { + return this.message; + }; + + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + isOrIsAncestorOf: isOrIsAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + isTextOrCommentNode: isTextOrCommentNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: util.getBody, + isWindow: isWindow, + getContentDocument: getContentDocument, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + isBrokenNode: isBrokenNode, + inspectNode: inspectNode, + getComputedStyleProperty: getComputedStyleProperty, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; + + api.DOMException = DOMException; +}); +rangy.createCoreModule("DomRange", ["DomUtil"], function(api, module) { + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + var isCharacterDataNode = dom.isCharacterDataNode; + var getNodeIndex = dom.getNodeIndex; + var isOrIsAncestorOf = dom.isOrIsAncestorOf; + var getDocument = dom.getDocument; + var comparePoints = dom.comparePoints; + var splitDataNode = dom.splitDataNode; + var getClosestAncestorIn = dom.getClosestAncestorIn; + var getNodeLength = dom.getNodeLength; + var arrayContains = dom.arrayContains; + var getRootContainer = dom.getRootContainer; + var crashyTextNodes = api.features.crashyTextNodes; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); + } + + function getRangeDocument(range) { + return range.document || getDocument(range.startContainer); + } + + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node)); + } + + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node) + 1); + } + + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); + } + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); + } + return firstNodeInserted; + } + + function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { + assertRangeValid(rangeA); + assertRangeValid(rangeB); + + if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + + var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), + endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + } + + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(true); + } + + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + if (rangeIterator.isPartiallySelectedSubtree()) { + if (func(node) === false) { + iteratorState.stop = true; + return; + } else { + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of + // the node selected by the Range. + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(true); + if (iteratorState.stop) { + return; + } + } + } else { + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendants + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } + } + } + } + + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(true); + } else { + iterator.remove(); + } + } + } + + function extractSubtree(iterator) { + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(true); + } else { + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function getNodesInRange(range, nodeTypes, filter) { + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } + + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if (filterNodeTypes && !regex.test(node.nodeType)) { + return; + } + if (filterExists && !filter(node)) { + return; + } + // Don't include a boundary container if it is a character data node and the range does not contain any + // of its character data. See issue 190. + var sc = range.startContainer; + if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { + return; + } + + var ec = range.endContainer; + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { + return; + } + + nodes.push(node); + }); + return nodes; + } + + function inspect(range) { + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); + } + } + } + + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, + + reset: function() { + this._current = null; + this._next = this._first; + }, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; + + // Check for partially selected text nodes + if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + current.parentNode.removeChild(current); + } else { + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(false); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); + + if (isOrIsAncestorOf(current, this.sc)) { + startContainer = this.sc; + startOffset = this.so; + } + if (isOrIsAncestorOf(current, this.ec)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function(detachRange) { + if (detachRange) { + this.range.detach(); + } + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Exceptions + + function RangeException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "RangeException: " + this.codeName; + } + + RangeException.prototype = { + BAD_BOUNDARYPOINTS_ERR: 1, + INVALID_NODE_TYPE_ERR: 2 + }; + + RangeException.prototype.toString = function() { + return this.message; + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; + + function createAncestorFinder(nodeTypes) { + return function(node, selfIsAncestor) { + var t, n = selfIsAncestor ? node : node.parentNode; + while (n) { + t = n.nodeType; + if (arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; + } + return null; + }; + } + + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertNotDetached(range) { + if (!range.startContainer) { + throw new DOMException("INVALID_STATE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!arrayContains(invalidTypes, node.nodeType)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } + + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } + + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } + + function isOrphan(node) { + return (crashyTextNodes && dom.isBrokenNode(node)) || + !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + } + + function isValidOffset(node, offset) { + return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + } + + function isRangeValid(range) { + return (!!range.startContainer && !!range.endContainer + && !isOrphan(range.startContainer) + && !isOrphan(range.endContainer) + && isValidOffset(range.startContainer, range.startOffset) + && isValidOffset(range.endContainer, range.endOffset)); + } + + function assertRangeValid(range) { + assertNotDetached(range); + if (!isRangeValid(range)) { + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "x"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" + && dom.isHtmlNamespace(getDocument(el).documentElement) + && dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + assertNotDetached(this); + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + function splitRangeBoundaries(range, positionsToPreserve) { + assertRangeValid(range); + + var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; + var startEndSame = (sc === ec); + + if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + splitDataNode(ec, eo, positionsToPreserve); + } + + if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { + sc = splitDataNode(sc, so, positionsToPreserve); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { + eo++; + } + so = 0; + } + range.setStartAndEnd(sc, so, ec, eo); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; + + util.extend(api.rangePrototype, { + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return comparePoints(nodeA, offsetA, nodeB, offsetB); + }, + + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (isOrIsAncestorOf(node, this.startContainer)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); + } + return clone; + } + }, + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); + }, + + cloneRange: function() { + assertRangeValid(this); + var range = new Range(getRangeDocument(this)); + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = this[prop]; + } + return range; + }, + + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textParts = [], iterator = new RangeIterator(this, true); + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments + if (node.nodeType == 3 || node.nodeType == 4) { + textParts.push(node.data); + } + }); + iterator.detach(); + return textParts.join(""); + } + }, + + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. + + compareNode: function(node) { + assertRangeValid(this); + + var parent = node.parentNode; + var nodeIndex = getNodeIndex(node); + + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } + + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); + + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, + + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, + + createContextualFragment: createContextualFragment, + + toHtml: function() { + assertRangeValid(this); + var container = this.commonAncestorContainer.parentNode.cloneNode(false); + container.appendChild(this.cloneContents()); + return container.innerHTML; + }, + + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + assertNode(node, "NOT_FOUND_ERR"); + if (getDocument(node) !== getRangeDocument(this)) { + return false; + } + + var parent = node.parentNode, offset = getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); + + var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, + + // The methods below are non-standard and invented by me. + + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range) { + return rangesIntersect(this, range, false); + }, + + // Sharing a boundary start-to-end or end-to-start does count as intersection. + intersectsOrTouchesRange: function(range) { + return rangesIntersect(this, range, true); + }, + + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); + + var intersectionRange = this.cloneRange(); + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); + } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, + + union: function(range) { + if (this.intersectsOrTouchesRange(range)) { + var unionRange = this.cloneRange(); + if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new RangeException("Ranges do not intersect"); + } + }, + + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, + + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; + }, + + containsRange: function(range) { + var intersection = this.intersection(range); + return intersection !== null && range.equals(intersection); + }, + + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + var contains = this.containsRange(nodeRange); + nodeRange.detach(); + return contains; + } else { + return this.containsNodeContents(node); + } + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + assertNotDetached(this); + + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + assertNotDetached(this); + + this.setStartAfter(node); + this.collapse(true); + }, + + getBookmark: function(containerNode) { + var doc = getRangeDocument(this); + var preSelectionRange = api.createRange(doc); + containerNode = containerNode || dom.getBody(doc); + preSelectionRange.selectNodeContents(containerNode); + var range = this.intersection(preSelectionRange); + var start = 0, end = 0; + if (range) { + preSelectionRange.setEnd(range.startContainer, range.startOffset); + start = preSelectionRange.toString().length; + end = start + range.toString().length; + preSelectionRange.detach(); + } + + return { + start: start, + end: end, + containerNode: containerNode + }; + }, + + moveToBookmark: function(bookmark) { + var containerNode = bookmark.containerNode; + var charIndex = 0; + this.setStart(containerNode, 0); + this.collapse(true); + var nodeStack = [containerNode], node, foundStart = false, stop = false; + var nextCharIndex, i, childNodes; + + while (!stop && (node = nodeStack.pop())) { + if (node.nodeType == 3) { + nextCharIndex = charIndex + node.length; + if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { + this.setStart(node, bookmark.start - charIndex); + foundStart = true; + } + if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { + this.setEnd(node, bookmark.end - charIndex); + stop = true; + } + charIndex = nextCharIndex; + } else { + childNodes = node.childNodes; + i = childNodes.length; + while (i--) { + nodeStack.push(childNodes[i]); + } + } + } + }, + + getName: function() { + return "DomRange"; + }, + + equals: function(range) { + return Range.rangesEqual(this, range); + }, + + isValid: function() { + return isRangeValid(this); + }, + + inspect: function() { + return inspect(this); + } + }); + + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; + + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } + + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } + + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + + var iterator = new RangeIterator(this, true); + + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } + + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); + + iterator.reset(); + + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); + + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); + + return returnValue; + }; + } + + function createPrototypeRange(constructor, boundaryUpdater, detacher) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertNotDetached(this); + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } + + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } + } + + // Set up inheritance + var F = function() {}; + F.prototype = api.rangePrototype; + constructor.prototype = new F(); + + util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeEnd(this, node, offset); + }, + + /** + * Convenience method to set a range's start and end boundaries. Overloaded as follows: + * - Two parameters (node, offset) creates a collapsed range at that position + * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at + * startOffset and ending at endOffset + * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in + * startNode and ending at endOffset in endNode + */ + setStartAndEnd: function() { + assertNotDetached(this); + + var args = arguments; + var sc = args[0], so = args[1], ec = sc, eo = so; + + switch (args.length) { + case 3: + eo = args[2]; + break; + case 4: + ec = args[2]; + eo = args[3]; + break; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + setBoundary: function(node, offset, isStart) { + this["set" + (isStart ? "Start" : "End")](node, offset); + }, + + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { + assertRangeValid(this); + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + } + }, + + selectNodeContents: function(node) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, getNodeLength(node)); + }, + + selectNode: function(node) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); + + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, + + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + detach: function() { + detacher(this); + }, + + splitBoundaries: function() { + splitRangeBoundaries(this); + }, + + splitBoundariesPreservingPositions: function(positionsToPreserve) { + splitRangeBoundaries(this, positionsToPreserve); + }, + + normalizeBoundaries: function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + + var mergeForward = function(node) { + var sibling = node.nextSibling; + if (sibling && sibling.nodeType == node.nodeType) { + ec = node; + eo = node.length; + node.appendData(sibling.data); + sibling.parentNode.removeChild(sibling); + } + }; + + var mergeBackward = function(node) { + var sibling = node.previousSibling; + if (sibling && sibling.nodeType == node.nodeType) { + sc = node; + var nodeLength = node.length; + so = sibling.length; + node.insertData(0, sibling.data); + sibling.parentNode.removeChild(sibling); + if (sc == ec) { + eo += so; + ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; + + var normalizeStart = true; + + if (isCharacterDataNode(ec)) { + if (ec.length == eo) { + mergeForward(ec); + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && isCharacterDataNode(endNode)) { + mergeForward(endNode); + } + } + normalizeStart = !this.collapsed; + } + + if (normalizeStart) { + if (isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); + } + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && isCharacterDataNode(startNode)) { + mergeBackward(startNode); + } + } + } + } else { + sc = ec; + so = eo; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + collapseToPoint: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + this.setStartAndEnd(node, offset); + } + }); + + copyComparisonConstants(constructor); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Updates commonAncestorContainer and collapsed after boundary change + function updateCollapsedAndCommonAncestor(range) { + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + range.commonAncestorContainer = range.collapsed ? + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); + } + + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; + range.document = dom.getDocument(startContainer); + + updateCollapsedAndCommonAncestor(range); + } + + function detach(range) { + assertNotDetached(range); + range.startContainer = range.startOffset = range.endContainer = range.endOffset = range.document = null; + range.collapsed = range.commonAncestorContainer = null; + } + + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this.document = doc; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries, detach); + + util.extend(Range, { + rangeProperties: rangeProperties, + RangeIterator: RangeIterator, + copyComparisonConstants: copyComparisonConstants, + createPrototypeRange: createPrototypeRange, + inspect: inspect, + getRangeDocument: getRangeDocument, + rangesEqual: function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + } + }); + + api.DomRange = Range; + api.RangeException = RangeException; +}); +rangy.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { + var WrappedRange, WrappedTextRange; + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; + var getBody = dom.getBody; + var getContentDocument = dom.getContentDocument; + var isCharacterDataNode = dom.isCharacterDataNode; + + + /*----------------------------------------------------------------------------------------------------------------*/ + + if (api.features.implementsDomRange) { + // This is a wrapper around the browser's native DOM Range. It has two aims: + // - Provide workarounds for specific browser bugs + // - provide convenient extensions, which are inherited from Rangy's DomRange + + (function() { + var rangeProto; + var rangeProperties = DomRange.rangeProperties; + + function updateRangeProperties(range) { + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = range.nativeRange[prop]; + } + // Fix for broken collapsed property in IE 9. + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + } + + function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); + var nativeRangeDifferent = !range.equals(range.nativeRange); + + // Always set both boundaries for the benefit of IE9 (see issue 35) + if (startMoved || endMoved || nativeRangeDifferent) { + range.setEnd(endContainer, endOffset); + range.setStart(startContainer, startOffset); + } + } + + function detach(range) { + range.nativeRange.detach(); + range.detached = true; + var i = rangeProperties.length; + while (i--) { + range[ rangeProperties[i] ] = null; + } + } + + var createBeforeAfterNodeSetter; + + WrappedRange = function(range) { + if (!range) { + throw module.createError("WrappedRange: Range must be specified"); + } + this.nativeRange = range; + updateRangeProperties(this); + }; + + DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach); + + rangeProto = WrappedRange.prototype; + + rangeProto.selectNode = function(node) { + this.nativeRange.selectNode(node); + updateRangeProperties(this); + }; + + rangeProto.cloneContents = function() { + return this.nativeRange.cloneContents(); + }; + + // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, + // insertNode() is never delegated to the native range. + + rangeProto.surroundContents = function(node) { + this.nativeRange.surroundContents(node); + updateRangeProperties(this); + }; + + rangeProto.collapse = function(isStart) { + this.nativeRange.collapse(isStart); + updateRangeProperties(this); + }; + + rangeProto.cloneRange = function() { + return new WrappedRange(this.nativeRange.cloneRange()); + }; + + rangeProto.refresh = function() { + updateRangeProperties(this); + }; + + rangeProto.toString = function() { + return this.nativeRange.toString(); + }; + + // Create test range and node for feature detection + + var testTextNode = document.createTextNode("test"); + getBody(document).appendChild(testTextNode); + var range = document.createRange(); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and + // correct for it + + range.setStart(testTextNode, 0); + range.setEnd(testTextNode, 0); + + try { + range.setStart(testTextNode, 1); + + rangeProto.setStart = function(node, offset) { + this.nativeRange.setStart(node, offset); + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + this.nativeRange.setEnd(node, offset); + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name) { + return function(node) { + this.nativeRange[name](node); + updateRangeProperties(this); + }; + }; + + } catch(ex) { + + rangeProto.setStart = function(node, offset) { + try { + this.nativeRange.setStart(node, offset); + } catch (ex) { + this.nativeRange.setEnd(node, offset); + this.nativeRange.setStart(node, offset); + } + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + try { + this.nativeRange.setEnd(node, offset); + } catch (ex) { + this.nativeRange.setStart(node, offset); + this.nativeRange.setEnd(node, offset); + } + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name, oppositeName) { + return function(node) { + try { + this.nativeRange[name](node); + } catch (ex) { + this.nativeRange[oppositeName](node); + this.nativeRange[name](node); + } + updateRangeProperties(this); + }; + }; + } + + rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); + rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); + rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); + rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing + // whether the native implementation can be trusted + rangeProto.selectNodeContents = function(node) { + this.setStartAndEnd(node, 0, dom.getNodeLength(node)); + }; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for + // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 + + range.selectNodeContents(testTextNode); + range.setEnd(testTextNode, 3); + + var range2 = document.createRange(); + range2.selectNodeContents(testTextNode); + range2.setEnd(testTextNode, 4); + range2.setStart(testTextNode, 2); + + if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && + range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { + // This is the wrong way round, so correct for it + + rangeProto.compareBoundaryPoints = function(type, range) { + range = range.nativeRange || range; + if (type == range.START_TO_END) { + type = range.END_TO_START; + } else if (type == range.END_TO_START) { + type = range.START_TO_END; + } + return this.nativeRange.compareBoundaryPoints(type, range); + }; + } else { + rangeProto.compareBoundaryPoints = function(type, range) { + return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for IE 9 deleteContents() and extractContents() bug and correct it. See issue 107. + + var el = document.createElement("div"); + el.innerHTML = "123"; + var textNode = el.firstChild; + var body = getBody(document); + body.appendChild(el); + + range.setStart(textNode, 1); + range.setEnd(textNode, 2); + range.deleteContents(); + + if (textNode.data == "13") { + // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and + // extractContents() + rangeProto.deleteContents = function() { + this.nativeRange.deleteContents(); + updateRangeProperties(this); + }; + + rangeProto.extractContents = function() { + var frag = this.nativeRange.extractContents(); + updateRangeProperties(this); + return frag; + }; + } else { + } + + body.removeChild(el); + body = null; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for existence of createContextualFragment and delegate to it if it exists + if (util.isHostMethod(range, "createContextualFragment")) { + rangeProto.createContextualFragment = function(fragmentStr) { + return this.nativeRange.createContextualFragment(fragmentStr); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Clean up + getBody(document).removeChild(testTextNode); + range.detach(); + range2.detach(); + + rangeProto.getName = function() { + return "WrappedRange"; + }; + + api.WrappedRange = WrappedRange; + + api.createNativeRange = function(doc) { + doc = getContentDocument(doc, module, "createNativeRange"); + return doc.createRange(); + }; + })(); + } + + if (api.features.implementsTextRange) { + /* + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() + method. For example, in the following (where pipes denote the selection boundaries): + +
  • | a
  • b |
+ + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" + + This method returns the common ancestor node of the following: + - the parentElement() of the textRange + - the parentElement() of the textRange after calling collapse(true) + - the parentElement() of the textRange after calling collapse(false) + */ + var getTextRangeContainerElement = function(textRange) { + var parentEl = textRange.parentElement(); + var range = textRange.duplicate(); + range.collapse(true); + var startEl = range.parentElement(); + range = textRange.duplicate(); + range.collapse(false); + var endEl = range.parentElement(); + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); + + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + }; + + var textRangeIsCollapsed = function(textRange) { + return textRange.compareEndPoints("StartToEnd", textRange) == 0; + }; + + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as + // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has + // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling + // for inputs and images, plus optimizations. + var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { + var workingRange = textRange.duplicate(); + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); + + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { + containerElement = wholeRangeContainerElement; + } + + + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx + if (!containerElement.canHaveHTML) { + var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + return { + boundaryPosition: pos, + nodeInfo: { + nodeIndex: pos.offset, + containerElement: pos.node + } + }; + } + + var workingNode = dom.getDocument(containerElement).createElement("span"); + + // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 + // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 + if (workingNode.parentNode) { + workingNode.parentNode.removeChild(workingNode); + } + + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; + var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; + var childNodeCount = containerElement.childNodes.length; + var end = childNodeCount; + + // Check end first. Code within the loop assumes that the endth child node of the container is definitely + // after the range boundary. + var nodeIndex = end; + + while (true) { + if (nodeIndex == childNodeCount) { + containerElement.appendChild(workingNode); + } else { + containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); + } + workingRange.moveToElementText(workingNode); + comparison = workingRange.compareEndPoints(workingComparisonType, textRange); + if (comparison == 0 || start == end) { + break; + } else if (comparison == -1) { + if (end == start + 1) { + // We know the endth child node is after the range boundary, so we must be done. + break; + } else { + start = nodeIndex; + } + } else { + end = (end == start + 1) ? start : nodeIndex; + } + nodeIndex = Math.floor((start + end) / 2); + containerElement.removeChild(workingNode); + } + + + // We've now reached or gone past the boundary of the text range we're interested in + // so have identified the node we want + boundaryNode = workingNode.nextSibling; + + if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the + // node containing the text range's boundary, so we move the end of the working range to the boundary point + // and measure the length of its text to get the boundary's offset within the node. + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); + + var offset; + + if (/[\r\n]/.test(boundaryNode.data)) { + /* + For the particular case of a boundary within a text node containing rendered line breaks (within a
+                    element, for example), we need a slightly complicated approach to get the boundary's offset in IE. The
+                    facts:
+                    
+                    - Each line break is represented as \r in the text node's data/nodeValue properties
+                    - Each line break is represented as \r\n in the TextRange's 'text' property
+                    - The 'text' property of the TextRange does not contain trailing line breaks
+                    
+                    To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                    moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
+                    the same as the number of characters it was instructed to move. The simplest approach is to use this to
+                    store the characters moved when moving both the start and end of the range to the start of the document
+                    body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
+                    However, this is extremely slow when the document is large and the range is near the end of it. Clearly
+                    doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
+                    problem.
+                    
+                    Another approach that works is to use moveStart() to move the start boundary of the range up to the end
+                    boundary one character at a time and incrementing a counter with the value returned by the moveStart()
+                    call. However, the check for whether the start boundary has reached the end boundary is expensive, so
+                    this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
+                    the range within the document).
+                    
+                    The method below is a hybrid of the two methods above. It uses the fact that a string containing the
+                    TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
+                    text of the TextRange, so the start of the range is moved that length initially and then a character at
+                    a time to make up for any trailing line breaks not contained in the 'text' property. This has good
+                    performance in most situations compared to the previous two methods.
+                    */
+                    var tempRange = workingRange.duplicate();
+                    var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                    offset = tempRange.moveStart("character", rangeLength);
+                    while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                        offset++;
+                        tempRange.moveStart("character", 1);
+                    }
+                } else {
+                    offset = workingRange.text.length;
+                }
+                boundaryPosition = new DomPosition(boundaryNode, offset);
+            } else {
+
+                // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+                // a position within that, and likewise for a start boundary preceding a character data node
+                previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+                nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+                if (nextNode && isCharacterDataNode(nextNode)) {
+                    boundaryPosition = new DomPosition(nextNode, 0);
+                } else if (previousNode && isCharacterDataNode(previousNode)) {
+                    boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
+                } else {
+                    boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+                }
+            }
+
+            // Clean up
+            workingNode.parentNode.removeChild(workingNode);
+
+            return {
+                boundaryPosition: boundaryPosition,
+                nodeInfo: {
+                    nodeIndex: nodeIndex,
+                    containerElement: containerElement
+                }
+            };
+        };
+
+        // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
+        // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+        // (http://code.google.com/p/ierange/)
+        var createBoundaryTextRange = function(boundaryPosition, isStart) {
+            var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+            var doc = dom.getDocument(boundaryPosition.node);
+            var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
+            var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
+
+            if (nodeIsDataNode) {
+                boundaryNode = boundaryPosition.node;
+                boundaryParent = boundaryNode.parentNode;
+            } else {
+                childNodes = boundaryPosition.node.childNodes;
+                boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+                boundaryParent = boundaryPosition.node;
+            }
+
+            // Position the range immediately before the node containing the boundary
+            workingNode = doc.createElement("span");
+
+            // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
+            // element rather than immediately before or after it
+            workingNode.innerHTML = "&#feff;";
+
+            // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+            // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+            if (boundaryNode) {
+                boundaryParent.insertBefore(workingNode, boundaryNode);
+            } else {
+                boundaryParent.appendChild(workingNode);
+            }
+
+            workingRange.moveToElementText(workingNode);
+            workingRange.collapse(!isStart);
+
+            // Clean up
+            boundaryParent.removeChild(workingNode);
+
+            // Move the working range to the text offset, if required
+            if (nodeIsDataNode) {
+                workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+            }
+
+            return workingRange;
+        };
+
+        /*------------------------------------------------------------------------------------------------------------*/
+
+        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+        // prototype
+
+        WrappedTextRange = function(textRange) {
+            this.textRange = textRange;
+            this.refresh();
+        };
+
+        WrappedTextRange.prototype = new DomRange(document);
+
+        WrappedTextRange.prototype.refresh = function() {
+            var start, end, startBoundary;
+
+            // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+            var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+            if (textRangeIsCollapsed(this.textRange)) {
+                end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
+                    true).boundaryPosition;
+            } else {
+                startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                start = startBoundary.boundaryPosition;
+
+                // An optimization used here is that if the start and end boundaries have the same parent element, the
+                // search scope for the end boundary can be limited to exclude the portion of the element that precedes
+                // the start boundary
+                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
+                    startBoundary.nodeInfo).boundaryPosition;
+            }
+
+            this.setStart(start.node, start.offset);
+            this.setEnd(end.node, end.offset);
+        };
+
+        WrappedTextRange.prototype.getName = function() {
+            return "WrappedTextRange";
+        };
+
+        DomRange.copyComparisonConstants(WrappedTextRange);
+
+        WrappedTextRange.rangeToTextRange = function(range) {
+            if (range.collapsed) {
+                return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+            } else {
+                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
+                textRange.setEndPoint("StartToStart", startRange);
+                textRange.setEndPoint("EndToEnd", endRange);
+                return textRange;
+            }
+        };
+
+        api.WrappedTextRange = WrappedTextRange;
+
+        // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
+        // implementation to use by default.
+        if (!api.features.implementsDomRange || api.config.preferTextRange) {
+            // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+            var globalObj = (function() { return this; })();
+            if (typeof globalObj.Range == "undefined") {
+                globalObj.Range = WrappedTextRange;
+            }
+
+            api.createNativeRange = function(doc) {
+                doc = getContentDocument(doc, module, "createNativeRange");
+                return getBody(doc).createTextRange();
+            };
+
+            api.WrappedRange = WrappedTextRange;
+        }
+    }
+
+    api.createRange = function(doc) {
+        doc = getContentDocument(doc, module, "createRange");
+        return new api.WrappedRange(api.createNativeRange(doc));
+    };
+
+    api.createRangyRange = function(doc) {
+        doc = getContentDocument(doc, module, "createRangyRange");
+        return new DomRange(doc);
+    };
+
+    api.createIframeRange = function(iframeEl) {
+        module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
+        return api.createRange(iframeEl);
+    };
+
+    api.createIframeRangyRange = function(iframeEl) {
+        module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
+        return api.createRangyRange(iframeEl);
+    };
+
+    api.addCreateMissingNativeApiListener(function(win) {
+        var doc = win.document;
+        if (typeof doc.createRange == "undefined") {
+            doc.createRange = function() {
+                return api.createRange(doc);
+            };
+        }
+        doc = win = null;
+    });
+});
+// This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
+// in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
+rangy.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
+    api.config.checkSelectionRanges = true;
+
+    var BOOLEAN = "boolean";
+    var NUMBER = "number";
+    var dom = api.dom;
+    var util = api.util;
+    var isHostMethod = util.isHostMethod;
+    var DomRange = api.DomRange;
+    var WrappedRange = api.WrappedRange;
+    var DOMException = api.DOMException;
+    var DomPosition = dom.DomPosition;
+    var getNativeSelection;
+    var selectionIsCollapsed;
+    var features = api.features;
+    var CONTROL = "Control";
+    var getDocument = dom.getDocument;
+    var getBody = dom.getBody;
+    var rangesEqual = DomRange.rangesEqual;
+
+
+    // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
+    // Boolean (true for backwards).
+    function isDirectionBackward(dir) {
+        return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
+    }
+
+    function getWindow(win, methodName) {
+        if (!win) {
+            return window;
+        } else if (dom.isWindow(win)) {
+            return win;
+        } else if (win instanceof WrappedSelection) {
+            return win.win;
+        } else {
+            var doc = dom.getContentDocument(win, module, methodName);
+            return dom.getWindow(doc);
+        }
+    }
+
+    function getWinSelection(winParam) {
+        return getWindow(winParam, "getWinSelection").getSelection();
+    }
+
+    function getDocSelection(winParam) {
+        return getWindow(winParam, "getDocSelection").document.selection;
+    }
+    
+    function winSelectionIsBackward(sel) {
+        var backward = false;
+        if (sel.anchorNode) {
+            backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+        }
+        return backward;
+    }
+
+    // Test for the Range/TextRange and Selection features required
+    // Test for ability to retrieve selection
+    var implementsWinGetSelection = isHostMethod(window, "getSelection"),
+        implementsDocSelection = util.isHostObject(document, "selection");
+
+    features.implementsWinGetSelection = implementsWinGetSelection;
+    features.implementsDocSelection = implementsDocSelection;
+
+    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+    if (useDocumentSelection) {
+        getNativeSelection = getDocSelection;
+        api.isSelectionValid = function(winParam) {
+            var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
+
+            // Check whether the selection TextRange is actually contained within the correct document
+            return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
+        };
+    } else if (implementsWinGetSelection) {
+        getNativeSelection = getWinSelection;
+        api.isSelectionValid = function() {
+            return true;
+        };
+    } else {
+        module.fail("Neither document.selection or window.getSelection() detected.");
+    }
+
+    api.getNativeSelection = getNativeSelection;
+
+    var testSelection = getNativeSelection();
+    var testRange = api.createNativeRange(document);
+    var body = getBody(document);
+
+    // Obtaining a range from a selection
+    var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
+        ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
+
+    features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+    // Test for existence of native selection extend() method
+    var selectionHasExtend = isHostMethod(testSelection, "extend");
+    features.selectionHasExtend = selectionHasExtend;
+    
+    // Test if rangeCount exists
+    var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
+    features.selectionHasRangeCount = selectionHasRangeCount;
+
+    var selectionSupportsMultipleRanges = false;
+    var collapsedNonEditableSelectionsSupported = true;
+
+    var addRangeBackwardToNative = selectionHasExtend ?
+        function(nativeSelection, range) {
+            var doc = DomRange.getRangeDocument(range);
+            var endRange = api.createRange(doc);
+            endRange.collapseToPoint(range.endContainer, range.endOffset);
+            nativeSelection.addRange(getNativeRange(endRange));
+            nativeSelection.extend(range.startContainer, range.startOffset);
+        } : null;
+
+    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+            typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
+
+        (function() {
+            // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
+            // performed on the current document's selection. See issue 109.
+
+            // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
+            // because initialization usually happens when the document loads, but could be a problem for a script that
+            // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
+            // selection.
+            var sel = window.getSelection();
+            if (sel) {
+                // Store the current selection
+                var originalSelectionRangeCount = sel.rangeCount;
+                var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
+                var originalSelectionRanges = [];
+                var originalSelectionBackward = winSelectionIsBackward(sel); 
+                for (var i = 0; i < originalSelectionRangeCount; ++i) {
+                    originalSelectionRanges[i] = sel.getRangeAt(i);
+                }
+                
+                // Create some test elements
+                var body = getBody(document);
+                var testEl = body.appendChild( document.createElement("div") );
+                testEl.contentEditable = "false";
+                var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
+
+                // Test whether the native selection will allow a collapsed selection within a non-editable element
+                var r1 = document.createRange();
+
+                r1.setStart(textNode, 1);
+                r1.collapse(true);
+                sel.addRange(r1);
+                collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+                sel.removeAllRanges();
+
+                // Test whether the native selection is capable of supporting multiple ranges
+                if (!selectionHasMultipleRanges) {
+                    var r2 = r1.cloneRange();
+                    r1.setStart(textNode, 0);
+                    r2.setEnd(textNode, 3);
+                    r2.setStart(textNode, 2);
+                    sel.addRange(r1);
+                    sel.addRange(r2);
+
+                    selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+                    r2.detach();
+                }
+
+                // Clean up
+                body.removeChild(testEl);
+                sel.removeAllRanges();
+                r1.detach();
+
+                for (i = 0; i < originalSelectionRangeCount; ++i) {
+                    if (i == 0 && originalSelectionBackward) {
+                        if (addRangeBackwardToNative) {
+                            addRangeBackwardToNative(sel, originalSelectionRanges[i]);
+                        } else {
+                            api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because browser does not support Selection.extend");
+                            sel.addRange(originalSelectionRanges[i])
+                        }
+                    } else {
+                        sel.addRange(originalSelectionRanges[i])
+                    }
+                }
+            }
+        })();
+    }
+
+    features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+    features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+    // ControlRanges
+    var implementsControlRange = false, testControlRange;
+
+    if (body && isHostMethod(body, "createControlRange")) {
+        testControlRange = body.createControlRange();
+        if (util.areHostProperties(testControlRange, ["item", "add"])) {
+            implementsControlRange = true;
+        }
+    }
+    features.implementsControlRange = implementsControlRange;
+
+    // Selection collapsedness
+    if (selectionHasAnchorAndFocus) {
+        selectionIsCollapsed = function(sel) {
+            return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+        };
+    } else {
+        selectionIsCollapsed = function(sel) {
+            return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+        };
+    }
+
+    function updateAnchorAndFocusFromRange(sel, range, backward) {
+        var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
+        sel.anchorNode = range[anchorPrefix + "Container"];
+        sel.anchorOffset = range[anchorPrefix + "Offset"];
+        sel.focusNode = range[focusPrefix + "Container"];
+        sel.focusOffset = range[focusPrefix + "Offset"];
+    }
+
+    function updateAnchorAndFocusFromNativeSelection(sel) {
+        var nativeSel = sel.nativeSelection;
+        sel.anchorNode = nativeSel.anchorNode;
+        sel.anchorOffset = nativeSel.anchorOffset;
+        sel.focusNode = nativeSel.focusNode;
+        sel.focusOffset = nativeSel.focusOffset;
+    }
+
+    function updateEmptySelection(sel) {
+        sel.anchorNode = sel.focusNode = null;
+        sel.anchorOffset = sel.focusOffset = 0;
+        sel.rangeCount = 0;
+        sel.isCollapsed = true;
+        sel._ranges.length = 0;
+    }
+
+    function getNativeRange(range) {
+        var nativeRange;
+        if (range instanceof DomRange) {
+            nativeRange = api.createNativeRange(range.getDocument());
+            nativeRange.setEnd(range.endContainer, range.endOffset);
+            nativeRange.setStart(range.startContainer, range.startOffset);
+        } else if (range instanceof WrappedRange) {
+            nativeRange = range.nativeRange;
+        } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
+            nativeRange = range;
+        }
+        return nativeRange;
+    }
+
+    function rangeContainsSingleElement(rangeNodes) {
+        if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+            return false;
+        }
+        for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+            if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    function getSingleElementFromRange(range) {
+        var nodes = range.getNodes();
+        if (!rangeContainsSingleElement(nodes)) {
+            throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+        }
+        return nodes[0];
+    }
+
+    // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
+    function isTextRange(range) {
+        return !!range && typeof range.text != "undefined";
+    }
+
+    function updateFromTextRange(sel, range) {
+        // Create a Range from the selected TextRange
+        var wrappedRange = new WrappedRange(range);
+        sel._ranges = [wrappedRange];
+
+        updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+        sel.rangeCount = 1;
+        sel.isCollapsed = wrappedRange.collapsed;
+    }
+
+    function updateControlSelection(sel) {
+        // Update the wrapped selection based on what's now in the native selection
+        sel._ranges.length = 0;
+        if (sel.docSelection.type == "None") {
+            updateEmptySelection(sel);
+        } else {
+            var controlRange = sel.docSelection.createRange();
+            if (isTextRange(controlRange)) {
+                // This case (where the selection type is "Control" and calling createRange() on the selection returns
+                // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+                // ControlRange have been removed from the ControlRange and removed from the document.
+                updateFromTextRange(sel, controlRange);
+            } else {
+                sel.rangeCount = controlRange.length;
+                var range, doc = getDocument(controlRange.item(0));
+                for (var i = 0; i < sel.rangeCount; ++i) {
+                    range = api.createRange(doc);
+                    range.selectNode(controlRange.item(i));
+                    sel._ranges.push(range);
+                }
+                sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+                updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+            }
+        }
+    }
+
+    function addRangeToControlSelection(sel, range) {
+        var controlRange = sel.docSelection.createRange();
+        var rangeElement = getSingleElementFromRange(range);
+
+        // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+        // contained by the supplied range
+        var doc = getDocument(controlRange.item(0));
+        var newControlRange = getBody(doc).createControlRange();
+        for (var i = 0, len = controlRange.length; i < len; ++i) {
+            newControlRange.add(controlRange.item(i));
+        }
+        try {
+            newControlRange.add(rangeElement);
+        } catch (ex) {
+            throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+        }
+        newControlRange.select();
+
+        // Update the wrapped selection based on what's now in the native selection
+        updateControlSelection(sel);
+    }
+
+    var getSelectionRangeAt;
+
+    if (isHostMethod(testSelection, "getRangeAt")) {
+        // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
+        // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
+        // lesson to us all, especially me.
+        getSelectionRangeAt = function(sel, index) {
+            try {
+                return sel.getRangeAt(index);
+            } catch (ex) {
+                return null;
+            }
+        };
+    } else if (selectionHasAnchorAndFocus) {
+        getSelectionRangeAt = function(sel) {
+            var doc = getDocument(sel.anchorNode);
+            var range = api.createRange(doc);
+            range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
+
+            // Handle the case when the selection was selected backwards (from the end to the start in the
+            // document)
+            if (range.collapsed !== this.isCollapsed) {
+                range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
+            }
+
+            return range;
+        };
+    }
+
+    function WrappedSelection(selection, docSelection, win) {
+        this.nativeSelection = selection;
+        this.docSelection = docSelection;
+        this._ranges = [];
+        this.win = win;
+        this.refresh();
+    }
+
+    WrappedSelection.prototype = api.selectionPrototype;
+
+    function deleteProperties(sel) {
+        sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
+        sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
+        sel.detached = true;
+    }
+
+    var cachedRangySelections = [];
+
+    function actOnCachedSelection(win, action) {
+        var i = cachedRangySelections.length, cached, sel;
+        while (i--) {
+            cached = cachedRangySelections[i];
+            sel = cached.selection;
+            if (action == "deleteAll") {
+                deleteProperties(sel);
+            } else if (cached.win == win) {
+                if (action == "delete") {
+                    cachedRangySelections.splice(i, 1);
+                    return true;
+                } else {
+                    return sel;
+                }
+            }
+        }
+        if (action == "deleteAll") {
+            cachedRangySelections.length = 0;
+        }
+        return null;
+    }
+
+    var getSelection = function(win) {
+        // Check if the parameter is a Rangy Selection object
+        if (win && win instanceof WrappedSelection) {
+            win.refresh();
+            return win;
+        }
+
+        win = getWindow(win, "getNativeSelection");
+
+        var sel = actOnCachedSelection(win);
+        var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+        if (sel) {
+            sel.nativeSelection = nativeSel;
+            sel.docSelection = docSel;
+            sel.refresh();
+        } else {
+            sel = new WrappedSelection(nativeSel, docSel, win);
+            cachedRangySelections.push( { win: win, selection: sel } );
+        }
+        return sel;
+    };
+
+    api.getSelection = getSelection;
+
+    api.getIframeSelection = function(iframeEl) {
+        module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
+        return api.getSelection(dom.getIframeWindow(iframeEl));
+    };
+
+    var selProto = WrappedSelection.prototype;
+
+    function createControlSelection(sel, ranges) {
+        // Ensure that the selection becomes of type "Control"
+        var doc = getDocument(ranges[0].startContainer);
+        var controlRange = getBody(doc).createControlRange();
+        for (var i = 0, el, len = ranges.length; i < len; ++i) {
+            el = getSingleElementFromRange(ranges[i]);
+            try {
+                controlRange.add(el);
+            } catch (ex) {
+                throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
+            }
+        }
+        controlRange.select();
+
+        // Update the wrapped selection based on what's now in the native selection
+        updateControlSelection(sel);
+    }
+
+    // Selecting a range
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+        selProto.removeAllRanges = function() {
+            this.nativeSelection.removeAllRanges();
+            updateEmptySelection(this);
+        };
+
+        var addRangeBackward = function(sel, range) {
+            addRangeBackwardToNative(sel.nativeSelection, range);
+            sel.refresh();
+        };
+
+        if (selectionHasRangeCount) {
+            selProto.addRange = function(range, direction) {
+                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    if (isDirectionBackward(direction) && selectionHasExtend) {
+                        addRangeBackward(this, range);
+                    } else {
+                        var previousRangeCount;
+                        if (selectionSupportsMultipleRanges) {
+                            previousRangeCount = this.rangeCount;
+                        } else {
+                            this.removeAllRanges();
+                            previousRangeCount = 0;
+                        }
+                        // Clone the native range so that changing the selected range does not affect the selection.
+                        // This is contrary to the spec but is the only way to achieve consistency between browsers. See
+                        // issue 80.
+                        this.nativeSelection.addRange(getNativeRange(range).cloneRange());
+
+                        // Check whether adding the range was successful
+                        this.rangeCount = this.nativeSelection.rangeCount;
+
+                        if (this.rangeCount == previousRangeCount + 1) {
+                            // The range was added successfully
+
+                            // Check whether the range that we added to the selection is reflected in the last range extracted from
+                            // the selection
+                            if (api.config.checkSelectionRanges) {
+                                var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+                                if (nativeRange && !rangesEqual(nativeRange, range)) {
+                                    // Happens in WebKit with, for example, a selection placed at the start of a text node
+                                    range = new WrappedRange(nativeRange);
+                                }
+                            }
+                            this._ranges[this.rangeCount - 1] = range;
+                            updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
+                            this.isCollapsed = selectionIsCollapsed(this);
+                        } else {
+                            // The range was not added successfully. The simplest thing is to refresh
+                            this.refresh();
+                        }
+                    }
+                }
+            };
+        } else {
+            selProto.addRange = function(range, direction) {
+                if (isDirectionBackward(direction) && selectionHasExtend) {
+                    addRangeBackward(this, range);
+                } else {
+                    this.nativeSelection.addRange(getNativeRange(range));
+                    this.refresh();
+                }
+            };
+        }
+
+        selProto.setRanges = function(ranges) {
+            if (implementsControlRange && ranges.length > 1) {
+                createControlSelection(this, ranges);
+            } else {
+                this.removeAllRanges();
+                for (var i = 0, len = ranges.length; i < len; ++i) {
+                    this.addRange(ranges[i]);
+                }
+            }
+        };
+    } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
+               implementsControlRange && useDocumentSelection) {
+
+        selProto.removeAllRanges = function() {
+            // Added try/catch as fix for issue #21
+            try {
+                this.docSelection.empty();
+
+                // Check for empty() not working (issue #24)
+                if (this.docSelection.type != "None") {
+                    // Work around failure to empty a control selection by instead selecting a TextRange and then
+                    // calling empty()
+                    var doc;
+                    if (this.anchorNode) {
+                        doc = getDocument(this.anchorNode);
+                    } else if (this.docSelection.type == CONTROL) {
+                        var controlRange = this.docSelection.createRange();
+                        if (controlRange.length) {
+                            doc = getDocument( controlRange.item(0) );
+                        }
+                    }
+                    if (doc) {
+                        var textRange = getBody(doc).createTextRange();
+                        textRange.select();
+                        this.docSelection.empty();
+                    }
+                }
+            } catch(ex) {}
+            updateEmptySelection(this);
+        };
+
+        selProto.addRange = function(range) {
+            if (this.docSelection.type == CONTROL) {
+                addRangeToControlSelection(this, range);
+            } else {
+                api.WrappedTextRange.rangeToTextRange(range).select();
+                this._ranges[0] = range;
+                this.rangeCount = 1;
+                this.isCollapsed = this._ranges[0].collapsed;
+                updateAnchorAndFocusFromRange(this, range, false);
+            }
+        };
+
+        selProto.setRanges = function(ranges) {
+            this.removeAllRanges();
+            var rangeCount = ranges.length;
+            if (rangeCount > 1) {
+                createControlSelection(this, ranges);
+            } else if (rangeCount) {
+                this.addRange(ranges[0]);
+            }
+        };
+    } else {
+        module.fail("No means of selecting a Range or TextRange was found");
+        return false;
+    }
+
+    selProto.getRangeAt = function(index) {
+        if (index < 0 || index >= this.rangeCount) {
+            throw new DOMException("INDEX_SIZE_ERR");
+        } else {
+            // Clone the range to preserve selection-range independence. See issue 80.
+            return this._ranges[index].cloneRange();
+        }
+    };
+
+    var refreshSelection;
+
+    if (useDocumentSelection) {
+        refreshSelection = function(sel) {
+            var range;
+            if (api.isSelectionValid(sel.win)) {
+                range = sel.docSelection.createRange();
+            } else {
+                range = getBody(sel.win.document).createTextRange();
+                range.collapse(true);
+            }
+
+            if (sel.docSelection.type == CONTROL) {
+                updateControlSelection(sel);
+            } else if (isTextRange(range)) {
+                updateFromTextRange(sel, range);
+            } else {
+                updateEmptySelection(sel);
+            }
+        };
+    } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
+        refreshSelection = function(sel) {
+            if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+                updateControlSelection(sel);
+            } else {
+                sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+                if (sel.rangeCount) {
+                    for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                        sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+                    }
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            }
+        };
+    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
+        refreshSelection = function(sel) {
+            var range, nativeSel = sel.nativeSelection;
+            if (nativeSel.anchorNode) {
+                range = getSelectionRangeAt(nativeSel, 0);
+                sel._ranges = [range];
+                sel.rangeCount = 1;
+                updateAnchorAndFocusFromNativeSelection(sel);
+                sel.isCollapsed = selectionIsCollapsed(sel);
+            } else {
+                updateEmptySelection(sel);
+            }
+        };
+    } else {
+        module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+        return false;
+    }
+
+    selProto.refresh = function(checkForChanges) {
+        var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+        var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
+
+        refreshSelection(this);
+        if (checkForChanges) {
+            // Check the range count first
+            var i = oldRanges.length;
+            if (i != this._ranges.length) {
+                return true;
+            }
+
+            // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
+            // ranges after this
+            if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
+                return true;
+            }
+
+            // Finally, compare each range in turn
+            while (i--) {
+                if (!rangesEqual(oldRanges[i], this._ranges[i])) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
+    // Removal of a single range
+    var removeRangeManually = function(sel, range) {
+        var ranges = sel.getAllRanges();
+        sel.removeAllRanges();
+        for (var i = 0, len = ranges.length; i < len; ++i) {
+            if (!rangesEqual(range, ranges[i])) {
+                sel.addRange(ranges[i]);
+            }
+        }
+        if (!sel.rangeCount) {
+            updateEmptySelection(sel);
+        }
+    };
+
+    if (implementsControlRange) {
+        selProto.removeRange = function(range) {
+            if (this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var rangeElement = getSingleElementFromRange(range);
+
+                // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                // element contained by the supplied range
+                var doc = getDocument(controlRange.item(0));
+                var newControlRange = getBody(doc).createControlRange();
+                var el, removed = false;
+                for (var i = 0, len = controlRange.length; i < len; ++i) {
+                    el = controlRange.item(i);
+                    if (el !== rangeElement || removed) {
+                        newControlRange.add(controlRange.item(i));
+                    } else {
+                        removed = true;
+                    }
+                }
+                newControlRange.select();
+
+                // Update the wrapped selection based on what's now in the native selection
+                updateControlSelection(this);
+            } else {
+                removeRangeManually(this, range);
+            }
+        };
+    } else {
+        selProto.removeRange = function(range) {
+            removeRangeManually(this, range);
+        };
+    }
+
+    // Detecting if a selection is backward
+    var selectionIsBackward;
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
+        selectionIsBackward = winSelectionIsBackward;
+
+        selProto.isBackward = function() {
+            return selectionIsBackward(this);
+        };
+    } else {
+        selectionIsBackward = selProto.isBackward = function() {
+            return false;
+        };
+    }
+
+    // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
+    selProto.isBackwards = selProto.isBackward;
+
+    // Selection stringifier
+    // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
+    // The current spec does not yet define this method.
+    selProto.toString = function() {
+        var rangeTexts = [];
+        for (var i = 0, len = this.rangeCount; i < len; ++i) {
+            rangeTexts[i] = "" + this._ranges[i];
+        }
+        return rangeTexts.join("");
+    };
+
+    function assertNodeInSameDocument(sel, node) {
+        if (sel.win.document != getDocument(node)) {
+            throw new DOMException("WRONG_DOCUMENT_ERR");
+        }
+    }
+
+    // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
+    selProto.collapse = function(node, offset) {
+        assertNodeInSameDocument(this, node);
+        var range = api.createRange(node);
+        range.collapseToPoint(node, offset);
+        this.setSingleRange(range);
+        this.isCollapsed = true;
+    };
+
+    selProto.collapseToStart = function() {
+        if (this.rangeCount) {
+            var range = this._ranges[0];
+            this.collapse(range.startContainer, range.startOffset);
+        } else {
+            throw new DOMException("INVALID_STATE_ERR");
+        }
+    };
+
+    selProto.collapseToEnd = function() {
+        if (this.rangeCount) {
+            var range = this._ranges[this.rangeCount - 1];
+            this.collapse(range.endContainer, range.endOffset);
+        } else {
+            throw new DOMException("INVALID_STATE_ERR");
+        }
+    };
+
+    // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
+    // never used by Rangy.
+    selProto.selectAllChildren = function(node) {
+        assertNodeInSameDocument(this, node);
+        var range = api.createRange(node);
+        range.selectNodeContents(node);
+        this.setSingleRange(range);
+    };
+
+    selProto.deleteFromDocument = function() {
+        // Sepcial behaviour required for IE's control selections
+        if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+            var controlRange = this.docSelection.createRange();
+            var element;
+            while (controlRange.length) {
+                element = controlRange.item(0);
+                controlRange.remove(element);
+                element.parentNode.removeChild(element);
+            }
+            this.refresh();
+        } else if (this.rangeCount) {
+            var ranges = this.getAllRanges();
+            if (ranges.length) {
+                this.removeAllRanges();
+                for (var i = 0, len = ranges.length; i < len; ++i) {
+                    ranges[i].deleteContents();
+                }
+                // The spec says nothing about what the selection should contain after calling deleteContents on each
+                // range. Firefox moves the selection to where the final selected range was, so we emulate that
+                this.addRange(ranges[len - 1]);
+            }
+        }
+    };
+
+    // The following are non-standard extensions
+    selProto.eachRange = function(func, returnValue) {
+        for (var i = 0, len = this._ranges.length; i < len; ++i) {
+            if ( func( this.getRangeAt(i) ) ) {
+                return returnValue;
+            }
+        }
+    };
+
+    selProto.getAllRanges = function() {
+        var ranges = [];
+        this.eachRange(function(range) {
+            ranges.push(range);
+        });
+        return ranges;
+    };
+
+    selProto.setSingleRange = function(range, direction) {
+        this.removeAllRanges();
+        this.addRange(range, direction);
+    };
+
+    selProto.callMethodOnEachRange = function(methodName, params) {
+        var results = [];
+        this.eachRange( function(range) {
+            results.push( range[methodName].apply(range, params) );
+        } );
+        return results;
+    };
+    
+    function createStartOrEndSetter(isStart) {
+        return function(node, offset) {
+            var range;
+            if (this.rangeCount) {
+                range = this.getRangeAt(0);
+                range["set" + (isStart ? "Start" : "End")](node, offset);
+            } else {
+                range = api.createRange(this.win.document);
+                range.setStartAndEnd(node, offset);
+            }
+            this.setSingleRange(range, this.isBackward());
+        };
+    }
+
+    selProto.setStart = createStartOrEndSetter(true);
+    selProto.setEnd = createStartOrEndSetter(false);
+    
+    // Add select() method to Range prototype. Any existing selection will be removed.
+    api.rangePrototype.select = function(direction) {
+        getSelection( this.getDocument() ).setSingleRange(this, direction);
+    };
+
+    selProto.changeEachRange = function(func) {
+        var ranges = [];
+        var backward = this.isBackward();
+
+        this.eachRange(function(range) {
+            func(range);
+            ranges.push(range);
+        });
+
+        this.removeAllRanges();
+        if (backward && ranges.length == 1) {
+            this.addRange(ranges[0], "backward");
+        } else {
+            this.setRanges(ranges);
+        }
+    };
+
+    selProto.containsNode = function(node, allowPartial) {
+        return this.eachRange( function(range) {
+            return range.containsNode(node, allowPartial);
+        }, true );
+    };
+
+    selProto.getBookmark = function(containerNode) {
+        return {
+            backward: this.isBackward(),
+            rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
+        };
+    };
+
+    selProto.moveToBookmark = function(bookmark) {
+        var selRanges = [];
+        for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
+            range = api.createRange(this.win);
+            range.moveToBookmark(rangeBookmark);
+            selRanges.push(range);
+        }
+        if (bookmark.backward) {
+            this.setSingleRange(selRanges[0], "backward");
+        } else {
+            this.setRanges(selRanges);
+        }
+    };
+
+    selProto.toHtml = function() {
+        return this.callMethodOnEachRange("toHtml").join("");
+    };
+
+    function inspect(sel) {
+        var rangeInspects = [];
+        var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+        var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+        var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+        if (typeof sel.rangeCount != "undefined") {
+            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+            }
+        }
+        return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+                ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+    }
+
+    selProto.getName = function() {
+        return "WrappedSelection";
+    };
+
+    selProto.inspect = function() {
+        return inspect(this);
+    };
+
+    selProto.detach = function() {
+        actOnCachedSelection(this.win, "delete");
+        deleteProperties(this);
+    };
+
+    WrappedSelection.detachAll = function() {
+        actOnCachedSelection(null, "deleteAll");
+    };
+
+    WrappedSelection.inspect = inspect;
+    WrappedSelection.isDirectionBackward = isDirectionBackward;
+
+    api.Selection = WrappedSelection;
+
+    api.selectionPrototype = selProto;
+
+    api.addCreateMissingNativeApiListener(function(win) {
+        if (typeof win.getSelection == "undefined") {
+            win.getSelection = function() {
+                return getSelection(win);
+            };
+        }
+        win = null;
+    });
+});
diff --git a/www/realtime-wysiwyg.js b/www/realtime-wysiwyg.js
new file mode 100644
index 000000000..33db9883f
--- /dev/null
+++ b/www/realtime-wysiwyg.js
@@ -0,0 +1,576 @@
+/*
+ * Copyright 2014 XWiki SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see .
+ */
+define([
+    'html-patcher',
+    'rangy',
+    'chainpad',
+    'otaml',
+    'bower/jquery/dist/jquery.min',
+    'bower/tweetnacl/nacl-fast.min'
+], function (HTMLPatcher) {
+    var $ = window.jQuery;
+    var Rangy = window.rangy;
+    Rangy.init();
+    var ChainPad = window.ChainPad;
+    var Otaml = window.Otaml;
+    var Nacl = window.nacl;
+
+var ErrorBox = {};
+
+    var PARANOIA = true;
+
+
+
+    var module = { exports: {} };
+
+    /**
+     * If an error is encountered but it is recoverable, do not immediately fail
+     * but if it keeps firing errors over and over, do fail.
+     */
+    var MAX_RECOVERABLE_ERRORS = 15;
+
+    /** Maximum number of milliseconds of lag before we fail the connection. */
+    var MAX_LAG_BEFORE_DISCONNECT = 20000;
+
+    /** Id of the element for getting debug info. */
+    var DEBUG_LINK_CLS = 'rtwysiwyg-debug-link';
+
+    /** Id of the div containing the user list. */
+    var USER_LIST_CLS = 'rtwysiwyg-user-list';
+
+    /** Id of the div containing the lag info. */
+    var LAG_ELEM_CLS = 'rtwysiwyg-lag';
+
+    /** The toolbar class which contains the user list, debug link and lag. */
+    var TOOLBAR_CLS = 'rtwysiwyg-toolbar';
+
+    /** Key in the localStore which indicates realtime activity should be disallowed. */
+    var LOCALSTORAGE_DISALLOW = 'rtwysiwyg-disallow';
+
+    // ------------------ Trapping Keyboard Events ---------------------- //
+
+    var bindEvents = function (element, events, callback, unbind) {
+        for (var i = 0; i < events.length; i++) {
+            var e = events[i];
+            if (element.addEventListener) {
+                if (unbind) {
+                    element.removeEventListener(e, callback, false);
+                } else {
+                    element.addEventListener(e, callback, false);
+                }
+            } else {
+                if (unbind) {
+                    element.detachEvent('on' + e, callback);
+                } else {
+                    element.attachEvent('on' + e, callback);
+                }
+            }
+        }
+    };
+
+    var bindAllEvents = function (wysiwygDiv, docBody, onEvent, unbind)
+    {
+        bindEvents(docBody,
+                   ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'],
+                   onEvent,
+                   unbind);
+        bindEvents(wysiwygDiv,
+                   ['mousedown','mouseup','click'],
+                   onEvent,
+                   unbind);
+    };
+
+    var checkLag = function (realtime, lagElement) {
+        var lag = realtime.getLag();
+        var lagSec = lag.lag/1000;
+        lagElement.textContent = "Lag: ";
+        if (lag.waiting && lagSec > 1) {
+            lagElement.textContent += "?? " + Math.floor(lagSec);
+        } else {
+            lagElement.textContent += lagSec;
+        }
+    };
+
+    var isSocketDisconnected = function (socket, realtime) {
+        return socket._socket.readyState === socket.CLOSING
+            || socket._socket.readyState === socket.CLOSED
+            || (realtime.getLag().waiting && realtime.getLag().lag > MAX_LAG_BEFORE_DISCONNECT);
+    };
+
+    var updateUserList = function (myUserName, listElement, userList, messages) {
+        var meIdx = userList.indexOf(myUserName);
+        if (meIdx === -1) {
+            listElement.text(messages.disconnected);
+            return;
+        }
+        listElement.text(messages.editingWith + ' ' + (userList.length - 1) + ' people');
+    };
+
+    var createUserList = function (realtime, myUserName, container, messages) {
+        var id = uid();
+        $(container).prepend('
'); + var listElement = $('#'+id); + realtime.onUserListChange(function (userList) { + updateUserList(myUserName, listElement, userList, messages); + }); + return listElement; + }; + + var abort = function (socket, realtime) { + realtime.abort(); + try { socket._socket.close(); } catch (e) { } + $('.'+USER_LIST_CLS).text("Disconnected"); + $('.'+LAG_ELEM_CLS).text(""); + }; + + var createDebugInfo = function (cause, realtime, docHTML, allMessages) { + return JSON.stringify({ + cause: cause, + realtimeUserDoc: realtime.getUserDoc(), + realtimeAuthDoc: realtime.getAuthDoc(), + docHTML: docHTML, + allMessages: allMessages, + }); + }; + + var handleError = function (socket, realtime, err, docHTML, allMessages) { + var internalError = createDebugInfo(err, realtime, docHTML, allMessages); + abort(socket, realtime); + ErrorBox.show('error', docHTML, internalError); + }; + + var getDocHTML = function (doc) { + return doc.body.innerHTML; + }; + + var makeHTMLOperation = function (oldval, newval) { + try { + var op = Otaml.makeHTMLOperation(oldval, newval); + + if (PARANOIA && op) { + // simulate running the patch. + var res = HTMLPatcher.patchString(oldval, op.offset, op.toRemove, op.toInsert); + if (res !== newval) { + console.log(op); + console.log(oldval); + console.log(newval); + console.log(res); + throw new Error(); + } + + // check matching bracket count + // TODO(cjd): this can fail even if the patch is valid because of brackets in + // html attributes. + var removeText = oldval.substring(op.offset, op.offset + op.toRemove); + if (((removeText).match(//g) || []).length) + { + throw new Error(); + } + + if (((op.toInsert).match(//g) || []).length) + { + throw new Error(); + } + } + + return op; + + } catch (e) { + if (PARANOIA) { + $(document.body).append(''); + $('#makeOperationErr').val(oldval + '\n\n\n\n\n\n\n\n\n\n' + newval); + console.log(e.stack); + } + return { + offset: 0, + toRemove: oldval.length, + toInsert: newval + }; + } + }; + + // chrome sometimes generates invalid html but it corrects it the next time around. + var fixChrome = function (docText, doc, contentWindow) { + for (var i = 0; i < 10; i++) { + var docElem = doc.createElement('div'); + docElem.innerHTML = docText; + var newDocText = docElem.innerHTML; + var fixChromeOp = makeHTMLOperation(docText, newDocText); + if (!fixChromeOp) { return docText; } + HTMLPatcher.applyOp(docText, + fixChromeOp, + doc.body, + Rangy, + contentWindow); + docText = getDocHTML(doc); + if (newDocText === docText) { return docText; } + } + throw new Error(); + }; + + var fixSafari_STATE_OUTSIDE = 0; + var fixSafari_STATE_IN_TAG = 1; + var fixSafari_STATE_IN_ATTR = 2; + var fixSafari_HTML_ENTITIES_REGEX = /('|"|<|>|<|>)/g; + + var fixSafari = function (html) { + var state = fixSafari_STATE_OUTSIDE; + return html.replace(fixSafari_HTML_ENTITIES_REGEX, function (x) { + switch (state) { + case fixSafari_STATE_OUTSIDE: { + if (x === '<') { state = fixSafari_STATE_IN_TAG; } + return x; + } + case fixSafari_STATE_IN_TAG: { + switch (x) { + case '"': state = fixSafari_STATE_IN_ATTR; break; + case '>': state = fixSafari_STATE_OUTSIDE; break; + case "'": throw new Error("single quoted attribute"); + } + return x; + } + case fixSafari_STATE_IN_ATTR: { + switch (x) { + case '<': return '<'; + case '>': return '>'; + case '"': state = fixSafari_STATE_IN_TAG; break; + } + return x; + } + }; + throw new Error(); + }); + }; + + var getFixedDocText = function (doc, ifrWindow) { + var docText = getDocHTML(doc); + docText = fixChrome(docText, doc, ifrWindow); + docText = fixSafari(docText); + return docText; + }; + + var uid = function () { + return 'rtwysiwyg-uid-' + String(Math.random()).substring(2); + }; + + var checkLag = function (realtime, lagElement, messages) { + var lag = realtime.getLag(); + var lagSec = lag.lag/1000; + var lagMsg = messages.lag + ' '; + if (lag.waiting && lagSec > 1) { + lagMsg += "?? " + Math.floor(lagSec); + } else { + lagMsg += lagSec; + } + lagElement.text(lagMsg); + }; + + var createLagElement = function (socket, realtime, container, messages) { + var id = uid(); + $(container).append('
'); + var lagElement = $('#'+id); + var intr = setInterval(function () { + checkLag(realtime, lagElement, messages); + }, 3000); + socket.onClose.push(function () { clearTimeout(intr); }); + return lagElement; + }; + + var createRealtimeToolbar = function (container) { + var id = uid(); + $(container).prepend( + '
' + + '
' + + '
' + + '
' + ); + var toolbar = $('#'+id); + toolbar.append([ + '' + ].join('\n')); + return toolbar; + }; + + var makeWebsocket = function (url) { + var socket = new WebSocket(url); + var out = { + onOpen: [], + onClose: [], + onError: [], + onMessage: [], + send: function (msg) { socket.send(msg); }, + close: function () { socket.close(); }, + _socket: socket + }; + var mkHandler = function (name) { + return function (evt) { + for (var i = 0; i < out[name].length; i++) { + if (out[name][i](evt) === false) { return; } + } + }; + }; + socket.onopen = mkHandler('onOpen'); + socket.onclose = mkHandler('onClose'); + socket.onerror = mkHandler('onError'); + socket.onmessage = mkHandler('onMessage'); + return out; + }; + + var encryptStr = function (str, key) { + var array = Nacl.util.decodeUTF8(str); + var nonce = Nacl.randomBytes(24); + var packed = Nacl.secretbox(array, nonce, key); + if (!packed) { throw new Error(); } + return Nacl.util.encodeBase64(nonce) + "|" + Nacl.util.encodeBase64(packed); + }; + var decryptStr = function (str, key) { + var arr = str.split('|'); + if (arr.length !== 2) { throw new Error(); } + var nonce = Nacl.util.decodeBase64(arr[0]); + var packed = Nacl.util.decodeBase64(arr[1]); + var unpacked = Nacl.secretbox.open(packed, nonce, key); + if (!unpacked) { throw new Error(); } + return Nacl.util.encodeUTF8(unpacked); + }; + + // this is crap because of bencoding messages... it should go away.... + var splitMessage = function (msg, sending) { + var idx = 0; + var nl; + for (var i = ((sending) ? 0 : 1); i < 3; i++) { + nl = msg.indexOf(':',idx); + idx = nl + Number(msg.substring(idx,nl)) + 1; + } + return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ]; + }; + + var encrypt = function (msg, key) { + var spl = splitMessage(msg, true); + var json = JSON.parse(spl[1]); + // non-patches are not encrypted. + if (json[0] !== 2) { return msg; } + json[1] = encryptStr(JSON.stringify(json[1]), key); + var res = JSON.stringify(json); + return spl[0] + res.length + ':' + res; + }; + + var decrypt = function (msg, key) { + var spl = splitMessage(msg, false); + var json = JSON.parse(spl[1]); + // non-patches are not encrypted. + if (json[0] !== 2) { return msg; } + if (typeof(json[1]) !== 'string') { throw new Error(); } + json[1] = JSON.parse(decryptStr(json[1], key)); + var res = JSON.stringify(json); + return spl[0] + res.length + ':' + res; + }; + + var start = module.exports.start = + function (websocketUrl, userName, messages, channel, cryptKey) + { + var passwd = 'y'; + var wysiwygDiv = document.getElementById('cke_1_contents'); + var ifr = wysiwygDiv.getElementsByTagName('iframe')[0]; + var doc = ifr.contentWindow.document; + var socket = makeWebsocket(websocketUrl); + var onEvent = function () { }; + + var toolbar = createRealtimeToolbar('#xwikieditcontent'); + + socket.onClose.push(function () { + $(toolbar).remove(); + }); + + var allMessages = []; + var isErrorState = false; + var initializing = true; + var recoverableErrorCount = 0; + var error = function (recoverable, err) { + console.log('error: ' + err.stack); + if (recoverable && recoverableErrorCount++ < MAX_RECOVERABLE_ERRORS) { return; } + var realtime = socket.realtime; + var docHtml = getDocHTML(doc); + isErrorState = true; + handleError(socket, realtime, err, docHtml, allMessages); + }; + var attempt = function (func) { + return function () { + var e; + try { return func.apply(func, arguments); } catch (ee) { e = ee; } + if (e) { + console.log(e.stack); + error(true, e); + } + }; + }; + var checkSocket = function () { + if (isSocketDisconnected(socket, socket.realtime) && !socket.intentionallyClosing) { + isErrorState = true; + abort(socket, socket.realtime); + ErrorBox.show('disconnected', getDocHTML(doc)); + return true; + } + return false; + }; + + socket.onOpen.push(function (evt) { + + var realtime = socket.realtime = + ChainPad.create(userName, + passwd, + channel, + getDocHTML(doc), + { transformFunction: Otaml.transform }); + + //createDebugLink(realtime, doc, allMessages, toolbar, messages); + + createLagElement(socket, + realtime, + toolbar.find('.rtwysiwyg-toolbar-rightside'), + messages); + + createUserList(realtime, + userName, + toolbar.find('.rtwysiwyg-toolbar-leftside'), + messages); + + onEvent = function () { + if (isErrorState) { return; } + if (initializing) { return; } + + var oldDocText = realtime.getUserDoc(); + var docText = getFixedDocText(doc, ifr.contentWindow); + var op = attempt(Otaml.makeTextOperation)(oldDocText, docText); + + if (!op) { return; } + + if (op.toRemove > 0) { + attempt(realtime.remove)(op.offset, op.toRemove); + } + if (op.toInsert.length > 0) { + attempt(realtime.insert)(op.offset, op.toInsert); + } + + if (realtime.getUserDoc() !== docText) { + error(false, 'realtime.getUserDoc() !== docText'); + } + }; + + var userDocBeforePatch; + var incomingPatch = function () { + if (isErrorState || initializing) { return; } + userDocBeforePatch = userDocBeforePatch || getFixedDocText(doc, ifr.contentWindow); + if (PARANOIA && userDocBeforePatch != getFixedDocText(doc, ifr.contentWindow)) { + error(false, "userDocBeforePatch != getFixedDocText(doc, ifr.contentWindow)"); + } + var op = attempt(makeHTMLOperation)(userDocBeforePatch, realtime.getUserDoc()); + if (!op) { return; } + attempt(HTMLPatcher.applyOp)( + userDocBeforePatch, op, doc.body, rangy, ifr.contentWindow); + }; + + realtime.onUserListChange(function (userList) { + if (!initializing && userList.indexOf(userName) === -1) { return; } + // if we spot ourselves being added to the document, we'll switch + // 'initializing' off because it means we're fully synced. + initializing = false; + userDocBeforePatch = realtime.getUserDoc(); + incomingPatch(); + }); + + socket.onMessage.push(function (evt) { + if (isErrorState) { return; } + var message = decrypt(evt.data, cryptKey); + allMessages.push(message); + if (!initializing) { + if (PARANOIA) { onEvent(); } + userDocBeforePatch = realtime.getUserDoc(); + } + realtime.message(message); + }); + realtime.onMessage(function (message) { + if (isErrorState) { return; } + message = encrypt(message, cryptKey); + try { + socket.send(message); + } catch (e) { + if (!checkSocket()) { error(true, e.stack); } + } + }); + + realtime.onPatch(incomingPatch); + + socket.onError.push(function (err) { + if (isErrorState) { return; } + if (!checkSocket()) { error(true, err); } + }); + + bindAllEvents(wysiwygDiv, doc.body, onEvent, false); + + setInterval(function () { + if (isErrorState || checkSocket()) { return; } + }, 200); + + realtime.start(); + + //console.log('started'); + }); + return { + onEvent: function () { onEvent(); } + }; + }; + + return module.exports; +});