From 1508c7ba71f5de5e51f061fbef45bc1f18493832 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 31 Oct 2014 16:42:58 +0100 Subject: [PATCH] and so it begins --- .bowerrc | 3 + .gitignore | 2 + ChainPadSrv.js | 189 ++ Storage.js | 55 + and_so_it_begins.png | Bin 0 -> 59640 bytes bower.json | 26 + package.json | 9 + readme.md | 17 + server.js | 24 + www/chainpad.js | 1434 +++++++++++++++ www/html-patcher.js | 483 +++++ www/index.html | 16 + www/main.js | 52 + www/otaml.js | 1003 +++++++++++ www/rangy.js | 3738 +++++++++++++++++++++++++++++++++++++++ www/realtime-wysiwyg.js | 576 ++++++ 16 files changed, 7627 insertions(+) create mode 100644 .bowerrc create mode 100644 .gitignore create mode 100644 ChainPadSrv.js create mode 100644 Storage.js create mode 100644 and_so_it_begins.png create mode 100644 bower.json create mode 100644 package.json create mode 100644 readme.md create mode 100644 server.js create mode 100644 www/chainpad.js create mode 100644 www/html-patcher.js create mode 100644 www/index.html create mode 100644 www/main.js create mode 100644 www/otaml.js create mode 100644 www/rangy.js create mode 100644 www/realtime-wysiwyg.js 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 0000000000000000000000000000000000000000..556d55a18883dd8b882df2039dff073ff707e015 GIT binary patch literal 59640 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

#_wdJMA2soLtbP_p4D?XwH-i6|Y!$pkxer zd0Cm5sCJ$0?*9J%(UITg;EPs$14Tv3IH|Ea@jpkzbITV83po0c&w4A{ZuWM@^LmV{ zIt*h6Oubr{oG#8vp*#Atj2T2L84P4ek)}DDoKXV z{g5e1Ca&`!$`Z9#hu@&qS`9$tCikOg?+p;kpz*;YA!&ZXQLpF5!$;xmmMu9zl5+*u zhTL`>$?1KwTgbn>_Om8rwqtY@YZLXvM4Jg#XEPg3`30N>1AYXHUK6)AAADND@#g5 zjr4u*%4%v7g{;{KKgOu5siD+QgYw@n&wmwy_LBGPkeJK%^T90C!o!~dk&$g^_Ij5 zV$?>2TcEcE;H%cssv!K9b9Nw%!na6~UNI0w1Un!gfWA4vk3Q7M1AlX)X_xcIr*9x1 z{~(AXShJZaOMPcH2s+f>uS5xHXi#qzJ~=%FU0>`UhNMAo8*eX<@(4pBBO~*Wi$DFF0oWN}L)Z0Q zj2dMCii?W+KNgUErBUicw=C8Et`TqH!as&>2wO;yaC3AgMf=A zasYP*U_-Roq;X5I(CPp? zh>CUs1P&-GfAt$ft@qsA+;84Um#aH0HvEIJwx6Az?yt1BV+)u(aP-#C*E?ioW@e_Q z?yj!`Bt&A`3C-(xw&_#=oqwov!lF@tHR}H;Q*L{%tKM7sjI6?iia1prIUgq z6flmG3bu;6S#NhYH}g-EzG!u`Px~|FbvH|-egAAYQlNan^<@`}ICr=WkI+1i$bwx! zS&XE-T?d(Nrw7nG=)=2CzI1Gb&X)VpvNX8(mS%*s3=9#u^go%J3#<6jUh(2^w<`4e zQ1E*gnqLbF3Yw#x9k-9~eSy!i1kGb@WPP`>6X3S6gzk>Ju!OQxKLNO31lsb``qQ(s z8g|SuB~`L$hq=AagPSK{TO@wyk1=IKf4bH)RHYoe`!)bt`5KimrSs?`V!{gx3bgf5 zrVQCqsA4P_PeIe+EJ~ZDu#+*8C0NhiW>!d{r=`_sJ68=*A{+$Dq8DNAG|f`?!{k&U zAW|$BZ0Ov-G*B}W6B9BrMsbml!fd?lL*uZQ1O!b@O}U<&({SB512vR1%1Bclmm~e1?YZ{t z;=^iP-h_v~l9Ccw?dEtuGN~}B;V}!4MGNYcdPJ_o_C))R&HGEq%FlLS)_K(3ygkTB zeLoEUI=f{iv4)cMIW+-aR<{zoq0(vgt=gdaz`#KJ<(6=QYx^<2U&i{C4f2J}o~P!*!v6Ww z&!0bEYH)fstXg?&+pHIn9e4>wc zd%{tTJEvB^)SQbLNAql5y!_QO|BLk0qI3A4x~2!=r!Bxpi=~86QPT3anTd1dve?94Pz6NBpEXS zt-H%&y>suDxyeF>1R5fIQqv1_7l+N<-tXU9<}7;Lc*)-R>M%q^NBedx=;)}^xO$hC zl`--165HX%kmwHCM3$+z*4;b#tKs*FT(`D7&Uz9-IQ8|>+Hqt5m@_&8J;|JO)WZg|Jh${~)92DF_M%qI zpLzW}*OJv_zc@F+aKKULeqefAsWVwq({vMe@-T(Fc3m!*MTbch3~?sCLFCPUwXNaRMWB(>56lAgjA^qjG-QDGZd4IAsJ@&E>)KW3mJK5vGJ7esKo`ua9U6| z=)}7~J1{&PwGDWn!pR8$i>Iy6%d~!8#e+LpGQ9^n@tN=60J!=KI&4G3?B?drQc_Z& zL$w%A3-LA*2>au6dm-+?%)z0GZCLMgw&2h%?#y)k`KZ;xpjz$ovzYhx6&e{U0;Qqb zj-<))@yh$0{C*ED0CS`V_{(dY;z8MNp7xa z>9jMc`}N5tz?Pt!sRP-RR%H}$Nr+hFWy-~bp>G%&0XWld_r1^f2FD9}6I!{nxI7%G^Y}R0xLs`EJM91~?$L0)mJiSs4-XFjF}UH~=D#P`hEJm6 zZaR+`Fbpb5|a8Mfu2j#Q-nGaK7`!g^y0;<&m#jCXqupTu> z>-0~Y9iY1k=&d^fpa1#u2lV<{T3P_GQAtmrq<#p)Y<2w82}o#A{FFZPdY$cn^&L*< zniY$x^EPsHJO;o94q|Cr09J25qrYE@=6Jr=CW@Gofq}tmzr22I0NGE~B#o4#p0}A6 z)TEg*Ep}JB{tgNgc{!>$`Dpf^q-s0I1Sp3YVZleM0#ryXJ zPQ|JbiZ8p^?ovVXt&k;%Iepg~MFP&D9h3~PIbP$U1RvjBpN52l+@S#V4Xlbcb=C)m zCln^IS!?~1>g-(j|r*_o1gDLy?<8rzTMvpaFhy} zy!omOQ4)FS>FFjUXjrq@2_*6*M?aP z+MeD4xK{*`jm^y``1{Kr4&o2UovrO{g%q{`nt{Q=!Pl}-*Q4cD_2s`^>d262ae|HU zHVfJ5#%48%9q45j--GSSNxuYL4skW6}y_X^$$yvWc6raDAzdo*IlOEAfnJsQEiYW|0F~ERbd1NYx zDjqg2c2Z-%HL_Kl8Ch=(386H<$wi4{$R3`%nJ#%YTf-5HNc;8CQM3#x*!yv7u6RfK zeu{WmR1!#X;7%rGWjzAb2Ji*o0s6t2J12z`jUMnp!Up`p2W@ilzl#=I4n%Re-AiKnJF$) z3$t`Xp>U{LKn7zaOvXRmLfLig+-yIsQ!~z9=23)jsZ;fKF7a$gW(Q+uc5X zL_|>Yi2hb{Q^*!_(TQSb^xng7ZnmRo3ai(z`Ss31WxHiA&sE2U!RUj*E#YwG$^lhq zB z`bItIoj=m9bAz?T%zW>>(GP3{E~MLgdw@IR7ZC7V)hF>lgKu)2oWe;bN927)$AYXR zEG5uVw>Ps(o%Lvcnri5U3mXs2C3I;8sA%c(3V^~U74l%Ix3a^rX8`G5;XF^b1!<)`W5mCWq zHOP-q(0r9vKojNty2%(-CUE)cGNuMu5kDU_{NI?FH3eqp%!_5*l3dx8;1A_L@I3Yb9OOhNP8XjjDJel{qQ9lnoK}{QKor$L);`~xM$7!P z`U~#ozjDB?!x;?_7b;}2Ff%`gNaat&$HzaZ+o^!oX>>m#_e&Ga*_p2`z5mCo<6(2Q zGyV(-^P_^oN}Er!d?B!ih}_c^tKQrmwHq`z8udiH05lh9@YQeCutjBC)O?um;ps0Z zZjb!1&YnXQu}19mxi8t5;-9xt(9SFIqB7{^Gxfm<58kP+5|?Fplmhyhw69$2Cn97F z0D<~tFIaWj(}7+~i9?)YlR{%WUm4!q90Uns1VKdml{U5M-IA(EksyYq_PCi6XjBNw zTtov-q^p#PQo7R6bqJDpjGr?^IAZBUE32po2nb*xekfHhC-qf{z8oZ*u&$-D zr`m-6%nU|a^t$-#d39`rZ@SK>K-zbHxU^vFE!)r@6BQ*sYi?nY7Y)A`D=s0SJ%}Ig zYI0r_7KS7@sa-x}$q^bHJjOBXcLa7so8MzwhoQ&mFqhq+JEjBNFJ-I*!^O_emySG+ zYfW|aTur)ViIAp3${FXAn+p42&Q)uz9`6@j?L|L)M{ia4G z#ux=Ml^XHoqe5O=a$gZ| zd-AdXkp@stm;)KyPR5Roj#^qzZh(uU6$4$}MIbLsVMxo!fGTEP+XNJ3%z>f)enJk* z*l|m-7c;xAAS;2LTe74j2pDc~1|T3n!m3@Bg)$AmVC&q`xkbQjA>eWRYwHf(CIQ24 z32m2(jSV~L*!mBsj|1b^nnXlIz}^9PMl5&t<0YxgE)m& zO@Mqyj>%q0By*KJ{8VUrwDZ6Z*!(M-NeK->aH z6W#0A&od1TFb#-wB!MSJO;`7c5S#%=~2V|BLD;PT`|^ewjH`rK@m_sIaDOMzU@ZZ$zuuD-v$y}iEP z#Wn;OKqcA`ymwFi7q|n!wKO)S7*7LkO^B!?!0eUk4p4r6{yV@U0|STXIgj&(6!2$( z)m4OtCuVbT{WlDBIw}JXV77_K1r+%h=x~84#lqZtW^L#K6feLhmaFpt`Vv7vMMVWP zblU)2J}n3^CI|rHb-A^<^_)1kyu|;^Uzk5QRwMFFo7lfmYmM4fKk&AfD5(JcgH?t= z0M)$Jlj{>pkVrL?&f@74rX&=PT|sYTzIMfhysE5cMJSDz2d2SpD;E{L2k!y zX}t2+!MydnIo;sQ-|`Wb_R_iaYu`3@$17S1a)XSJ_;ujD+nld%Ds$33_JF~qsk3UU^a}Bn>e)g!hX#W=3daZMes8T4#t_g2fN;wMq)9V{tDBqmHZy;L=>v^` zNmwrB!vp|9RB=)u2gEMx>gj=amsUztFMVo^eSHNX5OK~Bm{{BK#z2DQt?CBaD{AU1 zF`0nRivN~Alx_q0HE%!wpi46?AS41O1|4|N(mt7J`1o{aaK}09+}w^OgW74;FZ=N{ zm(g}RhKDhx+rf4P`?|n<5fIM+K?}Azi4ORI0w<9emCukN?w?^bOSo@gPHUl!u&FL@QKSVO`lI9PP{Mg(5h-EhoQB~u zin?&Pa;|DpRf(^9$M+t%!pM}eD(E!Y?U!AC5V>(un(p!yoqNt3X8Ze*y-J=mV;*|z z`-jj5rxqWQ|eSdeGh19b7dwOyLzuIsbCqQ5L;yeuHB_)GrwMzIQb#fn~ z11>4@^Yc5+4u4mDU?KotlS`|duMFIRla?HNuDk)bzY=Z{Q~zyjY=E^-pI7qsz5_rA z$h~^Hxx;))Y8z5O}152OUvR2UKNv# zdNmNDfzc8InW@l=l)?7qIQW;dY50*Vm?O!gPA6WCEVnisM>58cr>%$W>xcJvxVW88 zq-)4|IwLF9uO@vnPft!j8!a4fDlQ&kbq%u0OBP*1TwG&y{VuNCFYJi{wQI&igZ;SJ zqDym?7PjZNh8_Lb|IRvsZGjxXG-A)|IwFS;=4j1C1KvT*T~2`QcmcVF3@}`8sttxf7(m0lEso2JpT263AseD zL7qyK$uG!4m77Ei5-FYL)zGWaOWCEf17@|+5vvz8bad(G*~7fM;Ek@L&QenSfYG4A zMUjp#!XA!)PaIWyiwZ0SaiYBLhb^GL_%oZ^+f(DGr>txo%5S~a1dtE2fTXnaz>_uP z8-F9{N8Fb^=~!6of$td@A!-@s7?!Ke9Hg*--Bdzaxb8=_V_}bzuBD}=x;g1UwK$~71vMhcm=ujV>GJNR zThL+G@rr9yV=UopQPbDA7ouE8U}a&c5VtDw^v@3Z<6Z&b4&d&?@w_k~O9SV_v3?^nSNPfL;%)U_4RRnGag_Hn zyYT$0l>&KK3CT}aNZsM@W_O1T%2ELp?xV4sXEMW%HGF}@gH4u+%qzCc9E0E8>)#k$ zP>(lI$`KL%jwLaJUDRz=+&BJFFE5%oRu5WLPoHi2^uD%#nDknutvT;KpjDd8U<6Z) zv-{_kru}Uy_+RQXW`Z&c=7y+HoV1G;on7#B+}|>8$3caM(fah|IWp^x)iFrS_||ne z6Gk0F!;GpVbAE)*(=QOObb7#|pIX??yX!5tODdj#dg~o8o5Px+z<`1QvVtrvjLcZA z6RF|N-^BmKiGzV!`U%sy0m81Rrmc+lOtVcw} zJwwI~pTt#QXHQ|9#@#)?Nw2pD1||2}&t$2(wWU8740@&e?+o~4g;CmXPcO|~PMu$% zhBP<#q?Z?Ee^muki~eyhLTSI%i`3h4+Tk?K9v#$>wcc|QceEqkCIJN6kDsJ+6cG>+ z-(%QDTrjZL@+K*}-QS!Or)4;<7@mLBk=6Lk%Wzi^ZFe#O5y%kDu>`gS;Ymo%(%0Q| zHHO3|+~;@nfVP}M`GV7GUaW97wYDA@)@O!wG{x)6`Xg4VUxR!*VCEcF2k8qQ5UgS< z<=U~c6H|6cz-utCaQyxn!FjGvXPPf(8$Ps(^yx*;X2AEbBt-VVYh%`mSA63Cm-Nwd0CGXQ=0FKQ^hG6Cf8WlklZBc@}zW&5ET02y1J|3wiMF( z`cDnduEG4lvj3Hd=Bw|E_$VUH>zbq-E5FILb#1&@7cOftTgn7+5WpOUGopi0wC9IE zn3T+DG^)%Kq86fuae34w9r{~@Fke-?b`dZ-;emGgGFB%Q!@NBMFQSHAA;9~rINK(R zJC*$!%5-Gxmkkpn?gE1p*!+HIE<+>;^=4h4Y!__vMP%*khF%26LJ*LKw`D~zaS=L1 z821V0=lOe4avHw8z!a zTJ~ZzL81xA@1gP;p(vA)A@zp@i#;+OXZ$?n);=$O@G-)o`>OX&ZBdG)hpU=9g9pB? z?5(s5siPKb_d3kIsS2tY%226AKu|b<#~j(|BuB$5*n1j8VWpgKp0fJa4AqPw<>h-l z*J#o4|3+B8DOjj8%EP8irm6Qzahw34f=zrDsO7-;TMEWxe9z#N7ykWzP`-ij?=;)f;EhVFgzBfk(##SwI)2N&urgj|s2Qk^Qj;9M zWFSPI-eJf4{2yp&%(f`WNK2QXS7A-<0ak7ikq-V6PtKDg$Mx^?L*I0W}ITJQ&9DOUvlSpeWe>9j4YCnt)x{BbW=+ zc~7r~7X*u*$E)OhPT08z4Qzd+r-g%WmpjiEdD(8I(0=P-c3D%0b{NtFVvE5SLgnxO z%>Ngd>&X%XA*%A>{3Ep3|Cjx8qgtUEbk}~`>YoQ1M0gTkLOaDI`X5+sc}!!Th$jD4 z44JYf7Mg%{Zg_0c?x)XVfX`35>@5GK!%*jIGh@7862m_{1#)gLSl(#{e7qC{2Q)V= zH$AM%O>SXq$kQcAkghX-9R1UvQCkGF&e>EBHm^Xtba~s?B6y7Ww_fp)ik$bv}dlW5#CW`048vhL4I zbg&+%mIE{JbRF1sYP>S_B~{S7-a+7@CR(A>BRV6#F4kT>t@W1A4nYoX-E|9NWQHYYpK)VGV3*+0ZGfa zD#eI!qXru@2pd!$hXPpi56FP#hUGHE(G)8)Xocd0cXu8chkZbXtU@Ts zoxkvGZIMgM=OS!T z`UDscvd!yLRPFu%Y-iF)o-O{$8FR?DcC{|uuNfH`T+1HMuBkZ2%x2K3bO96wp5AaV zK>qujPixGUX{~Q<^*WJCuTA;!&uj|+8Y?qF_{-vJndCxRs9OB3Y^aMux%dnR`EzcG zOn!BlDv}trCSWi3lZiUhiF?Rpg867eoow^RI03I?r!WIQO8%w?~e z<+7WbUK+U@E7NMnO!9R_RM84Fbzry(Dn)bm1Dc75=0>1+_~b#mJ=^^W9GAdlcr1)z zQn|hSnWsjmTJ3v??P#Ob3t(iA!WUPPNh&RO0p!KuGUug^a&e*&Az0#`HR3%u(W2S? z=1;O$J9!FY+?nH+zX}4w=>j2kU7b*$`-8yIZL@DfiVR6KS}S@l=M?8(#nV*kHG?*q zDY#}xg4R|q#2@IJ?^2)qD-Sz)F^VL?CtGk3Zl#A1XLHfsrn07rzIC0>FJ_GlzDblUmY@5+3Jg{pg_YQg;ux?BoiWy}<*y zK|M~3L#Y%v->=5Myez9b4g05lo<%K!4ZNg)W;3oL6h{uROgLhq^uCz4sSFpRhWrB3 zQ#vpi0`z-EH>lLTwczic_`za^jc0kBo2rX#b2I4~QtM)=d+tXDxc7m<{G8TPJHYlz z`%F)fA+FAWV!Fad^`eozFFLQqke^1kP}OWjQj{IgKZYx&j=?zQPSFpLBS9(B((fQ1{VDY?1SlM#-j zXb_wi)8mtqHrvq$%Ee+*4ZXOwyBMJl($b{SjD{t3`DSw>V5;TFuN~+on8JeEfOw9} z6~+*G2`3ew_@!qgqz&W}(X2Hfz-vLpU+F{_M^Y-*{p$|L`&nJB?|1J8@dtKEt&1n# zQ6cN$pngRV%*X&!+D47z$e+m7p$ud~UWgNVue_7qM+aac2ZM>g6lI$VEi9yynbI(fiVfcj_jOI*tS(ap;8-H|IaA+MmDtZBZ-n8tegNLr_)Dddft58mYMJW|*}qoD#-Nvn1i*!FDu*CK7&Vph*5T&)6`sF8gtw0< ztuZqz-S=z;1-u?0 z03BwS#(*6aSXsc>jwJ`6NCD?y&~`TptN`>8d$UKcH8ftjawWc8+h5q(in4+92lFwz zmWw#t%|5moTLhzvUJWuZRRCTxhCufmH-*+_X)wm5x^b9m}co zKA#3@EwEQJmRZ_tB7SWxO^P5~%-0w0m1NZQLA}9=!wOX9^=SJ_nvkEby?w=pS30W$ z!B@ahVmDX430AV-D(8cx3u;Z9gv`@pFjqWM@Mb3uz%GBUjAs$8ag%flPC}x($-iy~ zH>*Lo5DVUMCJ>^Gw?FzHE`28DO?3YI`z?OK`=3>SK*J*AWnyCbnoG~o9^bBAIJs~n zHRqv>i7i%Nv7`yDTIvafK!Q5@XGtM!9>=iuqu%QI z?dM}i>M7a_azKyl{OhXT07V5(c+)=f`rk5L!p(;jeLhzGa|9V_y?QSji3)-CxxFg(ri^Jf>`*;3bHvUA*Sgei*YO8<=^H5vlmf$JW!~ zVR}}5Bu2vu)Aya7!qA$7J|^UOyL^@#o9nW>LzoO4WGd(x%-oLQOKO{?Oc}9db$#WM z7hAJ15=8O3CW?Y5XIm%|YAWe{+(VM1zTjE6*1LAl%5F^Z?;jUKatCGVromO*_(<`Qi3r_EZ8m*W+GE0?W<|9_Bv^ zuu9E-wjn@vcwHVI9390VHeFr2_Lt_+FxJ%l0dfnid!YGMXkk#K+9JSh+zbl2{$hYx;g{>pVfSmuzEUMt|O!?x(1mKuUm5W&Tu-8^@yyfy>IuhSGRgAjo^lMB9G;VLjFZ&})eL|ekeolNsg5RxA z-{U>wTqtai=f~zYGf^W4n^g^Gq2JQCyu*|EGw5FK(1Fe;W}OA+&ChWR#gG2GSv_O; zyu~|Lei~%4-3Z0s5Z>q{m%$Y2kUevEb^F+8wez7UV+}m67nR)h*;Hi z4)ZrjC7XlcIm!$1WXyi5qcyg6!p=0>R?05SI{OLOCGf)O>3Z3yBejV*n+VcuYWN{wLEa*dcLuNOqA zgDtu6LPV=vcP9Xc3rIrWwPY>XZb?d|4M(6&0U2VY(WOwixVt!XG;6<_lY;2C{0OLo5F+k>32nTX5wZ+<=$?s|Vst`nU+q`#=|2#AIh3&oi zfQ*3%Dw6Om5fN$FOx`Q_qBRt$4W-66Z*Lvi)&@FOj3J89IM22lzQqWb4rcQt{- z>?8PX2#8p(D;z{|f?%2Q)ukXt3k}5ogoNigalwg%Cm88LhSZfzN%fNqfLZ%rO8zy1 z1<o5h5b3D>B^jAr4~^!hvDFS>2T`Oq#m2>kkNZ+qRsy)h z=)yuS2w)ay7Y)b6#U%8ay#HPQ)4>)6tZlw*D?OUkOU^v!EAg0@G7YY3qvpo1ux;cw4L-&8*RZDG4M(VOj)m9MVuGV z0oi|Y_rKxaXRA3*P>n8!2_=-cq4v@XvxoKj_R4_Ww{zby{EE@B`e-+`RZi zb`|3c90axQh*PNZ@;H)%;}}IzcKB`y$~3l_N7qV_9Wk;!vG<#8W8r(iTaoyhj8bCk zJ;pPH1+`o_4_q7NG#jhWF0Vxfa;*8{pSeXR<)takMw{;6C75`t?OBzoTe@7bT%An# z_pmq7yAiE;d;i$Xh=~uHV(_n);G9dJ@kuC@@D{@h_w z(|JQ)|BdAWGGzE1wy9hjCU_*_DaopU!=|qOE;y_d9r9~0;_wX#=g%9c!CtZ@+m8Fu zd{o*D@ZW%13p`lbnWKG9x~0(apuoV$NQ(aNG^j5kdUC<&_t|cO)a2Q##4Q>N!a64t zAucXXvnf6dlszSEbaB-lOl>yE_QN_yTWR+04zTcOl-}}UNcl} z-YK32M=RsMCWr(YTV7rs7%L0OoH^awCUw)hkfx5qhS<-wgl}s8e39h|rhpj1ybyDa zRa14fa^5)p%a>0Bv=;|085xnq#l>@T+cnzEP3(Olp=MZrSLg`h3JhW9xIz%bR;QgY zjgritwlmH|a>(564h^&}lQX2}{z`h^kmsyMk9ZaF6Zx5jNbxN>w6o8Y1R2z?i~K93 zBzYVInNJRG{}l`yv8o>gp~I@;&q7{7VDIG+09lq?L$|L{@aeP6WPgy?F7Y3&8CCF3T?NX zuQVdd23Z%;67?C!oa4|dySpU}hI`W3xRCY{i-~4$MjLd%zekKC^aKRlItQw*Y zz6c&Ck~0BA2PEYwhadVs}y1_AZc((BJ|*--U_@ynM<{(T6RWVhrf z0$LSb?c&(KO7!r8U3_KJTdf|r=0r?hVqo}mida=&ng;xM^RI1mQ2G1A0Q>*Zblu@t z|55v~x2)_vGP1KbWlLseGP3s!ku7^=Z`q{mO(c{pGh}9OB|>?R-}}D)>B@B#&v$&z zIrq5-sUqo<;kgF?-79CErz2__GbP>}Jc34K2_%2F{s`#NjY&Q6D?-@dxHar(Je?@h zm(jN|_CavYOmb$au)cX_7@n zS!fBlu0oi1CT5&c6`Nld-_CB%hz1tn^u4adL34u<8-a>sl}T|kgEm%ohxd#AsXR~n zTodT&nX!_XADEx)qy&J_`0v(zmt1Xy`}5V+lRdAp@hzut+QVd~Zf=O@&aZ>ELr}hO zF*2?;%$R^~Z0Uv@U_F`(46iYu0x+U}cq_%r!52#aCJ7@M_xe3-*@CL53~5w2NkKRQ zZV1Ol+UC9^+1d9kp2#^dqz%Grtm)lEAUARn00Rx-0f@7t1OEAeCKPTq*i)=-Zv%(m zM!E~cugC|mU|&0=>vA^mIa~bxc8C}|Oin430AVAb1zc`z_LTfqG3gVv(1vUEb1uh` zl0Q#N{={7EeSc8*M>SIw<6TgyB;(t2?1l@ht0FF6~gMBgC4Rx45-eq0Pebyv?{G>4Y_lS?hWP0znEfwA6 z!FwMv6Uzq957ws2t#(-}kQbaixtiLZ zH7{kNye!ZjzrZyw)-tH_3)hMisJCQ$cyC16`}a$nDyEfWku`Qe0m-L99JbJ%(-V6VV?|AA1W!) zW19c?tO=YLg~G&=ZJJKxhPmMCy zDnE!~CQ0Wel9(Ro)VENy(3h#E-)?6+wWVSn|MA0y_G9Xvu_4X0)_xZj{%ceO_E;ly z>G+70wAAub_roKE~Z`VT(Y(0#5-)Vd?mE72F!o2gj=BJd3 zj%8KW2Pq@_a+0NLCA$1CTyKJkY;|}wQ{8``|EW+Qkg_uT-B$7Io<*c=$ZI@bl(RAXpa_x_dx5jV z@0BUHrcK8b<-KHn+T2XXP8-k$fQb9Syk!9^gR&MUDew}2Z1ylm%=1$OA%BJgP}hL* z<^zTsFhpaKY^l*QFcgLP%>p(DWe03ezOaso?|7eozXSTvsDp^_Jvc;$Zwq#j5hP@i zBGmyr*0PAev!!c#ILY3_HqAFtNx*B53VO*E5%oK-J3z4qKR;8<)6T`kJ?=$w)3b{j zCIL&5=1&OJgc6Dmeg7Qi8gH+9R^VlcpNyelG+KwGBWWgmG=6}VsW?oeo1CLsIgft# zp?W|uLa>j7xGMvWi7pjI{8{MZ%G?S>O`@|fcKTwSXS09aS>@Cy^3*HrH z6~F5_xN}&$705jf90((L#Y#`K>O4E2yrHwoR+_RqlBvXZq#!3)MxnJ~kxeWB{$A&! zjdFzU?CfaCCk@-Xos#DW#LnFG?Ip+WuTxWUa{T>917;35k!AUUR1nEr;^#eWrQAAb zpTb&E0O7!MSW}h!JmvB(_WPJd{N(rhX2jB``xDlnb_Vu>-2UuxcgP}e{X=Ob_nsSo z2h!iaDCC(d&97{0w90=RMw8iD7TgIp%M`-VVM`fls9d$4_j94vGcl0@iSZ=Ldefv_ zGq#Ul5;ORF0O5lVgybOmg&GwH90>r`fEw1}^Yc#dQ?^8!hlz!@zEc&tKxt-c-P%eD zFHwi7E=8sHeP8sCAVwg7z0?Nuf&J;KT^qfko#l-lyp&i4;KRSd302t=Fe2S2 zU;IkCL4xEse&OTC*xGuydfOtn2!XheeTQBa;x}YQPMMJw-uZhRb$_R7H+3IrXRLE# z@r4AF$}*N$1UdrY_$S03}Hy0Zkdbn)1b{GcQi8^e|BEd7Tym;r` z7rFr5`YrMkrKqnTEW5($DD|XA6)A65`u*Me^bQdCO-a;peVCNq=0i@526-*CoP7RlzFL^pXiHHkWhT}c< zZ_=vrvgZX6wn&YBJJ|b-N?%ilojCYsTosunvpmwAgt|_~W3m8jGZ3+mk)?qbfID?l z!1X^cFrlN%19ie+mur2e`yZ|-smL~GW*;#U&OBRwS~xE9YT?tU3KO$3kbBJcW@~VJ@^SB8 z#zt=o88Rl8y?L_=lpy%fs0c8L0RqUMt{@b+*8r}Y*PKH?d7rp(Y>Vu$HS~Q$5Gfb# zty^@Gz8>Ik0oq(2pFf~+hkpY~>LDwUda)I4>&%<%|e&-KK%$AMUsZ*Sa$%%>CS3D~g=xa;V(N)ELPP52$lPMIpM}wrnYM9c;X6hj`I;X9=G;c8U$FWHbK{bXgkVN>;g1k0O^0Sxa$U%tS0G`6ixyodMm?on(+ z)#rlpiA=cd4<3Ai&B~YzS-&eLr3!Nrj1RZ~bitd=MiLoIRmdHZXtyL4Y-W>csdjCY zs{i+JXXj&lO-AdZ%o3_ULjMAOzZ2(-s@z@L->b#J4kjsvCm!?0OJbeHJeKlqDzn`i zJ_Y}W=H`>_HJ@x8e27(vR6F%o?G@!2qwvrKDUojLBzHE~Tkh899LC>WtM008<3#ru zS#zh$$dPp4Jjl7sS`yvNUE7&!D336UFy{TNTs*CoEkJ>=>Op5p^^1XF($zexl}GXJ%%?$Io@573fh?5k|=Aue!B>uM6-%VGM@HHBZnU?4Lkd z27mSN^bB*JKBA7Q*$$FMbq4+(nB)#>9q0Gv-y~f6E`Q%QE(p~1!Z+pqrxBiCYwYI(JuH14}%uz38@&pSi zc|M;i#<$kzpA!o+$k-J)?KyxmYCM`t*wBDYBL6g$PqXfjE=kXkH8fn1{JzruTe}uR zF=Sa2`4o9z6K_6W^4~l-cnRJ^a5%Tt*ON^4Vt*B8(#CpY%KR6Gw{^E0y=Li*aulVU z_r$gb^sFI*$r-=e9Ea7F-k;gbs?X|3Fw_&sX){E<0b1DJ# zU5~W5xm4#%*f|3U8WT~}qKrmiR)po&V};TsmY132_uIo(Su?BJq;vH&qG#m_wxK|b z52j@@tw-rfPl;PK$sZB!GOxK&8G>mRur{Fu&jAP>IAgE^1*4zR^VhUXA9)?%Lx6w= z{2uXEVsk}l`P*R^0_@Ji|EPfX4&DjjgmQf;DR};%lu46>7*l@jj@k~{_Zy)`_xN#M zhpg1o{E2N$WNU9}CQ>pRcS^AzPp>I|r>>kJzsFa3dVC^F@X0#QKb#)ZZ(eXz&c@s4 z4JrEoQ?FGW2&>?&xCHq}=h@;5kOhJ@mY`&92C*A3aUxJg@qIYK6dhq(d`l^~eg9JxNPDqssZrfpv z(O)BkZ1|p}X_+W5I~xp5%Be}KI+SET2cgT*#sh!614NmyAOYH}1W12M6ecVE+kwyz zJCN(snWtlNHu5g2JBD3XQ3xXZ<+Kf&v^iDkGBMExEVOZi7F?>#lEz;isEvO;F9x%^fUuya3A>aPkSRA7a)BMZZrP&61*mq-|E4gRF%! z^6bIkx8c?_7?eZl+3u0X)Jrb}lYWG?Ah;)GN3jbN1+m?uk$gA0Hx+tu0G}G4RzisY zHEl5L1&mE_3B6og!0rT@v`YskVn{km}`^^Ej2liA<_V)G&8F1tQnK0->(~ds3>Z;N*$EOGeU>3(L46~4 zSCw~phLI0yotLB{AG{g2AtELQ zqF+kN6qE)j@J8C=wJ4!Pc~bFeMl~RtMf{iT7w4x)s)!Z$^7<1WA6wT%!>U?lQleJB zj18-o`@^LT@q<(31tJhwN%!s6m0$l`;3zp}y$|K;|@v?gJ79m6M z0GO%JgfLvClQdwS-scbn`{S}QHbfV!7y$xoHs%hdm!Q2dmpeW_h6l@3OQr*TY>G&> z=#k}@`Br?&A@l8L!cI0(KSQ7VH7wGoWKotgw?{#gRlHqxYw_#MFaBJ?GV)CKnUwZ7 z)pHto2~q?$o{33xXiH?pWICl~nvT@9M0ZCW-iH>&Lc*<&u2(P};yt1jbt{UE#k+^{ zG>HDI5rqN&qqB(Li<^%>Re_~Ta!N`D$@${gB*KR%m>z*3kq0t-c1lVEJb0M$yho1? zv4U)|wsV5mwOfo2Ds(Fu85xz5ie#dDR)(^K525AK@xp!mktZgmS_^PFi^(v%*TscW3 z{M+1|6wm8ckNvqMtkk=@e|?K3$}0C0=oN3v+3?cHCI~SGeW;9mOX|a^jrZ$$nXbhj z{)g_(Q_YD4vII$}U=vduXCnZ>&~FeWxlOV$6;z%K3>XaZe{7_CN}>O1T&epHjnVV$ z&&h8Pvp#28wWi-fc>!|K$;}Y;-M8?^L$V*)i?HSWokB4`;~XGa-rJ!%j9;pKH?8C>lDnB;GYCFer1HE-yh2fk9y z`iN?%W#C2I26evHa*#~{P1X{M8{GZSIkj3zEdwFU;2Q;=hsQ4qrx5oCa$FoQ!S~8{FlR4tR>J@QOoIs$+w9a%R$M3^I%K!)99fh$v%7D^#Mj+zZ*}ZW zx@QTM;FFoVbm^DX;{rcKHgXQ7DffxZWiZPJ>l2HTp$sCX&g;u0w0Sx~bLD#{^>^9~ zuOBMpUN=V_?vR1^?ef5T-^`anRpbS@7Q!PX3u$h~D2&11MZR8^D+_ZRr}{mR?Yht2 z{k+f=M~`$cXskOG1<|!`=|+EGJ}jIeVXxnn6&wc2)prihAh z*%CEgp#s+KpGF#~^Ap*zD5a#^z5Xb4t`>7YOvy%=d+S=5M(^DFi*6L=EbF$mvf_HY zD$jwGPqB>g&;>MC_+a%Wq0Fod5`1dPC2$c$Lq=Mc3>L3}Hxd@+!<#?N+|IyDS}A<1 zPjaVauGSJKEV1wGbuQ)3>qk<5_UcAqSWin&SJF){(&;W}2Gb5OpMpx`=;)ZSd@hXF zIuL(7Gac89##ZrmiqJ1*VZeXDFWRQ@xeMoA_VC}hLRrJ>ZXA}Q`!s6m>hF6ao}{T3 z{-rLSq9o1It&$t~F{eIlj8FC1E^>tCtv*7BodAO>=y*Cpfz{^;hok>{S@YF;cs0S) zq=bFC__MyKr?Op&NPg4&Uo|mm%#Rxmz~J(pF~ZuSd2r4DB@+(8*z<|rrYC2?i$Sy$##=2F5$ti;J6Cpq8I(d`s?EX^YUT(J&Y;%;uo~6-_raqj^iZ+1#3s zO*56<^*It50&!H4E#49@sC6mxUgiraWuNm6`*Hc@v0jO(>ykVXsnofCgq z1&EFQzl?w*@UVmy0=;#SdaI)4rJp(nmS_ z8ccbDPMLhI3X+BfBI57z<+my%sU7Wf($z7do~bx1cE3WA>_r~ARW2V2rwo!%)?{M7 zT+mJo1^MIG&AQgm9HGjOLzU^erW1AZ09^)eKy0qk!bR*g8@H8?^ZML)2E}Q&IPc!N zt$L8xGDpP&s@26fnzKJ0GeZtGb@vbQTB1*0#2?_z=qhiODY@>X)WzQp*=H%qv&yeL z|7oxIn$|s-SmE?*-J9b-)c6RVg{9`b#F%9fv{tz;LJ>4ES8Ns$Q!gXj9=cRI?5&MJE) z4~K|*`((b~tOHfIg+Di(qulDj?2n-+d|WxKB(IdQJnJA7x3=W>^C7ferxN$af?z@S z96ClA<7Jp}u&|WX)QYNKGk=+zn}f#o0IbkKnKzmz4bqq!tDO(mf1OF>|M$ml^N>8z`!@1HOhd7x@JooQ^7q`&}W=6sx2#o{Fh*2}DpXmap5HEp1#43otyh7cX zfW;ak=2tc+q|I(^RsnuP1DTlqF+1Up|04&jQKXg03BUf2JCF)?>l{} z%OrLfTE+1dGDV+$P2hUm)*VO*T-LGO(ho07(S79ngq^`5q=$N0>uE7_;1LVmN>om7 zzJ^H&(vv^~x^CLFudi58mxh;uaGJds?yq%dSpG}78{bRlYUV|q@0Xs+?HLh=S3^AC zLH$zhj|i9GlYz6C=)+4Q^=9+utS=8H-cH_4nzae&_x%}TKi^AAG7!DqoW?{JnEA0( zCGK^p{>3lN$hjp&RKy#2#pAL>2D-GX=KckhT@bM&hpqdxy|a)E!eZ zvlSR%fnb)Llk<=uRPD}PZtjvxAVJb#pMpnS9F;J>osd)*28}i!F6>{x8Sr}&i|UQ# zXo;5ow(Jbx3O6K0Se)H6;$K2d%=vD;d04>p*^=tAIF7UUPG zzuOVDvgHY0X|vT6VOQ<^<0(pdm+>c6O(1rp<`V+Lswa(~X4&;d)e- zet|?A%sqaeNr1g6w}e%)!*Y|9h?H=7ai#a+@ji8EzRKfbq&T8{HlLy&%?htPWb;a- zlPmn&A2ioCVm%kv|DLd8lHb37Qwcfnc7>L$k%wD8dv;?h30me-h5G?nNY%`Iz_ASEv*^3= ze&~1QP7@FKcLm&4j*w*K{NA-=4U#9#E-S!M1nSHL>_>3bRa#WDLMzUh+ylH{M z?^a~T!=u46fhb@N4T!COfD(#i(#XR|9ICD^mvA?QDL*9tSn7lEESHFoi|1miXQhu> zsLtdi%&5Xn3*KPpO-4r6V~_^E93YIIgo2o!o({$zb^?@`I@I_W2O8n%9$;?+F)P0V0ZEnoniZH!&n7?5-eH9wmpF3U##U>FO1|M;y@a;>w;|B zjL(ELj#`#*hZ_yuwUW@5&`HtW;kr8>!5lVH+*(At8cJxKoJQV(+a4oPU!O1bdP})-S~RFxTNoAdOhObN zQPb_RKT!QT;bu0J-!<}`lt8!F%~ZbvqM}cGc^a)pF;@2U%(=P7tSte`G(KZ zfIbVP{^BTD&cT?*9n`D=$5+>nMbSivb#K3u9YI^-I{LS9?d=tp+KL*t#?mVft_F(B z)ZBW-T4HUj*l*v)%$Pb4YrheRMyqFI_~GWJlFabrbmnDBYMG`5O_dKLQh&2`Z*{<6 ztzYGGyyf5s2a42%=lXKrl^z9!Zr2MdeQgb=&4WISjOW;iSYu%!N{5e;UIxbJ7?ftP zGQQ$A*Ld@G62TKRJCcf0WT_&7I=5(w$bDT$fPfAIDR7Wh1q;8|*v6HjMft<#wCC#d zxPKGk!%Jgy#Z^HUl2`7Gr>+AAD`lYy=k$83w`nqd3OszQ5r(RQionb<4qNWj;UJbw zcF$(afz~3j^uvBt%&RahRtoIBcGG&vG@ca{M7xEBzl~W8b!SysqEyI5kGXQECqS~d z?>?3peXaa0X{*usKaM{zv4N-!G*`t~O3&@DEX^tDBOS{DH{R$!exM@8EF>m>v*W+2 z1Ia2C6%}sJvM9I@zpbh&j!GeSW(d{T+}pc?HsdBm1DcUgqlOqHE)|rxHGF)0sA1;Q zzl1~GzJ=Z*Pdvn%_8Aepfo+yZP6x9jD>nRYlcZDqS;#BGJRsHua4(dvAM7 zdi`BQInUydsb_38KQz7r)AgkPryjE~5fJfuG)Ye&9s4vGOO=(=QsC zPuqq+T*&;oPJ5zWqC_gBd+qp z1PUdIGNu53@mR*4 zxZj@Dy6;DQg>95O8$7nP?0BlbJ;O2l>Ou{E#>j{t8ymS2K5k&W4QLrACT70Lz_Kn_ z0}}we4gG#Hc(X4U@3f6ADiJ&Ya}8j>pz;c3<$&)pI3#lnj5Ch~r!6)*W@)e)H@h%& zJT6M4%y-^{ji0-xr{&S|a1i&VYaN~RxzUq94?ay`&32*R<@k2#pLQnY=+mF8T8u~t zL}f;Zgu6B`sK;&awlK(gy?B@Uyq5IyRPzhgV=~1&^p=Q=i*I5o=PbIPQOYw1t#G_W zOtBttOny@s8%nkobY`umUaDw!JLe`i`~CP?w)E5IYOBN)+uS~8EbVvEy10hQX-n8g zcrvPYxIZjS-?Q&QO3v78{y3nqYuWawz=2p<_)ui2j z*kX*KbkvR>d9($d^zn;tnGxIcjkT{`RP4vp&EKhf>T%7{ta+?(xMa6|iNxq{j26i6 z-an_7`Ox|5eE??yCQR~G9A(H}d|s#b@y|yN)R#MFX_9z@Mi;86x-#)zJ$JZ&gRjlJ zHAXPq8WyWkUcqtfl^LkHjf}vIdk{e?HNwFR%#fN8>t_znOrMmA&PZ`E;1^l`u=>s@ zB3ByNoL(mOfN!mkjDTQE=9P}INj~bT%uUx;p2QL4s9ny#qtPQOHu5MlIqT(mqIOJM zQ)O*6v+yoEw3ne?PK1Qx_5OOu7<3pKjO%S^UriN%kZvWsl)vL~`sRt!4`Sg|_q%^b z))tJyG*Ya|NDxki{v}JxIBQgKYhYId-cnLhi+dQ`0HA;mQR0_pV+u1L$#k)a*!qbO z`QW(#Q8je_zckg^1l*-gK0>Hh)8!(X4S$8P4gpazn2R zLRE;u$6?e4kgp-DbQjPfJG&p{WZYSy&oZs!Ptw#q&sN<%TUX`6epdXQeksdEAsK7< zL`&b)l7(Jbt9abo*C*@8^jB6C%o@I8g5FsPA=UEkUb%NlU6ytF9H@#eAJvC;**1J< ze_mdCEmvnDadXF~Wo6BGya>$yN6X)0-1*`xH(uIgY?2IuzQo)&aE*+NHpZ*7mo3R| z<#KtDi@EBPb?;7CtDbOy>Ika6E$|i+j?;DKn#+5mqbvi9N?TjSz)eq7^jmLlF_4x4 zO9lmxREp-8iHV=!@BpPKU^H8ln2f9mTx42%zxMZIAo8c|wqKT8k!gIc*XNvug*Z5_ z(c4fEPsnCqFic#G2K@U8SvYdhEI)ZWf*5}tZP(g7>3*&%5}HaTHZ~_*U5qWy7?h#` z8B&O5FHd_c8zjU71?ts3vAG`8}&4);zyj+lf2eY4!>>kUbcz zgd%AKzA5kpPsTV<>A{uLUdeo!>KEj!!Gc;*k;#V2xW-&wMy3n?Ix#VUg!pCLibohR z5Z!HF^Y-mqpk^58>fXejgi~N<3G$kgH-ex7=r~DchR)5i3V0;fUKtwf=lJTRnzK_q zC_NhZuj~`WP>^X~{K1SjdUU(=-#fO-=>*O$WKImM(f4(1eNA;QSczQ{r}UM8$C@MI zqow@_Nn%7sZ&xPp_44UX9m{^G3yog64zMji+Qo~wrO!Q2^;bDC0rA8blzUW?zV$&I z3_1w3PwfEj-J~Xf8U18&%o}5S5Awp_oYza&f0vTkwZWOn(AZcl>>MS;^TXCgH(-)K z!0XOBD!qNh>!Yxux5P^m6O*$*L`M;?NE{QUIXx44+Td+WR9p|fuW$9;qPk8EIZ1Ce z4nWBndpzdgsaTgLO5JakybRePg>T-N%3Q%H2HRe6m;nYfG(ZIzkft~B^w?@7n}V45 zCs@vB1F;1tKnX9pbz@oyqI-k$1tA3loDIR_ue7nP?B?D z>p@C74UDM3|Gr?Rhpq85OH1|?m03K{EW*MV_^~g*S{b~HdU&b(8Cu6=?XS*{p$NPL zAP)#H<^4mr9=L9TB=U|94)ApMO)i-Kc*yjXn!7(njv$8DoO4WnOkL?|Gw=rjkx<~H z2f7k&T1|*Y@PP%K{UV$LtE;Q|DOxwsFuc;z40c*DRk{0E>Qj4sIqb@b}VjREWM$RyH59x4-WXKBNU2-mqkt zot>SVGvuJA%awV>M;d*bj0{$&hp^FyyrM4OC*V~K2Y@7;B8X+6ubm(F(?P7-Oq27| zs>e7VoDlVFMr@&ykq?r9JqgFi4Oa%%ZjceCHq3?Iw3sd**1PU@_bvyCO7X}ieg(Kr zF)l{di{Ox6-_-Q9-P;wKC$Pg0rcXU_6Eq$%b@4GVS%m8laCO`dv4wDBK)z1nTUFK& zBTr1-53n1I={`X+V{m7emzUuo`@K7J2pZ6v$={sCU3-`nyejPBL#=xgmQdciY>fG%}@0pcDARzCnT!$kCaGpF)(z^@{D{wkW z&^{`i-~w~r_V(=2%|f<05}R>$5{ZeK87LP^Pa5o|;8OwCw7`akFqMf;6pm6}6X}aV zx!DzO`34Pz53a)`EHkfauaCo?N4#)-e3K#OeH6(21rF;k-@nuJ@OHf6n4Bti)aTtG zI7pH5+|hwWK3I^>I(PvhxivOBHlWyVN*-b6>>L3226)JVAddtaX1|Hjc$f{>@~uJ% z707Kdn$dQIu0ZqW2|QtVpe9Qyrz5yR>5FtYtcTL!m;(XBO`;Xx+!}i8p}906PSQnJ z)Xd5!-tFLT@TsrwCbJBTsctyPn#uiZf}z~Lq-+tHP;+U(TDR@6^Conlxdsg;@r z(~-uqhzoOV!8|lEK*glj3p+@r8nmmh)B;a?jn8JTZK!4QZh09QK7hXhF$5LkzuY^> z#LREB78E$s43MM#$Qv`b&WOnm`%EC^f!!zkAFw}>%8NH6r)k|1k48h5b#trD%X0NIt3$CgOw9x-i&TGV}W5V{0S2wWs7kF5mt(i^41))?Azo9@XlfCjJO#Q;sz zjn`h7oa}9_pdj+Y_MiCGzZL}*<|3H$E?-liDvn{PqeawKF% zj)^NvEnqVSXHMBq3ZLO;SLd!n^t>NGSElLJ=YCv{05kEMP1FOEm7Ck4GXI1LF{IiDT4(Y}KVU77w1#MD@`G2Hju?;l`FuW%_&{@2eV07z*W85w?3AK-KWe!T6LXvp&m2vLR17Xr0T z1pJWDM6W2LlD_^>y_0?jt8BC4B|DD&~`6N)C3(gkpdT zkxDYIH?o|aAcY1B2N))FWhVGa-LqSxyw~yH!^g?}-or7B;T8lqEd^Zro14E$oH`f| z=Z-MqNU;Oq9^jQSnv+vgr*MqGu@%bBn|UgsnalgRl>jvZU^Vdi+#JbKUH>^Xh>&CH zzJ!Be5&oabRNVs0BFU(rtEd>>k4oY_TpDS2{RfiSVfU~4@L`zi^QvP9?Q)!gkb%+B z!@p-sikkJn6apK6BjAQe27XkU2A(ERg2chPFrphz7H2GJX>)xY1pNgXpVexGRhEsS za~U$bx!@l6H=`9q_nJeFBAfqV+mutFC=fsSKO}AwWI*P+d{&0{>Zqz zu0kXI`kS|5a5G^!13D*r;4!&~KoRc*|6Wvbi`3g<9u(Xm%8tmpa}9UP26ab=>V##N z7rI;}xVT~|@Zfc;a>1FDU)W!0nHU_jho1~(Eq`i2RSSs{t~9xz-HjScISHD-r6y;( zl-Z9;l6~~lVb*Z>tV?F7FrOrJ8x_UT{2w*!-Qo1~^!1<708$>4D=5G>uHtZ9xa__| z8;`Et=$6!lQr+gV0(qy-aP%m|kh}i)OqhOqrq&zFxqE4ej+Y{LQE33wKjk_@HgKQp z`SJpOpP2G)$89#P+2Vo7Ejzz-@<@5cVG?ZH?OA@#|`j*WpY-IWC0T)0%glc;W zkq#8ke0eSXA95PHU|f^lyBpibCBx4(!S8st;ab8;)@yxxThwxZBu5?NzK|h%B>FFj za-9mG!9(gFJs)2U>_bpxyP^$uOdu8o56{wN_aT%1X`^FeAhU9brD2Im%=WV4%W*vJXAS{JgldG#&b}rx+UX>Cw^n+DDDq&WX%e zrTE%j(Z|k$4v?z);&g|F1UsWR)s|}(4oYC_-^8|Nd%_z6bP0GktL8B5?RO(0BEW3; z<}+%HxRJ*+0k;IK@q>er2Mldo%`tXPNN?C@5MThzr(1MV0#eTv>J)Cs2lo`@(3>g4 zxMLMiv>$(ErGh&=XjGt4Hg01=T`(*LEd)`d;pGkjk*HKOZbLb8HJ$!*Pbka?qV0twU+6v+~_@55*X(_1x4C*&0s!QU6fs-F+VD7N9(c7MH^ zefMw6mot4<-|udP1XUXOjWktJY9`czDcntDBUHGm#G&Aq4BTm1xw$XlV1V%)j;Z;* zLCrf4g|hZHH^G^CK09MQYsCo9DBPCMv9jsZEW~iy1kEGyz4=I!s1d2aEr>n=dgAP7 zWhvxXNTQO}aKKB6HZDl0Ipix;>5 z7$!ltv3%K)ksHyk0MjOM3NtOK)C~n00txen6AX@ru{Zvo3(&UYv25pYru^h$pkxjn zuMD;6qnq%}$B$E24tE;l3S7MX**Q7EPD;?^e&Ie)L19Z$I%NlW&T?{cB~Zoy`Je^y zz~Lg0vtT_1Pe()5HZ<9XAm@js5Tqv0l18io-V052>QC>EipkuynxAuAyZ7OCUp3cs z(kq4l5GLI0E@9FyGUmGVyAXjscib08bGh=0uCKQ@k$HQ0c?>8KFfM55srRN2YKoba zuz_#`n(H1@ZL9BArj_G40+Vgl+BI6RUWL&bv;x2K%^>L&gwHT}pSSsP1=Iif%M88p zjlU2}7_%}s5!Ds8NI?i0Shq;!9j>l2rl>%dkDo^gkQ{CNjXnhCHJB^f_P0o|!{C3_ zV2xXYCP`su@+WP>-^TG4>-=k&+Yq2@D!WEOOxg+l)xJoynpd!XS?AvA-%q@w$(^Hq zV;2Yy=#7HGGI7{Ts-ty8A~5;Zo}a(;O=$}7f?Onng_{*t(}7$P7EFu*3#n*I0k2qu0Sx!%)$b3 zfmx1k*txk6z-b=xw>-9qzKh5v6+Ny55}3WLY*6{XnCY>MtSlVQMTq~<%z=cP;hjm< z5cB+AR|&{Thm`Bpz9#4;KtQDVGInGBqV660b!tO{ps|i9b(~iDjWrBJ5tU1qKZ36I zLp4^o3h`z;1y@t7^dCt1`3CRHkZOzSrhc;74L2AnK9#5&^Cwm%Fgh~^=>_0i5L}T5 zLihG>YLV5f1R9XL8AmNjL`ypoYQcMQvQJb^**M&Z3trRg(l-`b;0(bETx*2O;i4p* zegvT%9UaPtj z;?~v+m>Q_@O9dUcz^)NMRv@t7ydo9{f}UAWdtnKW2!v<3YoY3B6sV}pShxRuJIPqm z!QjOehg2irPF-K@@_0Xk5Eo8Td+3{vV3vLX3t5Q5j)d{+u+5RIDc}7ru(3c^4EM>( zVe<_(F)=CW^P$rh5JTc>@v4ViV zyH9a#_{?#a_$A&0={RsrB);FGMj!4{!mzstHHo}g7}A=!%%dk>zxNC}9>JCH_wyTS z<_V_9eO}nwIyZS@QB^QOuFwDW^++pj_HSsF0S|$J%AnDUo&|(d!&cCg%nuJ&eam~x z-Z^!ohHY7%j6)QO`j3<&1zR>D)d8E|YJ^V2Wf}e{0N8IHdqUd!1q5k`f@Uy@x%6Sy zw37=CcHSt|T2pw-VcuyFoINMw#*~Jk2{=nd6@GwQA*gfjBr3r&!yTqUD5u|B++Xpl z3-$h3#sc9+OniJl@7onsBqsQzDNuF3!4n57SRB+~h%?Gj2YXS-LV^f}^5b{ESOSNw zc2i7Y4!WcK6?Y4!F)eYcA#!~~*+&pVZOux6Sw^-1G^3pzI~$6V(^F3m4*{kvNKTe|c|~r8u&&VuuEzA+>c+-C z-aUw(g;oQqOYHS3o)wKh^%i6*-`;C%YHCt1ov>?wFSw51zESwf{OY_JhX6c?%v=O2 zTe`5bNnsF!!pI-!bC3`~hn}?ayw5ys*YNV?OHlU!tcQ2GH2sJ4RnPdg2VkeOz+Y5b zaB8>BZJmcM6^1e8!wDGqKlS(j{NuM1mON|)XZ2{ty-k4b9zKbfGap)aZdS^3TGj{bRB|MiRlq<)SJm9uA7VIl>Usg5&(4@T+zjAa7~Xwvk%Pt|+xa`_ zg`cp3O%B8Z^YbT-Zf5=3P)}m^u$N_(?b(&vHLm+>`^)I)nwP=E)3agG^X6K7EVD1W z$ISc#*zZS7-U||)Xmr+MZNb;C`m?^~H}R+(YU}Cgflvr$PI^q_NJ3cxXa^zLO}=O7Fe z2a|@V6p5A$st;!2<*% z2-efZSU>kiN=o_diVytc61B zd#tC|bKR6(2i_9!jsa9cz9AAD6%||=YRnDDS)vR*8y6QuMhj_K8yHaJjc&S#fZ*yV zES>oa-0Yruba)qzfBz2uRk`k0SyB>?%u#`cFa}r?HiFQTLpMPy=D{YR?hls=z8W(U zg`GygeIMF)kS^`*Ie3pi_py~h2bgs#rvX32MUcqP(=D0Ted8pBAo{M)>5~fMs)y{B zb>AT30}_pPAvpnI3av2chUf|M*;ImzdvG+Zo`Rkh;`$)h9KD{rhJFcL_aTz{xrayc z;5sDsjUm<#5A$+!;nG9w$Q3}eAi-r4#fW<2`!ciSO1Q+ysdzEKmHL%_W$EjYJhdt1 zGf%*Y6ym8SPP`>{{00o6MkGr*B)ou*9pJAd0Mtn6PM1k@tRylOesWd(1*i-X%t6U^i_Z-HQgP{b3qinrC$N0Gn@%0#e8=8uM8}IM!l}&$+Z4LKqYzL=$o?_x~UweT} zkj0dgl-$^0Sbn+X{?|xHl>jww6tZfHU!0m|>%rI!_(sfO9efoQGA&4}&@|Q5P>8tD z$M+XzlcD?CODqNN!%H+%WpoCc#-}$4H0y(~i2i+cb~sf@hRXF*Vewovo<72u`+Ws* zgCW?Yl}oR?i9V?Py}1+|dIv0BD=M&Ca}0BTM*3c|!F1m=CQwR&Yz z^MgCDx3>j`G-JlG;s>AcOO8BiAEA4B9`qH z^!C0HL04+9slAgmJ|l9z$`)>O+`Y@wZYHv#hCRTGE3I>f+_!kmO7Q+@Dik)jpmp5$ z%w4Y^gA+eIH;`UPLro2_=J;v6dU`OK%)zsSjf$K;h)7Rir*93mY2_dD7o^(6O?ICn7(O< z%*cNO*voBD`vY#&(X07tGZ^R{zKEog-N%`8{##WNIxuOTxW6?yn6Gvxgo;T`KSrPP z=Rgkc$0M$R>7~iG(v%ckX!Oi*8P>l9e))oog0eznwZqLliw{{$G8a!?*B7v1qZAr& zPQ!Bn4LIdu+v7@IHWEigYe2Fo#I{90&&%6ALHWmp^473G0~(DLSWBQG`~ZY@Axops z6Ygrl9r`o#+PT{mPAx+J9eow|hYufu2Q0V_9gAfcAKnz6ZuufJ9ASrTuAH$yOD@WS z<&Kjrj*UVUP#RJO*Hfn!ygal<+rF*+d*P46b@Nv4gqAl_nwW8DMo;UG4V5CHWL%XvVZ?`-Ufp9ERIdg@|+=k}&!^kAJ*{7pP z$5}=;h>xM3z8CYQ9z1IG?&0K{wdVZh7Y{Ll&LQOXSa3MZxx(S&Y>fjw9kZ8@ zs;!T~L|mC=6>?x_eHAYBG|wsp1_X4uBoWpmh6Qz6&m0^e8uZ%H;`U&3$yNzqE102O zlbimtoQpws-ay3qFnL-7N;&#GOoE>Bw#wJ{CuJwo`F;C{)}VZq@hfuWf?klvwP5~` zqg+SvrB2J|``>DJb3%tFY}`zxqQr;2)At&7m!RJ6bQXT}=uWK)H*Mk;W|BI(OoDKM zQgUDflSL2v6^ZHlEu~pO2hEx{IoII7`SK0U({La2HgnV*w`+UIP;WZ!0ApOKLP+3(P{K-Gb+ z;HzG=w!1rTHW%N>pp>~1wRH#YNQH4;=lex3o`R>4p2ihUdw69?XUK0jsqtcI_@0eb@AXQvJJX*QNO|U4DM;zzd0Fa>`!JNm-L|NPuvphqSmItS4!jG z6}$S2*YmmHc@(P@gwf2o2xa%ZiXjmQ+|+D%>$pB~u`2BPd+7`3*TbvS!*cgbK@17G z_>}`sZmUSJ3h2fN0wpGN1eggnC4BZWj!&NjI@PtP+x7kZaddQ)#LRm+pg?H_xewigNn9-7ie#%I1)Q zMl2K*v-Pa~soQT0J18Apr*69VOn-R9!i7N>@bOI)MK>Dp6Iu7YM=qP*#_+X2gl_iU zO6T!$x7VDv(Qw<`9lIG*khl1avn(h(L%6EO=`=7TV(ioL_^+&`!`AG~%M&w>=LY*7 z*MB{{o(D>|j?2Zr1CKNSn)KLO@aS3j)A!qLrOvIL&;K0Z$b(>69Y zr@I8EA^HpG62m@?(EK8yg2D~}i=hNUE+&?ou-+ZCw!=LP5ppt2(V z=X#vUv$L};ElAac;3x20qH99nwJ~mTK|_I5+$10rji(IfJeKc%0d}20B+29#%tcT- zpu)HR_Kn&qf60^|y+k7Q3SK`{`YuNOV+|Z}K)B)9sc{#x<9)g~lMtX7SLg8UYnvAr zURI)4@-1Iymff@O7i_sFN>QI{UOXcWh|^NLDynAxH~Ml%@#b%avV|&_ch15wTo8Rz z){>KR-(#pcMa*KXEYs$Rd;bC>3yXQfo-v67I~84%Y%;r^M{dcby4I5Ht0ZeJIFiXaZ?4Zb(2jG}xsA|Adnu;_HLiVNI*;3)?61 zii*MgV2yzrgW_lF$P7CM?7hAKHX{LU%Z)O*5s*rH>lSq5gLeEoi*{PTB-Bw;+lO-X zGytP5p{J+ky^zPZJZH!gz5*p73JRclQE%SxtCs(yMRnj8qmAli<>FEWp2Aji{r4qQ zacOC^-fG+|^)%cb{H#e`f2o`0$r?#uWZNt`MAl@1PFZ{Ud#`=(o7s$R`@5#KdS{A~ zQ&*CFIfIrgDw~mJXV)*Jg|Fuo>a=!`edDjEi*z--aflvF+*4&bAMgOvN03fSRsG_Pc?!00?A!!tr^l;8qjM(9ET<%UWF zgg-Fd$`tZ&1W|34L@?wx*`Kz=I&BULc56arXmoREZEbB>X6nop@czC|ARi|+L!vcV z`XaVbjc6~opa^13j7+UA4>tgxHFyT&{21L#*N4y9zOp4*-{d#eKwTFd6I=Y?*1aW- z+3qVCtuUQpEb%z>Md^3rLP;^-!e4SKVjdP(ahkT1mknm(l!97RM)eOSUnOt{k>2Yk z^^A9IBY22+;oJ8mYO;dy%BlNYl($yG$!X4O79&w^Z{Zi0`o;B^NB%7?e2*Uem0UY; z3TxL&{&Q5(L8Oe5e0cc1+ymu#RNB$s)L%LQqDO!3-XZN z-7$gyO6;DT1h95yY6|Dh{gu;$Wna-n9GJ0&0`=n^jH?kH$A2dLWxznt3;AvjUETYR z4lzn>KR-X{RutkHCcXgBvNqEDzD!C(S zOgLoRf!ZeMIAo?<_+VU571OtTzY{4zdrZ;X4@#wT@h!u#niyoD!XF5t2O6Lj0vy)d*x3cgY6-IPX4Xh419$sf8 zke8A9banZsLp@jK8j9VsX8;ST*6Tq&!#A_4yOfj$U6IVja!u4lXu7{%$(upKCkcs6 z{7NSHkU&Cwx{${u%<-8cOD61&h#>G?%iiZ6FqqA!97wsb;2maH6dBASs8 zZy?2#_chGq1Yo01z~7FkU+#zu&nv zVx5nL59HrKYg=2(aocZIRa7VAazTr?(32}Ve#puX#cr6s&0*PD%%E2E5W+maH~4$L zC2~tvXXXL^p0!?h-^x5GlfXC2T@gc+q8dCM5@t0N@?RMjA(n)5Xjv-Z4_*4@3Rf=N z!n#XxwgNJo?)WDIon+9e1LP}!dqF(|{Q!)z+?<_ln>t$oMgbTU{Z4}#@tb9$Q*}|> zyM1258Pom1ajF7pE8s_+x?i9*^zrcl5Yrm+8b7FH$Rpm^2s#d67tn-JP;j~d53AF6 zPz*bO-3MS%=q4wD$9E0o=g*(8J%C)FLH<4yQ@DbcT^3E7sEFN+B0v~Sp^{Il@v1S~ zEq_oTG!`bZjrs!ueEit>cvKYVGPrNeY1M^=V@Qv{+tMIC2Spf!f56Y(uiGO=krzVe zKsoI^``60Q-fkAFYmN7qhzlbu=h$6BWO4HK>f$9YA0d;w3BN8VP#yuDF3EKh=uxl>V1?$u zaFHW(J=RoW660iw;KoxbM_?4~fd40M4EW#ynGmnf8AV)6D?y$I_2CA9*w!6U;>SqBCBqgY8Ec-!FBdT}Y6VZwZv6EEd~NO>+SYQ=MY1 zq}0C;-#o5yA7IkUop1L(u`^gPz!!s&U)a+uzFkA6j;C>7fa03zwV+Y5D*1zc<3sqHBJT0=|&ItTi zK0ZFEc=0jy=}=#y^f|M7~$g3S*6~5`|zp*ofNnNEJRq-_P=2ih8e_d zE79tIaRxgm0ArBr$W;Kdd5CO|c=gKWOOEf+nzA5?m4(H-kkC*2<=`hbGd*ovrw@_o zZv$Ay{w7JAeySd*en|_?#t4X-cH|@>99TyuvF)^QpLm72(N5^=Yp$OH55F@DW)+Mt zN)Q(nV?XJ3#5g@R$CxQoV0boLc3?u)I)X|~Wb)PNK@7Ex_f1tzj@JVqsDl8S`g$GC zeNDY%k?|BbG?`Us4g z|8u8rhNb2K<2Wc2Iy*b>Z9+^pHIErkMn7~z?=C8O3H0oO1+G*U)5F8VGlZN(Aq)-^{b6Bkt}F5{>K*p5L$aDr^P~5>L~kL2qO?uih=9OZT;PC3?KSNUk{M5*pIvBFF^~d4KtMoq z0#UY0Fb@I1>U=`i1k#{Qfwu>>t3a&&<0JaT7u_qFE_nIkKn)ByjZVAU6(m?LONVZp(oj#WEu+w&J z^Z3u7%Ca&fJx;V(w{k7OZjY{i_ z8>*wc>;|Lx;)yL_Eq>(Hh1r+}GZBn??(vjcJ@SA>47jev{S=tRsT9^!RW$+c{`vEL zSj%jztO(x@y!i`wfx?4+O2T(*UrzJd_PuCf9Jc)Cys=Gk?O6iRlCjN39Ga961K{XV zz|gX2Aa$fd>c>dm9m7iOLFSfpgKNUFI;Z2e6E8puQ|GcR+@v>`w^D$xFZxp-3xFjTveX zM+XNZ;9(82bMv@jKcneH74NvZ6q+kW9ZdW1CVg5EqS2D$;}5~n3g%AT!m4nvO;?$R zx3!5x<`mdzvL+tCc4v<4s*Z#yMp4nLt{503FS}6*xdV5Xl?V%`TaO)B3~rEK97F@@ zigLJFj{lT%Gv82&oI#U&>@^O%J%pA)=unm3T{1F=2!hl_aT=S``D8W?!prh7-H>JR zc!sjS8A(Z>mM0*p=jl683frzg6LkXo^=l}gy|?zvQ%A<2=ynlzsPgOIWyZQZ8|kK= z397?)2O71}F993bB?6JIzyGMWn3aDoBv^Of^BFF4rC=Ho)?le>@NjoNkURBV#TctT zBe^^Eme2MMhx_G|(TtsB>ZtDSbmgv21vJUfX8I&mASz+*z||X@Yu5nES7X*k)rX;jimIy9)<c$54wohOI#jf^f`*sqmT` zX!}7leM5rXz;hUaiEhck^aQjfw&xyT7IM|m_97;#ME~Du>MRvU0foNg-}{M@r2G71 zXj>vmma$k#Z`^Mv%CLRbIO*RhsM z8zEe=6n`($@F#|G7B}b?2G;Sv%PKMMuRGHZYODjiGET~@o$2xp9c^rWEV@K*&2$7n zU40rB76ueE*b8=mc6smK8jN$0FalWAv-3y2ii}rf+ovXsXQHS7{*BSm2$p$z5zeZ# zHGr?3nCk}qMR|DaH{0KQxA%fn97y+Q(`N*Sv2xPx z)t5OacDj8GpGTJ-`&op%dlosmjJ2gFNv;%)d+)o`OP}=zp+e?s-UXEynmF+UHQBr( z_o8&&e^HUI`6X^irTlx2m*X^IXa1!nz{kD!ng5>EW3PplVj+VD_xpE+t!|OiZx1eQ z>90$C{(9!*;RC#C@^roO@4$948LIQH z#J>PL576lvUe2VYrouGN%d06lc@mB(!1qBN^#I6ylgs6Ac*PgPIRlJ$9XUv_9r9c_ zGL1~48(>e6W5~H;s%MF9Qy=cmI;r$?ANeei9dXdB`S3-G>z6UNFUj)*^z4kIg>Nb6 zB3im;%O2P)kIk{(4m<@nrOE08Nn z6-IhJw?DPx8EO8LmjCnVSL@g#;M+j6(Z1h>jnh5lbCZ2wX4kz~OTl}B?q2#g;xMa~ zzMJlMm3Z2p-uYYQpB`f|he|kh&!?_qVB;%8A{)K!;RoUuGrL~jnAa);m>9T~dYz`n zO$EQ3?v?*~adUKulWywLw&6HnF=Oh5?7H!ycfhGpkKoMj1yNPlrr`s+{1e!=Z3+IO zV+CI-F0HNcLjl@uY)Y3{I);7yq}S`OH1R~88)@nBoN(*K`Q!1QTPl@_ZY)Ejv9Tsa zqgLy-10`GJPbA4axTH-zE%IiDCJyn6W2^=oe+8Ajv+2cjy()3kY`? z-`=7bIT!*e1>hOCQ4G6c?+7whS_=2GHLI0w8Z!0~CnfD|B)#$fk^JuM8;S@Qx7y_7 z-9z(d2G}}O>;nL5tX}&35(Pyn^dDlgy7yXEbuMoocE~<%3#(5P{Fl9TGaw+qG1}C= zYPW)8pc*>4^r2u;2gj?bGgDW_XZmN;IyrS7mXcM8jrV!%x3>J z3=R(V`3t&F#ccZIBf7ngoSmGGAT;Y)pH{0i@S?D4$=rKS!_HxR8NMT^_V~_204aMzg z6bJmi`|F>M(=eS-QABREqKW?r_P0ejzWbRFyuAWp7=nTV*`xpK(~kzopIg!3U;lmj zg9G{V0(>RPf1kqj!Uz0h@Qy-0{XgINKcD`e@BE)1vOxFAw+_@lj2j$AK8^f_cknfz zZY~5@YxSTrch{Zhw#Dc_YxN((_=wy4^EtjGq-a_JOAX*$XfzxC5~8%>I>+oxWN{Qh zZn9I^uud8=))OwTat$XpU*T;g`VhSmjRJ-*_v?5Lw@+T99811eDO6)-XJbnPC0M5^ zP00B&_0{Q86c;ecAS$DW`*rtEd|-6EMEOO$v3QV_FJoYTsMaTQ#-FsRo8mJRMM;ET zqVpv`BsOfuj?l*_^LC&ynJwmxAb_{nnVG>x8FYj+F znH>kuszw8KYyfD@{_iDeYR8;ev~<%rHr9#}|@ zsAXxJ$kf{Ptbr_2gO{U{m6tVt+@>=EvjTc|L(hwoNjP*NNKC&RoQ*eKA%(O`?;OUt z044s0z!C6xPLN~h@iFL?#yRq+c8Dx>c$$c-F^%H zxt~r0!%i*U;Db{st3BqqDxR)3_D8JQUzISbwDM*1x7a1v>WTN{)N7_O z(cGl^Yv*mlE9cel(+t_Q3mw?l=i=THSg;u1)inK>g6U1tGS5kb1)&cJRt+Pxw82k| z*C}s>4_f6`O+pt%?X?mI%wm240UGoweVZkwL$gWSY9SB6V;u3>Kv+d&m4pHwS8>a1yP2qh1u)=p+4E{ zH!-@gf<|#K@n2Z}R(UI{r=O!w`zL{|OEdm_`p`Z-PS38c8NDGZWXhCUwTX^bl(DE= zps;Pyk!MR;ii(Kf$MPo2AI)Biwe`FkUO&Fx5COAg{0J#%#=m^|0_@AMCwD{eQz={p z)4_}#dhaL_j@0{>Sd<$<&Eg$LNlmOBWg`Z=zf>y6uFXz^Qu?tDGe1?heU`^YvoOz zOXdt42i}8*m&{2H7MRQan_Nmh zf9Rx0Vyo%9h1gWn>O9x&B!$oEmk7;K(W}<@@t9r&^DZ{xevgE74ElPm%D z)i*KWB8Y$!qh&Qn%ZPX1<>6g|2n*;TT)t=Cd|B>z@}Qd==jmL2k8+in`Rkb4#3ghd%Mg$qOJcZ1hw^z;hiS0t_U09t zz3SD=XOmMkqD*AqQpO8x692RO{9%aXQ+M~@pjKuiz!WH(aQq!Ja0jfM^;;lbD;o$j zYbz^Y>KukW9wJ%Gg)*7wDDrcQ4f|N?Hc8%oO9-7@u1o2d9jJO|`>W<~d(S>9uzzuz<${fTX>Fd4N(ylcro80OSZkXsqaFSInZKflEOAK6L7*s%LN~b+ zqufa5{A_$lsHk8PCq<{OqVf%FpH(ojLk~ZYHj28MXAEbJeJ5BXcBpl$+7~YzBhTTZ zjACOK8HtU}5Z08PYehAW&lyyQl3cz(VWyFWk59b!2>)#MmBJ)lSTkI^Ha^v_@G? zrwqY|_{AoYDyi&`9=Lf+(9^ySr(@s|;1`#{Q17vpNh7RYSNTG}7y0Vc^UraU;8IB| zdT4yt~)=0BpKdf)@k$s7Liq;W0)02$JtgX>*<|>$Pr&ML17h}tMvnnwQxRzB>$0TbMT6_{^@i1}=Z}u<3m+S|KZT| zi8~NL|1A3af!6}ilz-Fh_oq1;4r4wf2gK~Sx5zQ*gRdGm?90m5f1bD>;_Gxr@$&a)`WRsdfDM6dT5?fb@-xuhVHhj=wChIuJ?Hq~3>2pCb>_tisfUKowXB{* zciI%ODyVW;`*mN?atxPVO5eA+rAp0zN7(X3{&ps@?9z-Ni&Y%bqrlpX;~Q&#trrXjiX0*ZG-=n}RZ-hFjI;u_xMEBV&t@!Nfwzy-E)5MHgXuY;meUuGkxVPhV|6N6Sh z%h3WEUTb$$RaNaon+e+|Hbl@+JHbLD^YiD=&`ARg!24t1wa<4@p)f|#&CZ?>`qlb2 zGgCD@_u*nA9f!AdcsCD#OK_Tx>Pvx$a}l#cf@pG9jV`VX0DB+;9A7f?-GD;(hr+ca zIR8)XNoMauI0>AL>4_wyMI&(~FmDB-BTSAoF^#-3?w30}C0m0DC^%w5w(p`8-ED#} zq5$j@R7opd1r!;FLqV;tK0eCN~cMaQHD^ z^xa2?#f2HHEG&OP*$5UDD(@dk)Jb(0xQ~Usp9+!89<+*zilX4N8;44u>APBml#0Xn zs;eN9oCap|x4f;c2SV#3W|AjO!KIwa`^AR$J`O!ThCiS-fM>#Efz#n?UF-_!n89Qm z0c2eTZP#Lxt`2B9nnVTP@IhiBj6@nb z!hCI~cYmijwWqi*Gjb%-GmoLXYCt)CD;ZwL4~BqCJ(v+vUDKh#mkPgRx(k2>4({VX z&_uNEFxmJEKQ7Z}@6k+NoM?|-UH|GKs187Bl{RQ~=_s_VbyfsG3RpR$gAV;Z8ID#g zMk};kA2V-p>)CH(H`JDpEHv;F`oGS=LI?jlg801f~_GrI@rHbIyHmVh`WX#@fg2A>eeb=QyiRhClY~OH}opNfG^Xlq66~(pupleDDW{ro1UUD=_A?F)=Mgk4XnW6DvXW+ir$q^= zw`Pq5Mb6mqBTqRvQ4u!-ey4@=Df1YJ2y=wsZy+>?0o@1Yc#znlGb0rFM&%&4!Tpcw zAFG;a2&aH&h)i_!rzKv1J{k6v@$qq`_YV)CWh^X~^DTe{owX=g=L{N7Xn2ptf{n;R zmt;+~6nQJoplgM01KL>_zFIcS4)piKA3cDwG&cH|ze+dFG)iG$(^Yi;<8{z80Gpmi z_j_lw=Li{0+MGeH14z~GAu~DB;QP@}5nzSF;PeW9fyQs7lP)9cT*S(csZ+&-J@yRY zBm={ntG2^_HXwfh2?b=|`^U#CW?Y=jOL@IR_+a3D4z?!DEG$1&ua@4t)!Ci3uSfV( zr$57833$iR02(-Z`hk%k0^vS^yhpH1+VEN8c5_=1x$RyY)()sZeAZH|GyKmsVR6HN zWiV6hs-k5tC+rzL1uIAC1UV(;#mDP-Qqs}~liN#up@w{|dhB3a@D6Mi%+2W)R;~eV zA$FWJ?`TqM6W42&ZLXDPkW9QbbV0{4aa3EfKWP$R)aHExPVdH`RYXWHuz{SD;KUf+_0l+Y+lDe~-C1#pKaos0idY@RgYw9uAaNg?9!r1&x!P`R20U7 zw&ZVp>}7=tldAR0q2Gc6%pY7BI{vOvY>D}9Aj<_03T@c2AJ~rOot=3L-f|VutzE)X z0E_~v6qw$%*ViWrq3c`lK$Zh+RDg-6d^>Z=4;Ai62zIYlRZyUt=e4;BPwZp#OMWuQ zYeLSc#;}*16^2TlXxo!?*EtR3Fq(d~{{ztoG9S<%W+sWkqy_36C<49(p z4*at)yZ|{2q&GXxpyk%6_9)Xwy?R9xqF1gnPJ#-@NBd1&30J{+h(P<6DW=+@(rDZy zZYl0#_xwop`q%%bN;BKY*QIV0$(9ccY^`a;@{vf=5FY7*)mFqlHQu+$pjS@z2Ny&ZkXqda6 z9$#C6cR}L=NYHDlt1nL~1Jtj(IUIa~-KMXv4|qPnSd0ITeYd5ZnzK)C00Wt8G|U3) zOJ84K&~BVL$r4$+QN3OBHJUyJqvWE#r)-t2k{B#(Y(7g7Oc}62CmVZ5$fP{HgREMo zaN|MXPm{9ii3h){NEzC6g?ZUO*k@tXtgNC!5+OCW#gHCZYa=Ix;dS_h3DBkqJ4%@7 z!C|(!y{(7=LO;ZeqfsTe;@A7qlh`j7P(37mv4FD|xEtJQgP??)0wX67MGo^*!QOa$ z4S~bpK8J^fj?ff8^TV+10YJ7YHI-cQGfYpRGPZRRFR3WyRFJ$e*d*?Owfp-P&rN9rcr@?l6k0{S;hJ2qhtJ-qy^ z(Kq-+;a5Yz$J;OC#a({}%Egx%ke>AE6MkV~Ur6W~4>kN5oW(cSY)BXPiu{{nLk&9{ zL4;13UbYrUGYcV(%$8+1^MUo7PSdPV;`EX`Kbu7!rGJC^>9!BMF?KIr90Ay5Xl(31>(JoFnsFVeIR&3K%);Sj$hp`N5Rg21`s2Hk@6S+DaA#_Y zV<;SJviK4YIC>krUEs=As>Ql01#Kz@twR3ch%9yfJX@L0HgpEI+&#NV`x>v{s9Nsd zHWE{5uU#9;?64Wh0p2E@x6j#>Tv~FRdVz!q8hMxxJHkm;gbkI}F{e#z_Sh!y1EgQL z8ZomKCb2UK0)Tw(09%a}SH_$KrV2Ykm3HV>AP zP1x&acZYePYQ(JGj(R48m?a5o%m%(+1fg}n>|H{vJW{zXl%^s!=%1nU8O<9cCCD}UWe-}7P z7sAWSi$F?Z0mkek9(LbIJbDYYi2+~5z{XC4NdSedy3Gv|SZ)LaUGt~gz-ESZk2o#@ zX?ETb4Ad5Zme6^jO&50q4Q=EC)Z3Mi-jnU)-gY!=W6uvGWlBmvcp;n1FrCX!(3v=(Qmx(r ziw+-j`2c-UYc>HoAr2$XYvc-NA2r zb5mZ|n}OrPi>qN0Iu<<0-@?e&j+!fDIn(tf3-~)=0>da9MiJVT#j8t?w5J@CWAAE7 z%)xL8_&Ffx`iZOrr<9Gza~s~jVC}YK|M#uGEc|7|rBmSU2J8|_uPsRroIP5|BL+GN zRA-r)nTt@~nHU?ZO!EBnXw4U6*ZW`!4uB!C{I~H0{1g}nzzAV4$-priJL+LNgmqhk zGW(4|Sq6)#W@cuH`R*J&NmSD)X5rW)FqDEi zUi~HHR3XhW$a;U2+!bQvApdP?Wd*V9&lg&~=2liq+cg~VTBgn_f8u@HT3gbz$>n7OZk%c)|snX~&Nn)RAvra%m3U1R0 zZ{K!nx0y^2JIqJ`3O)!KG%8-7rcq(&7h1MY~qzLVp0Y3C$P`vPm8{_Zun2~ z#Meiiew5^wOzr`=Edg^IgopXX()xI24YMFLhxY-Yk?l75fUm3h&hgQ-*b(>^3*n5 z1(U(x3~^_R-qWxDK<_h>Xn5@=mE+=fe=K$q$0 z$eA3}vbFUly^b_sBZ}V+D&_^KJ#Cq5z_0`A=)$}_#G6wLo&&VVK#gu#f%Y^E6!$m-las)^PHg}p3V0b(OFF5EofwAHbB5!K% zjqGqLZ)OIDysWIOl9EWl*ZB8Aj|g17t&=k-k>^~;lyyoIYiLaL0cjZg^2KYnZW83@ ze&o*X|b$WZLR&XauLt!O>BT9{b$!0#&o`n6@PFMGG8!DfMjiGT^{+K?qd?(^MyvOmMcN&9z26-*xrG4PDD)|09DzT zj9Trv6TG)zM&=B6*&*0x!b&BN@qzq$INFUHcY&i0ze-N7;X@vz-u!ivBu>)^D*~`7 z@NIy)iCYqba(iomX=(BqMraoV8uJMVfL~H;W8-Q4tRsC04A2}2$Z-EbAJPOTC~Ok& zcC1ZXXI^aN#sJF`N?*WYD@Si>1Cy!H$Pd0I9bAlnG4Spkng6w)#G}E6EI0lBLSOX- zT=D=gGBz+!G)!B@L0TAG&1484_3!{;@jTQEK=H$w%FJ0EiGG`RylWLPu8W?iSRx3kwS%)fJlbES}x%HPM8? zhZ8vD=tI7~<5_N6&wwqc)>($)i#q1WH&t-p1BNu>KLZv1A#57}45;U8hyZMLE8H9w z6bymP$kwn%mxfTx$v$nmdi(gmi*X26+|Y>wn1e`#cdGg@XqoRJ7^R=MQSC=s28JHT zh8NG|+?_tXR$LrmF?0gHH-K*GD(i49uql`6ur(B6HI-$wE92k8$AI$>#5-R5KP0l% zPFLQl9!^Z{-Q^mMN68B%+uPfN=vLkoCFll#$y7VL3xEpn#^nf*E=}~1gBS~w7Uaud zS@1XvCp&)QcWl)5B_}=xIRyofmBnemApy=&k9jvz+b64e%0GIO@8EXXNSwpxUg z1Fl3uL+ou~9_6oVsF8%W`y`ag#!-&B_YhAxz|!3`)#J4Xig5HvL6 z*=ob}w?))*J1uv6%np!f$+9x`!m&F91oy#Pv~;5Xm%Jz1k@Gx;G~Eb=Q!5X!7+_O} z3N(eBN22W zfKb7~$OStfFK>d%?Pc$uV@7R2hPpw#k92P_lDR1mDBKWj+kC-dlNh4M4n|;OP)TlZ zAXd1rHy3+0k_72a;c1f^h29qtxd06M49YF2T;LH6RKwx}^GmLEu!M!i04jyE*R_Tj zd($wpcrhfxEB^}1^FIvYaRrQ1K&k^H`xA~GccDXTSE#kXLQ9$ke-5tT>U>=cHfP$s zVxtr(r9a)=N|u(ZfYu8^a|+uB*ms;l8}O9u)8b+P&`@eOf)|y8+ehIWbC z?1un`yNOFKrH|g`U35iAxee1Y78Vw8$0@i5mZv^&02DV@q!z9Gew&1$b z>#5!Wq^W$u4uFd16StbqFNRyGySWhy&twx$W@d!Hv8lkwEAV-d_=K_irmdr+jP!q- z((?I+>=-CN}>+Sg*qjd9C!TEr7p7X!LkH zT3})SKjD+M<_0DQ>y(3Oj zOlpy8+ZuJsQeigF1SW4f?D(H zZ|2oc{kh*ghw+9lW%TuDpxDjRC?4B<{pL-(OYWaH7wWoF;tMNYi3%fJE zt0yvqxxi_eN1AjID5U`9SH&}0wKbJ`CJvX=0HFDka-Hz*xP%1W=Rd^#!=v^NtGa)J zjnXU7bJ^b6Mn&F+OmTxT!MnF|N2Ut+eExiLuT3(di@Pi8>j`Bn%;6>{J>ayc(6w$n zm2!20{(ApMCv!i-wz|+X0*5rk~ z^KShtMiSRGiClmPmGFPo&jr;>aTzagM`B43NFu*n zMm3@L1-2!0J-{WdsPRcwmLCN38dOwNA}qAd*XFRP>@%q+&pRSWH-i1?uT2vAVK;%- zmlPD1NI9*@)#l34_Mq|2M|gI9ec|{)t8}o?K4zlD$yqea!X4&u-_au__1q~l-@pSt z4LO2U_`>8;PFFVXv=1ur49_LZ8^|sEq56BC1S?dMcGs?xk8Gzx(2eu44QQ$OX8}cq zfGt(90-1w_X0-9Qr@TX&RFn$v7U(sHh)Gb9ODzmcI{$li1{xUGNmeG`&fz*X0HRu9 zHV5lnDE%Agb|IXtw&vu|oG1WG&{e{!1JzPm8il9?@&l}Kn(p4}1FX+x^7peZB!_jf zlhJVMLrVd~azIpY8a}Lmr^aA;$By?p6RTv}pgW*o#6(1(Y|IPS*Z%JWyZKx&9r~!H zO4o^L2C4=*7|uBhrh^Y999(nY1q@~#K%vN|=A~!?S5fN>uQ1Ce5mHMEn47V8{qf&0i+6=Cgc*@vPS=qHX?fPVf*cHt3!pVY#zQcqg-RNqjv%5@Xc5Y4 zn1tojWHd>$SGKiz*8^5~3MMQfK;4Y*1} zwVsra055)H?t6v~_wJw1Uz70!z4`d_2-#->UrEtH_VkDi9P59eSsvC9C#4=(#RRk$ zK4#C|U|0biuQ^%RrNYf-7}MS+Ca#0x`Ax>F?Nk8l!Q&pb(KGlf&6KbkF;s|9E{TS5 z7T|?y;tZ+?e`a_R^6q9gx9fnw7^#VG6~KYAR9+Ua%e&Ghy5|ad3#FHzz(5$B?ianc z%THe;g^;*`c4JeM^KOFSSHjVj5exc7ch{fG0Byg(y?^<;p#!W-U)-iTnO;?u@}MKa za^Oz`Gy_^nVGzR1&|py$iUT-h^S1;W$dXwV72l>Mt?T-}#5`su`aU@cd7B{%OyHhK zgO6xf3vLQk)9XG44gV7;W#H9+tPgJpO6@WN8C}34%#RzZYnWcWhaCXA`(u(SJs|t( zmm3-syJv=;ef{|yM%_+ekIkQkJNn=7oR~Ke%ruZpN!Q8hdxmW?Mn;rZpV-Kvj$o=p z4W>t6uauZ>tf?SWQ z+=~|vOw8vAki7-2tW6W!a^zjvvw?J!|9K=*;$tCzIfW!DRXaAZBm;<(;16KtQ@{RhFF@U}wBVF;M7IH@?3 z70RXIt?l1_qi776@&3sDHWV6XtZmR#^$gt`b8JBTh)%%}>3L zo$Zig0OJCfd0IdDU7}8DU1yvn@Yrf2L3pg!q%{Y-j(kY;WLm$3Ajo=)oa=I}#Vo+_ z0}j&iyMf=O8ndwXv5l(I0$u9_J-Bqs>f;7X{A(z!;(S{1%G<7@{V3*0WiE|c5Q9lJ z6Aj@~SpP>znQigl+DtK%teAK!$FQUm(^?(fIL;^`LW?{AhaifV;->dgz-3pil*zj& zBNiWuZX_(hbDPG4fy1s$Xs8d}?~kd|J` zQCS9LMBsK+vdR(ODc;)P7M!b7H!;kb(B3HBBDc}%Z}jr?cpkV`T~xTFA8FW1LS)x1 z649%cl%8J7R?1&@H(Z~Pvfwab;l~gb)G>>axCb!aj8A%5M3w3dgSv_eQDDJnANd

Fxug|V;HFfrSVkzUVA)Yp#)2o(B4pv_gQ5@J3DPjVl5d6It3Oew<4F<)+qEluHy`O zTLrs2v)LNu@N;q2^t+f}(RO=K1o~qVPr!CDH$Dyv&Zj7s^Jm@O;OEcJAKuk{vo*v1 zX3pm@BrNFoP-f{T_3g~0xOy{*R|4I5xvta1C&FwA_X>4f@~IbYQ~c3Musf=gwlw)A zhhty$xcghk4mm03uhA%$ev(Wre*73<<%Rz1xb*(@rq(7P!$Hr_yM7_mNZ;;obf9UO zzI|l2y#mYyIfS$t={839>}qNZ(h`{}K2=YYo`iU!Ni!8#<}u3L^g8%8rS^V1=PeAQ z#S7gujEs~qZbwLokZ!0+fbM?_*b7;sTTwODQ|RC6*_p&1$dqHLp@lQ8u1I8aB z6$3vv=age@s{*t?S!8uXy|LdOJQO^Pe@T9lkG}9tpHjyB`oWupxXc_(ZL}guK9;6C zw7xHObjh78deC5EE|#5x9M5tw941`>bg+5->-?ie-$T{M!7UmuU`Ky~Ip%p(F^+B? z6@+7HnS5wX#hTbU;b`G+*P(czUX@*w?BU_&TKbf!SXGbV>S0Mgj_!Q8uy8|JPUt8R zniG4!(KiZLFvP5(q>|M$Bdfe6MUtg{bUtlr(-@aYjOa)*0V9L(QSiDO?JVEMzwSKm z^Utic_p~m3o$=_mJAEC;CcokU?dI}`oUE%j|dGW>_W=S9njG09^JwVegEY_o$yC{E6k>+ zmZ3;%g1;35wSz&q&gk3Tkd^AV5|xvQU6HtymIiBzJ93tL_bY~D3Q;vn)pE1z&#^3E_YG2reeI2KRST-xPRZN=7a8hem*N~ z55J$$8~F>(<+Yt2hXLf&a6kfVW}oAaEwI`>VP9+s06 z@Ged7$;r=7%8YRJ=x~W2He=>&*AnPDEzIjJ;uHxgcQXBV`6R?!66NE`@9$7#B33r> zNfmeeu!`q0b@i-AC&xY37br@P9l{5`)@iq<2@~nw;WRg}{Wfp#ZSsASg6=HdH##VL zFN<$>DfT;y9fw^f#JB;C9YI81OnXDdwQON$?e$HZ@~-q$S2yuM)`wc<5s~sPbsfEo zTUagz0=e8o>pCW9gUZ?a8(;Un;#y(7Ye5!vJTFbZN*focXs%ROYEBSox3R+cLRsC;P=a!Rpa0!f$q9N&gHPd~g%1y` z94p#EEJADb*j7V{q;h9Lg!k(|Ry4x<9!{51t4rf$Siul0qeGnk^?k}@b7oF<_4hea zv1_m46>?hU083KS3kNR&qQoO@A}=-X~I3`R^t;MoP^qo6Rc&d#ksT z2sX#2ON2c_8=h1SMS63Vri(SBeUdfwixIV})9qxOo*?;}uJLhJ+KgRZxgGR@B(i9_ z-k&ctUEmBG9 zVRz#J-+u8T4U%k30(|ma+`OM=wLj$QD9UNBxAbEZz;hD2!gp(I9(!i#{27(t5L zi0uL<_M4UG?>>GHO1wEx4daETtA!Sz&qMCc^w96bFOE%#0U@VUFFBun#z~K#)M(lx zHY%Dh^;;_q1it0}uYL@4YXI;|QE9p|qdQ%P_Tzs^&XAJw|D`^2Dc2Z9LBV(|FD0S% zzkeu{mj6q`hE$pVPYoMVMV; zLWxKTMOtV>e}G7WkVrF9L@D8sfb?QuKp(6nm`G6{C@3&jl)#_~iW&nW2>Nd3!>l!H z&8PV?f5BPz+_Ub!XYc(x$M4Q>Ok=eksfh&gF5!LSGmWm7CqSPZjBAmR@1l>Q#vDTjB%*zNiS5yr05 z`;5SiGp@EQC}#Z#)-R}1`=ACByoqZfH`vwFqd5Pka1X}ibprH^7W zpF@M>Zw+1Y2eTEeulcMUrz(Z!0Q_3Tue6i-`Gc~<+hX8OY6n5}u5vx@UN1mGg#Vijfbn<(=xC?DfWu&J_ z3&ORL)nEa==s=`_Tgf8}B}^e39=;0LQ1D@^B_mpzM}bL3qsqiXs**Dy)4TPt3Ws!V zsYDfZv)0|+iRJQ3ZR+O*Md_Y#u4l?Z|Hz2eNkeocpNo`+)JW<8U{|LTVGtwU@0+25 zfk;yg)w9vzMwGEU5{i!Y)Z0*y^EH*ICRIIofymd<)D*rJ82VBlIDO&7ICxdi(m%Hm zP9@<7@pkFN95=^-09;{@O&4RMIH*DPLqln)Z})b<`2wz*XLS+yGt!Ih-SkS=zDh&F zPwVL9Tb&9-3zM-B%)^R#Ygg@%C|Yzr;MFOEKP$R0c{|0C1fIO00pzKWDA_i@(x*O` zGxxu&*#zI!ElcGL_cOVr&CLK_?OJvHHIBWB^j%x{EC!7GY9`;I=P%%PpL;UILq)g9s zxWaw=CCkEu(%&|Yz=j&y1Qv_KVM!MckXlhhV;-HzUrmeZ#;taUj$AgQaAVEQRqz*0 zq9#2chJe*vqIFm8nIU^s7qal-b@9#+3@-;I|w4*Q?RwRRH_pkbQ3 zW|xgc>#yAn4sF*-%a2f;M~)L~kpau|Jz&E?H+Gc$-RZCYBO^poH&}aI8SLSv_TZH0 zMSv?B(ximNDIi5UBNpA%#B0d&Sy1E2{_W}z^~UaFQE89V6vOzPAIfpIYKtz7EgrD=d!xl6fNi(~tm>|^7h6E`c|w{ZSfs)n zI$8`mOarrYb})^h6u{4(y!SCol4~uir%2B_ApMcI`pUCrrQ%?<{i||~t|Iro!_&=8 zhX_ywl-l6U%eDJD;VEWgM<#D>+5TX zkM=o2Qh#`*>T8X-|wbfwS zcu_(J49QX4&N*lKa;BegNA}jb#S}hEHty^eJJ@3~MJ`&2pX7?p(k-0ygHn%G!=sk- zW@g?F2wIGF!Fzn8@A1+rij?%c*ekFmSUA_!|B(vsg{1-VbRg=DoK#(aR)Ya?$SS%C zk>k{Pr{a}g|?$HFI#P4Ea@(6XSW_19kl=Z|R_ z{FhLY?MGhjq+OlTo+-_;ZSIACF=pDSf(&EbxWKQ s?y7(m$)Du?cfI~Yp8p=L&NTV>O~(k?;=J;+szm3{V%;1%F~NEN2LOT13;+NC literal 0 HcmV?d00001 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; +});