diff --git a/doc/jsdoc/files.html b/doc/jsdoc/files.html
new file mode 100644
index 00000000..ca3c061e
--- /dev/null
+++ b/doc/jsdoc/files.html
@@ -0,0 +1,288 @@
+
+
+
+
+
+ JsDoc Reference - File Index
+
+
+
+
+
+
+
+
+
+
+
+
File Index
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documentation generated by
JsDoc Toolkit 2.3.2 on Sat Mar 26 2011 18:26:06 GMT-0000 (GMT)
+
+
+
\ No newline at end of file
diff --git a/doc/jsdoc/index.html b/doc/jsdoc/index.html
new file mode 100644
index 00000000..34592f3a
--- /dev/null
+++ b/doc/jsdoc/index.html
@@ -0,0 +1,210 @@
+
+
+
+
+
+ JsDoc Reference - Index
+
+
+
+
+
+
+
+
+
+
+
+
Class Index
+
+
+
+
+
+
+
+
+
+ Documentation generated by
JsDoc Toolkit 2.3.2 on Sat Mar 26 2011 18:26:06 GMT-0000 (GMT)
+
+
+
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/_global_.html b/doc/jsdoc/symbols/_global_.html
new file mode 100644
index 00000000..421a4f40
--- /dev/null
+++ b/doc/jsdoc/symbols/_global_.html
@@ -0,0 +1,1667 @@
+
+
+
+
+
+
+ JsDoc Reference - _global_
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Built-In Namespace _global_
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Field Summary
+
+
+ Field Attributes |
+ Field Name and Description |
+
+
+
+
+
+ |
+
+
+ Copyright 2009 Google Inc.
+ |
+
+
+
+ |
+
+
+ 2011 Peter 'Pita' Martischka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+ |
+
+
+
+ |
+
+
+ Saves all Authors as a assoative Array.
+ |
+
+
+
+ |
+
+
+ A Array with all known Pads
+ |
+
+
+
+ |
+
+
+ 2011 Peter 'Pita' Martischka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+ |
+
+
+
+ |
+
+
+ A associative array that saves which sessions belong to a pad
+ |
+
+
+
+ |
+
+
+ Copyright 2009 Google Inc.
+ |
+
+
+
+ |
+
+
+ A associative array that translates a session to a pad
+ |
+
+
+
+ |
+
+
+ A associative array that saves some general informations about a session
+key = sessionId
+values = author, rev
+ rev = That last revision that was send to this client
+ author = the author name of this session
+ |
+
+
+
+ |
+
+
+ Saves the Socket class we need to send and recieve data from the client
+ |
+
+
+
+ |
+
+
+ A easy key value pair.
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Method Summary
+
+
+ Method Attributes |
+ Method Name and Description |
+
+
+
+
+
+ |
+
+
+ Copied from the Etherpad Source Code.
+ |
+
+
+
+ |
+
+
+ Generates a random String with the given length.
+ |
+
+
+
+ |
+
+
+ Append a changeset to a pad
+ |
+
+
+
+ |
+
+
+ Returns the Attributed Text of a pad
+ |
+
+
+
+ |
+
+
+ Creates an empty pad
+ |
+
+
+
+ |
+
+
+ Returns all Authors of a Pad
+ |
+
+
+
+ |
+
+
+ Returns the latest Revision Number of the Pad
+ |
+
+
+
+ |
+
+
+ Returns the author of a specific revision
+ |
+
+
+
+ |
+
+
+ Returns the changeset of a specific revision
+ |
+
+
+
+ |
+
+
+ Handles a CLIENT_READY.
+ |
+
+
+
+ |
+
+
+ Handles a USERINFO_UPDATE, that means that a user have changed his color or name.
+ |
+
+
+
+ |
+
+
+ Handles a USERINFO_UPDATE, that means that a user have changed his color or name.
+ |
+
+
+
+ |
+
+
+ Returns the Attribute Pool whichs the Pad is using
+ |
+
+
+
+ |
+
+
+ Returns the plain text of a pad
+ |
+
+
+
+ |
+
+
+ A internal function that checks if the Author exist and throws a exception if not
+ |
+
+
+
+ |
+
+
+ A internal function that simply checks if client or socketio is null and throws a exception if yes
+ |
+
+
+
+ |
+
+
+ Check if the ID is a valid Pad ID and trows an Exeption if not
+ |
+
+
+
+ |
+
+
+ Check if the Revision of a Pad is valid and throws an Exeption if not
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Field Detail
+
+
+
+
+
+
+ AttributePoolFactory
+
+
+
+ Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
Defined in: Changeset.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Changeset
+
+
+
+ 2011 Peter 'Pita' Martischka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ globalAuthors
+
+
+
+ Saves all Authors as a assoative Array. The Key is the author id.
+Authors can have the following attributes:
+-name The Name of the Author as shown on the Pad
+-colorId The Id of Usercolor. A number between 0 and 31
+-timestamp The timestamp on which the user was last seen
+
+
+
Defined in: AuthorManager.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ globalPads
+
+
+
+ A Array with all known Pads
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http
+
+
+
+ 2011 Peter 'Pita' Martischka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
Defined in: server.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ pad2sessions
+
+
+
+ A associative array that saves which sessions belong to a pad
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ padManager
+
+
+
+ Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ session2pad
+
+
+
+ A associative array that translates a session to a pad
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sessioninfos
+
+
+
+ A associative array that saves some general informations about a session
+key = sessionId
+values = author, rev
+ rev = That last revision that was send to this client
+ author = the author name of this session
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ socketio
+
+
+
+ Saves the Socket class we need to send and recieve data from the client
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ token2author
+
+
+
+ A easy key value pair. The Key is the token, the value is the authorid
+
+
+
Defined in: AuthorManager.js.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Method Detail
+
+
+
+
+
+
+ _correctMarkersInPad(atext, apool)
+
+
+
+ Copied from the Etherpad Source Code. Don't know what this methode does excatly...
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ atext
+
+
+
+
+ -
+ apool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ _randomString(len)
+
+
+
+ Generates a random String with the given length. Is needed to generate the Author Ids
+
+
+
Defined in: AuthorManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ len
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ appendRevision(id, theChangeset, The)
+
+
+
+ Append a changeset to a pad
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+ -
+ theChangeset
+
+
+ - the changeset which should apply to the text
+
+ -
+ The
+
+
+ - author of the revision, can be null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ atext(id)
+
+
+
+ Returns the Attributed Text of a pad
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ createPad(id)
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getAllAuthors(id)
+
+
+
+ Returns all Authors of a Pad
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getHeadRevisionNumber(id)
+
+
+
+ Returns the latest Revision Number of the Pad
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getRevisionAuthor(id, revNum)
+
+
+
+ Returns the author of a specific revision
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+ -
+ revNum
+
+
+ - The Revision Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getRevisionChangeset(id, revNum)
+
+
+
+ Returns the changeset of a specific revision
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+ -
+ revNum
+
+
+ - The Revision Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleClientReady(client, message)
+
+
+
+ Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
+and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ client
+
+
+ - the client that send this message
+
+ -
+ message
+
+
+ - the message from the client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleUserChanges(client, message)
+
+
+
+ Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
+This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
+Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ client
+
+
+ - the client that send this message
+
+ -
+ message
+
+
+ - the message from the client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleUserInfoUpdate(client, message)
+
+
+
+ Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ client
+
+
+ - the client that send this message
+
+ -
+ message
+
+
+ - the message from the client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ pool(id)
+
+
+
+ Returns the Attribute Pool whichs the Pad is using
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text(id)
+
+
+
+ Returns the plain text of a pad
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ throwExceptionIfAuthorNotExist(author)
+
+
+
+ A internal function that checks if the Author exist and throws a exception if not
+
+
+
Defined in: AuthorManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ author
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ throwExceptionIfClientOrIOisInvalid(client)
+
+
+
+ A internal function that simply checks if client or socketio is null and throws a exception if yes
+
+
+
Defined in: MessageHandler.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ throwExceptionIfPadDontExist(id)
+
+
+
+ Check if the ID is a valid Pad ID and trows an Exeption if not
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ throwExceptionIfRevDontExist(id, revNum)
+
+
+
+ Check if the Revision of a Pad is valid and throws an Exeption if not
+
+
+
Defined in: PadManager.js.
+
+
+
+
+
+
+
+
+ - Parameters:
+
+ -
+ id
+
+
+ - The Pad id
+
+ -
+ revNum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documentation generated by
JsDoc Toolkit 2.3.2 on Sat Mar 26 2011 18:26:06 GMT-0000 (GMT)
+
+
+
diff --git a/doc/jsdoc/symbols/src/node_AttributePoolFactory.js.html b/doc/jsdoc/symbols/src/node_AttributePoolFactory.js.html
new file mode 100644
index 00000000..957fc617
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_AttributePoolFactory.js.html
@@ -0,0 +1,90 @@
+ 1 /**
+ 2 * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17 exports.createAttributePool = function () {
+ 18 var p = {};
+ 19 p.numToAttrib = {}; // e.g. {0: ['foo','bar']}
+ 20 p.attribToNum = {}; // e.g. {'foo,bar': 0}
+ 21 p.nextNum = 0;
+ 22
+ 23 p.putAttrib = function (attrib, dontAddIfAbsent) {
+ 24 var str = String(attrib);
+ 25 if (str in p.attribToNum) {
+ 26 return p.attribToNum[str];
+ 27 }
+ 28 if (dontAddIfAbsent) {
+ 29 return -1;
+ 30 }
+ 31 var num = p.nextNum++;
+ 32 p.attribToNum[str] = num;
+ 33 p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
+ 34 return num;
+ 35 };
+ 36
+ 37 p.getAttrib = function (num) {
+ 38 var pair = p.numToAttrib[num];
+ 39 if (!pair) {
+ 40 return pair;
+ 41 }
+ 42 return [pair[0], pair[1]]; // return a mutable copy
+ 43 };
+ 44
+ 45 p.getAttribKey = function (num) {
+ 46 var pair = p.numToAttrib[num];
+ 47 if (!pair) return '';
+ 48 return pair[0];
+ 49 };
+ 50
+ 51 p.getAttribValue = function (num) {
+ 52 var pair = p.numToAttrib[num];
+ 53 if (!pair) return '';
+ 54 return pair[1];
+ 55 };
+ 56
+ 57 p.eachAttrib = function (func) {
+ 58 for (var n in p.numToAttrib) {
+ 59 var pair = p.numToAttrib[n];
+ 60 func(pair[0], pair[1]);
+ 61 }
+ 62 };
+ 63
+ 64 p.toJsonable = function () {
+ 65 return {
+ 66 numToAttrib: p.numToAttrib,
+ 67 nextNum: p.nextNum
+ 68 };
+ 69 };
+ 70
+ 71 p.fromJsonable = function (obj) {
+ 72 p.numToAttrib = obj.numToAttrib;
+ 73 p.nextNum = obj.nextNum;
+ 74 p.attribToNum = {};
+ 75 for (var n in p.numToAttrib) {
+ 76 p.attribToNum[String(p.numToAttrib[n])] = Number(n);
+ 77 }
+ 78 return p;
+ 79 };
+ 80
+ 81 return p;
+ 82 }
+ 83
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/src/node_AuthorManager.js.html b/doc/jsdoc/symbols/src/node_AuthorManager.js.html
new file mode 100644
index 00000000..e3ef555a
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_AuthorManager.js.html
@@ -0,0 +1,139 @@
+ 1 /**
+ 2 * 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17
+ 18 /**
+ 19 * The AuthorManager controlls all information about the Pad authors
+ 20 */
+ 21
+ 22 /**
+ 23 * Saves all Authors as a assoative Array. The Key is the author id.
+ 24 * Authors can have the following attributes:
+ 25 * -name The Name of the Author as shown on the Pad
+ 26 * -colorId The Id of Usercolor. A number between 0 and 31
+ 27 * -timestamp The timestamp on which the user was last seen
+ 28 */
+ 29 var globalAuthors = {};
+ 30
+ 31 /**
+ 32 * A easy key value pair. The Key is the token, the value is the authorid
+ 33 */
+ 34 var token2author = {};
+ 35
+ 36 /**
+ 37 * Returns the Author Id for a token. If the token is unkown,
+ 38 * it creates a author for the token
+ 39 * @param token The token
+ 40 */
+ 41 exports.getAuthor4Token = function (token)
+ 42 {
+ 43 var author;
+ 44
+ 45 if(token2author[token] == null)
+ 46 {
+ 47 author = "g." + _randomString(16);
+ 48
+ 49 while(globalAuthors[author] != null)
+ 50 {
+ 51 author = "g." + _randomString(16);
+ 52 }
+ 53
+ 54 token2author[token]=author;
+ 55
+ 56 globalAuthors[author] = {};
+ 57 globalAuthors[author].colorId = Math.floor(Math.random()*32);
+ 58 globalAuthors[author].name = null;
+ 59 }
+ 60 else
+ 61 {
+ 62 author = token2author[token];
+ 63 }
+ 64
+ 65 globalAuthors[author].timestamp = new Date().getTime();
+ 66
+ 67 return author;
+ 68 }
+ 69
+ 70 /**
+ 71 * Returns the color Id of the author
+ 72 */
+ 73 exports.getAuthorColorId = function (author)
+ 74 {
+ 75 throwExceptionIfAuthorNotExist(author);
+ 76
+ 77 return globalAuthors[author].colorId;
+ 78 }
+ 79
+ 80 /**
+ 81 * Sets the color Id of the author
+ 82 */
+ 83 exports.setAuthorColorId = function (author, colorId)
+ 84 {
+ 85 throwExceptionIfAuthorNotExist(author);
+ 86
+ 87 globalAuthors[author].colorId = colorId;
+ 88 }
+ 89
+ 90 /**
+ 91 * Returns the name of the author
+ 92 */
+ 93 exports.getAuthorName = function (author)
+ 94 {
+ 95 throwExceptionIfAuthorNotExist(author);
+ 96
+ 97 return globalAuthors[author].name;
+ 98 }
+ 99
+100 /**
+101 * Sets the name of the author
+102 */
+103 exports.setAuthorName = function (author, name)
+104 {
+105 throwExceptionIfAuthorNotExist(author);
+106
+107 globalAuthors[author].name = name;
+108 }
+109
+110 /**
+111 * A internal function that checks if the Author exist and throws a exception if not
+112 */
+113 function throwExceptionIfAuthorNotExist(author)
+114 {
+115 if(globalAuthors[author] == null)
+116 {
+117 throw "Author '" + author + "' is unkown!";
+118 }
+119 }
+120
+121 /**
+122 * Generates a random String with the given length. Is needed to generate the Author Ids
+123 */
+124 function _randomString(len) {
+125 // use only numbers and lowercase letters
+126 var pieces = [];
+127 for(var i=0;i<len;i++) {
+128 pieces.push(Math.floor(Math.random()*36).toString(36).slice(-1));
+129 }
+130 return pieces.join('');
+131 }
+132
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/src/node_Changeset.js.html b/doc/jsdoc/symbols/src/node_Changeset.js.html
new file mode 100644
index 00000000..7d5f002e
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_Changeset.js.html
@@ -0,0 +1,1965 @@
+ 1 /**
+ 2 * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17 var AttributePoolFactory = require("./AttributePoolFactory");
+ 18
+ 19 var _opt = null;
+ 20
+ 21 //var exports = {};
+ 22 exports.error = function error(msg) {
+ 23 var e = new Error(msg);
+ 24 e.easysync = true;
+ 25 throw e;
+ 26 };
+ 27 exports.assert = function assert(b, msgParts) {
+ 28 if (!b) {
+ 29 var msg = Array.prototype.slice.call(arguments, 1).join('');
+ 30 exports.error("exports: " + msg);
+ 31 }
+ 32 };
+ 33
+ 34 exports.parseNum = function (str) {
+ 35 return parseInt(str, 36);
+ 36 };
+ 37 exports.numToString = function (num) {
+ 38 return num.toString(36).toLowerCase();
+ 39 };
+ 40 exports.toBaseTen = function (cs) {
+ 41 var dollarIndex = cs.indexOf('$');
+ 42 var beforeDollar = cs.substring(0, dollarIndex);
+ 43 var fromDollar = cs.substring(dollarIndex);
+ 44 return beforeDollar.replace(/[0-9a-z]+/g, function (s) {
+ 45 return String(exports.parseNum(s));
+ 46 }) + fromDollar;
+ 47 };
+ 48
+ 49 exports.oldLen = function (cs) {
+ 50 return exports.unpack(cs).oldLen;
+ 51 };
+ 52 exports.newLen = function (cs) {
+ 53 return exports.unpack(cs).newLen;
+ 54 };
+ 55
+ 56 exports.opIterator = function (opsStr, optStartIndex) {
+ 57 //print(opsStr);
+ 58 var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g;
+ 59 var startIndex = (optStartIndex || 0);
+ 60 var curIndex = startIndex;
+ 61 var prevIndex = curIndex;
+ 62
+ 63 function nextRegexMatch() {
+ 64 prevIndex = curIndex;
+ 65 var result;
+ 66 if (_opt) {
+ 67 result = _opt.nextOpInString(opsStr, curIndex);
+ 68 if (result) {
+ 69 if (result.opcode() == '?') {
+ 70 exports.error("Hit error opcode in op stream");
+ 71 }
+ 72 curIndex = result.lastIndex();
+ 73 }
+ 74 } else {
+ 75 regex.lastIndex = curIndex;
+ 76 result = regex.exec(opsStr);
+ 77 curIndex = regex.lastIndex;
+ 78 if (result[0] == '?') {
+ 79 exports.error("Hit error opcode in op stream");
+ 80 }
+ 81 }
+ 82 return result;
+ 83 }
+ 84 var regexResult = nextRegexMatch();
+ 85 var obj = exports.newOp();
+ 86
+ 87 function next(optObj) {
+ 88 var op = (optObj || obj);
+ 89 if (_opt && regexResult) {
+ 90 op.attribs = regexResult.attribs();
+ 91 op.lines = regexResult.lines();
+ 92 op.chars = regexResult.chars();
+ 93 op.opcode = regexResult.opcode();
+ 94 regexResult = nextRegexMatch();
+ 95 } else if ((!_opt) && regexResult[0]) {
+ 96 op.attribs = regexResult[1];
+ 97 op.lines = exports.parseNum(regexResult[2] || 0);
+ 98 op.opcode = regexResult[3];
+ 99 op.chars = exports.parseNum(regexResult[4]);
+100 regexResult = nextRegexMatch();
+101 } else {
+102 exports.clearOp(op);
+103 }
+104 return op;
+105 }
+106
+107 function hasNext() {
+108 return !!(_opt ? regexResult : regexResult[0]);
+109 }
+110
+111 function lastIndex() {
+112 return prevIndex;
+113 }
+114 return {
+115 next: next,
+116 hasNext: hasNext,
+117 lastIndex: lastIndex
+118 };
+119 };
+120
+121 exports.clearOp = function (op) {
+122 op.opcode = '';
+123 op.chars = 0;
+124 op.lines = 0;
+125 op.attribs = '';
+126 };
+127 exports.newOp = function (optOpcode) {
+128 return {
+129 opcode: (optOpcode || ''),
+130 chars: 0,
+131 lines: 0,
+132 attribs: ''
+133 };
+134 };
+135 exports.cloneOp = function (op) {
+136 return {
+137 opcode: op.opcode,
+138 chars: op.chars,
+139 lines: op.lines,
+140 attribs: op.attribs
+141 };
+142 };
+143 exports.copyOp = function (op1, op2) {
+144 op2.opcode = op1.opcode;
+145 op2.chars = op1.chars;
+146 op2.lines = op1.lines;
+147 op2.attribs = op1.attribs;
+148 };
+149 exports.opString = function (op) {
+150 // just for debugging
+151 if (!op.opcode) return 'null';
+152 var assem = exports.opAssembler();
+153 assem.append(op);
+154 return assem.toString();
+155 };
+156 exports.stringOp = function (str) {
+157 // just for debugging
+158 return exports.opIterator(str).next();
+159 };
+160
+161 exports.checkRep = function (cs) {
+162 // doesn't check things that require access to attrib pool (e.g. attribute order)
+163 // or original string (e.g. newline positions)
+164 var unpacked = exports.unpack(cs);
+165 var oldLen = unpacked.oldLen;
+166 var newLen = unpacked.newLen;
+167 var ops = unpacked.ops;
+168 var charBank = unpacked.charBank;
+169
+170 var assem = exports.smartOpAssembler();
+171 var oldPos = 0;
+172 var calcNewLen = 0;
+173 var numInserted = 0;
+174 var iter = exports.opIterator(ops);
+175 while (iter.hasNext()) {
+176 var o = iter.next();
+177 switch (o.opcode) {
+178 case '=':
+179 oldPos += o.chars;
+180 calcNewLen += o.chars;
+181 break;
+182 case '-':
+183 oldPos += o.chars;
+184 exports.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs);
+185 break;
+186 case '+':
+187 {
+188 calcNewLen += o.chars;
+189 numInserted += o.chars;
+190 exports.assert(calcNewLen < newLen, calcNewLen, " >= ", newLen, " in ", cs);
+191 break;
+192 }
+193 }
+194 assem.append(o);
+195 }
+196
+197 calcNewLen += oldLen - oldPos;
+198 charBank = charBank.substring(0, numInserted);
+199 while (charBank.length < numInserted) {
+200 charBank += "?";
+201 }
+202
+203 assem.endDocument();
+204 var normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank);
+205 exports.assert(normalized == cs, normalized, ' != ', cs);
+206
+207 return cs;
+208 }
+209
+210 exports.smartOpAssembler = function () {
+211 // Like opAssembler but able to produce conforming exportss
+212 // from slightly looser input, at the cost of speed.
+213 // Specifically:
+214 // - merges consecutive operations that can be merged
+215 // - strips final "="
+216 // - ignores 0-length changes
+217 // - reorders consecutive + and - (which margingOpAssembler doesn't do)
+218 var minusAssem = exports.mergingOpAssembler();
+219 var plusAssem = exports.mergingOpAssembler();
+220 var keepAssem = exports.mergingOpAssembler();
+221 var assem = exports.stringAssembler();
+222 var lastOpcode = '';
+223 var lengthChange = 0;
+224
+225 function flushKeeps() {
+226 assem.append(keepAssem.toString());
+227 keepAssem.clear();
+228 }
+229
+230 function flushPlusMinus() {
+231 assem.append(minusAssem.toString());
+232 minusAssem.clear();
+233 assem.append(plusAssem.toString());
+234 plusAssem.clear();
+235 }
+236
+237 function append(op) {
+238 if (!op.opcode) return;
+239 if (!op.chars) return;
+240
+241 if (op.opcode == '-') {
+242 if (lastOpcode == '=') {
+243 flushKeeps();
+244 }
+245 minusAssem.append(op);
+246 lengthChange -= op.chars;
+247 } else if (op.opcode == '+') {
+248 if (lastOpcode == '=') {
+249 flushKeeps();
+250 }
+251 plusAssem.append(op);
+252 lengthChange += op.chars;
+253 } else if (op.opcode == '=') {
+254 if (lastOpcode != '=') {
+255 flushPlusMinus();
+256 }
+257 keepAssem.append(op);
+258 }
+259 lastOpcode = op.opcode;
+260 }
+261
+262 function appendOpWithText(opcode, text, attribs, pool) {
+263 var op = exports.newOp(opcode);
+264 op.attribs = exports.makeAttribsString(opcode, attribs, pool);
+265 var lastNewlinePos = text.lastIndexOf('\n');
+266 if (lastNewlinePos < 0) {
+267 op.chars = text.length;
+268 op.lines = 0;
+269 append(op);
+270 } else {
+271 op.chars = lastNewlinePos + 1;
+272 op.lines = text.match(/\n/g).length;
+273 append(op);
+274 op.chars = text.length - (lastNewlinePos + 1);
+275 op.lines = 0;
+276 append(op);
+277 }
+278 }
+279
+280 function toString() {
+281 flushPlusMinus();
+282 flushKeeps();
+283 return assem.toString();
+284 }
+285
+286 function clear() {
+287 minusAssem.clear();
+288 plusAssem.clear();
+289 keepAssem.clear();
+290 assem.clear();
+291 lengthChange = 0;
+292 }
+293
+294 function endDocument() {
+295 keepAssem.endDocument();
+296 }
+297
+298 function getLengthChange() {
+299 return lengthChange;
+300 }
+301
+302 return {
+303 append: append,
+304 toString: toString,
+305 clear: clear,
+306 endDocument: endDocument,
+307 appendOpWithText: appendOpWithText,
+308 getLengthChange: getLengthChange
+309 };
+310 };
+311
+312 if (_opt) {
+313 exports.mergingOpAssembler = function () {
+314 var assem = _opt.mergingOpAssembler();
+315
+316 function append(op) {
+317 assem.append(op.opcode, op.chars, op.lines, op.attribs);
+318 }
+319
+320 function toString() {
+321 return assem.toString();
+322 }
+323
+324 function clear() {
+325 assem.clear();
+326 }
+327
+328 function endDocument() {
+329 assem.endDocument();
+330 }
+331
+332 return {
+333 append: append,
+334 toString: toString,
+335 clear: clear,
+336 endDocument: endDocument
+337 };
+338 };
+339 } else {
+340 exports.mergingOpAssembler = function () {
+341 // This assembler can be used in production; it efficiently
+342 // merges consecutive operations that are mergeable, ignores
+343 // no-ops, and drops final pure "keeps". It does not re-order
+344 // operations.
+345 var assem = exports.opAssembler();
+346 var bufOp = exports.newOp();
+347
+348 // If we get, for example, insertions [xxx\n,yyy], those don't merge,
+349 // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
+350 // This variable stores the length of yyy and any other newline-less
+351 // ops immediately after it.
+352 var bufOpAdditionalCharsAfterNewline = 0;
+353
+354 function flush(isEndDocument) {
+355 if (bufOp.opcode) {
+356 if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) {
+357 // final merged keep, leave it implicit
+358 } else {
+359 assem.append(bufOp);
+360 if (bufOpAdditionalCharsAfterNewline) {
+361 bufOp.chars = bufOpAdditionalCharsAfterNewline;
+362 bufOp.lines = 0;
+363 assem.append(bufOp);
+364 bufOpAdditionalCharsAfterNewline = 0;
+365 }
+366 }
+367 bufOp.opcode = '';
+368 }
+369 }
+370
+371 function append(op) {
+372 if (op.chars > 0) {
+373 if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) {
+374 if (op.lines > 0) {
+375 // bufOp and additional chars are all mergeable into a multi-line op
+376 bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars;
+377 bufOp.lines += op.lines;
+378 bufOpAdditionalCharsAfterNewline = 0;
+379 } else if (bufOp.lines == 0) {
+380 // both bufOp and op are in-line
+381 bufOp.chars += op.chars;
+382 } else {
+383 // append in-line text to multi-line bufOp
+384 bufOpAdditionalCharsAfterNewline += op.chars;
+385 }
+386 } else {
+387 flush();
+388 exports.copyOp(op, bufOp);
+389 }
+390 }
+391 }
+392
+393 function endDocument() {
+394 flush(true);
+395 }
+396
+397 function toString() {
+398 flush();
+399 return assem.toString();
+400 }
+401
+402 function clear() {
+403 assem.clear();
+404 exports.clearOp(bufOp);
+405 }
+406 return {
+407 append: append,
+408 toString: toString,
+409 clear: clear,
+410 endDocument: endDocument
+411 };
+412 };
+413 }
+414
+415 if (_opt) {
+416 exports.opAssembler = function () {
+417 var assem = _opt.opAssembler();
+418 // this function allows op to be mutated later (doesn't keep a ref)
+419
+420 function append(op) {
+421 assem.append(op.opcode, op.chars, op.lines, op.attribs);
+422 }
+423
+424 function toString() {
+425 return assem.toString();
+426 }
+427
+428 function clear() {
+429 assem.clear();
+430 }
+431 return {
+432 append: append,
+433 toString: toString,
+434 clear: clear
+435 };
+436 };
+437 } else {
+438 exports.opAssembler = function () {
+439 var pieces = [];
+440 // this function allows op to be mutated later (doesn't keep a ref)
+441
+442 function append(op) {
+443 pieces.push(op.attribs);
+444 if (op.lines) {
+445 pieces.push('|', exports.numToString(op.lines));
+446 }
+447 pieces.push(op.opcode);
+448 pieces.push(exports.numToString(op.chars));
+449 }
+450
+451 function toString() {
+452 return pieces.join('');
+453 }
+454
+455 function clear() {
+456 pieces.length = 0;
+457 }
+458 return {
+459 append: append,
+460 toString: toString,
+461 clear: clear
+462 };
+463 };
+464 }
+465
+466 exports.stringIterator = function (str) {
+467 var curIndex = 0;
+468
+469 function assertRemaining(n) {
+470 exports.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")");
+471 }
+472
+473 function take(n) {
+474 assertRemaining(n);
+475 var s = str.substr(curIndex, n);
+476 curIndex += n;
+477 return s;
+478 }
+479
+480 function peek(n) {
+481 assertRemaining(n);
+482 var s = str.substr(curIndex, n);
+483 return s;
+484 }
+485
+486 function skip(n) {
+487 assertRemaining(n);
+488 curIndex += n;
+489 }
+490
+491 function remaining() {
+492 return str.length - curIndex;
+493 }
+494 return {
+495 take: take,
+496 skip: skip,
+497 remaining: remaining,
+498 peek: peek
+499 };
+500 };
+501
+502 exports.stringAssembler = function () {
+503 var pieces = [];
+504
+505 function append(x) {
+506 pieces.push(String(x));
+507 }
+508
+509 function toString() {
+510 return pieces.join('');
+511 }
+512 return {
+513 append: append,
+514 toString: toString
+515 };
+516 };
+517
+518 // "lines" need not be an array as long as it supports certain calls (lines_foo inside).
+519 exports.textLinesMutator = function (lines) {
+520 // Mutates lines, an array of strings, in place.
+521 // Mutation operations have the same constraints as exports operations
+522 // with respect to newlines, but not the other additional constraints
+523 // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline).
+524 // Can be used to mutate lists of strings where the last char of each string
+525 // is not actually a newline, but for the purposes of N and L values,
+526 // the caller should pretend it is, and for things to work right in that case, the input
+527 // to insert() should be a single line with no newlines.
+528 var curSplice = [0, 0];
+529 var inSplice = false;
+530 // position in document after curSplice is applied:
+531 var curLine = 0,
+532 curCol = 0;
+533 // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
+534 // curLine >= curSplice[0]
+535 // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
+536 // curCol == 0
+537
+538 function lines_applySplice(s) {
+539 lines.splice.apply(lines, s);
+540 }
+541
+542 function lines_toSource() {
+543 return lines.toSource();
+544 }
+545
+546 function lines_get(idx) {
+547 if (lines.get) {
+548 return lines.get(idx);
+549 } else {
+550 return lines[idx];
+551 }
+552 }
+553 // can be unimplemented if removeLines's return value not needed
+554
+555 function lines_slice(start, end) {
+556 if (lines.slice) {
+557 return lines.slice(start, end);
+558 } else {
+559 return [];
+560 }
+561 }
+562
+563 function lines_length() {
+564 if ((typeof lines.length) == "number") {
+565 return lines.length;
+566 } else {
+567 return lines.length();
+568 }
+569 }
+570
+571 function enterSplice() {
+572 curSplice[0] = curLine;
+573 curSplice[1] = 0;
+574 if (curCol > 0) {
+575 putCurLineInSplice();
+576 }
+577 inSplice = true;
+578 }
+579
+580 function leaveSplice() {
+581 lines_applySplice(curSplice);
+582 curSplice.length = 2;
+583 curSplice[0] = curSplice[1] = 0;
+584 inSplice = false;
+585 }
+586
+587 function isCurLineInSplice() {
+588 return (curLine - curSplice[0] < (curSplice.length - 2));
+589 }
+590
+591 function debugPrint(typ) {
+592 print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource());
+593 }
+594
+595 function putCurLineInSplice() {
+596 if (!isCurLineInSplice()) {
+597 curSplice.push(lines_get(curSplice[0] + curSplice[1]));
+598 curSplice[1]++;
+599 }
+600 return 2 + curLine - curSplice[0];
+601 }
+602
+603 function skipLines(L, includeInSplice) {
+604 if (L) {
+605 if (includeInSplice) {
+606 if (!inSplice) {
+607 enterSplice();
+608 }
+609 for (var i = 0; i < L; i++) {
+610 curCol = 0;
+611 putCurLineInSplice();
+612 curLine++;
+613 }
+614 } else {
+615 if (inSplice) {
+616 if (L > 1) {
+617 leaveSplice();
+618 } else {
+619 putCurLineInSplice();
+620 }
+621 }
+622 curLine += L;
+623 curCol = 0;
+624 }
+625 //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length);
+626 /*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) {
+627 print("BLAH");
+628 putCurLineInSplice();
+629 }*/
+630 // tests case foo in remove(), which isn't otherwise covered in current impl
+631 }
+632 //debugPrint("skip");
+633 }
+634
+635 function skip(N, L, includeInSplice) {
+636 if (N) {
+637 if (L) {
+638 skipLines(L, includeInSplice);
+639 } else {
+640 if (includeInSplice && !inSplice) {
+641 enterSplice();
+642 }
+643 if (inSplice) {
+644 putCurLineInSplice();
+645 }
+646 curCol += N;
+647 //debugPrint("skip");
+648 }
+649 }
+650 }
+651
+652 function removeLines(L) {
+653 var removed = '';
+654 if (L) {
+655 if (!inSplice) {
+656 enterSplice();
+657 }
+658
+659 function nextKLinesText(k) {
+660 var m = curSplice[0] + curSplice[1];
+661 return lines_slice(m, m + k).join('');
+662 }
+663 if (isCurLineInSplice()) {
+664 //print(curCol);
+665 if (curCol == 0) {
+666 removed = curSplice[curSplice.length - 1];
+667 // print("FOO"); // case foo
+668 curSplice.length--;
+669 removed += nextKLinesText(L - 1);
+670 curSplice[1] += L - 1;
+671 } else {
+672 removed = nextKLinesText(L - 1);
+673 curSplice[1] += L - 1;
+674 var sline = curSplice.length - 1;
+675 removed = curSplice[sline].substring(curCol) + removed;
+676 curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]);
+677 curSplice[1] += 1;
+678 }
+679 } else {
+680 removed = nextKLinesText(L);
+681 curSplice[1] += L;
+682 }
+683 //debugPrint("remove");
+684 }
+685 return removed;
+686 }
+687
+688 function remove(N, L) {
+689 var removed = '';
+690 if (N) {
+691 if (L) {
+692 return removeLines(L);
+693 } else {
+694 if (!inSplice) {
+695 enterSplice();
+696 }
+697 var sline = putCurLineInSplice();
+698 removed = curSplice[sline].substring(curCol, curCol + N);
+699 curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N);
+700 //debugPrint("remove");
+701 }
+702 }
+703 return removed;
+704 }
+705
+706 function insert(text, L) {
+707 if (text) {
+708 if (!inSplice) {
+709 enterSplice();
+710 }
+711 if (L) {
+712 var newLines = exports.splitTextLines(text);
+713 if (isCurLineInSplice()) {
+714 //if (curCol == 0) {
+715 //curSplice.length--;
+716 //curSplice[1]--;
+717 //Array.prototype.push.apply(curSplice, newLines);
+718 //curLine += newLines.length;
+719 //}
+720 //else {
+721 var sline = curSplice.length - 1;
+722 var theLine = curSplice[sline];
+723 var lineCol = curCol;
+724 curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
+725 curLine++;
+726 newLines.splice(0, 1);
+727 Array.prototype.push.apply(curSplice, newLines);
+728 curLine += newLines.length;
+729 curSplice.push(theLine.substring(lineCol));
+730 curCol = 0;
+731 //}
+732 } else {
+733 Array.prototype.push.apply(curSplice, newLines);
+734 curLine += newLines.length;
+735 }
+736 } else {
+737 var sline = putCurLineInSplice();
+738 curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol);
+739 curCol += text.length;
+740 }
+741 //debugPrint("insert");
+742 }
+743 }
+744
+745 function hasMore() {
+746 //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]);
+747 var docLines = lines_length();
+748 if (inSplice) {
+749 docLines += curSplice.length - 2 - curSplice[1];
+750 }
+751 return curLine < docLines;
+752 }
+753
+754 function close() {
+755 if (inSplice) {
+756 leaveSplice();
+757 }
+758 //debugPrint("close");
+759 }
+760
+761 var self = {
+762 skip: skip,
+763 remove: remove,
+764 insert: insert,
+765 close: close,
+766 hasMore: hasMore,
+767 removeLines: removeLines,
+768 skipLines: skipLines
+769 };
+770 return self;
+771 };
+772
+773 exports.applyZip = function (in1, idx1, in2, idx2, func) {
+774 var iter1 = exports.opIterator(in1, idx1);
+775 var iter2 = exports.opIterator(in2, idx2);
+776 var assem = exports.smartOpAssembler();
+777 var op1 = exports.newOp();
+778 var op2 = exports.newOp();
+779 var opOut = exports.newOp();
+780 while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) {
+781 if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1);
+782 if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2);
+783 func(op1, op2, opOut);
+784 if (opOut.opcode) {
+785 //print(opOut.toSource());
+786 assem.append(opOut);
+787 opOut.opcode = '';
+788 }
+789 }
+790 assem.endDocument();
+791 return assem.toString();
+792 };
+793
+794 exports.unpack = function (cs) {
+795 var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
+796 var headerMatch = headerRegex.exec(cs);
+797 if ((!headerMatch) || (!headerMatch[0])) {
+798 exports.error("Not a exports: " + cs);
+799 }
+800 var oldLen = exports.parseNum(headerMatch[1]);
+801 var changeSign = (headerMatch[2] == '>') ? 1 : -1;
+802 var changeMag = exports.parseNum(headerMatch[3]);
+803 var newLen = oldLen + changeSign * changeMag;
+804 var opsStart = headerMatch[0].length;
+805 var opsEnd = cs.indexOf("$");
+806 if (opsEnd < 0) opsEnd = cs.length;
+807 return {
+808 oldLen: oldLen,
+809 newLen: newLen,
+810 ops: cs.substring(opsStart, opsEnd),
+811 charBank: cs.substring(opsEnd + 1)
+812 };
+813 };
+814
+815 exports.pack = function (oldLen, newLen, opsStr, bank) {
+816 var lenDiff = newLen - oldLen;
+817 var lenDiffStr = (lenDiff >= 0 ? '>' + exports.numToString(lenDiff) : '<' + exports.numToString(-lenDiff));
+818 var a = [];
+819 a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank);
+820 return a.join('');
+821 };
+822
+823 exports.applyToText = function (cs, str) {
+824 var unpacked = exports.unpack(cs);
+825 exports.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen);
+826 var csIter = exports.opIterator(unpacked.ops);
+827 var bankIter = exports.stringIterator(unpacked.charBank);
+828 var strIter = exports.stringIterator(str);
+829 var assem = exports.stringAssembler();
+830 while (csIter.hasNext()) {
+831 var op = csIter.next();
+832 switch (op.opcode) {
+833 case '+':
+834 assem.append(bankIter.take(op.chars));
+835 break;
+836 case '-':
+837 strIter.skip(op.chars);
+838 break;
+839 case '=':
+840 assem.append(strIter.take(op.chars));
+841 break;
+842 }
+843 }
+844 assem.append(strIter.take(strIter.remaining()));
+845 return assem.toString();
+846 };
+847
+848 exports.mutateTextLines = function (cs, lines) {
+849 var unpacked = exports.unpack(cs);
+850 var csIter = exports.opIterator(unpacked.ops);
+851 var bankIter = exports.stringIterator(unpacked.charBank);
+852 var mut = exports.textLinesMutator(lines);
+853 while (csIter.hasNext()) {
+854 var op = csIter.next();
+855 switch (op.opcode) {
+856 case '+':
+857 mut.insert(bankIter.take(op.chars), op.lines);
+858 break;
+859 case '-':
+860 mut.remove(op.chars, op.lines);
+861 break;
+862 case '=':
+863 mut.skip(op.chars, op.lines, ( !! op.attribs));
+864 break;
+865 }
+866 }
+867 mut.close();
+868 };
+869
+870 exports.composeAttributes = function (att1, att2, resultIsMutation, pool) {
+871 // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean.
+872 // Sometimes attribute (key,value) pairs are treated as attribute presence
+873 // information, while other times they are treated as operations that
+874 // mutate a set of attributes, and this affects whether an empty value
+875 // is a deletion or a change.
+876 // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result
+877 // ([], [(bold, )], true) -> [(bold, )]
+878 // ([], [(bold, )], false) -> []
+879 // ([], [(bold, true)], true) -> [(bold, true)]
+880 // ([], [(bold, true)], false) -> [(bold, true)]
+881 // ([(bold, true)], [(bold, )], true) -> [(bold, )]
+882 // ([(bold, true)], [(bold, )], false) -> []
+883 // pool can be null if att2 has no attributes.
+884 if ((!att1) && resultIsMutation) {
+885 // In the case of a mutation (i.e. composing two exportss),
+886 // an att2 composed with an empy att1 is just att2. If att1
+887 // is part of an attribution string, then att2 may remove
+888 // attributes that are already gone, so don't do this optimization.
+889 return att2;
+890 }
+891 if (!att2) return att1;
+892 var atts = [];
+893 att1.replace(/\*([0-9a-z]+)/g, function (_, a) {
+894 atts.push(pool.getAttrib(exports.parseNum(a)));
+895 return '';
+896 });
+897 att2.replace(/\*([0-9a-z]+)/g, function (_, a) {
+898 var pair = pool.getAttrib(exports.parseNum(a));
+899 var found = false;
+900 for (var i = 0; i < atts.length; i++) {
+901 var oldPair = atts[i];
+902 if (oldPair[0] == pair[0]) {
+903 if (pair[1] || resultIsMutation) {
+904 oldPair[1] = pair[1];
+905 } else {
+906 atts.splice(i, 1);
+907 }
+908 found = true;
+909 break;
+910 }
+911 }
+912 if ((!found) && (pair[1] || resultIsMutation)) {
+913 atts.push(pair);
+914 }
+915 return '';
+916 });
+917 atts.sort();
+918 var buf = exports.stringAssembler();
+919 for (var i = 0; i < atts.length; i++) {
+920 buf.append('*');
+921 buf.append(exports.numToString(pool.putAttrib(atts[i])));
+922 }
+923 //print(att1+" / "+att2+" / "+buf.toString());
+924 return buf.toString();
+925 };
+926
+927 exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) {
+928 // attOp is the op from the sequence that is being operated on, either an
+929 // attribution string or the earlier of two exportss being composed.
+930 // pool can be null if definitely not needed.
+931 //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
+932 if (attOp.opcode == '-') {
+933 exports.copyOp(attOp, opOut);
+934 attOp.opcode = '';
+935 } else if (!attOp.opcode) {
+936 exports.copyOp(csOp, opOut);
+937 csOp.opcode = '';
+938 } else {
+939 switch (csOp.opcode) {
+940 case '-':
+941 {
+942 if (csOp.chars <= attOp.chars) {
+943 // delete or delete part
+944 if (attOp.opcode == '=') {
+945 opOut.opcode = '-';
+946 opOut.chars = csOp.chars;
+947 opOut.lines = csOp.lines;
+948 opOut.attribs = '';
+949 }
+950 attOp.chars -= csOp.chars;
+951 attOp.lines -= csOp.lines;
+952 csOp.opcode = '';
+953 if (!attOp.chars) {
+954 attOp.opcode = '';
+955 }
+956 } else {
+957 // delete and keep going
+958 if (attOp.opcode == '=') {
+959 opOut.opcode = '-';
+960 opOut.chars = attOp.chars;
+961 opOut.lines = attOp.lines;
+962 opOut.attribs = '';
+963 }
+964 csOp.chars -= attOp.chars;
+965 csOp.lines -= attOp.lines;
+966 attOp.opcode = '';
+967 }
+968 break;
+969 }
+970 case '+':
+971 {
+972 // insert
+973 exports.copyOp(csOp, opOut);
+974 csOp.opcode = '';
+975 break;
+976 }
+977 case '=':
+978 {
+979 if (csOp.chars <= attOp.chars) {
+980 // keep or keep part
+981 opOut.opcode = attOp.opcode;
+982 opOut.chars = csOp.chars;
+983 opOut.lines = csOp.lines;
+984 opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
+985 csOp.opcode = '';
+986 attOp.chars -= csOp.chars;
+987 attOp.lines -= csOp.lines;
+988 if (!attOp.chars) {
+989 attOp.opcode = '';
+990 }
+991 } else {
+992 // keep and keep going
+993 opOut.opcode = attOp.opcode;
+994 opOut.chars = attOp.chars;
+995 opOut.lines = attOp.lines;
+996 opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
+997 attOp.opcode = '';
+998 csOp.chars -= attOp.chars;
+999 csOp.lines -= attOp.lines;
+1000 }
+1001 break;
+1002 }
+1003 case '':
+1004 {
+1005 exports.copyOp(attOp, opOut);
+1006 attOp.opcode = '';
+1007 break;
+1008 }
+1009 }
+1010 }
+1011 };
+1012
+1013 exports.applyToAttribution = function (cs, astr, pool) {
+1014 var unpacked = exports.unpack(cs);
+1015
+1016 return exports.applyZip(astr, 0, unpacked.ops, 0, function (op1, op2, opOut) {
+1017 return exports._slicerZipperFunc(op1, op2, opOut, pool);
+1018 });
+1019 };
+1020
+1021 /*exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) {
+1022 var iter = exports.opIterator(opsStr, optStartIndex);
+1023 var bankIndex = 0;
+1024
+1025 };*/
+1026
+1027 exports.mutateAttributionLines = function (cs, lines, pool) {
+1028 //dmesg(cs);
+1029 //dmesg(lines.toSource()+" ->");
+1030 var unpacked = exports.unpack(cs);
+1031 var csIter = exports.opIterator(unpacked.ops);
+1032 var csBank = unpacked.charBank;
+1033 var csBankIndex = 0;
+1034 // treat the attribution lines as text lines, mutating a line at a time
+1035 var mut = exports.textLinesMutator(lines);
+1036
+1037 var lineIter = null;
+1038
+1039 function isNextMutOp() {
+1040 return (lineIter && lineIter.hasNext()) || mut.hasMore();
+1041 }
+1042
+1043 function nextMutOp(destOp) {
+1044 if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) {
+1045 var line = mut.removeLines(1);
+1046 lineIter = exports.opIterator(line);
+1047 }
+1048 if (lineIter && lineIter.hasNext()) {
+1049 lineIter.next(destOp);
+1050 } else {
+1051 destOp.opcode = '';
+1052 }
+1053 }
+1054 var lineAssem = null;
+1055
+1056 function outputMutOp(op) {
+1057 //print("outputMutOp: "+op.toSource());
+1058 if (!lineAssem) {
+1059 lineAssem = exports.mergingOpAssembler();
+1060 }
+1061 lineAssem.append(op);
+1062 if (op.lines > 0) {
+1063 exports.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines");
+1064 // ship it to the mut
+1065 mut.insert(lineAssem.toString(), 1);
+1066 lineAssem = null;
+1067 }
+1068 }
+1069
+1070 var csOp = exports.newOp();
+1071 var attOp = exports.newOp();
+1072 var opOut = exports.newOp();
+1073 while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) {
+1074 if ((!csOp.opcode) && csIter.hasNext()) {
+1075 csIter.next(csOp);
+1076 }
+1077 //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
+1078 //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null));
+1079 //print("csOp: "+csOp.toSource());
+1080 if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) {
+1081 break; // done
+1082 } else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) {
+1083 // skip multiple lines; this is what makes small changes not order of the document size
+1084 mut.skipLines(csOp.lines);
+1085 //print("skipped: "+csOp.lines);
+1086 csOp.opcode = '';
+1087 } else if (csOp.opcode == '+') {
+1088 if (csOp.lines > 1) {
+1089 var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex;
+1090 exports.copyOp(csOp, opOut);
+1091 csOp.chars -= firstLineLen;
+1092 csOp.lines--;
+1093 opOut.lines = 1;
+1094 opOut.chars = firstLineLen;
+1095 } else {
+1096 exports.copyOp(csOp, opOut);
+1097 csOp.opcode = '';
+1098 }
+1099 outputMutOp(opOut);
+1100 csBankIndex += opOut.chars;
+1101 opOut.opcode = '';
+1102 } else {
+1103 if ((!attOp.opcode) && isNextMutOp()) {
+1104 nextMutOp(attOp);
+1105 }
+1106 //print("attOp: "+attOp.toSource());
+1107 exports._slicerZipperFunc(attOp, csOp, opOut, pool);
+1108 if (opOut.opcode) {
+1109 outputMutOp(opOut);
+1110 opOut.opcode = '';
+1111 }
+1112 }
+1113 }
+1114
+1115 exports.assert(!lineAssem, "line assembler not finished");
+1116 mut.close();
+1117
+1118 //dmesg("-> "+lines.toSource());
+1119 };
+1120
+1121 exports.joinAttributionLines = function (theAlines) {
+1122 var assem = exports.mergingOpAssembler();
+1123 for (var i = 0; i < theAlines.length; i++) {
+1124 var aline = theAlines[i];
+1125 var iter = exports.opIterator(aline);
+1126 while (iter.hasNext()) {
+1127 assem.append(iter.next());
+1128 }
+1129 }
+1130 return assem.toString();
+1131 };
+1132
+1133 exports.splitAttributionLines = function (attrOps, text) {
+1134 var iter = exports.opIterator(attrOps);
+1135 var assem = exports.mergingOpAssembler();
+1136 var lines = [];
+1137 var pos = 0;
+1138
+1139 function appendOp(op) {
+1140 assem.append(op);
+1141 if (op.lines > 0) {
+1142 lines.push(assem.toString());
+1143 assem.clear();
+1144 }
+1145 pos += op.chars;
+1146 }
+1147
+1148 while (iter.hasNext()) {
+1149 var op = iter.next();
+1150 var numChars = op.chars;
+1151 var numLines = op.lines;
+1152 while (numLines > 1) {
+1153 var newlineEnd = text.indexOf('\n', pos) + 1;
+1154 exports.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines");
+1155 op.chars = newlineEnd - pos;
+1156 op.lines = 1;
+1157 appendOp(op);
+1158 numChars -= op.chars;
+1159 numLines -= op.lines;
+1160 }
+1161 if (numLines == 1) {
+1162 op.chars = numChars;
+1163 op.lines = 1;
+1164 }
+1165 appendOp(op);
+1166 }
+1167
+1168 return lines;
+1169 };
+1170
+1171 exports.splitTextLines = function (text) {
+1172 return text.match(/[^\n]*(?:\n|[^\n]$)/g);
+1173 };
+1174
+1175 exports.compose = function (cs1, cs2, pool) {
+1176 var unpacked1 = exports.unpack(cs1);
+1177 var unpacked2 = exports.unpack(cs2);
+1178 var len1 = unpacked1.oldLen;
+1179 var len2 = unpacked1.newLen;
+1180 exports.assert(len2 == unpacked2.oldLen, "mismatched composition");
+1181 var len3 = unpacked2.newLen;
+1182 var bankIter1 = exports.stringIterator(unpacked1.charBank);
+1183 var bankIter2 = exports.stringIterator(unpacked2.charBank);
+1184 var bankAssem = exports.stringAssembler();
+1185
+1186 var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) {
+1187 //var debugBuilder = exports.stringAssembler();
+1188 //debugBuilder.append(exports.opString(op1));
+1189 //debugBuilder.append(',');
+1190 //debugBuilder.append(exports.opString(op2));
+1191 //debugBuilder.append(' / ');
+1192 var op1code = op1.opcode;
+1193 var op2code = op2.opcode;
+1194 if (op1code == '+' && op2code == '-') {
+1195 bankIter1.skip(Math.min(op1.chars, op2.chars));
+1196 }
+1197 exports._slicerZipperFunc(op1, op2, opOut, pool);
+1198 if (opOut.opcode == '+') {
+1199 if (op2code == '+') {
+1200 bankAssem.append(bankIter2.take(opOut.chars));
+1201 } else {
+1202 bankAssem.append(bankIter1.take(opOut.chars));
+1203 }
+1204 }
+1205
+1206 //debugBuilder.append(exports.opString(op1));
+1207 //debugBuilder.append(',');
+1208 //debugBuilder.append(exports.opString(op2));
+1209 //debugBuilder.append(' -> ');
+1210 //debugBuilder.append(exports.opString(opOut));
+1211 //print(debugBuilder.toString());
+1212 });
+1213
+1214 return exports.pack(len1, len3, newOps, bankAssem.toString());
+1215 };
+1216
+1217 exports.attributeTester = function (attribPair, pool) {
+1218 // returns a function that tests if a string of attributes
+1219 // (e.g. *3*4) contains a given attribute key,value that
+1220 // is already present in the pool.
+1221 if (!pool) {
+1222 return never;
+1223 }
+1224 var attribNum = pool.putAttrib(attribPair, true);
+1225 if (attribNum < 0) {
+1226 return never;
+1227 } else {
+1228 var re = new RegExp('\\*' + exports.numToString(attribNum) + '(?!\\w)');
+1229 return function (attribs) {
+1230 return re.test(attribs);
+1231 };
+1232 }
+1233
+1234 function never(attribs) {
+1235 return false;
+1236 }
+1237 };
+1238
+1239 exports.identity = function (N) {
+1240 return exports.pack(N, N, "", "");
+1241 };
+1242
+1243 exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) {
+1244 var oldLen = oldFullText.length;
+1245
+1246 if (spliceStart >= oldLen) {
+1247 spliceStart = oldLen - 1;
+1248 }
+1249 if (numRemoved > oldFullText.length - spliceStart - 1) {
+1250 numRemoved = oldFullText.length - spliceStart - 1;
+1251 }
+1252 var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved);
+1253 var newLen = oldLen + newText.length - oldText.length;
+1254
+1255 var assem = exports.smartOpAssembler();
+1256 assem.appendOpWithText('=', oldFullText.substring(0, spliceStart));
+1257 assem.appendOpWithText('-', oldText);
+1258 assem.appendOpWithText('+', newText, optNewTextAPairs, pool);
+1259 assem.endDocument();
+1260 return exports.pack(oldLen, newLen, assem.toString(), newText);
+1261 };
+1262
+1263 exports.toSplices = function (cs) {
+1264 // get a list of splices, [startChar, endChar, newText]
+1265 var unpacked = exports.unpack(cs);
+1266 var splices = [];
+1267
+1268 var oldPos = 0;
+1269 var iter = exports.opIterator(unpacked.ops);
+1270 var charIter = exports.stringIterator(unpacked.charBank);
+1271 var inSplice = false;
+1272 while (iter.hasNext()) {
+1273 var op = iter.next();
+1274 if (op.opcode == '=') {
+1275 oldPos += op.chars;
+1276 inSplice = false;
+1277 } else {
+1278 if (!inSplice) {
+1279 splices.push([oldPos, oldPos, ""]);
+1280 inSplice = true;
+1281 }
+1282 if (op.opcode == '-') {
+1283 oldPos += op.chars;
+1284 splices[splices.length - 1][1] += op.chars;
+1285 } else if (op.opcode == '+') {
+1286 splices[splices.length - 1][2] += charIter.take(op.chars);
+1287 }
+1288 }
+1289 }
+1290
+1291 return splices;
+1292 };
+1293
+1294 exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) {
+1295 var newStartChar = startChar;
+1296 var newEndChar = endChar;
+1297 var splices = exports.toSplices(cs);
+1298 var lengthChangeSoFar = 0;
+1299 for (var i = 0; i < splices.length; i++) {
+1300 var splice = splices[i];
+1301 var spliceStart = splice[0] + lengthChangeSoFar;
+1302 var spliceEnd = splice[1] + lengthChangeSoFar;
+1303 var newTextLength = splice[2].length;
+1304 var thisLengthChange = newTextLength - (spliceEnd - spliceStart);
+1305
+1306 if (spliceStart <= newStartChar && spliceEnd >= newEndChar) {
+1307 // splice fully replaces/deletes range
+1308 // (also case that handles insertion at a collapsed selection)
+1309 if (insertionsAfter) {
+1310 newStartChar = newEndChar = spliceStart;
+1311 } else {
+1312 newStartChar = newEndChar = spliceStart + newTextLength;
+1313 }
+1314 } else if (spliceEnd <= newStartChar) {
+1315 // splice is before range
+1316 newStartChar += thisLengthChange;
+1317 newEndChar += thisLengthChange;
+1318 } else if (spliceStart >= newEndChar) {
+1319 // splice is after range
+1320 } else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) {
+1321 // splice is inside range
+1322 newEndChar += thisLengthChange;
+1323 } else if (spliceEnd < newEndChar) {
+1324 // splice overlaps beginning of range
+1325 newStartChar = spliceStart + newTextLength;
+1326 newEndChar += thisLengthChange;
+1327 } else {
+1328 // splice overlaps end of range
+1329 newEndChar = spliceStart;
+1330 }
+1331
+1332 lengthChangeSoFar += thisLengthChange;
+1333 }
+1334
+1335 return [newStartChar, newEndChar];
+1336 };
+1337
+1338 exports.moveOpsToNewPool = function (cs, oldPool, newPool) {
+1339 // works on exports or attribution string
+1340 var dollarPos = cs.indexOf('$');
+1341 if (dollarPos < 0) {
+1342 dollarPos = cs.length;
+1343 }
+1344 var upToDollar = cs.substring(0, dollarPos);
+1345 var fromDollar = cs.substring(dollarPos);
+1346 // order of attribs stays the same
+1347 return upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) {
+1348 var oldNum = exports.parseNum(a);
+1349 var pair = oldPool.getAttrib(oldNum);
+1350 var newNum = newPool.putAttrib(pair);
+1351 return '*' + exports.numToString(newNum);
+1352 }) + fromDollar;
+1353 };
+1354
+1355 exports.makeAttribution = function (text) {
+1356 var assem = exports.smartOpAssembler();
+1357 assem.appendOpWithText('+', text);
+1358 return assem.toString();
+1359 };
+1360
+1361 // callable on a exports, attribution string, or attribs property of an op
+1362 exports.eachAttribNumber = function (cs, func) {
+1363 var dollarPos = cs.indexOf('$');
+1364 if (dollarPos < 0) {
+1365 dollarPos = cs.length;
+1366 }
+1367 var upToDollar = cs.substring(0, dollarPos);
+1368
+1369 upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) {
+1370 func(exports.parseNum(a));
+1371 return '';
+1372 });
+1373 };
+1374
+1375 // callable on a exports, attribution string, or attribs property of an op,
+1376 // though it may easily create adjacent ops that can be merged.
+1377 exports.filterAttribNumbers = function (cs, filter) {
+1378 return exports.mapAttribNumbers(cs, filter);
+1379 };
+1380
+1381 exports.mapAttribNumbers = function (cs, func) {
+1382 var dollarPos = cs.indexOf('$');
+1383 if (dollarPos < 0) {
+1384 dollarPos = cs.length;
+1385 }
+1386 var upToDollar = cs.substring(0, dollarPos);
+1387
+1388 var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function (s, a) {
+1389 var n = func(exports.parseNum(a));
+1390 if (n === true) {
+1391 return s;
+1392 } else if ((typeof n) === "number") {
+1393 return '*' + exports.numToString(n);
+1394 } else {
+1395 return '';
+1396 }
+1397 });
+1398
+1399 return newUpToDollar + cs.substring(dollarPos);
+1400 };
+1401
+1402 exports.makeAText = function (text, attribs) {
+1403 return {
+1404 text: text,
+1405 attribs: (attribs || exports.makeAttribution(text))
+1406 };
+1407 };
+1408
+1409 exports.applyToAText = function (cs, atext, pool) {
+1410 return {
+1411 text: exports.applyToText(cs, atext.text),
+1412 attribs: exports.applyToAttribution(cs, atext.attribs, pool)
+1413 };
+1414 };
+1415
+1416 exports.cloneAText = function (atext) {
+1417 return {
+1418 text: atext.text,
+1419 attribs: atext.attribs
+1420 };
+1421 };
+1422
+1423 exports.copyAText = function (atext1, atext2) {
+1424 atext2.text = atext1.text;
+1425 atext2.attribs = atext1.attribs;
+1426 };
+1427
+1428 exports.appendATextToAssembler = function (atext, assem) {
+1429 // intentionally skips last newline char of atext
+1430 var iter = exports.opIterator(atext.attribs);
+1431 var op = exports.newOp();
+1432 while (iter.hasNext()) {
+1433 iter.next(op);
+1434 if (!iter.hasNext()) {
+1435 // last op, exclude final newline
+1436 if (op.lines <= 1) {
+1437 op.lines = 0;
+1438 op.chars--;
+1439 if (op.chars) {
+1440 assem.append(op);
+1441 }
+1442 } else {
+1443 var nextToLastNewlineEnd =
+1444 atext.text.lastIndexOf('\n', atext.text.length - 2) + 1;
+1445 var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1;
+1446 op.lines--;
+1447 op.chars -= (lastLineLength + 1);
+1448 assem.append(op);
+1449 op.lines = 0;
+1450 op.chars = lastLineLength;
+1451 if (op.chars) {
+1452 assem.append(op);
+1453 }
+1454 }
+1455 } else {
+1456 assem.append(op);
+1457 }
+1458 }
+1459 };
+1460
+1461 exports.prepareForWire = function (cs, pool) {
+1462 var newPool = AttributePoolFactory.createAttributePool();;
+1463 var newCs = exports.moveOpsToNewPool(cs, pool, newPool);
+1464 return {
+1465 translated: newCs,
+1466 pool: newPool
+1467 };
+1468 };
+1469
+1470 exports.isIdentity = function (cs) {
+1471 var unpacked = exports.unpack(cs);
+1472 return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen;
+1473 };
+1474
+1475 exports.opAttributeValue = function (op, key, pool) {
+1476 return exports.attribsAttributeValue(op.attribs, key, pool);
+1477 };
+1478
+1479 exports.attribsAttributeValue = function (attribs, key, pool) {
+1480 var value = '';
+1481 if (attribs) {
+1482 exports.eachAttribNumber(attribs, function (n) {
+1483 if (pool.getAttribKey(n) == key) {
+1484 value = pool.getAttribValue(n);
+1485 }
+1486 });
+1487 }
+1488 return value;
+1489 };
+1490
+1491 exports.builder = function (oldLen) {
+1492 var assem = exports.smartOpAssembler();
+1493 var o = exports.newOp();
+1494 var charBank = exports.stringAssembler();
+1495
+1496 var self = {
+1497 // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case)
+1498 keep: function (N, L, attribs, pool) {
+1499 o.opcode = '=';
+1500 o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || '';
+1501 o.chars = N;
+1502 o.lines = (L || 0);
+1503 assem.append(o);
+1504 return self;
+1505 },
+1506 keepText: function (text, attribs, pool) {
+1507 assem.appendOpWithText('=', text, attribs, pool);
+1508 return self;
+1509 },
+1510 insert: function (text, attribs, pool) {
+1511 assem.appendOpWithText('+', text, attribs, pool);
+1512 charBank.append(text);
+1513 return self;
+1514 },
+1515 remove: function (N, L) {
+1516 o.opcode = '-';
+1517 o.attribs = '';
+1518 o.chars = N;
+1519 o.lines = (L || 0);
+1520 assem.append(o);
+1521 return self;
+1522 },
+1523 toString: function () {
+1524 assem.endDocument();
+1525 var newLen = oldLen + assem.getLengthChange();
+1526 return exports.pack(oldLen, newLen, assem.toString(), charBank.toString());
+1527 }
+1528 };
+1529
+1530 return self;
+1531 };
+1532
+1533 exports.makeAttribsString = function (opcode, attribs, pool) {
+1534 // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work
+1535 if (!attribs) {
+1536 return '';
+1537 } else if ((typeof attribs) == "string") {
+1538 return attribs;
+1539 } else if (pool && attribs && attribs.length) {
+1540 if (attribs.length > 1) {
+1541 attribs = attribs.slice();
+1542 attribs.sort();
+1543 }
+1544 var result = [];
+1545 for (var i = 0; i < attribs.length; i++) {
+1546 var pair = attribs[i];
+1547 if (opcode == '=' || (opcode == '+' && pair[1])) {
+1548 result.push('*' + exports.numToString(pool.putAttrib(pair)));
+1549 }
+1550 }
+1551 return result.join('');
+1552 }
+1553 };
+1554
+1555 // like "substring" but on a single-line attribution string
+1556 exports.subattribution = function (astr, start, optEnd) {
+1557 var iter = exports.opIterator(astr, 0);
+1558 var assem = exports.smartOpAssembler();
+1559 var attOp = exports.newOp();
+1560 var csOp = exports.newOp();
+1561 var opOut = exports.newOp();
+1562
+1563 function doCsOp() {
+1564 if (csOp.chars) {
+1565 while (csOp.opcode && (attOp.opcode || iter.hasNext())) {
+1566 if (!attOp.opcode) iter.next(attOp);
+1567
+1568 if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) {
+1569 csOp.lines++;
+1570 }
+1571
+1572 exports._slicerZipperFunc(attOp, csOp, opOut, null);
+1573 if (opOut.opcode) {
+1574 assem.append(opOut);
+1575 opOut.opcode = '';
+1576 }
+1577 }
+1578 }
+1579 }
+1580
+1581 csOp.opcode = '-';
+1582 csOp.chars = start;
+1583
+1584 doCsOp();
+1585
+1586 if (optEnd === undefined) {
+1587 if (attOp.opcode) {
+1588 assem.append(attOp);
+1589 }
+1590 while (iter.hasNext()) {
+1591 iter.next(attOp);
+1592 assem.append(attOp);
+1593 }
+1594 } else {
+1595 csOp.opcode = '=';
+1596 csOp.chars = optEnd - start;
+1597 doCsOp();
+1598 }
+1599
+1600 return assem.toString();
+1601 };
+1602
+1603 exports.inverse = function (cs, lines, alines, pool) {
+1604 // lines and alines are what the exports is meant to apply to.
+1605 // They may be arrays or objects with .get(i) and .length methods.
+1606 // They include final newlines on lines.
+1607
+1608 function lines_get(idx) {
+1609 if (lines.get) {
+1610 return lines.get(idx);
+1611 } else {
+1612 return lines[idx];
+1613 }
+1614 }
+1615
+1616 function lines_length() {
+1617 if ((typeof lines.length) == "number") {
+1618 return lines.length;
+1619 } else {
+1620 return lines.length();
+1621 }
+1622 }
+1623
+1624 function alines_get(idx) {
+1625 if (alines.get) {
+1626 return alines.get(idx);
+1627 } else {
+1628 return alines[idx];
+1629 }
+1630 }
+1631
+1632 function alines_length() {
+1633 if ((typeof alines.length) == "number") {
+1634 return alines.length;
+1635 } else {
+1636 return alines.length();
+1637 }
+1638 }
+1639
+1640 var curLine = 0;
+1641 var curChar = 0;
+1642 var curLineOpIter = null;
+1643 var curLineOpIterLine;
+1644 var curLineNextOp = exports.newOp('+');
+1645
+1646 var unpacked = exports.unpack(cs);
+1647 var csIter = exports.opIterator(unpacked.ops);
+1648 var builder = exports.builder(unpacked.newLen);
+1649
+1650 function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) {
+1651
+1652 if ((!curLineOpIter) || (curLineOpIterLine != curLine)) {
+1653 // create curLineOpIter and advance it to curChar
+1654 curLineOpIter = exports.opIterator(alines_get(curLine));
+1655 curLineOpIterLine = curLine;
+1656 var indexIntoLine = 0;
+1657 var done = false;
+1658 while (!done) {
+1659 curLineOpIter.next(curLineNextOp);
+1660 if (indexIntoLine + curLineNextOp.chars >= curChar) {
+1661 curLineNextOp.chars -= (curChar - indexIntoLine);
+1662 done = true;
+1663 } else {
+1664 indexIntoLine += curLineNextOp.chars;
+1665 }
+1666 }
+1667 }
+1668
+1669 while (numChars > 0) {
+1670 if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
+1671 curLine++;
+1672 curChar = 0;
+1673 curLineOpIterLine = curLine;
+1674 curLineNextOp.chars = 0;
+1675 curLineOpIter = exports.opIterator(alines_get(curLine));
+1676 }
+1677 if (!curLineNextOp.chars) {
+1678 curLineOpIter.next(curLineNextOp);
+1679 }
+1680 var charsToUse = Math.min(numChars, curLineNextOp.chars);
+1681 func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0);
+1682 numChars -= charsToUse;
+1683 curLineNextOp.chars -= charsToUse;
+1684 curChar += charsToUse;
+1685 }
+1686
+1687 if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
+1688 curLine++;
+1689 curChar = 0;
+1690 }
+1691 }
+1692
+1693 function skip(N, L) {
+1694 if (L) {
+1695 curLine += L;
+1696 curChar = 0;
+1697 } else {
+1698 if (curLineOpIter && curLineOpIterLine == curLine) {
+1699 consumeAttribRuns(N, function () {});
+1700 } else {
+1701 curChar += N;
+1702 }
+1703 }
+1704 }
+1705
+1706 function nextText(numChars) {
+1707 var len = 0;
+1708 var assem = exports.stringAssembler();
+1709 var firstString = lines_get(curLine).substring(curChar);
+1710 len += firstString.length;
+1711 assem.append(firstString);
+1712
+1713 var lineNum = curLine + 1;
+1714 while (len < numChars) {
+1715 var nextString = lines_get(lineNum);
+1716 len += nextString.length;
+1717 assem.append(nextString);
+1718 lineNum++;
+1719 }
+1720
+1721 return assem.toString().substring(0, numChars);
+1722 }
+1723
+1724 function cachedStrFunc(func) {
+1725 var cache = {};
+1726 return function (s) {
+1727 if (!cache[s]) {
+1728 cache[s] = func(s);
+1729 }
+1730 return cache[s];
+1731 };
+1732 }
+1733
+1734 var attribKeys = [];
+1735 var attribValues = [];
+1736 while (csIter.hasNext()) {
+1737 var csOp = csIter.next();
+1738 if (csOp.opcode == '=') {
+1739 if (csOp.attribs) {
+1740 attribKeys.length = 0;
+1741 attribValues.length = 0;
+1742 exports.eachAttribNumber(csOp.attribs, function (n) {
+1743 attribKeys.push(pool.getAttribKey(n));
+1744 attribValues.push(pool.getAttribValue(n));
+1745 });
+1746 var undoBackToAttribs = cachedStrFunc(function (attribs) {
+1747 var backAttribs = [];
+1748 for (var i = 0; i < attribKeys.length; i++) {
+1749 var appliedKey = attribKeys[i];
+1750 var appliedValue = attribValues[i];
+1751 var oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool);
+1752 if (appliedValue != oldValue) {
+1753 backAttribs.push([appliedKey, oldValue]);
+1754 }
+1755 }
+1756 return exports.makeAttribsString('=', backAttribs, pool);
+1757 });
+1758 consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) {
+1759 builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs));
+1760 });
+1761 } else {
+1762 skip(csOp.chars, csOp.lines);
+1763 builder.keep(csOp.chars, csOp.lines);
+1764 }
+1765 } else if (csOp.opcode == '+') {
+1766 builder.remove(csOp.chars, csOp.lines);
+1767 } else if (csOp.opcode == '-') {
+1768 var textBank = nextText(csOp.chars);
+1769 var textBankIndex = 0;
+1770 consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) {
+1771 builder.insert(textBank.substr(textBankIndex, len), attribs);
+1772 textBankIndex += len;
+1773 });
+1774 }
+1775 }
+1776
+1777 return exports.checkRep(builder.toString());
+1778 };
+1779
+1780 // %CLIENT FILE ENDS HERE%
+1781 exports.follow = function (cs1, cs2, reverseInsertOrder, pool) {
+1782 var unpacked1 = exports.unpack(cs1);
+1783 var unpacked2 = exports.unpack(cs2);
+1784 var len1 = unpacked1.oldLen;
+1785 var len2 = unpacked2.oldLen;
+1786 exports.assert(len1 == len2, "mismatched follow");
+1787 var chars1 = exports.stringIterator(unpacked1.charBank);
+1788 var chars2 = exports.stringIterator(unpacked2.charBank);
+1789
+1790 var oldLen = unpacked1.newLen;
+1791 var oldPos = 0;
+1792 var newLen = 0;
+1793
+1794 var hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool);
+1795
+1796 var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) {
+1797 if (op1.opcode == '+' || op2.opcode == '+') {
+1798 var whichToDo;
+1799 if (op2.opcode != '+') {
+1800 whichToDo = 1;
+1801 } else if (op1.opcode != '+') {
+1802 whichToDo = 2;
+1803 } else {
+1804 // both +
+1805 var firstChar1 = chars1.peek(1);
+1806 var firstChar2 = chars2.peek(1);
+1807 var insertFirst1 = hasInsertFirst(op1.attribs);
+1808 var insertFirst2 = hasInsertFirst(op2.attribs);
+1809 if (insertFirst1 && !insertFirst2) {
+1810 whichToDo = 1;
+1811 } else if (insertFirst2 && !insertFirst1) {
+1812 whichToDo = 2;
+1813 }
+1814 // insert string that doesn't start with a newline first so as not to break up lines
+1815 else if (firstChar1 == '\n' && firstChar2 != '\n') {
+1816 whichToDo = 2;
+1817 } else if (firstChar1 != '\n' && firstChar2 == '\n') {
+1818 whichToDo = 1;
+1819 }
+1820 // break symmetry:
+1821 else if (reverseInsertOrder) {
+1822 whichToDo = 2;
+1823 } else {
+1824 whichToDo = 1;
+1825 }
+1826 }
+1827 if (whichToDo == 1) {
+1828 chars1.skip(op1.chars);
+1829 opOut.opcode = '=';
+1830 opOut.lines = op1.lines;
+1831 opOut.chars = op1.chars;
+1832 opOut.attribs = '';
+1833 op1.opcode = '';
+1834 } else {
+1835 // whichToDo == 2
+1836 chars2.skip(op2.chars);
+1837 exports.copyOp(op2, opOut);
+1838 op2.opcode = '';
+1839 }
+1840 } else if (op1.opcode == '-') {
+1841 if (!op2.opcode) {
+1842 op1.opcode = '';
+1843 } else {
+1844 if (op1.chars <= op2.chars) {
+1845 op2.chars -= op1.chars;
+1846 op2.lines -= op1.lines;
+1847 op1.opcode = '';
+1848 if (!op2.chars) {
+1849 op2.opcode = '';
+1850 }
+1851 } else {
+1852 op1.chars -= op2.chars;
+1853 op1.lines -= op2.lines;
+1854 op2.opcode = '';
+1855 }
+1856 }
+1857 } else if (op2.opcode == '-') {
+1858 exports.copyOp(op2, opOut);
+1859 if (!op1.opcode) {
+1860 op2.opcode = '';
+1861 } else if (op2.chars <= op1.chars) {
+1862 // delete part or all of a keep
+1863 op1.chars -= op2.chars;
+1864 op1.lines -= op2.lines;
+1865 op2.opcode = '';
+1866 if (!op1.chars) {
+1867 op1.opcode = '';
+1868 }
+1869 } else {
+1870 // delete all of a keep, and keep going
+1871 opOut.lines = op1.lines;
+1872 opOut.chars = op1.chars;
+1873 op2.lines -= op1.lines;
+1874 op2.chars -= op1.chars;
+1875 op1.opcode = '';
+1876 }
+1877 } else if (!op1.opcode) {
+1878 exports.copyOp(op2, opOut);
+1879 op2.opcode = '';
+1880 } else if (!op2.opcode) {
+1881 exports.copyOp(op1, opOut);
+1882 op1.opcode = '';
+1883 } else {
+1884 // both keeps
+1885 opOut.opcode = '=';
+1886 opOut.attribs = exports.followAttributes(op1.attribs, op2.attribs, pool);
+1887 if (op1.chars <= op2.chars) {
+1888 opOut.chars = op1.chars;
+1889 opOut.lines = op1.lines;
+1890 op2.chars -= op1.chars;
+1891 op2.lines -= op1.lines;
+1892 op1.opcode = '';
+1893 if (!op2.chars) {
+1894 op2.opcode = '';
+1895 }
+1896 } else {
+1897 opOut.chars = op2.chars;
+1898 opOut.lines = op2.lines;
+1899 op1.chars -= op2.chars;
+1900 op1.lines -= op2.lines;
+1901 op2.opcode = '';
+1902 }
+1903 }
+1904 switch (opOut.opcode) {
+1905 case '=':
+1906 oldPos += opOut.chars;
+1907 newLen += opOut.chars;
+1908 break;
+1909 case '-':
+1910 oldPos += opOut.chars;
+1911 break;
+1912 case '+':
+1913 newLen += opOut.chars;
+1914 break;
+1915 }
+1916 });
+1917 newLen += oldLen - oldPos;
+1918
+1919 return exports.pack(oldLen, newLen, newOps, unpacked2.charBank);
+1920 };
+1921
+1922 exports.followAttributes = function (att1, att2, pool) {
+1923 // The merge of two sets of attribute changes to the same text
+1924 // takes the lexically-earlier value if there are two values
+1925 // for the same key. Otherwise, all key/value changes from
+1926 // both attribute sets are taken. This operation is the "follow",
+1927 // so a set of changes is produced that can be applied to att1
+1928 // to produce the merged set.
+1929 if ((!att2) || (!pool)) return '';
+1930 if (!att1) return att2;
+1931 var atts = [];
+1932 att2.replace(/\*([0-9a-z]+)/g, function (_, a) {
+1933 atts.push(pool.getAttrib(exports.parseNum(a)));
+1934 return '';
+1935 });
+1936 att1.replace(/\*([0-9a-z]+)/g, function (_, a) {
+1937 var pair1 = pool.getAttrib(exports.parseNum(a));
+1938 for (var i = 0; i < atts.length; i++) {
+1939 var pair2 = atts[i];
+1940 if (pair1[0] == pair2[0]) {
+1941 if (pair1[1] <= pair2[1]) {
+1942 // winner of merge is pair1, delete this attribute
+1943 atts.splice(i, 1);
+1944 }
+1945 break;
+1946 }
+1947 }
+1948 return '';
+1949 });
+1950 // we've only removed attributes, so they're already sorted
+1951 var buf = exports.stringAssembler();
+1952 for (var i = 0; i < atts.length; i++) {
+1953 buf.append('*');
+1954 buf.append(exports.numToString(pool.putAttrib(atts[i])));
+1955 }
+1956 return buf.toString();
+1957 };
+1958
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/src/node_MessageHandler.js.html b/doc/jsdoc/symbols/src/node_MessageHandler.js.html
new file mode 100644
index 00000000..aba4471d
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_MessageHandler.js.html
@@ -0,0 +1,508 @@
+ 1 /**
+ 2 * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17 var padManager = require("./PadManager");
+ 18 var Changeset = require("./Changeset");
+ 19 var AttributePoolFactory = require("./AttributePoolFactory");
+ 20 var authorManager = require("./AuthorManager");
+ 21
+ 22 /**
+ 23 * A associative array that translates a session to a pad
+ 24 */
+ 25 var session2pad = {};
+ 26 /**
+ 27 * A associative array that saves which sessions belong to a pad
+ 28 */
+ 29 var pad2sessions = {};
+ 30
+ 31 /**
+ 32 * A associative array that saves some general informations about a session
+ 33 * key = sessionId
+ 34 * values = author, rev
+ 35 * rev = That last revision that was send to this client
+ 36 * author = the author name of this session
+ 37 */
+ 38 var sessioninfos = {};
+ 39
+ 40 /**
+ 41 * Saves the Socket class we need to send and recieve data from the client
+ 42 */
+ 43 var socketio;
+ 44
+ 45 /**
+ 46 * This Method is called by server.js to tell the message handler on which socket it should send
+ 47 * @param socket_io The Socket
+ 48 */
+ 49 exports.setSocketIO = function(socket_io)
+ 50 {
+ 51 socketio=socket_io;
+ 52 }
+ 53
+ 54 /**
+ 55 * Handles the connection of a new user
+ 56 * @param client the new client
+ 57 */
+ 58 exports.handleConnect = function(client)
+ 59 {
+ 60 //check if all ok
+ 61 throwExceptionIfClientOrIOisInvalid(client);
+ 62
+ 63 //Initalize session2pad and sessioninfos for this new session
+ 64 session2pad[client.sessionId]=null;
+ 65 sessioninfos[client.sessionId]={};
+ 66 }
+ 67
+ 68 /**
+ 69 * Handles the disconnection of a user
+ 70 * @param client the client that leaves
+ 71 */
+ 72 exports.handleDisconnect = function(client)
+ 73 {
+ 74 //check if all ok
+ 75 throwExceptionIfClientOrIOisInvalid(client);
+ 76
+ 77 //save the padname of this session
+ 78 var sessionPad=session2pad[client.sessionId];
+ 79
+ 80 //Go trough all sessions of this pad, search and destroy the entry of this client
+ 81 for(i in pad2sessions[sessionPad])
+ 82 {
+ 83 if(pad2sessions[sessionPad][i] == client.sessionId)
+ 84 {
+ 85 delete pad2sessions[sessionPad][i];
+ 86 break;
+ 87 }
+ 88 }
+ 89
+ 90 //Delete the session2pad and sessioninfos entrys of this session
+ 91 delete session2pad[client.sessionId];
+ 92 delete sessioninfos[client.sessionId];
+ 93 }
+ 94
+ 95 /**
+ 96 * Handles a message from a user
+ 97 * @param client the client that send this message
+ 98 * @param message the message from the client
+ 99 */
+100 exports.handleMessage = function(client, message)
+101 {
+102 //check if all ok
+103 throwExceptionIfClientOrIOisInvalid(client);
+104
+105 if(message == null)
+106 {
+107 throw "Message is null!";
+108 }
+109 //Etherpad sometimes send JSON and sometimes a JSONstring...
+110 if(typeof message == "string")
+111 {
+112 message = JSON.parse(message);
+113 }
+114 if(!message.type)
+115 {
+116 throw "Message have no type attribute!";
+117 }
+118
+119 //Check what type of message we get and delegate to the other methodes
+120 if(message.type == "CLIENT_READY")
+121 {
+122 handleClientReady(client, message);
+123 }
+124 else if(message.type == "COLLABROOM" &&
+125 message.data.type == "USER_CHANGES")
+126 {
+127 console.error(JSON.stringify(message));
+128 handleUserChanges(client, message);
+129 }
+130 else if(message.type == "COLLABROOM" &&
+131 message.data.type == "USERINFO_UPDATE")
+132 {
+133 console.error(JSON.stringify(message));
+134 handleUserInfoUpdate(client, message);
+135 }
+136 //if the message type is unkown, throw an exception
+137 else
+138 {
+139 console.error(message);
+140 throw "unkown Message Type: '" + message.type + "'";
+141 }
+142 }
+143
+144 /**
+145 * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
+146 * @param client the client that send this message
+147 * @param message the message from the client
+148 */
+149 function handleUserInfoUpdate(client, message)
+150 {
+151 //check if all ok
+152 if(message.data.userInfo.name == null)
+153 {
+154 throw "USERINFO_UPDATE Message have no name!";
+155 }
+156 if(message.data.userInfo.colorId == null)
+157 {
+158 throw "USERINFO_UPDATE Message have no colorId!";
+159 }
+160
+161 //Find out the author name of this session
+162 var author = sessioninfos[client.sessionId].author;
+163
+164 //Tell the authorManager about the new attributes
+165 authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
+166 authorManager.setAuthorName(author, message.data.userInfo.name);
+167 }
+168
+169 /**
+170 * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
+171 * This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
+172 * Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
+173 * @param client the client that send this message
+174 * @param message the message from the client
+175 */
+176 function handleUserChanges(client, message)
+177 {
+178 //check if all ok
+179 if(message.data.baseRev == null)
+180 {
+181 throw "USER_CHANGES Message have no baseRev!";
+182 }
+183 if(message.data.apool == null)
+184 {
+185 throw "USER_CHANGES Message have no apool!";
+186 }
+187 if(message.data.changeset == null)
+188 {
+189 throw "USER_CHANGES Message have no changeset!";
+190 }
+191
+192 //get all Vars we need
+193 var baseRev = message.data.baseRev;
+194 var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool);
+195 var changeset = message.data.changeset;
+196 var pad = padManager.getPad(session2pad[client.sessionId], false);
+197
+198 //ex. _checkChangesetAndPool
+199
+200 //Copied from Etherpad, don't know what it does exactly
+201 Changeset.checkRep(changeset);
+202 Changeset.eachAttribNumber(changeset, function(n) {
+203 if (! wireApool.getAttrib(n)) {
+204 throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
+205 }
+206 });
+207
+208 //ex. adoptChangesetAttribs
+209
+210 //Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
+211 Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool());
+212
+213 //ex. applyUserChanges
+214
+215 var apool = pad.pool();
+216 var r = baseRev;
+217
+218 while (r < pad.getHeadRevisionNumber()) {
+219 r++;
+220 var c = pad.getRevisionChangeset(r);
+221 changeset = Changeset.follow(c, changeset, false, apool);
+222 }
+223
+224 var prevText = pad.text();
+225 if (Changeset.oldLen(changeset) != prevText.length) {
+226 throw "Can't apply USER_CHANGES "+changeset+" with oldLen "
+227 + Changeset.oldLen(changeset) + " to document of length " + prevText.length;
+228 }
+229
+230 var thisAuthor = sessioninfos[client.sessionId].author;
+231
+232 pad.appendRevision(changeset, thisAuthor);
+233
+234 var correctionChangeset = _correctMarkersInPad(pad.atext(), pad.pool());
+235 if (correctionChangeset) {
+236 pad.appendRevision(correctionChangeset);
+237 }
+238
+239 if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
+240 var nlChangeset = Changeset.makeSplice(
+241 pad.text(), pad.text().length-1, 0, "\n");
+242 pad.appendRevision(nlChangeset);
+243 }
+244
+245 console.error(JSON.stringify(pad.pool()));
+246
+247 //ex. updatePadClients
+248
+249 for(i in pad2sessions[pad.id])
+250 {
+251 var session = pad2sessions[pad.id][i];
+252 var lastRev = sessioninfos[session].rev;
+253
+254 while (lastRev < pad.getHeadRevisionNumber())
+255 {
+256 var r = ++lastRev;
+257 var author = pad.getRevisionAuthor(r);
+258
+259 if(author == sessioninfos[session].author)
+260 {
+261 socketio.clients[session].send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
+262 }
+263 else
+264 {
+265 var forWire = Changeset.prepareForWire(pad.getRevisionChangeset(r), pad.pool());
+266 var wireMsg = {"type":"COLLABROOM","data":{type:"NEW_CHANGES", newRev:r,
+267 changeset: forWire.translated,
+268 apool: forWire.pool,
+269 author: author}};
+270 socketio.clients[session].send(wireMsg);
+271 }
+272 }
+273
+274 sessioninfos[session].rev = pad.getHeadRevisionNumber();
+275 }
+276 }
+277
+278 /**
+279 * Copied from the Etherpad Source Code. Don't know what this methode does excatly...
+280 */
+281 function _correctMarkersInPad(atext, apool) {
+282 var text = atext.text;
+283
+284 // collect char positions of line markers (e.g. bullets) in new atext
+285 // that aren't at the start of a line
+286 var badMarkers = [];
+287 var iter = Changeset.opIterator(atext.attribs);
+288 var offset = 0;
+289 while (iter.hasNext()) {
+290 var op = iter.next();
+291 var listValue = Changeset.opAttributeValue(op, 'list', apool);
+292 if (listValue) {
+293 for(var i=0;i<op.chars;i++) {
+294 if (offset > 0 && text.charAt(offset-1) != '\n') {
+295 badMarkers.push(offset);
+296 }
+297 offset++;
+298 }
+299 }
+300 else {
+301 offset += op.chars;
+302 }
+303 }
+304
+305 if (badMarkers.length == 0) {
+306 return null;
+307 }
+308
+309 // create changeset that removes these bad markers
+310 offset = 0;
+311 var builder = Changeset.builder(text.length);
+312 badMarkers.forEach(function(pos) {
+313 builder.keepText(text.substring(offset, pos));
+314 builder.remove(1);
+315 offset = pos+1;
+316 });
+317 return builder.toString();
+318 }
+319
+320 /**
+321 * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
+322 * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
+323 * @param client the client that send this message
+324 * @param message the message from the client
+325 */
+326 function handleClientReady(client, message)
+327 {
+328 //check if all ok
+329 if(!message.token)
+330 {
+331 throw "CLIENT_READY Message have no token!";
+332 }
+333 if(!message.padId)
+334 {
+335 throw "CLIENT_READY Message have no padId!";
+336 }
+337 if(!message.protocolVersion)
+338 {
+339 throw "CLIENT_READY Message have no protocolVersion!";
+340 }
+341 if(message.protocolVersion != 1)
+342 {
+343 throw "CLIENT_READY Message have a unkown protocolVersion '" + protocolVersion + "'!";
+344 }
+345
+346 //Ask the author Manager for a authorname of this token.
+347 var author = authorManager.getAuthor4Token(message.token);
+348
+349 //Save in session2pad that this session belonges to this pad
+350 var sessionId=String(client.sessionId);
+351 session2pad[sessionId] = message.padId;
+352
+353 //check if there is already a pad2sessions entry, if not, create one
+354 if(!pad2sessions[message.padId])
+355 {
+356 pad2sessions[message.padId] = [];
+357 }
+358
+359 //Saves in pad2sessions that this session belongs to this pad
+360 pad2sessions[message.padId].push(sessionId);
+361
+362 //Tell the PadManager that it should ensure that this Pad exist
+363 padManager.ensurePadExists(message.padId);
+364
+365 //Ask the PadManager for a function Wrapper for this Pad
+366 var pad = padManager.getPad(message.padId, false);
+367
+368 //prepare all values for the wire
+369 atext = pad.atext();
+370 var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool());
+371 var apool = attribsForWire.pool.toJsonable();
+372 atext.attribs = attribsForWire.translated;
+373
+374 var clientVars = {
+375 "accountPrivs": {
+376 "maxRevisions": 100
+377 },
+378 "initialRevisionList": [],
+379 "initialOptions": {
+380 "guestPolicy": "deny"
+381 },
+382 "collab_client_vars": {
+383 "initialAttributedText": atext,
+384 "clientIp": client.request.connection.remoteAddress,
+385 //"clientAgent": "Anonymous Agent",
+386 "padId": message.padId,
+387 "historicalAuthorData": {},
+388 "apool": apool,
+389 "rev": pad.getHeadRevisionNumber(),
+390 "globalPadId": message.padId
+391 },
+392 "colorPalette": ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"],
+393 "clientIp": client.request.connection.remoteAddress,
+394 "userIsGuest": true,
+395 "userColor": authorManager.getAuthorColorId(author),
+396 "padId": message.padId,
+397 "initialTitle": "Pad: " + message.padId,
+398 "opts": {},
+399 "chatHistory": {
+400 "start": 0,
+401 "historicalAuthorData": {},
+402 "end": 0,
+403 "lines": []
+404 },
+405 "numConnectedUsers": pad2sessions[message.padId].length,
+406 "isProPad": false,
+407 "serverTimestamp": new Date().getTime(),
+408 "globalPadId": message.padId,
+409 "userId": author,
+410 "cookiePrefsToSet": {
+411 "fullWidth": false,
+412 "hideSidebar": false
+413 },
+414 "hooks": {}
+415 }
+416
+417 //Add a username to the clientVars if one avaiable
+418 if(authorManager.getAuthorName(author) != null)
+419 {
+420 clientVars.userName = authorManager.getAuthorName(author);
+421 }
+422
+423 //Add all authors that worked on this pad, to the historicalAuthorData on clientVars
+424 var allAuthors = pad.getAllAuthors();
+425 for(i in allAuthors)
+426 {
+427 clientVars.collab_client_vars.historicalAuthorData[allAuthors[i]] = {};
+428 if(authorManager.getAuthorName(author) != null)
+429 clientVars.collab_client_vars.historicalAuthorData[allAuthors[i]].name = authorManager.getAuthorName(author);
+430 clientVars.collab_client_vars.historicalAuthorData[allAuthors[i]].colorId = authorManager.getAuthorColorId(author);
+431 }
+432
+433 //Send the clientVars to the Client
+434 client.send(clientVars);
+435
+436 //Save the revision and the author id in sessioninfos
+437 sessioninfos[client.sessionId].rev = pad.getHeadRevisionNumber();
+438 sessioninfos[client.sessionId].author = author;
+439
+440 //prepare the notification for the other users on the pad, that this user joined
+441 var messageToTheOtherUsers = {
+442 "type": "COLLABROOM",
+443 "data": {
+444 type: "USER_NEWINFO",
+445 userInfo: {
+446 "ip": "127.0.0.1",
+447 "colorId": authorManager.getAuthorColorId(author),
+448 "userAgent": "Anonymous",
+449 "userId": author
+450 }
+451 }
+452 };
+453
+454 //Add the authorname of this new User, if avaiable
+455 if(authorManager.getAuthorName(author) != null)
+456 {
+457 messageToTheOtherUsers.data.userInfo.name = authorManager.getAuthorName(author);
+458 }
+459
+460 //
+461 for(i in pad2sessions[message.padId])
+462 {
+463 if(pad2sessions[message.padId][i] != client.sessionId)
+464 {
+465 socketio.clients[pad2sessions[message.padId][i]].send(messageToTheOtherUsers);
+466
+467 var messageToNotifyTheClientAboutTheOthers = {
+468 "type": "COLLABROOM",
+469 "data": {
+470 type: "USER_NEWINFO",
+471 userInfo: {
+472 "ip": "127.0.0.1",
+473 "colorId": authorManager.getAuthorColorId(sessioninfos[pad2sessions[message.padId][i]].author),
+474 "userAgent": "Anonymous",
+475 "userId": sessioninfos[pad2sessions[message.padId][i]].author
+476 }
+477 }
+478 };
+479
+480 client.send(messageToNotifyTheClientAboutTheOthers);
+481 }
+482 }
+483
+484
+485 }
+486
+487 /**
+488 * A internal function that simply checks if client or socketio is null and throws a exception if yes
+489 */
+490 function throwExceptionIfClientOrIOisInvalid(client)
+491 {
+492 if(client == null)
+493 {
+494 throw "Client is null!";
+495 }
+496 if(socketio == null)
+497 {
+498 throw "SocketIO is not set or null! Please use setSocketIO(io) to set it";
+499 }
+500 }
+501
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/src/node_PadManager.js.html b/doc/jsdoc/symbols/src/node_PadManager.js.html
new file mode 100644
index 00000000..3ef71578
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_PadManager.js.html
@@ -0,0 +1,272 @@
+ 1 /**
+ 2 * 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17 /*
+ 18 The Pad Module trys to simulate the pad object from EtherPad. You can find the original code in /etherpad/src/etherpad/pad/model.js
+ 19 see https://github.com/ether/pad/blob/master/etherpad/src/etherpad/pad/model.js
+ 20 */
+ 21
+ 22 var Changeset = require("./Changeset");
+ 23 var AttributePoolFactory = require("./AttributePoolFactory");
+ 24
+ 25 /**
+ 26 * The initial Text of a Pad
+ 27 */
+ 28 exports.startText = "Hello World\nGoodbye Etherpad";
+ 29
+ 30 /**
+ 31 * A Array with all known Pads
+ 32 */
+ 33 globalPads = [];
+ 34
+ 35 /**
+ 36 * Return a Function Wrapper to work with the Pad
+ 37 * @param id A String with the id of the pad
+ 38 * @param createIfNotExist A Boolean which says the function if it should create the Pad if it not exist
+ 39 */
+ 40 exports.getPad = function(id, createIfNotExist)
+ 41 {
+ 42 if(!globalPads[id] && createIfNotExist == true)
+ 43 {
+ 44 createPad(id);
+ 45 }
+ 46
+ 47 if(!globalPads[id])
+ 48 return null;
+ 49
+ 50 globalPads[id].timestamp = new Date().getTime();
+ 51
+ 52 var functionWrapper = {};
+ 53
+ 54 functionWrapper.id = id;
+ 55 functionWrapper.appendRevision = function (theChangeset, author) {return appendRevision(id, theChangeset, author)};
+ 56 functionWrapper.text = function () {return text(id)};
+ 57 functionWrapper.atext = function () {return atext(id)};
+ 58 functionWrapper.pool = function () {return pool(id)};
+ 59 functionWrapper.getHeadRevisionNumber = function () {return getHeadRevisionNumber(id)};
+ 60 functionWrapper.getRevisionChangeset = function (revNum) {return getRevisionChangeset(id, revNum)};
+ 61 functionWrapper.getRevisionAuthor = function (revNum) {return getRevisionAuthor(id, revNum)};
+ 62 functionWrapper.getAllAuthors = function () {return getAllAuthors(id)};
+ 63
+ 64 return functionWrapper;
+ 65 }
+ 66
+ 67 /**
+ 68 * Ensures that the Pad exists
+ 69 * @param id The Pad id
+ 70 */
+ 71 exports.ensurePadExists = function(id)
+ 72 {
+ 73 if(!globalPads[id])
+ 74 {
+ 75 createPad(id);
+ 76 }
+ 77 }
+ 78
+ 79 /**
+ 80 * Creates an empty pad
+ 81 * @param id The Pad id
+ 82 */
+ 83 function createPad(id)
+ 84 {
+ 85 var pad = {};
+ 86 globalPads[id] = pad;
+ 87
+ 88 pad.id = id;
+ 89 pad.rev = [];
+ 90 pad.head = -1;
+ 91 pad.atext = Changeset.makeAText("\n");
+ 92 pad.apool = AttributePoolFactory.createAttributePool();
+ 93 pad.authors = [];
+ 94
+ 95 var firstChangeset = Changeset.makeSplice("\n", 0, 0,
+ 96 exports.cleanText(exports.startText));
+ 97 appendRevision(id, firstChangeset, '');
+ 98 }
+ 99
+100 /**
+101 * Append a changeset to a pad
+102 * @param id The Pad id
+103 * @param theChangeset the changeset which should apply to the text
+104 * @param The author of the revision, can be null
+105 */
+106 function appendRevision(id, theChangeset, author)
+107 {
+108 throwExceptionIfPadDontExist(id);
+109
+110 if(!author)
+111 author = '';
+112
+113 var atext = globalPads[id].atext;
+114 var apool = globalPads[id].apool;
+115 var newAText = Changeset.applyToAText(theChangeset, atext, apool);
+116 Changeset.copyAText(newAText, atext);
+117
+118 var newRev = ++globalPads[id].head;
+119 globalPads[id].rev[newRev] = {};
+120 globalPads[id].rev[newRev].changeset = theChangeset;
+121 globalPads[id].rev[newRev].meta = {};
+122 globalPads[id].rev[newRev].meta.author = author;
+123 globalPads[id].rev[newRev].meta.timestamp = new Date().getTime();
+124
+125 //ex. getNumForAuthor
+126 apool.putAttrib(['author',author||'']);
+127
+128 if(newRev%100==0)
+129 {
+130 globalPads[id].rev[newRev].meta.atext=atext;
+131 }
+132 }
+133
+134 /**
+135 * Returns all Authors of a Pad
+136 * @param id The Pad id
+137 */
+138 function getAllAuthors(id)
+139 {
+140 var authors = [];
+141
+142 for(key in globalPads[id].apool.numToAttrib)
+143 {
+144 if(globalPads[id].apool.numToAttrib[key][0] == "author" && globalPads[id].apool.numToAttrib[key][1] != "")
+145 {
+146 authors.push(globalPads[id].apool.numToAttrib[key][1]);
+147 }
+148 }
+149
+150 return authors;
+151 }
+152
+153 /**
+154 * Returns the plain text of a pad
+155 * @param id The Pad id
+156 */
+157
+158 function text(id)
+159 {
+160 throwExceptionIfPadDontExist(id);
+161
+162 return globalPads[id].atext.text;
+163 }
+164
+165 /**
+166 * Returns the Attributed Text of a pad
+167 * @param id The Pad id
+168 */
+169 function atext(id)
+170 {
+171 throwExceptionIfPadDontExist(id);
+172
+173 return globalPads[id].atext;
+174 }
+175
+176 /**
+177 * Returns the Attribute Pool whichs the Pad is using
+178 * @param id The Pad id
+179 */
+180 function pool(id)
+181 {
+182 throwExceptionIfPadDontExist(id);
+183
+184 return globalPads[id].apool;
+185 }
+186
+187 /**
+188 * Returns the latest Revision Number of the Pad
+189 * @param id The Pad id
+190 */
+191 function getHeadRevisionNumber(id)
+192 {
+193 throwExceptionIfPadDontExist(id);
+194
+195 return globalPads[id].head;
+196 }
+197
+198 /**
+199 * Returns the changeset of a specific revision
+200 * @param id The Pad id
+201 * @param revNum The Revision Number
+202 */
+203 function getRevisionChangeset(id, revNum)
+204 {
+205 throwExceptionIfPadDontExist(id);
+206 throwExceptionIfRevDontExist(id, revNum);
+207
+208 return globalPads[id].rev[revNum].changeset;
+209 }
+210
+211 /**
+212 * Returns the author of a specific revision
+213 * @param id The Pad id
+214 * @param revNum The Revision Number
+215 */
+216 function getRevisionAuthor(id, revNum)
+217 {
+218 throwExceptionIfPadDontExist(id);
+219 throwExceptionIfRevDontExist(id, revNum);
+220
+221 return globalPads[id].rev[revNum].meta.author;
+222 }
+223
+224 /**
+225 * Check if the ID is a valid Pad ID and trows an Exeption if not
+226 * @param id The Pad id
+227 */
+228 function throwExceptionIfPadDontExist(id)
+229 {
+230 if(id == null)
+231 {
+232 throw "Padname is null!";
+233 }
+234 if(!globalPads[id])
+235 {
+236 throw "Pad don't exist!'";
+237 }
+238 }
+239
+240 /**
+241 * Check if the Revision of a Pad is valid and throws an Exeption if not
+242 * @param id The Pad id
+243 */
+244 function throwExceptionIfRevDontExist(id, revNum)
+245 {
+246 if(revNum == null)
+247 throw "revNum is null";
+248
+249 if((typeof revNum) != "number")
+250 throw revNum + " is no Number";
+251
+252 if(revNum < 0 || revNum > globalPads[id].head)
+253 throw "The Revision " + revNum + " don't exist'";
+254 }
+255
+256 /**
+257 * Copied from the Etherpad source code, don't know what its good for
+258 * @param txt
+259 */
+260 exports.cleanText = function (txt) {
+261 return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
+262 }
+263
+264
+265
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/src/node_easysync_tests.js.html b/doc/jsdoc/symbols/src/node_easysync_tests.js.html
new file mode 100644
index 00000000..50f30a00
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_easysync_tests.js.html
@@ -0,0 +1,950 @@
+ 1 /**
+ 2 * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17 var Changeset = require('./Changeset');
+ 18 var AttributePoolFactory = require("./AttributePoolFactory");
+ 19
+ 20 function random() {
+ 21 this.nextInt = function (maxValue) {
+ 22 return Math.floor(Math.random() * maxValue);
+ 23 }
+ 24
+ 25 this.nextDouble = function (maxValue) {
+ 26 return Math.random();
+ 27 }
+ 28 }
+ 29
+ 30 function runTests() {
+ 31
+ 32 function print(str) {
+ 33 console.log(str);
+ 34 }
+ 35
+ 36 function assert(code, optMsg) {
+ 37 if (!eval(code)) throw new Error("FALSE: " + (optMsg || code));
+ 38 }
+ 39
+ 40 function literal(v) {
+ 41 if ((typeof v) == "string") {
+ 42 return '"' + v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n') + '"';
+ 43 } else
+ 44 return JSON.stringify(v);
+ 45 }
+ 46
+ 47 function assertEqualArrays(a, b) {
+ 48 assert("JSON.stringify(" + literal(a) + ") == JSON.stringify(" + literal(b) + ")");
+ 49 }
+ 50
+ 51 function assertEqualStrings(a, b) {
+ 52 assert(literal(a) + " == " + literal(b));
+ 53 }
+ 54
+ 55 function throughIterator(opsStr) {
+ 56 var iter = Changeset.opIterator(opsStr);
+ 57 var assem = Changeset.opAssembler();
+ 58 while (iter.hasNext()) {
+ 59 assem.append(iter.next());
+ 60 }
+ 61 return assem.toString();
+ 62 }
+ 63
+ 64 function throughSmartAssembler(opsStr) {
+ 65 var iter = Changeset.opIterator(opsStr);
+ 66 var assem = Changeset.smartOpAssembler();
+ 67 while (iter.hasNext()) {
+ 68 assem.append(iter.next());
+ 69 }
+ 70 assem.endDocument();
+ 71 return assem.toString();
+ 72 }
+ 73
+ 74 (function () {
+ 75 print("> throughIterator");
+ 76 var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
+ 77 assert("throughIterator(" + literal(x) + ") == " + literal(x));
+ 78 })();
+ 79
+ 80 (function () {
+ 81 print("> throughSmartAssembler");
+ 82 var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
+ 83 assert("throughSmartAssembler(" + literal(x) + ") == " + literal(x));
+ 84 })();
+ 85
+ 86 function applyMutations(mu, arrayOfArrays) {
+ 87 arrayOfArrays.forEach(function (a) {
+ 88 var result = mu[a[0]].apply(mu, a.slice(1));
+ 89 if (a[0] == 'remove' && a[3]) {
+ 90 assertEqualStrings(a[3], result);
+ 91 }
+ 92 });
+ 93 }
+ 94
+ 95 function mutationsToChangeset(oldLen, arrayOfArrays) {
+ 96 var assem = Changeset.smartOpAssembler();
+ 97 var op = Changeset.newOp();
+ 98 var bank = Changeset.stringAssembler();
+ 99 var oldPos = 0;
+100 var newLen = 0;
+101 arrayOfArrays.forEach(function (a) {
+102 if (a[0] == 'skip') {
+103 op.opcode = '=';
+104 op.chars = a[1];
+105 op.lines = (a[2] || 0);
+106 assem.append(op);
+107 oldPos += op.chars;
+108 newLen += op.chars;
+109 } else if (a[0] == 'remove') {
+110 op.opcode = '-';
+111 op.chars = a[1];
+112 op.lines = (a[2] || 0);
+113 assem.append(op);
+114 oldPos += op.chars;
+115 } else if (a[0] == 'insert') {
+116 op.opcode = '+';
+117 bank.append(a[1]);
+118 op.chars = a[1].length;
+119 op.lines = (a[2] || 0);
+120 assem.append(op);
+121 newLen += op.chars;
+122 }
+123 });
+124 newLen += oldLen - oldPos;
+125 assem.endDocument();
+126 return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
+127 }
+128
+129 function runMutationTest(testId, origLines, muts, correct) {
+130 print("> runMutationTest#" + testId);
+131 var lines = origLines.slice();
+132 var mu = Changeset.textLinesMutator(lines);
+133 applyMutations(mu, muts);
+134 mu.close();
+135 assertEqualArrays(correct, lines);
+136
+137 var inText = origLines.join('');
+138 var cs = mutationsToChangeset(inText.length, muts);
+139 lines = origLines.slice();
+140 Changeset.mutateTextLines(cs, lines);
+141 assertEqualArrays(correct, lines);
+142
+143 var correctText = correct.join('');
+144 //print(literal(cs));
+145 var outText = Changeset.applyToText(cs, inText);
+146 assertEqualStrings(correctText, outText);
+147 }
+148
+149 runMutationTest(1, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
+150 ['remove', 1, 0, "a"],
+151 ['insert', "tu"],
+152 ['remove', 1, 0, "p"],
+153 ['skip', 4, 1],
+154 ['skip', 7, 1],
+155 ['insert', "cream\npie\n", 2],
+156 ['skip', 2],
+157 ['insert', "bot"],
+158 ['insert', "\n", 1],
+159 ['insert', "bu"],
+160 ['skip', 3],
+161 ['remove', 3, 1, "ge\n"],
+162 ['remove', 6, 0, "duffle"]
+163 ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
+164
+165 runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
+166 ['remove', 1, 0, "a"],
+167 ['remove', 1, 0, "p"],
+168 ['insert', "tu"],
+169 ['skip', 11, 2],
+170 ['insert', "cream\npie\n", 2],
+171 ['skip', 2],
+172 ['insert', "bot"],
+173 ['insert', "\n", 1],
+174 ['insert', "bu"],
+175 ['skip', 3],
+176 ['remove', 3, 1, "ge\n"],
+177 ['remove', 6, 0, "duffle"]
+178 ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
+179
+180 runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
+181 ['remove', 6, 1, "apple\n"],
+182 ['skip', 15, 2],
+183 ['skip', 6],
+184 ['remove', 1, 1, "\n"],
+185 ['remove', 8, 0, "eggplant"],
+186 ['skip', 1, 1]
+187 ], ["banana\n", "cabbage\n", "duffle\n"]);
+188
+189 runMutationTest(4, ["15\n"], [
+190 ['skip', 1],
+191 ['insert', "\n2\n3\n4\n", 4],
+192 ['skip', 2, 1]
+193 ], ["1\n", "2\n", "3\n", "4\n", "5\n"]);
+194
+195 runMutationTest(5, ["1\n", "2\n", "3\n", "4\n", "5\n"], [
+196 ['skip', 1],
+197 ['remove', 7, 4, "\n2\n3\n4\n"],
+198 ['skip', 2, 1]
+199 ], ["15\n"]);
+200
+201 runMutationTest(6, ["123\n", "abc\n", "def\n", "ghi\n", "xyz\n"], [
+202 ['insert', "0"],
+203 ['skip', 4, 1],
+204 ['skip', 4, 1],
+205 ['remove', 8, 2, "def\nghi\n"],
+206 ['skip', 4, 1]
+207 ], ["0123\n", "abc\n", "xyz\n"]);
+208
+209 runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
+210 ['remove', 6, 1, "apple\n"],
+211 ['skip', 15, 2, true],
+212 ['skip', 6, 0, true],
+213 ['remove', 1, 1, "\n"],
+214 ['remove', 8, 0, "eggplant"],
+215 ['skip', 1, 1, true]
+216 ], ["banana\n", "cabbage\n", "duffle\n"]);
+217
+218 function poolOrArray(attribs) {
+219 if (attribs.getAttrib) {
+220 return attribs; // it's already an attrib pool
+221 } else {
+222 // assume it's an array of attrib strings to be split and added
+223 var p = AttributePoolFactory.createAttributePool();
+224 attribs.forEach(function (kv) {
+225 p.putAttrib(kv.split(','));
+226 });
+227 return p;
+228 }
+229 }
+230
+231 function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) {
+232 print("> applyToAttribution#" + testId);
+233 var p = poolOrArray(attribs);
+234 var result = Changeset.applyToAttribution(
+235 Changeset.checkRep(cs), inAttr, p);
+236 assertEqualStrings(outCorrect, result);
+237 }
+238
+239 // turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n
+240 runApplyToAttributionTest(1, ['bold,', 'bold,true'], "Z:7>3-1*0=1*1=1=3+4$abcd", "+1*1+1|1+5", "+1*1+1|1+8");
+241
+242 // turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n"
+243 runApplyToAttributionTest(2, ['bold,', 'bold,true'], "Z:g<4*1|1=6*1=5-4$", "|2+g", "*1|1+6*1+5|1+1");
+244
+245 (function () {
+246 print("> mutatorHasMore");
+247 var lines = ["1\n", "2\n", "3\n", "4\n"];
+248 var mu;
+249
+250 mu = Changeset.textLinesMutator(lines);
+251 assert(mu.hasMore() + ' == true');
+252 mu.skip(8, 4);
+253 assert(mu.hasMore() + ' == false');
+254 mu.close();
+255 assert(mu.hasMore() + ' == false');
+256
+257 // still 1,2,3,4
+258 mu = Changeset.textLinesMutator(lines);
+259 assert(mu.hasMore() + ' == true');
+260 mu.remove(2, 1);
+261 assert(mu.hasMore() + ' == true');
+262 mu.skip(2, 1);
+263 assert(mu.hasMore() + ' == true');
+264 mu.skip(2, 1);
+265 assert(mu.hasMore() + ' == true');
+266 mu.skip(2, 1);
+267 assert(mu.hasMore() + ' == false');
+268 mu.insert("5\n", 1);
+269 assert(mu.hasMore() + ' == false');
+270 mu.close();
+271 assert(mu.hasMore() + ' == false');
+272
+273 // 2,3,4,5 now
+274 mu = Changeset.textLinesMutator(lines);
+275 assert(mu.hasMore() + ' == true');
+276 mu.remove(6, 3);
+277 assert(mu.hasMore() + ' == true');
+278 mu.remove(2, 1);
+279 assert(mu.hasMore() + ' == false');
+280 mu.insert("hello\n", 1);
+281 assert(mu.hasMore() + ' == false');
+282 mu.close();
+283 assert(mu.hasMore() + ' == false');
+284
+285 })();
+286
+287 function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) {
+288 print("> runMutateAttributionTest#" + testId);
+289 var p = poolOrArray(attribs);
+290 var alines2 = Array.prototype.slice.call(alines);
+291 var result = Changeset.mutateAttributionLines(
+292 Changeset.checkRep(cs), alines2, p);
+293 assertEqualArrays(outCorrect, alines2);
+294
+295 print("> runMutateAttributionTest#" + testId + ".applyToAttribution");
+296
+297 function removeQuestionMarks(a) {
+298 return a.replace(/\?/g, '');
+299 }
+300 var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks));
+301 var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks));
+302 var mergedResult = Changeset.applyToAttribution(cs, inMerged, p);
+303 assertEqualStrings(correctMerged, mergedResult);
+304 }
+305
+306 // turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n
+307 runMutateAttributionTest(1, ["bold,true"], "Z:c>0|1=4=1*0=1$", ["|1+4", "|1+4", "|1+4"], ["|1+4", "+1*0+1|1+2", "|1+4"]);
+308
+309 // make a document bold
+310 runMutateAttributionTest(2, ["bold,true"], "Z:c>0*0|3=c$", ["|1+4", "|1+4", "|1+4"], ["*0|1+4", "*0|1+4", "*0|1+4"]);
+311
+312 // clear bold on document
+313 runMutateAttributionTest(3, ["bold,", "bold,true"], "Z:c>0*0|3=c$", ["*1+1+1*1+1|1+1", "+1*1+1|1+2", "*1+1+1*1+1|1+1"], ["|1+4", "|1+4", "|1+4"]);
+314
+315 // add a character on line 3 of a document with 5 blank lines, and make sure
+316 // the optimization that skips purely-kept lines is working; if any attribution string
+317 // with a '?' is parsed it will cause an error.
+318 runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]);
+319
+320 var testPoolWithChars = (function () {
+321 var p = AttributePoolFactory.createAttributePool();
+322 p.putAttrib(['char', 'newline']);
+323 for (var i = 1; i < 36; i++) {
+324 p.putAttrib(['char', Changeset.numToString(i)]);
+325 }
+326 p.putAttrib(['char', '']);
+327 return p;
+328 })();
+329
+330 // based on runMutationTest#1
+331 runMutateAttributionTest(5, testPoolWithChars, "Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$" + "tucream\npie\nbot\nbu", ["*a+1*p+2*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1", "*d+1*u+1*f+2*l+1*e+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"], ["*t+1*u+1*p+1*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "|1+6", "|1+4", "*c+1*a+1*b+1*o+1*t+1*0|1+1", "*b+1*u+1*b+2*a+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"]);
+332
+333 // based on runMutationTest#3
+334 runMutateAttributionTest(6, testPoolWithChars, "Z:11<f|1-6|2=f=6|1-1-8$", ["*a|1+6", "*b|1+7", "*c|1+8", "*d|1+7", "*e|1+9"], ["*b|1+7", "*c|1+8", "*d+6*e|1+1"]);
+335
+336 // based on runMutationTest#4
+337 runMutateAttributionTest(7, testPoolWithChars, "Z:3>7=1|4+7$\n2\n3\n4\n", ["*1+1*5|1+2"], ["*1+1|1+1", "|1+2", "|1+2", "|1+2", "*5|1+2"]);
+338
+339 // based on runMutationTest#5
+340 runMutateAttributionTest(8, testPoolWithChars, "Z:a<7=1|4-7$", ["*1|1+2", "*2|1+2", "*3|1+2", "*4|1+2", "*5|1+2"], ["*1+1*5|1+2"]);
+341
+342 // based on runMutationTest#6
+343 runMutateAttributionTest(9, testPoolWithChars, "Z:k<7*0+1*10|2=8|2-8$0", ["*1+1*2+1*3+1|1+1", "*a+1*b+1*c+1|1+1", "*d+1*e+1*f+1|1+1", "*g+1*h+1*i+1|1+1", "?*x+1*y+1*z+1|1+1"], ["*0+1|1+4", "|1+4", "?*x+1*y+1*z+1|1+1"]);
+344
+345 runMutateAttributionTest(10, testPoolWithChars, "Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd", ["|1+3", "|1+3"], ["|1+5", "+2*0+1|1+2"]);
+346
+347
+348 runMutateAttributionTest(11, testPoolWithChars, "Z:s>1|1=4=6|1+1$\n", ["*0|1+4", "*0|1+8", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"], ["*0|1+4", "*0+6|1+1", "*0|1+2", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"]);
+349
+350 function randomInlineString(len, rand) {
+351 var assem = Changeset.stringAssembler();
+352 for (var i = 0; i < len; i++) {
+353 assem.append(String.fromCharCode(rand.nextInt(26) + 97));
+354 }
+355 return assem.toString();
+356 }
+357
+358 function randomMultiline(approxMaxLines, approxMaxCols, rand) {
+359 var numParts = rand.nextInt(approxMaxLines * 2) + 1;
+360 var txt = Changeset.stringAssembler();
+361 txt.append(rand.nextInt(2) ? '\n' : '');
+362 for (var i = 0; i < numParts; i++) {
+363 if ((i % 2) == 0) {
+364 if (rand.nextInt(10)) {
+365 txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand));
+366 } else {
+367 txt.append('\n');
+368 }
+369 } else {
+370 txt.append('\n');
+371 }
+372 }
+373 return txt.toString();
+374 }
+375
+376 function randomStringOperation(numCharsLeft, rand) {
+377 var result;
+378 switch (rand.nextInt(9)) {
+379 case 0:
+380 {
+381 // insert char
+382 result = {
+383 insert: randomInlineString(1, rand)
+384 };
+385 break;
+386 }
+387 case 1:
+388 {
+389 // delete char
+390 result = {
+391 remove: 1
+392 };
+393 break;
+394 }
+395 case 2:
+396 {
+397 // skip char
+398 result = {
+399 skip: 1
+400 };
+401 break;
+402 }
+403 case 3:
+404 {
+405 // insert small
+406 result = {
+407 insert: randomInlineString(rand.nextInt(4) + 1, rand)
+408 };
+409 break;
+410 }
+411 case 4:
+412 {
+413 // delete small
+414 result = {
+415 remove: rand.nextInt(4) + 1
+416 };
+417 break;
+418 }
+419 case 5:
+420 {
+421 // skip small
+422 result = {
+423 skip: rand.nextInt(4) + 1
+424 };
+425 break;
+426 }
+427 case 6:
+428 {
+429 // insert multiline;
+430 result = {
+431 insert: randomMultiline(5, 20, rand)
+432 };
+433 break;
+434 }
+435 case 7:
+436 {
+437 // delete multiline
+438 result = {
+439 remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
+440 };
+441 break;
+442 }
+443 case 8:
+444 {
+445 // skip multiline
+446 result = {
+447 skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
+448 };
+449 break;
+450 }
+451 case 9:
+452 {
+453 // delete to end
+454 result = {
+455 remove: numCharsLeft
+456 };
+457 break;
+458 }
+459 case 10:
+460 {
+461 // skip to end
+462 result = {
+463 skip: numCharsLeft
+464 };
+465 break;
+466 }
+467 }
+468 var maxOrig = numCharsLeft - 1;
+469 if ('remove' in result) {
+470 result.remove = Math.min(result.remove, maxOrig);
+471 } else if ('skip' in result) {
+472 result.skip = Math.min(result.skip, maxOrig);
+473 }
+474 return result;
+475 }
+476
+477 function randomTwoPropAttribs(opcode, rand) {
+478 // assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
+479 if (opcode == '-' || rand.nextInt(3)) {
+480 return '';
+481 } else if (rand.nextInt(3)) {
+482 if (opcode == '+' || rand.nextInt(2)) {
+483 return '*' + Changeset.numToString(rand.nextInt(2) * 2 + 1);
+484 } else {
+485 return '*' + Changeset.numToString(rand.nextInt(2) * 2);
+486 }
+487 } else {
+488 if (opcode == '+' || rand.nextInt(4) == 0) {
+489 return '*1*3';
+490 } else {
+491 return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)];
+492 }
+493 }
+494 }
+495
+496 function randomTestChangeset(origText, rand, withAttribs) {
+497 var charBank = Changeset.stringAssembler();
+498 var textLeft = origText; // always keep final newline
+499 var outTextAssem = Changeset.stringAssembler();
+500 var opAssem = Changeset.smartOpAssembler();
+501 var oldLen = origText.length;
+502
+503 var nextOp = Changeset.newOp();
+504
+505 function appendMultilineOp(opcode, txt) {
+506 nextOp.opcode = opcode;
+507 if (withAttribs) {
+508 nextOp.attribs = randomTwoPropAttribs(opcode, rand);
+509 }
+510 txt.replace(/\n|[^\n]+/g, function (t) {
+511 if (t == '\n') {
+512 nextOp.chars = 1;
+513 nextOp.lines = 1;
+514 opAssem.append(nextOp);
+515 } else {
+516 nextOp.chars = t.length;
+517 nextOp.lines = 0;
+518 opAssem.append(nextOp);
+519 }
+520 return '';
+521 });
+522 }
+523
+524 function doOp() {
+525 var o = randomStringOperation(textLeft.length, rand);
+526 if (o.insert) {
+527 var txt = o.insert;
+528 charBank.append(txt);
+529 outTextAssem.append(txt);
+530 appendMultilineOp('+', txt);
+531 } else if (o.skip) {
+532 var txt = textLeft.substring(0, o.skip);
+533 textLeft = textLeft.substring(o.skip);
+534 outTextAssem.append(txt);
+535 appendMultilineOp('=', txt);
+536 } else if (o.remove) {
+537 var txt = textLeft.substring(0, o.remove);
+538 textLeft = textLeft.substring(o.remove);
+539 appendMultilineOp('-', txt);
+540 }
+541 }
+542
+543 while (textLeft.length > 1) doOp();
+544 for (var i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
+545 var outText = outTextAssem.toString() + '\n';
+546 opAssem.endDocument();
+547 var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
+548 Changeset.checkRep(cs);
+549 return [cs, outText];
+550 }
+551
+552 function testCompose(randomSeed) {
+553 var rand = new random();
+554 print("> testCompose#" + randomSeed);
+555
+556 var p = AttributePoolFactory.createAttributePool();
+557
+558 var startText = randomMultiline(10, 20, rand) + '\n';
+559
+560 var x1 = randomTestChangeset(startText, rand);
+561 var change1 = x1[0];
+562 var text1 = x1[1];
+563
+564 var x2 = randomTestChangeset(text1, rand);
+565 var change2 = x2[0];
+566 var text2 = x2[1];
+567
+568 var x3 = randomTestChangeset(text2, rand);
+569 var change3 = x3[0];
+570 var text3 = x3[1];
+571
+572 //print(literal(Changeset.toBaseTen(startText)));
+573 //print(literal(Changeset.toBaseTen(change1)));
+574 //print(literal(Changeset.toBaseTen(change2)));
+575 var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p));
+576 var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p));
+577 var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p));
+578 var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p));
+579 assertEqualStrings(change123, change123a);
+580
+581 assertEqualStrings(text2, Changeset.applyToText(change12, startText));
+582 assertEqualStrings(text3, Changeset.applyToText(change23, text1));
+583 assertEqualStrings(text3, Changeset.applyToText(change123, startText));
+584 }
+585
+586 for (var i = 0; i < 30; i++) testCompose(i);
+587
+588 (function simpleComposeAttributesTest() {
+589 print("> simpleComposeAttributesTest");
+590 var p = AttributePoolFactory.createAttributePool();
+591 p.putAttrib(['bold', '']);
+592 p.putAttrib(['bold', 'true']);
+593 var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x");
+594 var cs2 = Changeset.checkRep("Z:3>0*0|1=3$");
+595 var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p));
+596 assertEqualStrings("Z:2>1+1*0|1=2$x", cs12);
+597 })();
+598
+599 (function followAttributesTest() {
+600 var p = AttributePoolFactory.createAttributePool();
+601 p.putAttrib(['x', '']);
+602 p.putAttrib(['x', 'abc']);
+603 p.putAttrib(['x', 'def']);
+604 p.putAttrib(['y', '']);
+605 p.putAttrib(['y', 'abc']);
+606 p.putAttrib(['y', 'def']);
+607
+608 function testFollow(a, b, afb, bfa, merge) {
+609 assertEqualStrings(afb, Changeset.followAttributes(a, b, p));
+610 assertEqualStrings(bfa, Changeset.followAttributes(b, a, p));
+611 assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p));
+612 assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p));
+613 }
+614
+615 testFollow('', '', '', '', '');
+616 testFollow('*0', '', '', '*0', '*0');
+617 testFollow('*0', '*0', '', '', '*0');
+618 testFollow('*0', '*1', '', '*0', '*0');
+619 testFollow('*1', '*2', '', '*1', '*1');
+620 testFollow('*0*1', '', '', '*0*1', '*0*1');
+621 testFollow('*0*4', '*2*3', '*3', '*0', '*0*3');
+622 testFollow('*0*4', '*2', '', '*0*4', '*0*4');
+623 })();
+624
+625 function testFollow(randomSeed) {
+626 var rand = new random();
+627 print("> testFollow#" + randomSeed);
+628
+629 var p = AttributePoolFactory.createAttributePool();
+630
+631 var startText = randomMultiline(10, 20, rand) + '\n';
+632
+633 var cs1 = randomTestChangeset(startText, rand)[0];
+634 var cs2 = randomTestChangeset(startText, rand)[0];
+635
+636 var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p));
+637 var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p));
+638
+639 var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb));
+640 var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa));
+641
+642 assertEqualStrings(merge1, merge2);
+643 }
+644
+645 for (var i = 0; i < 30; i++) testFollow(i);
+646
+647 function testSplitJoinAttributionLines(randomSeed) {
+648 var rand = new random();
+649 print("> testSplitJoinAttributionLines#" + randomSeed);
+650
+651 var doc = randomMultiline(10, 20, rand) + '\n';
+652
+653 function stringToOps(str) {
+654 var assem = Changeset.mergingOpAssembler();
+655 var o = Changeset.newOp('+');
+656 o.chars = 1;
+657 for (var i = 0; i < str.length; i++) {
+658 var c = str.charAt(i);
+659 o.lines = (c == '\n' ? 1 : 0);
+660 o.attribs = (c == 'a' || c == 'b' ? '*' + c : '');
+661 assem.append(o);
+662 }
+663 return assem.toString();
+664 }
+665
+666 var theJoined = stringToOps(doc);
+667 var theSplit = doc.match(/[^\n]*\n/g).map(stringToOps);
+668
+669 assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc));
+670 assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit));
+671 }
+672
+673 for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i);
+674
+675 (function testMoveOpsToNewPool() {
+676 print("> testMoveOpsToNewPool");
+677
+678 var pool1 = AttributePoolFactory.createAttributePool();
+679 var pool2 = AttributePoolFactory.createAttributePool();
+680
+681 pool1.putAttrib(['baz', 'qux']);
+682 pool1.putAttrib(['foo', 'bar']);
+683
+684 pool2.putAttrib(['foo', 'bar']);
+685
+686 assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab');
+687 assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1');
+688 })();
+689
+690
+691 (function testMakeSplice() {
+692 print("> testMakeSplice");
+693
+694 var t = "a\nb\nc\n";
+695 var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t);
+696 assertEqualStrings("a\nb\ncdef\n", t2);
+697
+698 })();
+699
+700 (function testToSplices() {
+701 print("> testToSplices");
+702
+703 var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk');
+704 var correctSplices = [
+705 [5, 8, "123456789"],
+706 [9, 17, "abcdefghijk"]
+707 ];
+708 assertEqualArrays(correctSplices, Changeset.toSplices(cs));
+709 })();
+710
+711 function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) {
+712 print("> testCharacterRangeFollow#" + testId);
+713
+714 var cs = Changeset.checkRep(cs);
+715 assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter));
+716
+717 }
+718
+719 testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]);
+720 testCharacterRangeFollow(2, "Z:bc<6|x=b4|2-6$", [400, 407], false, [400, 401]);
+721 testCharacterRangeFollow(3, "Z:4>0-3+3$abc", [0, 3], false, [3, 3]);
+722 testCharacterRangeFollow(4, "Z:4>0-3+3$abc", [0, 3], true, [0, 0]);
+723 testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1, 4], false, [5, 5]);
+724 testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1, 4], true, [2, 2]);
+725 testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0, 6], false, [1, 7]);
+726 testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0, 3], false, [1, 2]);
+727 testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2, 5], false, [5, 6]);
+728 testCharacterRangeFollow(10, "Z:2>1+1$a", [0, 0], false, [1, 1]);
+729 testCharacterRangeFollow(11, "Z:2>1+1$a", [0, 0], true, [0, 0]);
+730
+731 (function testOpAttributeValue() {
+732 print("> testOpAttributeValue");
+733
+734 var p = AttributePoolFactory.createAttributePool();
+735 p.putAttrib(['name', 'david']);
+736 p.putAttrib(['color', 'green']);
+737
+738 assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
+739 assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
+740 assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
+741 assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p));
+742 assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p));
+743 assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p));
+744 assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p));
+745 assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p));
+746 })();
+747
+748 function testAppendATextToAssembler(testId, atext, correctOps) {
+749 print("> testAppendATextToAssembler#" + testId);
+750
+751 var assem = Changeset.smartOpAssembler();
+752 Changeset.appendATextToAssembler(atext, assem);
+753 assertEqualStrings(correctOps, assem.toString());
+754 }
+755
+756 testAppendATextToAssembler(1, {
+757 text: "\n",
+758 attribs: "|1+1"
+759 }, "");
+760 testAppendATextToAssembler(2, {
+761 text: "\n\n",
+762 attribs: "|2+2"
+763 }, "|1+1");
+764 testAppendATextToAssembler(3, {
+765 text: "\n\n",
+766 attribs: "*x|2+2"
+767 }, "*x|1+1");
+768 testAppendATextToAssembler(4, {
+769 text: "\n\n",
+770 attribs: "*x|1+1|1+1"
+771 }, "*x|1+1");
+772 testAppendATextToAssembler(5, {
+773 text: "foo\n",
+774 attribs: "|1+4"
+775 }, "+3");
+776 testAppendATextToAssembler(6, {
+777 text: "\nfoo\n",
+778 attribs: "|2+5"
+779 }, "|1+1+3");
+780 testAppendATextToAssembler(7, {
+781 text: "\nfoo\n",
+782 attribs: "*x|2+5"
+783 }, "*x|1+1*x+3");
+784 testAppendATextToAssembler(8, {
+785 text: "\n\n\nfoo\n",
+786 attribs: "|2+2*x|2+5"
+787 }, "|2+2*x|1+1*x+3");
+788
+789 function testMakeAttribsString(testId, pool, opcode, attribs, correctString) {
+790 print("> testMakeAttribsString#" + testId);
+791
+792 var p = poolOrArray(pool);
+793 var str = Changeset.makeAttribsString(opcode, attribs, p);
+794 assertEqualStrings(correctString, str);
+795 }
+796
+797 testMakeAttribsString(1, ['bold,'], '+', [
+798 ['bold', '']
+799 ], '');
+800 testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [
+801 ['bold', '']
+802 ], '*1');
+803 testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [
+804 ['abc', 'def'],
+805 ['bold', 'true']
+806 ], '*0*1');
+807 testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [
+808 ['bold', 'true'],
+809 ['abc', 'def']
+810 ], '*0*1');
+811
+812 function testSubattribution(testId, astr, start, end, correctOutput) {
+813 print("> testSubattribution#" + testId);
+814
+815 var str = Changeset.subattribution(astr, start, end);
+816 assertEqualStrings(correctOutput, str);
+817 }
+818
+819 testSubattribution(1, "+1", 0, 0, "");
+820 testSubattribution(2, "+1", 0, 1, "+1");
+821 testSubattribution(3, "+1", 0, undefined, "+1");
+822 testSubattribution(4, "|1+1", 0, 0, "");
+823 testSubattribution(5, "|1+1", 0, 1, "|1+1");
+824 testSubattribution(6, "|1+1", 0, undefined, "|1+1");
+825 testSubattribution(7, "*0+1", 0, 0, "");
+826 testSubattribution(8, "*0+1", 0, 1, "*0+1");
+827 testSubattribution(9, "*0+1", 0, undefined, "*0+1");
+828 testSubattribution(10, "*0|1+1", 0, 0, "");
+829 testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1");
+830 testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1");
+831 testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1");
+832 testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2");
+833 testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1");
+834 testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1");
+835 testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2");
+836 testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3");
+837 testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3");
+838 testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3");
+839 testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3");
+840 testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3");
+841 testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3");
+842 testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2");
+843 testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1");
+844 testSubattribution(26, "*0+2+1*1+3", 6, undefined, "");
+845 testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1");
+846 testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2");
+847 testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1");
+848 testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1");
+849 testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2");
+850 testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3");
+851 testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3");
+852 testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3");
+853 testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3");
+854 testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3");
+855 testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3");
+856 testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2");
+857 testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1");
+858 testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2");
+859 testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3");
+860 testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3");
+861
+862 function testFilterAttribNumbers(testId, cs, filter, correctOutput) {
+863 print("> testFilterAttribNumbers#" + testId);
+864
+865 var str = Changeset.filterAttribNumbers(cs, filter);
+866 assertEqualStrings(correctOutput, str);
+867 }
+868
+869 testFilterAttribNumbers(1, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
+870 return (n % 2) == 0;
+871 }, "*0+1+2+3+4*2+5*0*2*c+6");
+872 testFilterAttribNumbers(2, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
+873 return (n % 2) == 1;
+874 }, "*1+1+2+3*1+4+5*1*b+6");
+875
+876 function testInverse(testId, cs, lines, alines, pool, correctOutput) {
+877 print("> testInverse#" + testId);
+878
+879 pool = poolOrArray(pool);
+880 var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool);
+881 assertEqualStrings(correctOutput, str);
+882 }
+883
+884 // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--"
+885 testInverse(1, "Z:9>0=1*0=1*1=1=2*0=2*1|1=2$", null, ["+4*1+5"], ['bold,', 'bold,true'], "Z:9>0=2*0=1=2*1=2$");
+886
+887 function testMutateTextLines(testId, cs, lines, correctLines) {
+888 print("> testMutateTextLines#" + testId);
+889
+890 var a = lines.slice();
+891 Changeset.mutateTextLines(cs, a);
+892 assertEqualArrays(correctLines, a);
+893 }
+894
+895 testMutateTextLines(1, "Z:4<1|1-2-1|1+1+1$\nc", ["a\n", "b\n"], ["\n", "c\n"]);
+896 testMutateTextLines(2, "Z:4>0|1-2-1|2+3$\nc\n", ["a\n", "b\n"], ["\n", "c\n", "\n"]);
+897
+898 function testInverseRandom(randomSeed) {
+899 var rand = new random();
+900 print("> testInverseRandom#" + randomSeed);
+901
+902 var p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']);
+903
+904 var startText = randomMultiline(10, 20, rand) + '\n';
+905 var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText);
+906 var lines = startText.slice(0, -1).split('\n').map(function (s) {
+907 return s + '\n';
+908 });
+909
+910 var stylifier = randomTestChangeset(startText, rand, true)[0];
+911
+912 //print(alines.join('\n'));
+913 Changeset.mutateAttributionLines(stylifier, alines, p);
+914 //print(stylifier);
+915 //print(alines.join('\n'));
+916 Changeset.mutateTextLines(stylifier, lines);
+917
+918 var changeset = randomTestChangeset(lines.join(''), rand, true)[0];
+919 var inverseChangeset = Changeset.inverse(changeset, lines, alines, p);
+920
+921 var origLines = lines.slice();
+922 var origALines = alines.slice();
+923
+924 Changeset.mutateTextLines(changeset, lines);
+925 Changeset.mutateAttributionLines(changeset, alines, p);
+926 //print(origALines.join('\n'));
+927 //print(changeset);
+928 //print(inverseChangeset);
+929 //print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n'));
+930 //print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n'));
+931 //print(alines.join('\n'));
+932 Changeset.mutateTextLines(inverseChangeset, lines);
+933 Changeset.mutateAttributionLines(inverseChangeset, alines, p);
+934 //print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n'));
+935 assertEqualArrays(origLines, lines);
+936 assertEqualArrays(origALines, alines);
+937 }
+938
+939 for (var i = 0; i < 30; i++) testInverseRandom(i);
+940 }
+941
+942 runTests();
+943
\ No newline at end of file
diff --git a/doc/jsdoc/symbols/src/node_server.js.html b/doc/jsdoc/symbols/src/node_server.js.html
new file mode 100644
index 00000000..74f7a386
--- /dev/null
+++ b/doc/jsdoc/symbols/src/node_server.js.html
@@ -0,0 +1,150 @@
+ 1 /**
+ 2 * 2011 Peter 'Pita' Martischka
+ 3 *
+ 4 * Licensed under the Apache License, Version 2.0 (the "License");
+ 5 * you may not use this file except in compliance with the License.
+ 6 * You may obtain a copy of the License at
+ 7 *
+ 8 * http://www.apache.org/licenses/LICENSE-2.0
+ 9 *
+ 10 * Unless required by applicable law or agreed to in writing, software
+ 11 * distributed under the License is distributed on an "AS-IS" BASIS,
+ 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ 13 * See the License for the specific language governing permissions and
+ 14 * limitations under the License.
+ 15 */
+ 16
+ 17 var http = require('http')
+ 18 , url = require('url')
+ 19 , fs = require('fs')
+ 20 , io = require('socket.io')
+ 21 , sys = require('sys')
+ 22 , server;
+ 23
+ 24 server = http.createServer(function(req, res){
+ 25 var path = url.parse(req.url).pathname;
+ 26
+ 27 if(path.substring(0,"/static".length) == "/static" || path.substring(0,"/p/".length) == "/p/")
+ 28 {
+ 29 if(path.substring(0,"/p/".length) == "/p/")
+ 30 {
+ 31 if(path.length < 7)
+ 32 send404(res, path);
+ 33
+ 34 path = "/static/padhtml";
+ 35 }
+ 36
+ 37 sendFile(res, path, __dirname + "/.." + path);
+ 38 }
+ 39 else if(path == "/")
+ 40 {
+ 41 sendRedirect(res, path, "/p/test");
+ 42 }
+ 43 else if(path == "/newpad")
+ 44 {
+ 45 sendRedirect(res, path, "/p/" + randomPadName());
+ 46 }
+ 47 else if(path == "/ep/pad/reconnect")
+ 48 {
+ 49 if(req.headers.referer != null)
+ 50 sendRedirect(res, path, req.headers.referer);
+ 51 else
+ 52 send404(res, path);
+ 53 }
+ 54 else
+ 55 {
+ 56 send404(res, path);
+ 57 }
+ 58 });
+ 59 server.listen(9001);
+ 60
+ 61 function randomPadName() {
+ 62 var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
+ 63 var string_length = 10;
+ 64 var randomstring = '';
+ 65 for (var i=0; i<string_length; i++) {
+ 66 var rnum = Math.floor(Math.random() * chars.length);
+ 67 randomstring += chars.substring(rnum,rnum+1);
+ 68 }
+ 69 return randomstring;
+ 70 }
+ 71
+ 72 function sendFile(res, reqPath, path)
+ 73 {
+ 74 fs.readFile(path, function(err, data){
+ 75 if (err){
+ 76 send404(res, reqPath);
+ 77 } else {
+ 78 var contentType = "text/html";
+ 79
+ 80 if (path.substring(path.length -3, path.length) == ".js")
+ 81 contentType = "text/javascript";
+ 82 else if (path.substring(path.length -4, path.length) == ".css")
+ 83 contentType = "text/css";
+ 84 else if (path.substring(path.length -4, path.length) == ".gif")
+ 85 contentType = "image/gif";
+ 86
+ 87 res.writeHead(200, {'Content-Type': contentType});
+ 88 res.write(data, 'utf8');
+ 89 res.end();
+ 90
+ 91 requestLog(200, reqPath, "-> " + path);
+ 92 }
+ 93 });
+ 94 }
+ 95
+ 96 function send404(res, reqPath)
+ 97 {
+ 98 res.writeHead(404);
+ 99 res.write("404 - Not Found");
+100 res.end();
+101
+102 requestLog(404, reqPath, "NOT FOUND!");
+103 }
+104
+105 function sendRedirect(res, reqPath, location)
+106 {
+107 res.writeHead(302, {'Location': location});
+108 res.end();
+109
+110 requestLog(302, reqPath, "-> " + location);
+111 }
+112
+113 function requestLog(code, path, desc)
+114 {
+115 console.log(code +", " + path + ", " + desc);
+116 }
+117
+118 var io = io.listen(server);
+119 var messageHandler = require("./MessageHandler");
+120 messageHandler.setSocketIO(io);
+121
+122 io.on('connection', function(client){
+123 try{
+124 messageHandler.handleConnect(client);
+125 }catch(e){console.error(e);}
+126
+127 client.on('message', function(message){
+128 try{
+129 messageHandler.handleMessage(client, message);
+130 }catch(e){console.error(e);}
+131 });
+132
+133 client.on('disconnect', function(){
+134 try{
+135 messageHandler.handleDisconnect(client);
+136 }catch(e){console.error(e);}
+137 });
+138 });
+139
+140
+141
+142
+143
\ No newline at end of file
diff --git a/docs.html b/docs.html
new file mode 100644
index 00000000..213312fb
--- /dev/null
+++ b/docs.html
@@ -0,0 +1,111 @@
+
+
+ Connect
+
+
+
+
+
+Connectmarkdown here | |
\ No newline at end of file