summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xlib/ot/client.js312
-rwxr-xr-xlib/ot/editor-socketio-server.js146
-rw-r--r--lib/ot/index.js8
-rw-r--r--lib/ot/selection.js117
-rw-r--r--lib/ot/server.js46
-rw-r--r--lib/ot/simple-text-operation.js188
-rw-r--r--lib/ot/text-operation.js530
-rw-r--r--lib/ot/wrapped-operation.js80
-rw-r--r--lib/realtime.js201
-rw-r--r--public/js/index.js202
-rwxr-xr-xpublic/vendor/ot/ajax-adapter.js116
-rwxr-xr-xpublic/vendor/ot/client.js312
-rwxr-xr-xpublic/vendor/ot/codemirror-adapter.js393
-rw-r--r--public/vendor/ot/compress.sh10
-rwxr-xr-xpublic/vendor/ot/editor-client.js354
-rw-r--r--public/vendor/ot/ot.min.js1
-rwxr-xr-xpublic/vendor/ot/selection.js117
-rwxr-xr-xpublic/vendor/ot/socketio-adapter.js66
-rwxr-xr-xpublic/vendor/ot/text-operation.js530
-rwxr-xr-xpublic/vendor/ot/undo-manager.js111
-rwxr-xr-xpublic/vendor/ot/wrapped-operation.js80
-rw-r--r--public/views/body.ejs2
-rw-r--r--public/views/foot.ejs2
23 files changed, 3668 insertions, 256 deletions
diff --git a/lib/ot/client.js b/lib/ot/client.js
new file mode 100755
index 00000000..7ee19fdc
--- /dev/null
+++ b/lib/ot/client.js
@@ -0,0 +1,312 @@
+// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
+
+if (typeof ot === 'undefined') {
+ var ot = {};
+}
+
+ot.Client = (function (global) {
+ 'use strict';
+
+ // Client constructor
+ function Client (revision) {
+ this.revision = revision; // the next expected revision number
+ this.setState(synchronized_); // start state
+ }
+
+ Client.prototype.setState = function (state) {
+ this.state = state;
+ };
+
+ // Call this method when the user changes the document.
+ Client.prototype.applyClient = function (operation) {
+ this.setState(this.state.applyClient(this, operation));
+ };
+
+ // Call this method with a new operation from the server
+ Client.prototype.applyServer = function (revision, operation) {
+ this.setState(this.state.applyServer(this, revision, operation));
+ };
+
+ Client.prototype.applyOperations = function (head, operations) {
+ this.setState(this.state.applyOperations(this, head, operations));
+ };
+
+ Client.prototype.serverAck = function (revision) {
+ this.setState(this.state.serverAck(this, revision));
+ };
+
+ Client.prototype.serverReconnect = function () {
+ if (typeof this.state.resend === 'function') { this.state.resend(this); }
+ };
+
+ // Transforms a selection from the latest known server state to the current
+ // client state. For example, if we get from the server the information that
+ // another user's cursor is at position 3, but the server hasn't yet received
+ // our newest operation, an insertion of 5 characters at the beginning of the
+ // document, the correct position of the other user's cursor in our current
+ // document is 8.
+ Client.prototype.transformSelection = function (selection) {
+ return this.state.transformSelection(selection);
+ };
+
+ // Override this method.
+ Client.prototype.sendOperation = function (revision, operation) {
+ throw new Error("sendOperation must be defined in child class");
+ };
+
+ // Override this method.
+ Client.prototype.applyOperation = function (operation) {
+ throw new Error("applyOperation must be defined in child class");
+ };
+
+
+ // In the 'Synchronized' state, there is no pending operation that the client
+ // has sent to the server.
+ function Synchronized () {}
+ Client.Synchronized = Synchronized;
+
+ Synchronized.prototype.applyClient = function (client, operation) {
+ // When the user makes an edit, send the operation to the server and
+ // switch to the 'AwaitingConfirm' state
+ client.sendOperation(client.revision, operation);
+ return new AwaitingConfirm(operation);
+ };
+
+ Synchronized.prototype.applyServer = function (client, revision, operation) {
+ if (revision - client.revision > 1) {
+ throw new Error("Invalid revision.");
+ }
+ client.revision = revision;
+ // When we receive a new operation from the server, the operation can be
+ // simply applied to the current document
+ client.applyOperation(operation);
+ return this;
+ };
+
+ Synchronized.prototype.serverAck = function (client, revision) {
+ throw new Error("There is no pending operation.");
+ };
+
+ // Nothing to do because the latest server state and client state are the same.
+ Synchronized.prototype.transformSelection = function (x) { return x; };
+
+ // Singleton
+ var synchronized_ = new Synchronized();
+
+
+ // In the 'AwaitingConfirm' state, there's one operation the client has sent
+ // to the server and is still waiting for an acknowledgement.
+ function AwaitingConfirm (outstanding) {
+ // Save the pending operation
+ this.outstanding = outstanding;
+ }
+ Client.AwaitingConfirm = AwaitingConfirm;
+
+ AwaitingConfirm.prototype.applyClient = function (client, operation) {
+ // When the user makes an edit, don't send the operation immediately,
+ // instead switch to 'AwaitingWithBuffer' state
+ return new AwaitingWithBuffer(this.outstanding, operation);
+ };
+
+ AwaitingConfirm.prototype.applyServer = function (client, revision, operation) {
+ if (revision - client.revision > 1) {
+ throw new Error("Invalid revision.");
+ }
+ client.revision = revision;
+ // This is another client's operation. Visualization:
+ //
+ // /\
+ // this.outstanding / \ operation
+ // / \
+ // \ /
+ // pair[1] \ / pair[0] (new outstanding)
+ // (can be applied \/
+ // to the client's
+ // current document)
+ var pair = operation.constructor.transform(this.outstanding, operation);
+ client.applyOperation(pair[1]);
+ return new AwaitingConfirm(pair[0]);
+ };
+
+ AwaitingConfirm.prototype.serverAck = function (client, revision) {
+ if (revision - client.revision > 1) {
+ return new Stale(this.outstanding, client, revision).getOperations();
+ }
+ client.revision = revision;
+ // The client's operation has been acknowledged
+ // => switch to synchronized state
+ return synchronized_;
+ };
+
+ AwaitingConfirm.prototype.transformSelection = function (selection) {
+ return selection.transform(this.outstanding);
+ };
+
+ AwaitingConfirm.prototype.resend = function (client) {
+ // The confirm didn't come because the client was disconnected.
+ // Now that it has reconnected, we resend the outstanding operation.
+ client.sendOperation(client.revision, this.outstanding);
+ };
+
+
+ // In the 'AwaitingWithBuffer' state, the client is waiting for an operation
+ // to be acknowledged by the server while buffering the edits the user makes
+ function AwaitingWithBuffer (outstanding, buffer) {
+ // Save the pending operation and the user's edits since then
+ this.outstanding = outstanding;
+ this.buffer = buffer;
+ }
+ Client.AwaitingWithBuffer = AwaitingWithBuffer;
+
+ AwaitingWithBuffer.prototype.applyClient = function (client, operation) {
+ // Compose the user's changes onto the buffer
+ var newBuffer = this.buffer.compose(operation);
+ return new AwaitingWithBuffer(this.outstanding, newBuffer);
+ };
+
+ AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) {
+ if (revision - client.revision > 1) {
+ throw new Error("Invalid revision.");
+ }
+ client.revision = revision;
+ // Operation comes from another client
+ //
+ // /\
+ // this.outstanding / \ operation
+ // / \
+ // /\ /
+ // this.buffer / \* / pair1[0] (new outstanding)
+ // / \/
+ // \ /
+ // pair2[1] \ / pair2[0] (new buffer)
+ // the transformed \/
+ // operation -- can
+ // be applied to the
+ // client's current
+ // document
+ //
+ // * pair1[1]
+ var transform = operation.constructor.transform;
+ var pair1 = transform(this.outstanding, operation);
+ var pair2 = transform(this.buffer, pair1[1]);
+ client.applyOperation(pair2[1]);
+ return new AwaitingWithBuffer(pair1[0], pair2[0]);
+ };
+
+ AwaitingWithBuffer.prototype.serverAck = function (client, revision) {
+ if (revision - client.revision > 1) {
+ return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
+ }
+ client.revision = revision;
+ // The pending operation has been acknowledged
+ // => send buffer
+ client.sendOperation(client.revision, this.buffer);
+ return new AwaitingConfirm(this.buffer);
+ };
+
+ AwaitingWithBuffer.prototype.transformSelection = function (selection) {
+ return selection.transform(this.outstanding).transform(this.buffer);
+ };
+
+ AwaitingWithBuffer.prototype.resend = function (client) {
+ // The confirm didn't come because the client was disconnected.
+ // Now that it has reconnected, we resend the outstanding operation.
+ client.sendOperation(client.revision, this.outstanding);
+ };
+
+
+ function Stale(acknowlaged, client, revision) {
+ this.acknowlaged = acknowlaged;
+ this.client = client;
+ this.revision = revision;
+ }
+ Client.Stale = Stale;
+
+ Stale.prototype.applyClient = function (client, operation) {
+ return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
+ };
+
+ Stale.prototype.applyServer = function (client, revision, operation) {
+ throw new Error("Ignored server-side change.");
+ };
+
+ Stale.prototype.applyOperations = function (client, head, operations) {
+ var transform = this.acknowlaged.constructor.transform;
+ for (var i = 0; i < operations.length; i++) {
+ var op = ot.TextOperation.fromJSON(operations[i]);
+ var pair = transform(this.acknowlaged, op);
+ client.applyOperation(pair[1]);
+ this.acknowlaged = pair[0];
+ }
+ client.revision = this.revision;
+ return synchronized_;
+ };
+
+ Stale.prototype.serverAck = function (client, revision) {
+ throw new Error("There is no pending operation.");
+ };
+
+ Stale.prototype.transformSelection = function (selection) {
+ return selection;
+ };
+
+ Stale.prototype.getOperations = function () {
+ this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
+ return this;
+ };
+
+
+ function StaleWithBuffer(acknowlaged, buffer, client, revision) {
+ this.acknowlaged = acknowlaged;
+ this.buffer = buffer;
+ this.client = client;
+ this.revision = revision;
+ }
+ Client.StaleWithBuffer = StaleWithBuffer;
+
+ StaleWithBuffer.prototype.applyClient = function (client, operation) {
+ var buffer = this.buffer.compose(operation);
+ return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
+ };
+
+ StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
+ throw new Error("Ignored server-side change.");
+ };
+
+ StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
+ var transform = this.acknowlaged.constructor.transform;
+ for (var i = 0; i < operations.length; i++) {
+ var op = ot.TextOperation.fromJSON(operations[i]);
+ var pair1 = transform(this.acknowlaged, op);
+ var pair2 = transform(this.buffer, pair1[1]);
+ client.applyOperation(pair2[1]);
+ this.acknowlaged = pair1[0];
+ this.buffer = pair2[0];
+ }
+ client.revision = this.revision;
+ client.sendOperation(client.revision, this.buffer);
+ return new AwaitingConfirm(this.buffer);
+ };
+
+ StaleWithBuffer.prototype.serverAck = function (client, revision) {
+ throw new Error("There is no pending operation.");
+ };
+
+ StaleWithBuffer.prototype.transformSelection = function (selection) {
+ return selection;
+ };
+
+ StaleWithBuffer.prototype.getOperations = function () {
+ this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
+ return this;
+ };
+
+
+ return Client;
+
+}(this));
+
+if (typeof module === 'object') {
+ module.exports = ot.Client;
+}
+
diff --git a/lib/ot/editor-socketio-server.js b/lib/ot/editor-socketio-server.js
new file mode 100755
index 00000000..aae156fc
--- /dev/null
+++ b/lib/ot/editor-socketio-server.js
@@ -0,0 +1,146 @@
+'use strict';
+
+var EventEmitter = require('events').EventEmitter;
+var TextOperation = require('./text-operation');
+var WrappedOperation = require('./wrapped-operation');
+var Server = require('./server');
+var Selection = require('./selection');
+var util = require('util');
+
+var LZString = require('lz-string');
+
+function EditorSocketIOServer(document, operations, docId, mayWrite) {
+ EventEmitter.call(this);
+ Server.call(this, document, operations);
+ this.users = {};
+ this.docId = docId;
+ this.mayWrite = mayWrite || function (_, cb) {
+ cb(true);
+ };
+}
+
+util.inherits(EditorSocketIOServer, Server);
+extend(EditorSocketIOServer.prototype, EventEmitter.prototype);
+
+function extend(target, source) {
+ for (var key in source) {
+ if (source.hasOwnProperty(key)) {
+ target[key] = source[key];
+ }
+ }
+}
+
+EditorSocketIOServer.prototype.addClient = function (socket) {
+ var self = this;
+ socket.join(this.docId);
+ var docOut = {
+ str: this.document,
+ revision: this.operations.length,
+ clients: this.users
+ };
+ socket.emit('doc', LZString.compressToUTF16(JSON.stringify(docOut)));
+ socket.on('operation', function (revision, operation, selection) {
+ operation = LZString.decompressFromUTF16(operation);
+ operation = JSON.parse(operation);
+ self.mayWrite(socket, function (mayWrite) {
+ if (!mayWrite) {
+ console.log("User doesn't have the right to edit.");
+ return;
+ }
+ self.onOperation(socket, revision, operation, selection);
+ });
+ });
+ socket.on('get_operations', function (base, head) {
+ self.onGetOperations(socket, base, head);
+ });
+ socket.on('selection', function (obj) {
+ self.mayWrite(socket, function (mayWrite) {
+ if (!mayWrite) {
+ console.log("User doesn't have the right to edit.");
+ return;
+ }
+ self.updateSelection(socket, obj && Selection.fromJSON(obj));
+ });
+ });
+ socket.on('disconnect', function () {
+ //console.log("Disconnect");
+ socket.leave(self.docId);
+ self.onDisconnect(socket);
+ /*
+ if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) {
+ self.emit('empty-room');
+ }
+ */
+ });
+};
+
+EditorSocketIOServer.prototype.onOperation = function (socket, revision, operation, selection) {
+ var wrapped;
+ try {
+ wrapped = new WrappedOperation(
+ TextOperation.fromJSON(operation),
+ selection && Selection.fromJSON(selection)
+ );
+ } catch (exc) {
+ console.error("Invalid operation received: " + exc);
+ return;
+ }
+
+ try {
+ var clientId = socket.id;
+ var wrappedPrime = this.receiveOperation(revision, wrapped);
+ //console.log("new operation: " + JSON.stringify(wrapped));
+ this.getClient(clientId).selection = wrappedPrime.meta;
+ revision = this.operations.length;
+ socket.emit('ack', revision);
+ socket.broadcast.in(this.docId).emit(
+ 'operation', clientId, revision,
+ wrappedPrime.wrapped.toJSON(), wrappedPrime.meta
+ );
+ this.isDirty = true;
+ } catch (exc) {
+ console.error(exc);
+ }
+};
+
+EditorSocketIOServer.prototype.onGetOperations = function (socket, base, head) {
+ var operations = this.operations.slice(base, head).map(function (op) {
+ return op.wrapped.toJSON();
+ });
+ operations = LZString.compressToUTF16(JSON.stringify(operations));
+ socket.emit('operations', head, operations);
+};
+
+EditorSocketIOServer.prototype.updateSelection = function (socket, selection) {
+ var clientId = socket.id;
+ if (selection) {
+ this.getClient(clientId).selection = selection;
+ } else {
+ delete this.getClient(clientId).selection;
+ }
+ socket.broadcast.to(this.docId).emit('selection', clientId, selection);
+};
+
+EditorSocketIOServer.prototype.setName = function (socket, name) {
+ var clientId = socket.id;
+ this.getClient(clientId).name = name;
+ socket.broadcast.to(this.docId).emit('set_name', clientId, name);
+};
+
+EditorSocketIOServer.prototype.setColor = function (socket, color) {
+ var clientId = socket.id;
+ this.getClient(clientId).color = color;
+ socket.broadcast.to(this.docId).emit('set_color', clientId, color);
+};
+
+EditorSocketIOServer.prototype.getClient = function (clientId) {
+ return this.users[clientId] || (this.users[clientId] = {});
+};
+
+EditorSocketIOServer.prototype.onDisconnect = function (socket) {
+ var clientId = socket.id;
+ delete this.users[clientId];
+ socket.broadcast.to(this.docId).emit('client_left', clientId);
+};
+
+module.exports = EditorSocketIOServer; \ No newline at end of file
diff --git a/lib/ot/index.js b/lib/ot/index.js
new file mode 100644
index 00000000..fcc94c11
--- /dev/null
+++ b/lib/ot/index.js
@@ -0,0 +1,8 @@
+exports.version = '0.0.15';
+
+exports.TextOperation = require('./text-operation');
+exports.SimpleTextOperation = require('./simple-text-operation');
+exports.Client = require('./client');
+exports.Server = require('./server');
+exports.Selection = require('./selection');
+exports.EditorSocketIOServer = require('./editor-socketio-server');
diff --git a/lib/ot/selection.js b/lib/ot/selection.js
new file mode 100644
index 00000000..72bf8bd6
--- /dev/null
+++ b/lib/ot/selection.js
@@ -0,0 +1,117 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.Selection = (function (global) {
+ 'use strict';
+
+ var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
+
+ // Range has `anchor` and `head` properties, which are zero-based indices into
+ // the document. The `anchor` is the side of the selection that stays fixed,
+ // `head` is the side of the selection where the cursor is. When both are
+ // equal, the range represents a cursor.
+ function Range (anchor, head) {
+ this.anchor = anchor;
+ this.head = head;
+ }
+
+ Range.fromJSON = function (obj) {
+ return new Range(obj.anchor, obj.head);
+ };
+
+ Range.prototype.equals = function (other) {
+ return this.anchor === other.anchor && this.head === other.head;
+ };
+
+ Range.prototype.isEmpty = function () {
+ return this.anchor === this.head;
+ };
+
+ Range.prototype.transform = function (other) {
+ function transformIndex (index) {
+ var newIndex = index;
+ var ops = other.ops;
+ for (var i = 0, l = other.ops.length; i < l; i++) {
+ if (TextOperation.isRetain(ops[i])) {
+ index -= ops[i];
+ } else if (TextOperation.isInsert(ops[i])) {
+ newIndex += ops[i].length;
+ } else {
+ newIndex -= Math.min(index, -ops[i]);
+ index += ops[i];
+ }
+ if (index < 0) { break; }
+ }
+ return newIndex;
+ }
+
+ var newAnchor = transformIndex(this.anchor);
+ if (this.anchor === this.head) {
+ return new Range(newAnchor, newAnchor);
+ }
+ return new Range(newAnchor, transformIndex(this.head));
+ };
+
+ // A selection is basically an array of ranges. Every range represents a real
+ // selection or a cursor in the document (when the start position equals the
+ // end position of the range). The array must not be empty.
+ function Selection (ranges) {
+ this.ranges = ranges || [];
+ }
+
+ Selection.Range = Range;
+
+ // Convenience method for creating selections only containing a single cursor
+ // and no real selection range.
+ Selection.createCursor = function (position) {
+ return new Selection([new Range(position, position)]);
+ };
+
+ Selection.fromJSON = function (obj) {
+ var objRanges = obj.ranges || obj;
+ for (var i = 0, ranges = []; i < objRanges.length; i++) {
+ ranges[i] = Range.fromJSON(objRanges[i]);
+ }
+ return new Selection(ranges);
+ };
+
+ Selection.prototype.equals = function (other) {
+ if (this.position !== other.position) { return false; }
+ if (this.ranges.length !== other.ranges.length) { return false; }
+ // FIXME: Sort ranges before comparing them?
+ for (var i = 0; i < this.ranges.length; i++) {
+ if (!this.ranges[i].equals(other.ranges[i])) { return false; }
+ }
+ return true;
+ };
+
+ Selection.prototype.somethingSelected = function () {
+ for (var i = 0; i < this.ranges.length; i++) {
+ if (!this.ranges[i].isEmpty()) { return true; }
+ }
+ return false;
+ };
+
+ // Return the more current selection information.
+ Selection.prototype.compose = function (other) {
+ return other;
+ };
+
+ // Update the selection with respect to an operation.
+ Selection.prototype.transform = function (other) {
+ for (var i = 0, newRanges = []; i < this.ranges.length; i++) {
+ newRanges[i] = this.ranges[i].transform(other);
+ }
+ return new Selection(newRanges);
+ };
+
+ return Selection;
+
+}(this));
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.Selection;
+}
diff --git a/lib/ot/server.js b/lib/ot/server.js
new file mode 100644
index 00000000..608d434b
--- /dev/null
+++ b/lib/ot/server.js
@@ -0,0 +1,46 @@
+if (typeof ot === 'undefined') {
+ var ot = {};
+}
+
+ot.Server = (function (global) {
+ 'use strict';
+
+ // Constructor. Takes the current document as a string and optionally the array
+ // of all operations.
+ function Server (document, operations) {
+ this.document = document;
+ this.operations = operations || [];
+ }
+
+ // Call this method whenever you receive an operation from a client.
+ Server.prototype.receiveOperation = function (revision, operation) {
+ if (revision < 0 || this.operations.length < revision) {
+ throw new Error("operation revision not in history");
+ }
+ // Find all operations that the client didn't know of when it sent the
+ // operation ...
+ var concurrentOperations = this.operations.slice(revision);
+
+ // ... and transform the operation against all these operations ...
+ var transform = operation.constructor.transform;
+ for (var i = 0; i < concurrentOperations.length; i++) {
+ operation = transform(operation, concurrentOperations[i])[0];
+ }
+
+ // ... and apply that on the document.
+ this.document = operation.apply(this.document);
+ // Store operation in history.
+ this.operations.push(operation);
+
+ // It's the caller's responsibility to send the operation to all connected
+ // clients and an acknowledgement to the creator.
+ return operation;
+ };
+
+ return Server;
+
+}(this));
+
+if (typeof module === 'object') {
+ module.exports = ot.Server;
+} \ No newline at end of file
diff --git a/lib/ot/simple-text-operation.js b/lib/ot/simple-text-operation.js
new file mode 100644
index 00000000..6db296e0
--- /dev/null
+++ b/lib/ot/simple-text-operation.js
@@ -0,0 +1,188 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.SimpleTextOperation = (function (global) {
+
+ var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
+
+ function SimpleTextOperation () {}
+
+
+ // Insert the string `str` at the zero-based `position` in the document.
+ function Insert (str, position) {
+ if (!this || this.constructor !== SimpleTextOperation) {
+ // => function was called without 'new'
+ return new Insert(str, position);
+ }
+ this.str = str;
+ this.position = position;
+ }
+
+ Insert.prototype = new SimpleTextOperation();
+ SimpleTextOperation.Insert = Insert;
+
+ Insert.prototype.toString = function () {
+ return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')';
+ };
+
+ Insert.prototype.equals = function (other) {
+ return other instanceof Insert &&
+ this.str === other.str &&
+ this.position === other.position;
+ };
+
+ Insert.prototype.apply = function (doc) {
+ return doc.slice(0, this.position) + this.str + doc.slice(this.position);
+ };
+
+
+ // Delete `count` many characters at the zero-based `position` in the document.
+ function Delete (count, position) {
+ if (!this || this.constructor !== SimpleTextOperation) {
+ return new Delete(count, position);
+ }
+ this.count = count;
+ this.position = position;
+ }
+
+ Delete.prototype = new SimpleTextOperation();
+ SimpleTextOperation.Delete = Delete;
+
+ Delete.prototype.toString = function () {
+ return 'Delete(' + this.count + ', ' + this.position + ')';
+ };
+
+ Delete.prototype.equals = function (other) {
+ return other instanceof Delete &&
+ this.count === other.count &&
+ this.position === other.position;
+ };
+
+ Delete.prototype.apply = function (doc) {
+ return doc.slice(0, this.position) + doc.slice(this.position + this.count);
+ };
+
+
+ // An operation that does nothing. This is needed for the result of the
+ // transformation of two deletions of the same character.
+ function Noop () {
+ if (!this || this.constructor !== SimpleTextOperation) { return new Noop(); }
+ }
+
+ Noop.prototype = new SimpleTextOperation();
+ SimpleTextOperation.Noop = Noop;
+
+ Noop.prototype.toString = function () {
+ return 'Noop()';
+ };
+
+ Noop.prototype.equals = function (other) { return other instanceof Noop; };
+
+ Noop.prototype.apply = function (doc) { return doc; };
+
+ var noop = new Noop();
+
+
+ SimpleTextOperation.transform = function (a, b) {
+ if (a instanceof Noop || b instanceof Noop) { return [a, b]; }
+
+ if (a instanceof Insert && b instanceof Insert) {
+ if (a.position < b.position || (a.position === b.position && a.str < b.str)) {
+ return [a, new Insert(b.str, b.position + a.str.length)];
+ }
+ if (a.position > b.position || (a.position === b.position && a.str > b.str)) {
+ return [new Insert(a.str, a.position + b.str.length), b];
+ }
+ return [noop, noop];
+ }
+
+ if (a instanceof Insert && b instanceof Delete) {
+ if (a.position <= b.position) {
+ return [a, new Delete(b.count, b.position + a.str.length)];
+ }
+ if (a.position >= b.position + b.count) {
+ return [new Insert(a.str, a.position - b.count), b];
+ }
+ // Here, we have to delete the inserted string of operation a.
+ // That doesn't preserve the intention of operation a, but it's the only
+ // thing we can do to get a valid transform function.
+ return [noop, new Delete(b.count + a.str.length, b.position)];
+ }
+
+ if (a instanceof Delete && b instanceof Insert) {
+ if (a.position >= b.position) {
+ return [new Delete(a.count, a.position + b.str.length), b];
+ }
+ if (a.position + a.count <= b.position) {
+ return [a, new Insert(b.str, b.position - a.count)];
+ }
+ // Same problem as above. We have to delete the string that was inserted
+ // in operation b.
+ return [new Delete(a.count + b.str.length, a.position), noop];
+ }
+
+ if (a instanceof Delete && b instanceof Delete) {
+ if (a.position === b.position) {
+ if (a.count === b.count) {
+ return [noop, noop];
+ } else if (a.count < b.count) {
+ return [noop, new Delete(b.count - a.count, b.position)];
+ }
+ return [new Delete(a.count - b.count, a.position), noop];
+ }
+ if (a.position < b.position) {
+ if (a.position + a.count <= b.position) {
+ return [a, new Delete(b.count, b.position - a.count)];
+ }
+ if (a.position + a.count >= b.position + b.count) {
+ return [new Delete(a.count - b.count, a.position), noop];
+ }
+ return [
+ new Delete(b.position - a.position, a.position),
+ new Delete(b.position + b.count - (a.position + a.count), a.position)
+ ];
+ }
+ if (a.position > b.position) {
+ if (a.position >= b.position + b.count) {
+ return [new Delete(a.count, a.position - b.count), b];
+ }
+ if (a.position + a.count <= b.position + b.count) {
+ return [noop, new Delete(b.count - a.count, b.position)];
+ }
+ return [
+ new Delete(a.position + a.count - (b.position + b.count), b.position),
+ new Delete(a.position - b.position, b.position)
+ ];
+ }
+ }
+ };
+
+ // Convert a normal, composable `TextOperation` into an array of
+ // `SimpleTextOperation`s.
+ SimpleTextOperation.fromTextOperation = function (operation) {
+ var simpleOperations = [];
+ var index = 0;
+ for (var i = 0; i < operation.ops.length; i++) {
+ var op = operation.ops[i];
+ if (TextOperation.isRetain(op)) {
+ index += op;
+ } else if (TextOperation.isInsert(op)) {
+ simpleOperations.push(new Insert(op, index));
+ index += op.length;
+ } else {
+ simpleOperations.push(new Delete(Math.abs(op), index));
+ }
+ }
+ return simpleOperations;
+ };
+
+
+ return SimpleTextOperation;
+})(this);
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.SimpleTextOperation;
+} \ No newline at end of file
diff --git a/lib/ot/text-operation.js b/lib/ot/text-operation.js
new file mode 100644
index 00000000..d5468497
--- /dev/null
+++ b/lib/ot/text-operation.js
@@ -0,0 +1,530 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.TextOperation = (function () {
+ 'use strict';
+
+ // Constructor for new operations.
+ function TextOperation () {
+ if (!this || this.constructor !== TextOperation) {
+ // => function was called without 'new'
+ return new TextOperation();
+ }
+
+ // When an operation is applied to an input string, you can think of this as
+ // if an imaginary cursor runs over the entire string and skips over some
+ // parts, deletes some parts and inserts characters at some positions. These
+ // actions (skip/delete/insert) are stored as an array in the "ops" property.
+ this.ops = [];
+ // An operation's baseLength is the length of every string the operation
+ // can be applied to.
+ this.baseLength = 0;
+ // The targetLength is the length of every string that results from applying
+ // the operation on a valid input string.
+ this.targetLength = 0;
+ }
+
+ TextOperation.prototype.equals = function (other) {
+ if (this.baseLength !== other.baseLength) { return false; }
+ if (this.targetLength !== other.targetLength) { return false; }
+ if (this.ops.length !== other.ops.length) { return false; }
+ for (var i = 0; i < this.ops.length; i++) {
+ if (this.ops[i] !== other.ops[i]) { return false; }
+ }
+ return true;
+ };
+
+ // Operation are essentially lists of ops. There are three types of ops:
+ //
+ // * Retain ops: Advance the cursor position by a given number of characters.
+ // Represented by positive ints.
+ // * Insert ops: Insert a given string at the current cursor position.
+ // Represented by strings.
+ // * Delete ops: Delete the next n characters. Represented by negative ints.
+
+ var isRetain = TextOperation.isRetain = function (op) {
+ return typeof op === 'number' && op > 0;
+ };
+
+ var isInsert = TextOperation.isInsert = function (op) {
+ return typeof op === 'string';
+ };
+
+ var isDelete = TextOperation.isDelete = function (op) {
+ return typeof op === 'number' && op < 0;
+ };
+
+
+ // After an operation is constructed, the user of the library can specify the
+ // actions of an operation (skip/insert/delete) with these three builder
+ // methods. They all return the operation for convenient chaining.
+
+ // Skip over a given number of characters.
+ TextOperation.prototype.retain = function (n) {
+ if (typeof n !== 'number') {
+ throw new Error("retain expects an integer");
+ }
+ if (n === 0) { return this; }
+ this.baseLength += n;
+ this.targetLength += n;
+ if (isRetain(this.ops[this.ops.length-1])) {
+ // The last op is a retain op => we can merge them into one op.
+ this.ops[this.ops.length-1] += n;
+ } else {
+ // Create a new op.
+ this.ops.push(n);
+ }
+ return this;
+ };
+
+ // Insert a string at the current position.
+ TextOperation.prototype.insert = function (str) {
+ if (typeof str !== 'string') {
+ throw new Error("insert expects a string");
+ }
+ if (str === '') { return this; }
+ this.targetLength += str.length;
+ var ops = this.ops;
+ if (isInsert(ops[ops.length-1])) {
+ // Merge insert op.
+ ops[ops.length-1] += str;
+ } else if (isDelete(ops[ops.length-1])) {
+ // It doesn't matter when an operation is applied whether the operation
+ // is delete(3), insert("something") or insert("something"), delete(3).
+ // Here we enforce that in this case, the insert op always comes first.
+ // This makes all operations that have the same effect when applied to
+ // a document of the right length equal in respect to the `equals` method.
+ if (isInsert(ops[ops.length-2])) {
+ ops[ops.length-2] += str;
+ } else {
+ ops[ops.length] = ops[ops.length-1];
+ ops[ops.length-2] = str;
+ }
+ } else {
+ ops.push(str);
+ }
+ return this;
+ };
+
+ // Delete a string at the current position.
+ TextOperation.prototype['delete'] = function (n) {
+ if (typeof n === 'string') { n = n.length; }
+ if (typeof n !== 'number') {
+ throw new Error("delete expects an integer or a string");
+ }
+ if (n === 0) { return this; }
+ if (n > 0) { n = -n; }
+ this.baseLength -= n;
+ if (isDelete(this.ops[this.ops.length-1])) {
+ this.ops[this.ops.length-1] += n;
+ } else {
+ this.ops.push(n);
+ }
+ return this;
+ };
+
+ // Tests whether this operation has no effect.
+ TextOperation.prototype.isNoop = function () {
+ return this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0]));
+ };
+
+ // Pretty printing.
+ TextOperation.prototype.toString = function () {
+ // map: build a new array by applying a function to every element in an old
+ // array.
+ var map = Array.prototype.map || function (fn) {
+ var arr = this;
+ var newArr = [];
+ for (var i = 0, l = arr.length; i < l; i++) {
+ newArr[i] = fn(arr[i]);
+ }
+ return newArr;
+ };
+ return map.call(this.ops, function (op) {
+ if (isRetain(op)) {
+ return "retain " + op;
+ } else if (isInsert(op)) {
+ return "insert '" + op + "'";
+ } else {
+ return "delete " + (-op);
+ }
+ }).join(', ');
+ };
+
+ // Converts operation into a JSON value.
+ TextOperation.prototype.toJSON = function () {
+ return this.ops;
+ };
+
+ // Converts a plain JS object into an operation and validates it.
+ TextOperation.fromJSON = function (ops) {
+ var o = new TextOperation();
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (isRetain(op)) {
+ o.retain(op);
+ } else if (isInsert(op)) {
+ o.insert(op);
+ } else if (isDelete(op)) {
+ o['delete'](op);
+ } else {
+ throw new Error("unknown operation: " + JSON.stringify(op));
+ }
+ }
+ return o;
+ };
+
+ // Apply an operation to a string, returning a new string. Throws an error if
+ // there's a mismatch between the input string and the operation.
+ TextOperation.prototype.apply = function (str) {
+ var operation = this;
+ if (str.length !== operation.baseLength) {
+ throw new Error("The operation's base length must be equal to the string's length.");
+ }
+ var newStr = [], j = 0;
+ var strIndex = 0;
+ var ops = this.ops;
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (isRetain(op)) {
+ if (strIndex + op > str.length) {
+ throw new Error("Operation can't retain more characters than are left in the string.");
+ }
+ // Copy skipped part of the old string.
+ newStr[j++] = str.slice(strIndex, strIndex + op);
+ strIndex += op;
+ } else if (isInsert(op)) {
+ // Insert string.
+ newStr[j++] = op;
+ } else { // delete op
+ strIndex -= op;
+ }
+ }
+ if (strIndex !== str.length) {
+ throw new Error("The operation didn't operate on the whole string.");
+ }
+ return newStr.join('');
+ };
+
+ // Computes the inverse of an operation. The inverse of an operation is the
+ // operation that reverts the effects of the operation, e.g. when you have an
+ // operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello ");
+ // skip(6);'. The inverse should be used for implementing undo.
+ TextOperation.prototype.invert = function (str) {
+ var strIndex = 0;
+ var inverse = new TextOperation();
+ var ops = this.ops;
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (isRetain(op)) {
+ inverse.retain(op);
+ strIndex += op;
+ } else if (isInsert(op)) {
+ inverse['delete'](op.length);
+ } else { // delete op
+ inverse.insert(str.slice(strIndex, strIndex - op));
+ strIndex -= op;
+ }
+ }
+ return inverse;
+ };
+
+ // Compose merges two consecutive operations into one operation, that
+ // preserves the changes of both. Or, in other words, for each input string S
+ // and a pair of consecutive operations A and B,
+ // apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
+ TextOperation.prototype.compose = function (operation2) {
+ var operation1 = this;
+ if (operation1.targetLength !== operation2.baseLength) {
+ throw new Error("The base length of the second operation has to be the target length of the first operation");
+ }
+
+ var operation = new TextOperation(); // the combined operation
+ var ops1 = operation1.ops, ops2 = operation2.ops; // for fast access
+ var i1 = 0, i2 = 0; // current index into ops1 respectively ops2
+ var op1 = ops1[i1++], op2 = ops2[i2++]; // current ops
+ while (true) {
+ // Dispatch on the type of op1 and op2
+ if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
+ // end condition: both ops1 and ops2 have been processed
+ break;
+ }
+
+ if (isDelete(op1)) {
+ operation['delete'](op1);
+ op1 = ops1[i1++];
+ continue;
+ }
+ if (isInsert(op2)) {
+ operation.insert(op2);
+ op2 = ops2[i2++];
+ continue;
+ }
+
+ if (typeof op1 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too short.");
+ }
+ if (typeof op2 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too long.");
+ }
+
+ if (isRetain(op1) && isRetain(op2)) {
+ if (op1 > op2) {
+ operation.retain(op2);
+ op1 = op1 - op2;
+ op2 = ops2[i2++];
+ } else if (op1 === op2) {
+ operation.retain(op1);
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ operation.retain(op1);
+ op2 = op2 - op1;
+ op1 = ops1[i1++];
+ }
+ } else if (isInsert(op1) && isDelete(op2)) {
+ if (op1.length > -op2) {
+ op1 = op1.slice(-op2);
+ op2 = ops2[i2++];
+ } else if (op1.length === -op2) {
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ op2 = op2 + op1.length;
+ op1 = ops1[i1++];
+ }
+ } else if (isInsert(op1) && isRetain(op2)) {
+ if (op1.length > op2) {
+ operation.insert(op1.slice(0, op2));
+ op1 = op1.slice(op2);
+ op2 = ops2[i2++];
+ } else if (op1.length === op2) {
+ operation.insert(op1);
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ operation.insert(op1);
+ op2 = op2 - op1.length;
+ op1 = ops1[i1++];
+ }
+ } else if (isRetain(op1) && isDelete(op2)) {
+ if (op1 > -op2) {
+ operation['delete'](op2);
+ op1 = op1 + op2;
+ op2 = ops2[i2++];
+ } else if (op1 === -op2) {
+ operation['delete'](op2);
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ operation['delete'](op1);
+ op2 = op2 + op1;
+ op1 = ops1[i1++];
+ }
+ } else {
+ throw new Error(
+ "This shouldn't happen: op1: " +
+ JSON.stringify(op1) + ", op2: " +
+ JSON.stringify(op2)
+ );
+ }
+ }
+ return operation;
+ };
+
+ function getSimpleOp (operation, fn) {
+ var ops = operation.ops;
+ var isRetain = TextOperation.isRetain;
+ switch (ops.length) {
+ case 1:
+ return ops[0];
+ case 2:
+ return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null);
+ case 3:
+ if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; }
+ }
+ return null;
+ }
+
+ function getStartIndex (operation) {
+ if (isRetain(operation.ops[0])) { return operation.ops[0]; }
+ return 0;
+ }
+
+ // When you use ctrl-z to undo your latest changes, you expect the program not
+ // to undo every single keystroke but to undo your last sentence you wrote at
+ // a stretch or the deletion you did by holding the backspace key down. This
+ // This can be implemented by composing operations on the undo stack. This
+ // method can help decide whether two operations should be composed. It
+ // returns true if the operations are consecutive insert operations or both
+ // operations delete text at the same position. You may want to include other
+ // factors like the time since the last change in your decision.
+ TextOperation.prototype.shouldBeComposedWith = function (other) {
+ if (this.isNoop() || other.isNoop()) { return true; }
+
+ var startA = getStartIndex(this), startB = getStartIndex(other);
+ var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
+ if (!simpleA || !simpleB) { return false; }
+
+ if (isInsert(simpleA) && isInsert(simpleB)) {
+ return startA + simpleA.length === startB;
+ }
+
+ if (isDelete(simpleA) && isDelete(simpleB)) {
+ // there are two possibilities to delete: with backspace and with the
+ // delete key.
+ return (startB - simpleB === startA) || startA === startB;
+ }
+
+ return false;
+ };
+
+ // Decides whether two operations should be composed with each other
+ // if they were inverted, that is
+ // `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`.
+ TextOperation.prototype.shouldBeComposedWithInverted = function (other) {
+ if (this.isNoop() || other.isNoop()) { return true; }
+
+ var startA = getStartIndex(this), startB = getStartIndex(other);
+ var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
+ if (!simpleA || !simpleB) { return false; }
+
+ if (isInsert(simpleA) && isInsert(simpleB)) {
+ return startA + simpleA.length === startB || startA === startB;
+ }
+
+ if (isDelete(simpleA) && isDelete(simpleB)) {
+ return startB - simpleB === startA;
+ }
+
+ return false;
+ };
+
+ // Transform takes two operations A and B that happened concurrently and
+ // produces two operations A' and B' (in an array) such that
+ // `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
+ // heart of OT.
+ TextOperation.transform = function (operation1, operation2) {
+ if (operation1.baseLength !== operation2.baseLength) {
+ throw new Error("Both operations have to have the same base length");
+ }
+
+ var operation1prime = new TextOperation();
+ var operation2prime = new TextOperation();
+ var ops1 = operation1.ops, ops2 = operation2.ops;
+ var i1 = 0, i2 = 0;
+ var op1 = ops1[i1++], op2 = ops2[i2++];
+ while (true) {
+ // At every iteration of the loop, the imaginary cursor that both
+ // operation1 and operation2 have that operates on the input string must
+ // have the same position in the input string.
+
+ if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
+ // end condition: both ops1 and ops2 have been processed
+ break;
+ }
+
+ // next two cases: one or both ops are insert ops
+ // => insert the string in the corresponding prime operation, skip it in
+ // the other one. If both op1 and op2 are insert ops, prefer op1.
+ if (isInsert(op1)) {
+ operation1prime.insert(op1);
+ operation2prime.retain(op1.length);
+ op1 = ops1[i1++];
+ continue;
+ }
+ if (isInsert(op2)) {
+ operation1prime.retain(op2.length);
+ operation2prime.insert(op2);
+ op2 = ops2[i2++];
+ continue;
+ }
+
+ if (typeof op1 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too short.");
+ }
+ if (typeof op2 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too long.");
+ }
+
+ var minl;
+ if (isRetain(op1) && isRetain(op2)) {
+ // Simple case: retain/retain
+ if (op1 > op2) {
+ minl = op2;
+ op1 = op1 - op2;
+ op2 = ops2[i2++];
+ } else if (op1 === op2) {
+ minl = op2;
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ minl = op1;
+ op2 = op2 - op1;
+ op1 = ops1[i1++];
+ }
+ operation1prime.retain(minl);
+ operation2prime.retain(minl);
+ } else if (isDelete(op1) && isDelete(op2)) {
+ // Both operations delete the same string at the same position. We don't
+ // need to produce any operations, we just skip over the delete ops and
+ // handle the case that one operation deletes more than the other.
+ if (-op1 > -op2) {
+ op1 = op1 - op2;
+ op2 = ops2[i2++];
+ } else if (op1 === op2) {
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ op2 = op2 - op1;
+ op1 = ops1[i1++];
+ }
+ // next two cases: delete/retain and retain/delete
+ } else if (isDelete(op1) && isRetain(op2)) {
+ if (-op1 > op2) {
+ minl = op2;
+ op1 = op1 + op2;
+ op2 = ops2[i2++];
+ } else if (-op1 === op2) {
+ minl = op2;
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ minl = -op1;
+ op2 = op2 + op1;
+ op1 = ops1[i1++];
+ }
+ operation1prime['delete'](minl);
+ } else if (isRetain(op1) && isDelete(op2)) {
+ if (op1 > -op2) {
+ minl = -op2;
+ op1 = op1 + op2;
+ op2 = ops2[i2++];
+ } else if (op1 === -op2) {
+ minl = op1;
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ minl = op1;
+ op2 = op2 + op1;
+ op1 = ops1[i1++];
+ }
+ operation2prime['delete'](minl);
+ } else {
+ throw new Error("The two operations aren't compatible");
+ }
+ }
+
+ return [operation1prime, operation2prime];
+ };
+
+ return TextOperation;
+
+}());
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.TextOperation;
+} \ No newline at end of file
diff --git a/lib/ot/wrapped-operation.js b/lib/ot/wrapped-operation.js
new file mode 100644
index 00000000..91050f4e
--- /dev/null
+++ b/lib/ot/wrapped-operation.js
@@ -0,0 +1,80 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.WrappedOperation = (function (global) {
+ 'use strict';
+
+ // A WrappedOperation contains an operation and corresponing metadata.
+ function WrappedOperation (operation, meta) {
+ this.wrapped = operation;
+ this.meta = meta;
+ }
+
+ WrappedOperation.prototype.apply = function () {
+ return this.wrapped.apply.apply(this.wrapped, arguments);
+ };
+
+ WrappedOperation.prototype.invert = function () {
+ var meta = this.meta;
+ return new WrappedOperation(
+ this.wrapped.invert.apply(this.wrapped, arguments),
+ meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
+ meta.invert.apply(meta, arguments) : meta
+ );
+ };
+
+ // Copy all properties from source to target.
+ function copy (source, target) {
+ for (var key in source) {
+ if (source.hasOwnProperty(key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ function composeMeta (a, b) {
+ if (a && typeof a === 'object') {
+ if (typeof a.compose === 'function') { return a.compose(b); }
+ var meta = {};
+ copy(a, meta);
+ copy(b, meta);
+ return meta;
+ }
+ return b;
+ }
+
+ WrappedOperation.prototype.compose = function (other) {
+ return new WrappedOperation(
+ this.wrapped.compose(other.wrapped),
+ composeMeta(this.meta, other.meta)
+ );
+ };
+
+ function transformMeta (meta, operation) {
+ if (meta && typeof meta === 'object') {
+ if (typeof meta.transform === 'function') {
+ return meta.transform(operation);
+ }
+ }
+ return meta;
+ }
+
+ WrappedOperation.transform = function (a, b) {
+ var transform = a.wrapped.constructor.transform;
+ var pair = transform(a.wrapped, b.wrapped);
+ return [
+ new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
+ new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
+ ];
+ };
+
+ return WrappedOperation;
+
+}(this));
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.WrappedOperation;
+} \ No newline at end of file
diff --git a/lib/realtime.js b/lib/realtime.js
index ca9cecd8..66a3c9c5 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -16,6 +16,9 @@ var moment = require('moment');
var config = require("../config.js");
var logger = require("./logger.js");
+//ot
+var ot = require("./ot/index.js");
+
//others
var db = require("./db.js");
var Note = require("./note.js");
@@ -60,28 +63,41 @@ function secure(socket, next) {
}
}
+function emitCheck(note) {
+ var out = {
+ updatetime: note.updatetime
+ };
+ for (var i = 0, l = note.socks.length; i < l; i++) {
+ var sock = note.socks[i];
+ sock.emit('check', out);
+ };
+}
+
//actions
var users = {};
var notes = {};
var updater = setInterval(function () {
async.each(Object.keys(notes), function (key, callback) {
var note = notes[key];
- if (note.isDirty) {
+ if (note.server.isDirty) {
if (config.debug)
logger.info("updater found dirty note: " + key);
- var body = LZString.decompressFromUTF16(note.body);
+ var body = note.server.document;
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
- db.saveToDB(key, title, body,
- function (err, result) {});
- note.isDirty = false;
+ db.saveToDB(key, title, body, function (err, result) {
+ if (err) return;
+ note.server.isDirty = false;
+ note.updatetime = Date.now();
+ emitCheck(note);
+ });
}
callback();
}, function (err) {
if (err) return logger.error('updater error', err);
});
-}, 5000);
+}, 1000);
function getStatus(callback) {
db.countFromDB(function (err, data) {
@@ -189,9 +205,6 @@ function emitRefresh(socket) {
socket.emit('refresh', {
owner: note.owner,
permission: note.permission,
- body: note.body,
- otk: note.otk,
- hash: note.hash,
updatetime: note.updatetime
});
}
@@ -202,8 +215,13 @@ var isDisconnectBusy = false;
var disconnectSocketQueue = [];
function finishConnection(socket, notename) {
- notes[notename].users[socket.id] = users[socket.id];
- notes[notename].socks.push(socket);
+ var note = notes[notename];
+ note.users[socket.id] = users[socket.id];
+ note.socks.push(socket);
+ note.server.addClient(socket);
+ note.server.setName(socket, users[socket.id].name);
+ note.server.setColor(socket, users[socket.id].color);
+
emitOnlineUsers(socket);
emitRefresh(socket);
@@ -260,18 +278,16 @@ function startConnection(socket) {
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
- body = LZString.compressToUTF16(body);
+ //body = LZString.compressToUTF16(body);
var updatetime = data.rows[0].update_time;
+ var server = new ot.EditorSocketIOServer(body, [], notename, ifMayEdit);
notes[notename] = {
owner: owner,
permission: note.permission,
socks: [],
- body: body,
- isDirty: false,
users: {},
- otk: shortId.generate(),
- hash: md5(body),
- updatetime: moment(updatetime).valueOf()
+ updatetime: moment(updatetime).valueOf(),
+ server: server
};
finishConnection(socket, notename);
});
@@ -294,17 +310,18 @@ function disconnect(socket) {
if (users[socket.id]) {
delete users[socket.id];
}
- if (notes[notename]) {
- delete notes[notename].users[socket.id];
+ var note = notes[notename];
+ if (note) {
+ delete note.users[socket.id];
do {
- var index = notes[notename].socks.indexOf(socket);
+ var index = note.socks.indexOf(socket);
if (index != -1) {
- notes[notename].socks.splice(index, 1);
+ note.socks.splice(index, 1);
}
} while (index != -1);
- if (Object.keys(notes[notename].users).length <= 0) {
- if (notes[notename].isDirty) {
- var body = LZString.decompressFromUTF16(notes[notename].body);
+ if (Object.keys(note.users).length <= 0) {
+ if (note.server.isDirty) {
+ var body = note.server.document;
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
@@ -363,20 +380,20 @@ function updateUserData(socket, user) {
if (socket.request.user && socket.request.user.logged_in) {
var profile = JSON.parse(socket.request.user.profile);
var photo = null;
- switch(profile.provider) {
- case "facebook":
- photo = 'https://graph.facebook.com/' + profile.id + '/picture';
- break;
- case "twitter":
- photo = profile.photos[0].value;
- break;
- case "github":
- photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
- break;
- case "dropbox":
- //no image api provided, use gravatar
- photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
- break;
+ switch (profile.provider) {
+ case "facebook":
+ photo = 'https://graph.facebook.com/' + profile.id + '/picture';
+ break;
+ case "twitter":
+ photo = profile.photos[0].value;
+ break;
+ case "github":
+ photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
+ break;
+ case "dropbox":
+ //no image api provided, use gravatar
+ photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
+ break;
}
user.photo = photo;
user.name = profile.displayName || profile.username;
@@ -389,6 +406,29 @@ function updateUserData(socket, user) {
}
}
+function ifMayEdit(socket, callback) {
+ var notename = getNotenameFromSocket(socket);
+ if (!notename || !notes[notename]) return;
+ var note = notes[notename];
+ var mayEdit = true;
+ switch (note.permission) {
+ case "freely":
+ //not blocking anyone
+ break;
+ case "editable":
+ //only login user can change
+ if (!socket.request.user || !socket.request.user.logged_in)
+ mayEdit = false;
+ break;
+ case "locked":
+ //only owner can change
+ if (note.owner != socket.request.user._id)
+ mayEdit = false;
+ break;
+ }
+ callback(mayEdit);
+}
+
function connection(socket) {
//split notename from socket
var notename = getNotenameFromSocket(socket);
@@ -442,30 +482,6 @@ function connection(socket) {
emitRefresh(socket);
});
- //received client data updated
- socket.on('update', function (body_) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
- if (config.debug)
- logger.info('SERVER received [' + notename + '] data updated: ' + socket.id);
- var note = notes[notename];
- if (note.body != body_) {
- note.body = body_;
- note.hash = md5(body_);
- note.updatetime = Date.now();
- note.isDirty = true;
- }
- var out = {
- id: socket.id,
- hash: note.hash,
- updatetime: note.updatetime
- };
- for (var i = 0, l = note.socks.length; i < l; i++) {
- var sock = note.socks[i];
- sock.emit('check', out);
- };
- });
-
//received user status
socket.on('user status', function (data) {
var notename = getNotenameFromSocket(socket);
@@ -591,67 +607,6 @@ function connection(socket) {
disconnectSocketQueue.push(socket);
disconnect(socket);
});
-
- //when received client change data request
- socket.on('change', function (op) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
- var note = notes[notename];
- switch (note.permission) {
- case "freely":
- //not blocking anyone
- break;
- case "editable":
- //only login user can change
- if (!socket.request.user || !socket.request.user.logged_in)
- return;
- break;
- case "locked":
- //only owner can change
- if (note.owner != socket.request.user._id)
- return;
- break;
- }
- op = LZString.decompressFromUTF16(op);
- if (op)
- op = JSON.parse(op);
- else
- return;
- if (config.debug)
- logger.info('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
- switch (op.origin) {
- case '+input':
- case '+delete':
- case '+transpose':
- case 'paste':
- case 'cut':
- case 'undo':
- case 'redo':
- case 'drag':
- case '*compose':
- case 'case':
- case '+insert':
- case '+insertLine':
- case '+swapLine':
- case '+joinLines':
- case '+duplicateLine':
- case '+sortLines':
- op.id = socket.id;
- op.otk = note.otk;
- op.nextotk = note.otk = shortId.generate();
- var stringop = JSON.stringify(op);
- var compressstringop = LZString.compressToUTF16(stringop);
- for (var i = 0, l = note.socks.length; i < l; i++) {
- var sock = note.socks[i];
- if (config.debug)
- logger.info('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + stringop);
- sock.emit('change', compressstringop);
- };
- break;
- default:
- logger.info('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
- }
- });
}
module.exports = realtime; \ No newline at end of file
diff --git a/public/js/index.js b/public/js/index.js
index 43b02ad5..1caba825 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -386,7 +386,10 @@ $(window).resize(function () {
});
//when page unload
$(window).unload(function () {
- emitUpdate();
+ //na
+});
+$(window).error(function () {
+ setNeedRefresh();
});
//when page hash change
@@ -898,21 +901,9 @@ socket.on('version', function (data) {
setNeedRefresh();
});
socket.on('check', function (data) {
- if (data.id == socket.id) {
- lastchangetime = data.updatetime;
- lastchangeui = ui.infobar.lastchange;
- updateLastChange();
- return;
- }
- var currentHash = md5(LZString.compressToUTF16(editor.getValue()));
- var hashMismatch = (currentHash != data.hash);
- if (hashMismatch)
- socket.emit('refresh');
- else {
- lastchangetime = data.updatetime;
- lastchangeui = ui.infobar.lastchange;
- updateLastChange();
- }
+ lastchangetime = data.updatetime;
+ lastchangeui = ui.infobar.lastchange;
+ updateLastChange();
});
socket.on('permission', function (data) {
permission = data.permission;
@@ -922,26 +913,46 @@ var otk = null;
var owner = null;
var permission = null;
socket.on('refresh', function (data) {
- var currentHash = md5(LZString.compressToUTF16(editor.getValue()));
- var hashMismatch = (currentHash != data.hash);
- saveInfo();
-
otk = data.otk;
owner = data.owner;
permission = data.permission;
+ lastchangetime = data.updatetime;
+ lastchangeui = ui.infobar.lastchange;
+ updateLastChange();
+ checkPermission();
+});
+
+var EditorClient = ot.EditorClient;
+var SocketIOAdapter = ot.SocketIOAdapter;
+var CodeMirrorAdapter = ot.CodeMirrorAdapter;
+var cmClient = null;
- if (hashMismatch) {
- var body = data.body;
- body = LZString.decompressFromUTF16(body);
+socket.on('doc', function (obj) {
+ obj = LZString.decompressFromUTF16(obj);
+ obj = JSON.parse(obj);
+ var body = obj.str;
+ var bodyMismatch = (editor.getValue() != body);
+
+ saveInfo();
+ if (bodyMismatch) {
if (body)
editor.setValue(body);
else
editor.setValue("");
}
-
- lastchangetime = data.updatetime;
- lastchangeui = ui.infobar.lastchange;
- updateLastChange();
+ if (!cmClient) {
+ cmClient = window.cmClient = new EditorClient(
+ obj.revision, obj.clients,
+ new SocketIOAdapter(socket), new CodeMirrorAdapter(editor)
+ );
+ } else {
+ cmClient.revision = obj.revision;
+ cmClient.initializeClients(obj.clients);
+ if (bodyMismatch) {
+ cmClient.undoManager.undoStack.length = 0;
+ cmClient.undoManager.redoStack.length = 0;
+ }
+ }
if (!loaded) {
editor.clearHistory();
@@ -963,7 +974,7 @@ socket.on('refresh', function (data) {
}, 1);
} else {
//if current doc is equal to the doc before disconnect
- if (hashMismatch)
+ if (bodyMismatch)
editor.clearHistory();
else {
if (lastInfo.history)
@@ -972,57 +983,26 @@ socket.on('refresh', function (data) {
lastInfo.history = null;
}
- if (hashMismatch)
+ if (bodyMismatch) {
+ isDirty = true;
updateView();
+ }
if (editor.getOption('readOnly'))
editor.setOption('readOnly', false);
restoreInfo();
- checkPermission();
});
-var changeStack = [];
-var changeBusy = false;
-
-socket.on('change', function (data) {
- data = LZString.decompressFromUTF16(data);
- data = JSON.parse(data);
- changeStack.push(data);
- if (!changeBusy)
- executeChange();
-});
+socket.on('ack', _.debounce(function () {
+ isDirty = true;
+ updateView();
+}, finishChangeDelay));
-function executeChange() {
- if (changeStack.length > 0) {
- changeBusy = true;
- var data = changeStack.shift();
- if (data.otk != otk) {
- var found = false;
- for (var i = 0, l = changeStack.length; i < l; i++) {
- if (changeStack[i].otk == otk) {
- changeStack.unshift(data);
- data = changeStack[i];
- found = true;
- break;
- }
- }
- if (!found) {
- socket.emit('refresh');
- changeBusy = false;
- return;
- }
- }
- otk = data.nextotk;
- if (data.id == personalInfo.id)
- editor.replaceRange(data.text, data.from, data.to, 'self::' + data.origin);
- else
- editor.replaceRange(data.text, data.from, data.to, "ignoreHistory");
- executeChange();
- } else {
- changeBusy = false;
- }
-}
+socket.on('operation', _.debounce(function () {
+ isDirty = true;
+ updateView();
+}, finishChangeDelay));
socket.on('online users', function (data) {
data = LZString.decompressFromUTF16(data);
@@ -1214,7 +1194,7 @@ function renderUserStatusList(list) {
var item = items[j];
var userstatus = $(item.elm).find('.ui-user-status');
var usericon = $(item.elm).find('.ui-user-icon');
- if(item.values().login && item.values().photo) {
+ if (item.values().login && item.values().photo) {
usericon.css('background-image', 'url(' + item.values().photo + ')');
usericon.css('box-shadow', '0px 0px 2px ' + item.values().color);
//add 1px more to right, make it feel aligned
@@ -1420,6 +1400,7 @@ function buildCursor(user) {
checkCursorTag(coord, cursortag);
} else {
var cursor = $('#' + user.id);
+ var lineDiff = Math.abs(cursor.attr('data-line') - user.cursor.line);
cursor.attr('data-line', user.cursor.line);
cursor.attr('data-ch', user.cursor.ch);
@@ -1454,65 +1435,31 @@ function buildCursor(user) {
}
//editor actions
+var ignoreEmitEvents = ['setValue', 'ignoreHistory'];
editor.on('beforeChange', function (cm, change) {
if (debug)
console.debug(change);
- var self = change.origin.split('self::');
- if (self.length == 2) {
- change.origin = self[1];
- self = true;
- } else {
- self = false;
- }
- if (self) {
- change.canceled = true;
- } else {
- var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) != -1);
- if (!isIgnoreEmitEvent) {
- switch (permission) {
- case "freely":
- //na
- break;
- case "editable":
- if (!personalInfo.login) {
- change.canceled = true;
- $('.signin-modal').modal('show');
- }
- break;
- case "locked":
- if (personalInfo.userid != owner) {
- change.canceled = true;
- $('.locked-modal').modal('show');
- }
- break;
+ var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) != -1);
+ if (!isIgnoreEmitEvent) {
+ switch (permission) {
+ case "freely":
+ //na
+ break;
+ case "editable":
+ if (!personalInfo.login) {
+ change.canceled = true;
+ $('.signin-modal').modal('show');
+ }
+ break;
+ case "locked":
+ if (personalInfo.userid != owner) {
+ change.canceled = true;
+ $('.locked-modal').modal('show');
}
+ break;
}
}
});
-
-var ignoreEmitEvents = ['setValue', 'ignoreHistory'];
-editor.on('change', function (i, op) {
- if (debug)
- console.debug(op);
- var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(op.origin) != -1);
- if (!isIgnoreEmitEvent) {
- var out = {
- text: op.text,
- from: op.from,
- to: op.to,
- origin: op.origin
- };
- socket.emit('change', LZString.compressToUTF16(JSON.stringify(out)));
- }
- isDirty = true;
- clearTimeout(finishChangeTimer);
- finishChangeTimer = setTimeout(function () {
- if (!isIgnoreEmitEvent)
- finishChange(true);
- else
- finishChange(false);
- }, finishChangeDelay);
-});
editor.on('focus', function (cm) {
for (var i = 0; i < onlineUsers.length; i++) {
if (onlineUsers[i].id == personalInfo.id) {
@@ -1617,24 +1564,15 @@ function restoreInfo() {
var finishChangeTimer = null;
function finishChange(emit) {
- if (emit)
- emitUpdate();
updateView();
}
-function emitUpdate() {
- var value = editor.getValue();
- socket.emit('update', LZString.compressToUTF16(value));
-}
-
var lastResult = null;
function updateView() {
if (currentMode == modeType.edit || !isDirty) return;
var value = editor.getValue();
var result = postProcess(md.render(value)).children().toArray();
- //ui.area.markdown.html(result);
- //finishView(ui.area.markdown);
partialUpdate(result, lastResult, ui.area.markdown.children().toArray());
if (result && lastResult && result.length != lastResult.length)
updateDataAttrs(result, ui.area.markdown.children().toArray());
diff --git a/public/vendor/ot/ajax-adapter.js b/public/vendor/ot/ajax-adapter.js
new file mode 100755
index 00000000..51ea9eab
--- /dev/null
+++ b/public/vendor/ot/ajax-adapter.js
@@ -0,0 +1,116 @@
+/*global ot, $ */
+
+ot.AjaxAdapter = (function () {
+ 'use strict';
+
+ function AjaxAdapter (path, ownUserName, revision) {
+ if (path[path.length - 1] !== '/') { path += '/'; }
+ this.path = path;
+ this.ownUserName = ownUserName;
+ this.majorRevision = revision.major || 0;
+ this.minorRevision = revision.minor || 0;
+ this.poll();
+ }
+
+ AjaxAdapter.prototype.renderRevisionPath = function () {
+ return 'revision/' + this.majorRevision + '-' + this.minorRevision;
+ };
+
+ AjaxAdapter.prototype.handleResponse = function (data) {
+ var i;
+ var operations = data.operations;
+ for (i = 0; i < operations.length; i++) {
+ if (operations[i].user === this.ownUserName) {
+ this.trigger('ack');
+ } else {
+ this.trigger('operation', operations[i].operation);
+ }
+ }
+ if (operations.length > 0) {
+ this.majorRevision += operations.length;
+ this.minorRevision = 0;
+ }
+
+ var events = data.events;
+ if (events) {
+ for (i = 0; i < events.length; i++) {
+ var user = events[i].user;
+ if (user === this.ownUserName) { continue; }
+ switch (events[i].event) {
+ case 'joined': this.trigger('set_name', user, user); break;
+ case 'left': this.trigger('client_left', user); break;
+ case 'selection': this.trigger('selection', user, events[i].selection); break;
+ }
+ }
+ this.minorRevision += events.length;
+ }
+
+ var users = data.users;
+ if (users) {
+ delete users[this.ownUserName];
+ this.trigger('clients', users);
+ }
+
+ if (data.revision) {
+ this.majorRevision = data.revision.major;
+ this.minorRevision = data.revision.minor;
+ }
+ };
+
+ AjaxAdapter.prototype.poll = function () {
+ var self = this;
+ $.ajax({
+ url: this.path + this.renderRevisionPath(),
+ type: 'GET',
+ dataType: 'json',
+ timeout: 5000,
+ success: function (data) {
+ self.handleResponse(data);
+ self.poll();
+ },
+ error: function () {
+ setTimeout(function () { self.poll(); }, 500);
+ }
+ });
+ };
+
+ AjaxAdapter.prototype.sendOperation = function (revision, operation, selection) {
+ if (revision !== this.majorRevision) { throw new Error("Revision numbers out of sync"); }
+ var self = this;
+ $.ajax({
+ url: this.path + this.renderRevisionPath(),
+ type: 'POST',
+ data: JSON.stringify({ operation: operation, selection: selection }),
+ contentType: 'application/json',
+ processData: false,
+ success: function (data) {},
+ error: function () {
+ setTimeout(function () { self.sendOperation(revision, operation, selection); }, 500);
+ }
+ });
+ };
+
+ AjaxAdapter.prototype.sendSelection = function (obj) {
+ $.ajax({
+ url: this.path + this.renderRevisionPath() + '/selection',
+ type: 'POST',
+ data: JSON.stringify(obj),
+ contentType: 'application/json',
+ processData: false,
+ timeout: 1000
+ });
+ };
+
+ AjaxAdapter.prototype.registerCallbacks = function (cb) {
+ this.callbacks = cb;
+ };
+
+ AjaxAdapter.prototype.trigger = function (event) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var action = this.callbacks && this.callbacks[event];
+ if (action) { action.apply(this, args); }
+ };
+
+ return AjaxAdapter;
+
+})(); \ No newline at end of file
diff --git a/public/vendor/ot/client.js b/public/vendor/ot/client.js
new file mode 100755
index 00000000..7ee19fdc
--- /dev/null
+++ b/public/vendor/ot/client.js
@@ -0,0 +1,312 @@
+// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
+
+if (typeof ot === 'undefined') {
+ var ot = {};
+}
+
+ot.Client = (function (global) {
+ 'use strict';
+
+ // Client constructor
+ function Client (revision) {
+ this.revision = revision; // the next expected revision number
+ this.setState(synchronized_); // start state
+ }
+
+ Client.prototype.setState = function (state) {
+ this.state = state;
+ };
+
+ // Call this method when the user changes the document.
+ Client.prototype.applyClient = function (operation) {
+ this.setState(this.state.applyClient(this, operation));
+ };
+
+ // Call this method with a new operation from the server
+ Client.prototype.applyServer = function (revision, operation) {
+ this.setState(this.state.applyServer(this, revision, operation));
+ };
+
+ Client.prototype.applyOperations = function (head, operations) {
+ this.setState(this.state.applyOperations(this, head, operations));
+ };
+
+ Client.prototype.serverAck = function (revision) {
+ this.setState(this.state.serverAck(this, revision));
+ };
+
+ Client.prototype.serverReconnect = function () {
+ if (typeof this.state.resend === 'function') { this.state.resend(this); }
+ };
+
+ // Transforms a selection from the latest known server state to the current
+ // client state. For example, if we get from the server the information that
+ // another user's cursor is at position 3, but the server hasn't yet received
+ // our newest operation, an insertion of 5 characters at the beginning of the
+ // document, the correct position of the other user's cursor in our current
+ // document is 8.
+ Client.prototype.transformSelection = function (selection) {
+ return this.state.transformSelection(selection);
+ };
+
+ // Override this method.
+ Client.prototype.sendOperation = function (revision, operation) {
+ throw new Error("sendOperation must be defined in child class");
+ };
+
+ // Override this method.
+ Client.prototype.applyOperation = function (operation) {
+ throw new Error("applyOperation must be defined in child class");
+ };
+
+
+ // In the 'Synchronized' state, there is no pending operation that the client
+ // has sent to the server.
+ function Synchronized () {}
+ Client.Synchronized = Synchronized;
+
+ Synchronized.prototype.applyClient = function (client, operation) {
+ // When the user makes an edit, send the operation to the server and
+ // switch to the 'AwaitingConfirm' state
+ client.sendOperation(client.revision, operation);
+ return new AwaitingConfirm(operation);
+ };
+
+ Synchronized.prototype.applyServer = function (client, revision, operation) {
+ if (revision - client.revision > 1) {
+ throw new Error("Invalid revision.");
+ }
+ client.revision = revision;
+ // When we receive a new operation from the server, the operation can be
+ // simply applied to the current document
+ client.applyOperation(operation);
+ return this;
+ };
+
+ Synchronized.prototype.serverAck = function (client, revision) {
+ throw new Error("There is no pending operation.");
+ };
+
+ // Nothing to do because the latest server state and client state are the same.
+ Synchronized.prototype.transformSelection = function (x) { return x; };
+
+ // Singleton
+ var synchronized_ = new Synchronized();
+
+
+ // In the 'AwaitingConfirm' state, there's one operation the client has sent
+ // to the server and is still waiting for an acknowledgement.
+ function AwaitingConfirm (outstanding) {
+ // Save the pending operation
+ this.outstanding = outstanding;
+ }
+ Client.AwaitingConfirm = AwaitingConfirm;
+
+ AwaitingConfirm.prototype.applyClient = function (client, operation) {
+ // When the user makes an edit, don't send the operation immediately,
+ // instead switch to 'AwaitingWithBuffer' state
+ return new AwaitingWithBuffer(this.outstanding, operation);
+ };
+
+ AwaitingConfirm.prototype.applyServer = function (client, revision, operation) {
+ if (revision - client.revision > 1) {
+ throw new Error("Invalid revision.");
+ }
+ client.revision = revision;
+ // This is another client's operation. Visualization:
+ //
+ // /\
+ // this.outstanding / \ operation
+ // / \
+ // \ /
+ // pair[1] \ / pair[0] (new outstanding)
+ // (can be applied \/
+ // to the client's
+ // current document)
+ var pair = operation.constructor.transform(this.outstanding, operation);
+ client.applyOperation(pair[1]);
+ return new AwaitingConfirm(pair[0]);
+ };
+
+ AwaitingConfirm.prototype.serverAck = function (client, revision) {
+ if (revision - client.revision > 1) {
+ return new Stale(this.outstanding, client, revision).getOperations();
+ }
+ client.revision = revision;
+ // The client's operation has been acknowledged
+ // => switch to synchronized state
+ return synchronized_;
+ };
+
+ AwaitingConfirm.prototype.transformSelection = function (selection) {
+ return selection.transform(this.outstanding);
+ };
+
+ AwaitingConfirm.prototype.resend = function (client) {
+ // The confirm didn't come because the client was disconnected.
+ // Now that it has reconnected, we resend the outstanding operation.
+ client.sendOperation(client.revision, this.outstanding);
+ };
+
+
+ // In the 'AwaitingWithBuffer' state, the client is waiting for an operation
+ // to be acknowledged by the server while buffering the edits the user makes
+ function AwaitingWithBuffer (outstanding, buffer) {
+ // Save the pending operation and the user's edits since then
+ this.outstanding = outstanding;
+ this.buffer = buffer;
+ }
+ Client.AwaitingWithBuffer = AwaitingWithBuffer;
+
+ AwaitingWithBuffer.prototype.applyClient = function (client, operation) {
+ // Compose the user's changes onto the buffer
+ var newBuffer = this.buffer.compose(operation);
+ return new AwaitingWithBuffer(this.outstanding, newBuffer);
+ };
+
+ AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) {
+ if (revision - client.revision > 1) {
+ throw new Error("Invalid revision.");
+ }
+ client.revision = revision;
+ // Operation comes from another client
+ //
+ // /\
+ // this.outstanding / \ operation
+ // / \
+ // /\ /
+ // this.buffer / \* / pair1[0] (new outstanding)
+ // / \/
+ // \ /
+ // pair2[1] \ / pair2[0] (new buffer)
+ // the transformed \/
+ // operation -- can
+ // be applied to the
+ // client's current
+ // document
+ //
+ // * pair1[1]
+ var transform = operation.constructor.transform;
+ var pair1 = transform(this.outstanding, operation);
+ var pair2 = transform(this.buffer, pair1[1]);
+ client.applyOperation(pair2[1]);
+ return new AwaitingWithBuffer(pair1[0], pair2[0]);
+ };
+
+ AwaitingWithBuffer.prototype.serverAck = function (client, revision) {
+ if (revision - client.revision > 1) {
+ return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
+ }
+ client.revision = revision;
+ // The pending operation has been acknowledged
+ // => send buffer
+ client.sendOperation(client.revision, this.buffer);
+ return new AwaitingConfirm(this.buffer);
+ };
+
+ AwaitingWithBuffer.prototype.transformSelection = function (selection) {
+ return selection.transform(this.outstanding).transform(this.buffer);
+ };
+
+ AwaitingWithBuffer.prototype.resend = function (client) {
+ // The confirm didn't come because the client was disconnected.
+ // Now that it has reconnected, we resend the outstanding operation.
+ client.sendOperation(client.revision, this.outstanding);
+ };
+
+
+ function Stale(acknowlaged, client, revision) {
+ this.acknowlaged = acknowlaged;
+ this.client = client;
+ this.revision = revision;
+ }
+ Client.Stale = Stale;
+
+ Stale.prototype.applyClient = function (client, operation) {
+ return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
+ };
+
+ Stale.prototype.applyServer = function (client, revision, operation) {
+ throw new Error("Ignored server-side change.");
+ };
+
+ Stale.prototype.applyOperations = function (client, head, operations) {
+ var transform = this.acknowlaged.constructor.transform;
+ for (var i = 0; i < operations.length; i++) {
+ var op = ot.TextOperation.fromJSON(operations[i]);
+ var pair = transform(this.acknowlaged, op);
+ client.applyOperation(pair[1]);
+ this.acknowlaged = pair[0];
+ }
+ client.revision = this.revision;
+ return synchronized_;
+ };
+
+ Stale.prototype.serverAck = function (client, revision) {
+ throw new Error("There is no pending operation.");
+ };
+
+ Stale.prototype.transformSelection = function (selection) {
+ return selection;
+ };
+
+ Stale.prototype.getOperations = function () {
+ this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
+ return this;
+ };
+
+
+ function StaleWithBuffer(acknowlaged, buffer, client, revision) {
+ this.acknowlaged = acknowlaged;
+ this.buffer = buffer;
+ this.client = client;
+ this.revision = revision;
+ }
+ Client.StaleWithBuffer = StaleWithBuffer;
+
+ StaleWithBuffer.prototype.applyClient = function (client, operation) {
+ var buffer = this.buffer.compose(operation);
+ return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
+ };
+
+ StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
+ throw new Error("Ignored server-side change.");
+ };
+
+ StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
+ var transform = this.acknowlaged.constructor.transform;
+ for (var i = 0; i < operations.length; i++) {
+ var op = ot.TextOperation.fromJSON(operations[i]);
+ var pair1 = transform(this.acknowlaged, op);
+ var pair2 = transform(this.buffer, pair1[1]);
+ client.applyOperation(pair2[1]);
+ this.acknowlaged = pair1[0];
+ this.buffer = pair2[0];
+ }
+ client.revision = this.revision;
+ client.sendOperation(client.revision, this.buffer);
+ return new AwaitingConfirm(this.buffer);
+ };
+
+ StaleWithBuffer.prototype.serverAck = function (client, revision) {
+ throw new Error("There is no pending operation.");
+ };
+
+ StaleWithBuffer.prototype.transformSelection = function (selection) {
+ return selection;
+ };
+
+ StaleWithBuffer.prototype.getOperations = function () {
+ this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
+ return this;
+ };
+
+
+ return Client;
+
+}(this));
+
+if (typeof module === 'object') {
+ module.exports = ot.Client;
+}
+
diff --git a/public/vendor/ot/codemirror-adapter.js b/public/vendor/ot/codemirror-adapter.js
new file mode 100755
index 00000000..93727c58
--- /dev/null
+++ b/public/vendor/ot/codemirror-adapter.js
@@ -0,0 +1,393 @@
+/*global ot */
+
+ot.CodeMirrorAdapter = (function (global) {
+ 'use strict';
+
+ var TextOperation = ot.TextOperation;
+ var Selection = ot.Selection;
+
+ function CodeMirrorAdapter(cm) {
+ this.cm = cm;
+ this.ignoreNextChange = false;
+ this.changeInProgress = false;
+ this.selectionChanged = false;
+
+ bind(this, 'onChanges');
+ bind(this, 'onChange');
+ bind(this, 'onCursorActivity');
+ bind(this, 'onFocus');
+ bind(this, 'onBlur');
+
+ cm.on('changes', this.onChanges);
+ cm.on('change', this.onChange);
+ cm.on('cursorActivity', this.onCursorActivity);
+ cm.on('focus', this.onFocus);
+ cm.on('blur', this.onBlur);
+ }
+
+ // Removes all event listeners from the CodeMirror instance.
+ CodeMirrorAdapter.prototype.detach = function () {
+ this.cm.off('changes', this.onChanges);
+ this.cm.off('change', this.onChange);
+ this.cm.off('cursorActivity', this.onCursorActivity);
+ this.cm.off('focus', this.onFocus);
+ this.cm.off('blur', this.onBlur);
+ };
+
+ function cmpPos(a, b) {
+ if (a.line < b.line) {
+ return -1;
+ }
+ if (a.line > b.line) {
+ return 1;
+ }
+ if (a.ch < b.ch) {
+ return -1;
+ }
+ if (a.ch > b.ch) {
+ return 1;
+ }
+ return 0;
+ }
+
+ function posEq(a, b) {
+ return cmpPos(a, b) === 0;
+ }
+
+ function posLe(a, b) {
+ return cmpPos(a, b) <= 0;
+ }
+
+ function minPos(a, b) {
+ return posLe(a, b) ? a : b;
+ }
+
+ function maxPos(a, b) {
+ return posLe(a, b) ? b : a;
+ }
+
+ function codemirrorDocLength(doc) {
+ return doc.indexFromPos({
+ line: doc.lastLine(),
+ ch: 0
+ }) +
+ doc.getLine(doc.lastLine()).length;
+ }
+
+ // Converts a CodeMirror change array (as obtained from the 'changes' event
+ // in CodeMirror v4) or single change or linked list of changes (as returned
+ // by the 'change' event in CodeMirror prior to version 4) into a
+ // TextOperation and its inverse and returns them as a two-element array.
+ CodeMirrorAdapter.operationFromCodeMirrorChanges = function (changes, doc) {
+ // Approach: Replay the changes, beginning with the most recent one, and
+ // construct the operation and its inverse. We have to convert the position
+ // in the pre-change coordinate system to an index. We have a method to
+ // convert a position in the coordinate system after all changes to an index,
+ // namely CodeMirror's `indexFromPos` method. We can use the information of
+ // a single change object to convert a post-change coordinate system to a
+ // pre-change coordinate system. We can now proceed inductively to get a
+ // pre-change coordinate system for all changes in the linked list.
+ // A disadvantage of this approach is its complexity `O(n^2)` in the length
+ // of the linked list of changes.
+
+ var docEndLength = codemirrorDocLength(doc);
+ var operation = new TextOperation().retain(docEndLength);
+ var inverse = new TextOperation().retain(docEndLength);
+
+ var indexFromPos = function (pos) {
+ return doc.indexFromPos(pos);
+ };
+
+ function last(arr) {
+ return arr[arr.length - 1];
+ }
+
+ function sumLengths(strArr) {
+ if (strArr.length === 0) {
+ return 0;
+ }
+ var sum = 0;
+ for (var i = 0; i < strArr.length; i++) {
+ sum += strArr[i].length;
+ }
+ return sum + strArr.length - 1;
+ }
+
+ function updateIndexFromPos(indexFromPos, change) {
+ return function (pos) {
+ if (posLe(pos, change.from)) {
+ return indexFromPos(pos);
+ }
+ if (posLe(change.to, pos)) {
+ return indexFromPos({
+ line: pos.line + change.text.length - 1 - (change.to.line - change.from.line),
+ ch: (change.to.line < pos.line) ?
+ pos.ch : (change.text.length <= 1) ?
+ pos.ch - (change.to.ch - change.from.ch) + sumLengths(change.text) : pos.ch - change.to.ch + last(change.text).length
+ }) + sumLengths(change.removed) - sumLengths(change.text);
+ }
+ if (change.from.line === pos.line) {
+ return indexFromPos(change.from) + pos.ch - change.from.ch;
+ }
+ return indexFromPos(change.from) +
+ sumLengths(change.removed.slice(0, pos.line - change.from.line)) +
+ 1 + pos.ch;
+ };
+ }
+
+ for (var i = changes.length - 1; i >= 0; i--) {
+ var change = changes[i];
+ indexFromPos = updateIndexFromPos(indexFromPos, change);
+
+ var fromIndex = indexFromPos(change.from);
+ var restLength = docEndLength - fromIndex - sumLengths(change.text);
+
+ operation = new TextOperation()
+ .retain(fromIndex)['delete'](sumLengths(change.removed))
+ .insert(change.text.join('\n'))
+ .retain(restLength)
+ .compose(operation);
+
+ inverse = inverse.compose(new TextOperation()
+ .retain(fromIndex)['delete'](sumLengths(change.text))
+ .insert(change.removed.join('\n'))
+ .retain(restLength)
+ );
+
+ docEndLength += sumLengths(change.removed) - sumLengths(change.text);
+ }
+
+ return [operation, inverse];
+ };
+
+ // Singular form for backwards compatibility.
+ CodeMirrorAdapter.operationFromCodeMirrorChange =
+ CodeMirrorAdapter.operationFromCodeMirrorChanges;
+
+ // Apply an operation to a CodeMirror instance.
+ CodeMirrorAdapter.applyOperationToCodeMirror = function (operation, cm) {
+ cm.operation(function () {
+ var ops = operation.ops;
+ var index = 0; // holds the current index into CodeMirror's content
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (TextOperation.isRetain(op)) {
+ index += op;
+ } else if (TextOperation.isInsert(op)) {
+ cm.replaceRange(op, cm.posFromIndex(index), null, 'ignoreHistory');
+ index += op.length;
+ } else if (TextOperation.isDelete(op)) {
+ var from = cm.posFromIndex(index);
+ var to = cm.posFromIndex(index - op);
+ cm.replaceRange('', from, to, 'ignoreHistory');
+ }
+ }
+ });
+ };
+
+ CodeMirrorAdapter.prototype.registerCallbacks = function (cb) {
+ this.callbacks = cb;
+ };
+
+ CodeMirrorAdapter.prototype.onChange = function () {
+ // By default, CodeMirror's event order is the following:
+ // 1. 'change', 2. 'cursorActivity', 3. 'changes'.
+ // We want to fire the 'selectionChange' event after the 'change' event,
+ // but need the information from the 'changes' event. Therefore, we detect
+ // when a change is in progress by listening to the change event, setting
+ // a flag that makes this adapter defer all 'cursorActivity' events.
+ this.changeInProgress = true;
+ };
+
+ CodeMirrorAdapter.prototype.onChanges = function (_, changes) {
+ if (!this.ignoreNextChange) {
+ var pair = CodeMirrorAdapter.operationFromCodeMirrorChanges(changes, this.cm);
+ this.trigger('change', pair[0], pair[1]);
+ }
+ if (this.selectionChanged) {
+ this.trigger('selectionChange');
+ }
+ this.changeInProgress = false;
+ this.ignoreNextChange = false;
+ };
+
+ CodeMirrorAdapter.prototype.onCursorActivity =
+ CodeMirrorAdapter.prototype.onFocus = function () {
+ if (this.changeInProgress) {
+ this.selectionChanged = true;
+ } else {
+ this.trigger('selectionChange');
+ }
+ };
+
+ CodeMirrorAdapter.prototype.onBlur = function () {
+ if (!this.cm.somethingSelected()) {
+ this.trigger('blur');
+ }
+ };
+
+ CodeMirrorAdapter.prototype.getValue = function () {
+ return this.cm.getValue();
+ };
+
+ CodeMirrorAdapter.prototype.getSelection = function () {
+ var cm = this.cm;
+
+ var selectionList = cm.listSelections();
+ var ranges = [];
+ for (var i = 0; i < selectionList.length; i++) {
+ ranges[i] = new Selection.Range(
+ cm.indexFromPos(selectionList[i].anchor),
+ cm.indexFromPos(selectionList[i].head)
+ );
+ }
+
+ return new Selection(ranges);
+ };
+
+ CodeMirrorAdapter.prototype.setSelection = function (selection) {
+ var ranges = [];
+ for (var i = 0; i < selection.ranges.length; i++) {
+ var range = selection.ranges[i];
+ ranges[i] = {
+ anchor: this.cm.posFromIndex(range.anchor),
+ head: this.cm.posFromIndex(range.head)
+ };
+ }
+ this.cm.setSelections(ranges);
+ };
+
+ var addStyleRule = (function () {
+ var added = {};
+ var styleElement = document.createElement('style');
+ document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement);
+ var styleSheet = styleElement.sheet;
+
+ return function (css) {
+ if (added[css]) {
+ return;
+ }
+ added[css] = true;
+ styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length);
+ };
+ }());
+
+ CodeMirrorAdapter.prototype.setOtherCursor = function (position, color, clientId) {
+ var cursorPos = this.cm.posFromIndex(position);
+ var cursorCoords = this.cm.cursorCoords(cursorPos);
+ var cursorEl = document.createElement('span');
+ cursorEl.className = 'other-client';
+ cursorEl.style.display = 'none';
+ /*
+ cursorEl.style.padding = '0';
+ cursorEl.style.marginLeft = cursorEl.style.marginRight = '-1px';
+ cursorEl.style.borderLeftWidth = '2px';
+ cursorEl.style.borderLeftStyle = 'solid';
+ cursorEl.style.borderLeftColor = color;
+ cursorEl.style.height = (cursorCoords.bottom - cursorCoords.top) * 0.9 + 'px';
+ cursorEl.style.zIndex = 0;
+ */
+ cursorEl.setAttribute('data-clientid', clientId);
+ return this.cm.setBookmark(cursorPos, {
+ widget: cursorEl,
+ insertLeft: true
+ });
+ };
+
+ CodeMirrorAdapter.prototype.setOtherSelectionRange = function (range, color, clientId) {
+ var match = /^#([0-9a-fA-F]{6})$/.exec(color);
+ if (!match) {
+ throw new Error("only six-digit hex colors are allowed.");
+ }
+ var selectionClassName = 'selection-' + match[1];
+ var rgbcolor = hex2rgb(color);
+ var rule = '.' + selectionClassName + ' { background: rgba(' + rgbcolor.red + ',' + rgbcolor.green + ',' + rgbcolor.blue + ',0.2); }';
+ addStyleRule(rule);
+
+ var anchorPos = this.cm.posFromIndex(range.anchor);
+ var headPos = this.cm.posFromIndex(range.head);
+
+ return this.cm.markText(
+ minPos(anchorPos, headPos),
+ maxPos(anchorPos, headPos), {
+ className: selectionClassName
+ }
+ );
+ };
+
+ CodeMirrorAdapter.prototype.setOtherSelection = function (selection, color, clientId) {
+ var selectionObjects = [];
+ for (var i = 0; i < selection.ranges.length; i++) {
+ var range = selection.ranges[i];
+ if (range.isEmpty()) {
+ selectionObjects[i] = this.setOtherCursor(range.head, color, clientId);
+ } else {
+ selectionObjects[i] = this.setOtherSelectionRange(range, color, clientId);
+ }
+ }
+ return {
+ clear: function () {
+ for (var i = 0; i < selectionObjects.length; i++) {
+ selectionObjects[i].clear();
+ }
+ }
+ };
+ };
+
+ CodeMirrorAdapter.prototype.trigger = function (event) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var action = this.callbacks && this.callbacks[event];
+ if (action) {
+ action.apply(this, args);
+ }
+ };
+
+ CodeMirrorAdapter.prototype.applyOperation = function (operation) {
+ this.ignoreNextChange = true;
+ CodeMirrorAdapter.applyOperationToCodeMirror(operation, this.cm);
+ };
+
+ CodeMirrorAdapter.prototype.registerUndo = function (undoFn) {
+ this.cm.undo = undoFn;
+ };
+
+ CodeMirrorAdapter.prototype.registerRedo = function (redoFn) {
+ this.cm.redo = redoFn;
+ };
+
+ // Throws an error if the first argument is falsy. Useful for debugging.
+ function assert(b, msg) {
+ if (!b) {
+ throw new Error(msg || "assertion error");
+ }
+ }
+
+ // Bind a method to an object, so it doesn't matter whether you call
+ // object.method() directly or pass object.method as a reference to another
+ // function.
+ function bind(obj, method) {
+ var fn = obj[method];
+ obj[method] = function () {
+ fn.apply(obj, arguments);
+ };
+ }
+
+ return CodeMirrorAdapter;
+
+}(this));
+
+function hex2rgb(hex) {
+ if (hex[0] == "#") hex = hex.substr(1);
+ if (hex.length == 3) {
+ var temp = hex;
+ hex = '';
+ temp = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp).slice(1);
+ for (var i = 0; i < 3; i++) hex += temp[i] + temp[i];
+ }
+ var triplets = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex).slice(1);
+ return {
+ red: parseInt(triplets[0], 16),
+ green: parseInt(triplets[1], 16),
+ blue: parseInt(triplets[2], 16)
+ }
+} \ No newline at end of file
diff --git a/public/vendor/ot/compress.sh b/public/vendor/ot/compress.sh
new file mode 100644
index 00000000..b7cb423f
--- /dev/null
+++ b/public/vendor/ot/compress.sh
@@ -0,0 +1,10 @@
+uglifyjs --compress --mangle --output ot.min.js \
+./text-operation.js \
+./selection.js \
+./wrapped-operation.js \
+./undo-manager.js \
+./client.js \
+./codemirror-adapter.js \
+./socketio-adapter.js \
+./ajax-adapter.js \
+./editor-client.js \ No newline at end of file
diff --git a/public/vendor/ot/editor-client.js b/public/vendor/ot/editor-client.js
new file mode 100755
index 00000000..b01afb46
--- /dev/null
+++ b/public/vendor/ot/editor-client.js
@@ -0,0 +1,354 @@
+/*global ot */
+
+ot.EditorClient = (function () {
+ 'use strict';
+
+ var Client = ot.Client;
+ var Selection = ot.Selection;
+ var UndoManager = ot.UndoManager;
+ var TextOperation = ot.TextOperation;
+ var WrappedOperation = ot.WrappedOperation;
+
+
+ function SelfMeta (selectionBefore, selectionAfter) {
+ this.selectionBefore = selectionBefore;
+ this.selectionAfter = selectionAfter;
+ }
+
+ SelfMeta.prototype.invert = function () {
+ return new SelfMeta(this.selectionAfter, this.selectionBefore);
+ };
+
+ SelfMeta.prototype.compose = function (other) {
+ return new SelfMeta(this.selectionBefore, other.selectionAfter);
+ };
+
+ SelfMeta.prototype.transform = function (operation) {
+ return new SelfMeta(
+ this.selectionBefore.transform(operation),
+ this.selectionAfter.transform(operation)
+ );
+ };
+
+
+ function OtherMeta (clientId, selection) {
+ this.clientId = clientId;
+ this.selection = selection;
+ }
+
+ OtherMeta.fromJSON = function (obj) {
+ return new OtherMeta(
+ obj.clientId,
+ obj.selection && Selection.fromJSON(obj.selection)
+ );
+ };
+
+ OtherMeta.prototype.transform = function (operation) {
+ return new OtherMeta(
+ this.clientId,
+ this.selection && this.selection.transform(operation)
+ );
+ };
+
+
+ function OtherClient (id, listEl, editorAdapter, name, color, selection) {
+ this.id = id;
+ this.listEl = listEl;
+ this.editorAdapter = editorAdapter;
+ this.name = name;
+ this.color = color;
+
+ this.li = document.createElement('li');
+ if (name) {
+ this.li.textContent = name;
+ this.listEl.appendChild(this.li);
+ }
+
+ if(!color)
+ this.setColor(name ? hueFromName(name) : Math.random());
+ else
+ this.setForceColor(color);
+ if (selection) { this.updateSelection(selection); }
+ }
+
+ OtherClient.prototype.setColor = function (hue) {
+ this.hue = hue;
+ this.color = hsl2hex(hue, 0.75, 0.5);
+ this.lightColor = hsl2hex(hue, 0.5, 0.9);
+ if (this.li) { this.li.style.color = this.color; }
+ };
+
+ OtherClient.prototype.setForceColor = function (color) {
+ this.hue = null;
+ this.color = color;
+ this.lightColor = color;
+ if (this.li) { this.li.style.color = this.color; }
+ };
+
+ OtherClient.prototype.setName = function (name) {
+ if (this.name === name) { return; }
+ this.name = name;
+
+ this.li.textContent = name;
+ if (!this.li.parentNode) {
+ this.listEl.appendChild(this.li);
+ }
+
+ this.setColor(hueFromName(name));
+ };
+
+ OtherClient.prototype.updateSelection = function (selection) {
+ this.removeSelection();
+ this.selection = selection;
+ this.mark = this.editorAdapter.setOtherSelection(
+ selection,
+ selection.position === selection.selectionEnd ? this.color : this.lightColor,
+ this.id
+ );
+ };
+
+ OtherClient.prototype.remove = function () {
+ if (this.li) { removeElement(this.li); }
+ this.removeSelection();
+ };
+
+ OtherClient.prototype.removeSelection = function () {
+ if (this.mark) {
+ this.mark.clear();
+ this.mark = null;
+ }
+ };
+
+
+ function EditorClient (revision, clients, serverAdapter, editorAdapter) {
+ Client.call(this, revision);
+ this.serverAdapter = serverAdapter;
+ this.editorAdapter = editorAdapter;
+ this.undoManager = new UndoManager();
+
+ this.initializeClientList();
+ this.initializeClients(clients);
+
+ var self = this;
+
+ this.editorAdapter.registerCallbacks({
+ change: function (operation, inverse) { self.onChange(operation, inverse); },
+ selectionChange: function () { self.onSelectionChange(); },
+ blur: function () { self.onBlur(); }
+ });
+ this.editorAdapter.registerUndo(function () { self.undo(); });
+ this.editorAdapter.registerRedo(function () { self.redo(); });
+
+ this.serverAdapter.registerCallbacks({
+ client_left: function (clientId) { self.onClientLeft(clientId); },
+ set_name: function (clientId, name) { self.getClientObject(clientId).setName(name); },
+ set_color: function (clientId, color) { self.getClientObject(clientId).setForceColor(color); },
+ ack: function (revision) { self.serverAck(revision); },
+ operation: function (revision, operation) {
+ self.applyServer(revision, TextOperation.fromJSON(operation));
+ },
+ operations: function (head, operations) {
+ self.applyOperations(head, operations);
+ },
+ selection: function (clientId, selection) {
+ if (selection) {
+ self.getClientObject(clientId).updateSelection(
+ self.transformSelection(Selection.fromJSON(selection))
+ );
+ } else {
+ self.getClientObject(clientId).removeSelection();
+ }
+ },
+ clients: function (clients) {
+ var clientId;
+ for (clientId in self.clients) {
+ if (self.clients.hasOwnProperty(clientId) && !clients.hasOwnProperty(clientId)) {
+ self.onClientLeft(clientId);
+ }
+ }
+
+ for (clientId in clients) {
+ if (clients.hasOwnProperty(clientId)) {
+ var clientObject = self.getClientObject(clientId);
+
+ if (clients[clientId].name) {
+ clientObject.setName(clients[clientId].name);
+ }
+
+ var selection = clients[clientId].selection;
+ if (selection) {
+ self.clients[clientId].updateSelection(
+ self.transformSelection(Selection.fromJSON(selection))
+ );
+ } else {
+ self.clients[clientId].removeSelection();
+ }
+ }
+ }
+ },
+ reconnect: function () { self.serverReconnect(); }
+ });
+ }
+
+ inherit(EditorClient, Client);
+
+ EditorClient.prototype.addClient = function (clientId, clientObj) {
+ this.clients[clientId] = new OtherClient(
+ clientId,
+ this.clientListEl,
+ this.editorAdapter,
+ clientObj.name || clientId,
+ clientObj.color || null,
+ clientObj.selection ? Selection.fromJSON(clientObj.selection) : null
+ );
+ };
+
+ EditorClient.prototype.initializeClients = function (clients) {
+ this.clients = {};
+ for (var clientId in clients) {
+ if (clients.hasOwnProperty(clientId)) {
+ this.addClient(clientId, clients[clientId]);
+ }
+ }
+ };
+
+ EditorClient.prototype.getClientObject = function (clientId) {
+ var client = this.clients[clientId];
+ if (client) { return client; }
+ return this.clients[clientId] = new OtherClient(
+ clientId,
+ this.clientListEl,
+ this.editorAdapter
+ );
+ };
+
+ EditorClient.prototype.onClientLeft = function (clientId) {
+ //console.log("User disconnected: " + clientId);
+ var client = this.clients[clientId];
+ if (!client) { return; }
+ client.remove();
+ delete this.clients[clientId];
+ };
+
+ EditorClient.prototype.initializeClientList = function () {
+ this.clientListEl = document.createElement('ul');
+ };
+
+ EditorClient.prototype.applyUnredo = function (operation) {
+ this.undoManager.add(operation.invert(this.editorAdapter.getValue()));
+ this.editorAdapter.applyOperation(operation.wrapped);
+ this.selection = operation.meta.selectionAfter;
+ this.editorAdapter.setSelection(this.selection);
+ this.applyClient(operation.wrapped);
+ };
+
+ EditorClient.prototype.undo = function () {
+ var self = this;
+ if (!this.undoManager.canUndo()) { return; }
+ this.undoManager.performUndo(function (o) { self.applyUnredo(o); });
+ };
+
+ EditorClient.prototype.redo = function () {
+ var self = this;
+ if (!this.undoManager.canRedo()) { return; }
+ this.undoManager.performRedo(function (o) { self.applyUnredo(o); });
+ };
+
+ EditorClient.prototype.onChange = function (textOperation, inverse) {
+ var selectionBefore = this.selection;
+ this.updateSelection();
+ var meta = new SelfMeta(selectionBefore, this.selection);
+ var operation = new WrappedOperation(textOperation, meta);
+
+ var compose = this.undoManager.undoStack.length > 0 &&
+ inverse.shouldBeComposedWithInverted(last(this.undoManager.undoStack).wrapped);
+ var inverseMeta = new SelfMeta(this.selection, selectionBefore);
+ this.undoManager.add(new WrappedOperation(inverse, inverseMeta), compose);
+ this.applyClient(textOperation);
+ };
+
+ EditorClient.prototype.updateSelection = function () {
+ this.selection = this.editorAdapter.getSelection();
+ };
+
+ EditorClient.prototype.onSelectionChange = function () {
+ var oldSelection = this.selection;
+ this.updateSelection();
+ if (oldSelection && this.selection.equals(oldSelection)) { return; }
+ this.sendSelection(this.selection);
+ };
+
+ EditorClient.prototype.onBlur = function () {
+ this.selection = null;
+ this.sendSelection(null);
+ };
+
+ EditorClient.prototype.sendSelection = function (selection) {
+ if (this.state instanceof Client.AwaitingWithBuffer) { return; }
+ this.serverAdapter.sendSelection(selection);
+ };
+
+ EditorClient.prototype.sendOperation = function (revision, operation) {
+ this.serverAdapter.sendOperation(revision, operation.toJSON(), this.selection);
+ };
+
+ EditorClient.prototype.getOperations = function (base, head) {
+ this.serverAdapter.getOperations(base, head);
+ };
+
+ EditorClient.prototype.applyOperation = function (operation) {
+ this.editorAdapter.applyOperation(operation);
+ this.updateSelection();
+ this.undoManager.transform(new WrappedOperation(operation, null));
+ };
+
+ function rgb2hex (r, g, b) {
+ function digits (n) {
+ var m = Math.round(255*n).toString(16);
+ return m.length === 1 ? '0'+m : m;
+ }
+ return '#' + digits(r) + digits(g) + digits(b);
+ }
+
+ function hsl2hex (h, s, l) {
+ if (s === 0) { return rgb2hex(l, l, l); }
+ var var2 = l < 0.5 ? l * (1+s) : (l+s) - (s*l);
+ var var1 = 2 * l - var2;
+ var hue2rgb = function (hue) {
+ if (hue < 0) { hue += 1; }
+ if (hue > 1) { hue -= 1; }
+ if (6*hue < 1) { return var1 + (var2-var1)*6*hue; }
+ if (2*hue < 1) { return var2; }
+ if (3*hue < 2) { return var1 + (var2-var1)*6*(2/3 - hue); }
+ return var1;
+ };
+ return rgb2hex(hue2rgb(h+1/3), hue2rgb(h), hue2rgb(h-1/3));
+ }
+
+ function hueFromName (name) {
+ var a = 1;
+ for (var i = 0; i < name.length; i++) {
+ a = 17 * (a+name.charCodeAt(i)) % 360;
+ }
+ return a/360;
+ }
+
+ // Set Const.prototype.__proto__ to Super.prototype
+ function inherit (Const, Super) {
+ function F () {}
+ F.prototype = Super.prototype;
+ Const.prototype = new F();
+ Const.prototype.constructor = Const;
+ }
+
+ function last (arr) { return arr[arr.length - 1]; }
+
+ // Remove an element from the DOM.
+ function removeElement (el) {
+ if (el.parentNode) {
+ el.parentNode.removeChild(el);
+ }
+ }
+
+ return EditorClient;
+}());
diff --git a/public/vendor/ot/ot.min.js b/public/vendor/ot/ot.min.js
new file mode 100644
index 00000000..2051bfeb
--- /dev/null
+++ b/public/vendor/ot/ot.min.js
@@ -0,0 +1 @@
+function hex2rgb(t){if("#"==t[0]&&(t=t.substr(1)),3==t.length){var e=t;t="",e=/^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(e).slice(1);for(var n=0;3>n;n++)t+=e[n]+e[n]}var o=/^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(t).slice(1);return{red:parseInt(o[0],16),green:parseInt(o[1],16),blue:parseInt(o[2],16)}}if("undefined"==typeof ot)var ot={};if(ot.TextOperation=function(){"use strict";function t(){return this&&this.constructor===t?(this.ops=[],this.baseLength=0,void(this.targetLength=0)):new t}function e(e,n){var o=e.ops,r=t.isRetain;switch(o.length){case 1:return o[0];case 2:return r(o[0])?o[1]:r(o[1])?o[0]:null;case 3:if(r(o[0])&&r(o[2]))return o[1]}return null}function n(t){return o(t.ops[0])?t.ops[0]:0}t.prototype.equals=function(t){if(this.baseLength!==t.baseLength)return!1;if(this.targetLength!==t.targetLength)return!1;if(this.ops.length!==t.ops.length)return!1;for(var e=0;e<this.ops.length;e++)if(this.ops[e]!==t.ops[e])return!1;return!0};var o=t.isRetain=function(t){return"number"==typeof t&&t>0},r=t.isInsert=function(t){return"string"==typeof t},i=t.isDelete=function(t){return"number"==typeof t&&0>t};return t.prototype.retain=function(t){if("number"!=typeof t)throw new Error("retain expects an integer");return 0===t?this:(this.baseLength+=t,this.targetLength+=t,o(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=t:this.ops.push(t),this)},t.prototype.insert=function(t){if("string"!=typeof t)throw new Error("insert expects a string");if(""===t)return this;this.targetLength+=t.length;var e=this.ops;return r(e[e.length-1])?e[e.length-1]+=t:i(e[e.length-1])?r(e[e.length-2])?e[e.length-2]+=t:(e[e.length]=e[e.length-1],e[e.length-2]=t):e.push(t),this},t.prototype["delete"]=function(t){if("string"==typeof t&&(t=t.length),"number"!=typeof t)throw new Error("delete expects an integer or a string");return 0===t?this:(t>0&&(t=-t),this.baseLength-=t,i(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=t:this.ops.push(t),this)},t.prototype.isNoop=function(){return 0===this.ops.length||1===this.ops.length&&o(this.ops[0])},t.prototype.toString=function(){var t=Array.prototype.map||function(t){for(var e=this,n=[],o=0,r=e.length;r>o;o++)n[o]=t(e[o]);return n};return t.call(this.ops,function(t){return o(t)?"retain "+t:r(t)?"insert '"+t+"'":"delete "+-t}).join(", ")},t.prototype.toJSON=function(){return this.ops},t.fromJSON=function(e){for(var n=new t,s=0,a=e.length;a>s;s++){var h=e[s];if(o(h))n.retain(h);else if(r(h))n.insert(h);else{if(!i(h))throw new Error("unknown operation: "+JSON.stringify(h));n["delete"](h)}}return n},t.prototype.apply=function(t){var e=this;if(t.length!==e.baseLength)throw new Error("The operation's base length must be equal to the string's length.");for(var n=[],i=0,s=0,a=this.ops,h=0,p=a.length;p>h;h++){var c=a[h];if(o(c)){if(s+c>t.length)throw new Error("Operation can't retain more characters than are left in the string.");n[i++]=t.slice(s,s+c),s+=c}else r(c)?n[i++]=c:s-=c}if(s!==t.length)throw new Error("The operation didn't operate on the whole string.");return n.join("")},t.prototype.invert=function(e){for(var n=0,i=new t,s=this.ops,a=0,h=s.length;h>a;a++){var p=s[a];o(p)?(i.retain(p),n+=p):r(p)?i["delete"](p.length):(i.insert(e.slice(n,n-p)),n-=p)}return i},t.prototype.compose=function(e){var n=this;if(n.targetLength!==e.baseLength)throw new Error("The base length of the second operation has to be the target length of the first operation");for(var s=new t,a=n.ops,h=e.ops,p=0,c=0,l=a[p++],u=h[c++];;){if("undefined"==typeof l&&"undefined"==typeof u)break;if(i(l))s["delete"](l),l=a[p++];else if(r(u))s.insert(u),u=h[c++];else{if("undefined"==typeof l)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof u)throw new Error("Cannot compose operations: first operation is too long.");if(o(l)&&o(u))l>u?(s.retain(u),l-=u,u=h[c++]):l===u?(s.retain(l),l=a[p++],u=h[c++]):(s.retain(l),u-=l,l=a[p++]);else if(r(l)&&i(u))l.length>-u?(l=l.slice(-u),u=h[c++]):l.length===-u?(l=a[p++],u=h[c++]):(u+=l.length,l=a[p++]);else if(r(l)&&o(u))l.length>u?(s.insert(l.slice(0,u)),l=l.slice(u),u=h[c++]):l.length===u?(s.insert(l),l=a[p++],u=h[c++]):(s.insert(l),u-=l.length,l=a[p++]);else{if(!o(l)||!i(u))throw new Error("This shouldn't happen: op1: "+JSON.stringify(l)+", op2: "+JSON.stringify(u));l>-u?(s["delete"](u),l+=u,u=h[c++]):l===-u?(s["delete"](u),l=a[p++],u=h[c++]):(s["delete"](l),u+=l,l=a[p++])}}}return s},t.prototype.shouldBeComposedWith=function(t){if(this.isNoop()||t.isNoop())return!0;var o=n(this),s=n(t),a=e(this),h=e(t);return a&&h?r(a)&&r(h)?o+a.length===s:i(a)&&i(h)?s-h===o||o===s:!1:!1},t.prototype.shouldBeComposedWithInverted=function(t){if(this.isNoop()||t.isNoop())return!0;var o=n(this),s=n(t),a=e(this),h=e(t);return a&&h?r(a)&&r(h)?o+a.length===s||o===s:i(a)&&i(h)?s-h===o:!1:!1},t.transform=function(e,n){if(e.baseLength!==n.baseLength)throw new Error("Both operations have to have the same base length");for(var s=new t,a=new t,h=e.ops,p=n.ops,c=0,l=0,u=h[c++],f=p[l++];;){if("undefined"==typeof u&&"undefined"==typeof f)break;if(r(u))s.insert(u),a.retain(u.length),u=h[c++];else if(r(f))s.retain(f.length),a.insert(f),f=p[l++];else{if("undefined"==typeof u)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof f)throw new Error("Cannot compose operations: first operation is too long.");var d;if(o(u)&&o(f))u>f?(d=f,u-=f,f=p[l++]):u===f?(d=f,u=h[c++],f=p[l++]):(d=u,f-=u,u=h[c++]),s.retain(d),a.retain(d);else if(i(u)&&i(f))-u>-f?(u-=f,f=p[l++]):u===f?(u=h[c++],f=p[l++]):(f-=u,u=h[c++]);else if(i(u)&&o(f))-u>f?(d=f,u+=f,f=p[l++]):-u===f?(d=f,u=h[c++],f=p[l++]):(d=-u,f+=u,u=h[c++]),s["delete"](d);else{if(!o(u)||!i(f))throw new Error("The two operations aren't compatible");u>-f?(d=-f,u+=f,f=p[l++]):u===-f?(d=u,u=h[c++],f=p[l++]):(d=u,f+=u,u=h[c++]),a["delete"](d)}}}return[s,a]},t}(),"object"==typeof module&&(module.exports=ot.TextOperation),"undefined"==typeof ot)var ot={};if(ot.Selection=function(t){"use strict";function e(t,e){this.anchor=t,this.head=e}function n(t){this.ranges=t||[]}var o=t.ot?t.ot.TextOperation:require("./text-operation");return e.fromJSON=function(t){return new e(t.anchor,t.head)},e.prototype.equals=function(t){return this.anchor===t.anchor&&this.head===t.head},e.prototype.isEmpty=function(){return this.anchor===this.head},e.prototype.transform=function(t){function n(e){for(var n=e,r=t.ops,i=0,s=t.ops.length;s>i&&(o.isRetain(r[i])?e-=r[i]:o.isInsert(r[i])?n+=r[i].length:(n-=Math.min(e,-r[i]),e+=r[i]),!(0>e));i++);return n}var r=n(this.anchor);return this.anchor===this.head?new e(r,r):new e(r,n(this.head))},n.Range=e,n.createCursor=function(t){return new n([new e(t,t)])},n.fromJSON=function(t){for(var o=t.ranges||t,r=0,i=[];r<o.length;r++)i[r]=e.fromJSON(o[r]);return new n(i)},n.prototype.equals=function(t){if(this.position!==t.position)return!1;if(this.ranges.length!==t.ranges.length)return!1;for(var e=0;e<this.ranges.length;e++)if(!this.ranges[e].equals(t.ranges[e]))return!1;return!0},n.prototype.somethingSelected=function(){for(var t=0;t<this.ranges.length;t++)if(!this.ranges[t].isEmpty())return!0;return!1},n.prototype.compose=function(t){return t},n.prototype.transform=function(t){for(var e=0,o=[];e<this.ranges.length;e++)o[e]=this.ranges[e].transform(t);return new n(o)},n}(this),"object"==typeof module&&(module.exports=ot.Selection),"undefined"==typeof ot)var ot={};if(ot.WrappedOperation=function(t){"use strict";function e(t,e){this.wrapped=t,this.meta=e}function n(t,e){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}function o(t,e){if(t&&"object"==typeof t){if("function"==typeof t.compose)return t.compose(e);var o={};return n(t,o),n(e,o),o}return e}function r(t,e){return t&&"object"==typeof t&&"function"==typeof t.transform?t.transform(e):t}return e.prototype.apply=function(){return this.wrapped.apply.apply(this.wrapped,arguments)},e.prototype.invert=function(){var t=this.meta;return new e(this.wrapped.invert.apply(this.wrapped,arguments),t&&"object"==typeof t&&"function"==typeof t.invert?t.invert.apply(t,arguments):t)},e.prototype.compose=function(t){return new e(this.wrapped.compose(t.wrapped),o(this.meta,t.meta))},e.transform=function(t,n){var o=t.wrapped.constructor.transform,i=o(t.wrapped,n.wrapped);return[new e(i[0],r(t.meta,n.wrapped)),new e(i[1],r(n.meta,t.wrapped))]},e}(this),"object"==typeof module&&(module.exports=ot.WrappedOperation),"undefined"==typeof ot)var ot={};if(ot.UndoManager=function(){"use strict";function t(t){this.maxItems=t||50,this.state=n,this.dontCompose=!1,this.undoStack=[],this.redoStack=[]}function e(t,e){for(var n=[],o=e.constructor,r=t.length-1;r>=0;r--){var i=o.transform(t[r],e);"function"==typeof i[0].isNoop&&i[0].isNoop()||n.push(i[0]),e=i[1]}return n.reverse()}var n="normal",o="undoing",r="redoing";return t.prototype.add=function(t,e){if(this.state===o)this.redoStack.push(t),this.dontCompose=!0;else if(this.state===r)this.undoStack.push(t),this.dontCompose=!0;else{var n=this.undoStack;!this.dontCompose&&e&&n.length>0?n.push(t.compose(n.pop())):(n.push(t),n.length>this.maxItems&&n.shift()),this.dontCompose=!1,this.redoStack=[]}},t.prototype.transform=function(t){this.undoStack=e(this.undoStack,t),this.redoStack=e(this.redoStack,t)},t.prototype.performUndo=function(t){if(this.state=o,0===this.undoStack.length)throw new Error("undo not possible");t(this.undoStack.pop()),this.state=n},t.prototype.performRedo=function(t){if(this.state=r,0===this.redoStack.length)throw new Error("redo not possible");t(this.redoStack.pop()),this.state=n},t.prototype.canUndo=function(){return 0!==this.undoStack.length},t.prototype.canRedo=function(){return 0!==this.redoStack.length},t.prototype.isUndoing=function(){return this.state===o},t.prototype.isRedoing=function(){return this.state===r},t}(),"object"==typeof module&&(module.exports=ot.UndoManager),"undefined"==typeof ot)var ot={};ot.Client=function(t){"use strict";function e(t){this.revision=t,this.setState(a)}function n(){}function o(t){this.outstanding=t}function r(t,e){this.outstanding=t,this.buffer=e}function i(t,e,n){this.acknowlaged=t,this.client=e,this.revision=n}function s(t,e,n,o){this.acknowlaged=t,this.buffer=e,this.client=n,this.revision=o}e.prototype.setState=function(t){this.state=t},e.prototype.applyClient=function(t){this.setState(this.state.applyClient(this,t))},e.prototype.applyServer=function(t,e){this.setState(this.state.applyServer(this,t,e))},e.prototype.applyOperations=function(t,e){this.setState(this.state.applyOperations(this,t,e))},e.prototype.serverAck=function(t){this.setState(this.state.serverAck(this,t))},e.prototype.serverReconnect=function(){"function"==typeof this.state.resend&&this.state.resend(this)},e.prototype.transformSelection=function(t){return this.state.transformSelection(t)},e.prototype.sendOperation=function(t,e){throw new Error("sendOperation must be defined in child class")},e.prototype.applyOperation=function(t){throw new Error("applyOperation must be defined in child class")},e.Synchronized=n,n.prototype.applyClient=function(t,e){return t.sendOperation(t.revision,e),new o(e)},n.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");return t.revision=e,t.applyOperation(n),this},n.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},n.prototype.transformSelection=function(t){return t};var a=new n;return e.AwaitingConfirm=o,o.prototype.applyClient=function(t,e){return new r(this.outstanding,e)},o.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");t.revision=e;var r=n.constructor.transform(this.outstanding,n);return t.applyOperation(r[1]),new o(r[0])},o.prototype.serverAck=function(t,e){return e-t.revision>1?new i(this.outstanding,t,e).getOperations():(t.revision=e,a)},o.prototype.transformSelection=function(t){return t.transform(this.outstanding)},o.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},e.AwaitingWithBuffer=r,r.prototype.applyClient=function(t,e){var n=this.buffer.compose(e);return new r(this.outstanding,n)},r.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");t.revision=e;var o=n.constructor.transform,i=o(this.outstanding,n),s=o(this.buffer,i[1]);return t.applyOperation(s[1]),new r(i[0],s[0])},r.prototype.serverAck=function(t,e){return e-t.revision>1?new s(this.outstanding,this.buffer,t,e).getOperations():(t.revision=e,t.sendOperation(t.revision,this.buffer),new o(this.buffer))},r.prototype.transformSelection=function(t){return t.transform(this.outstanding).transform(this.buffer)},r.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},e.Stale=i,i.prototype.applyClient=function(t,e){return new s(this.acknowlaged,e,t,this.revision)},i.prototype.applyServer=function(t,e,n){throw new Error("Ignored server-side change.")},i.prototype.applyOperations=function(t,e,n){for(var o=this.acknowlaged.constructor.transform,r=0;r<n.length;r++){var i=ot.TextOperation.fromJSON(n[r]),s=o(this.acknowlaged,i);t.applyOperation(s[1]),this.acknowlaged=s[0]}return t.revision=this.revision,a},i.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},i.prototype.transformSelection=function(t){return t},i.prototype.getOperations=function(){return this.client.getOperations(this.client.revision,this.revision-1),this},e.StaleWithBuffer=s,s.prototype.applyClient=function(t,e){var n=this.buffer.compose(e);return new s(this.acknowlaged,n,t,this.revision)},s.prototype.applyServer=function(t,e,n){throw new Error("Ignored server-side change.")},s.prototype.applyOperations=function(t,e,n){for(var r=this.acknowlaged.constructor.transform,i=0;i<n.length;i++){var s=ot.TextOperation.fromJSON(n[i]),a=r(this.acknowlaged,s),h=r(this.buffer,a[1]);t.applyOperation(h[1]),this.acknowlaged=a[0],this.buffer=h[0]}return t.revision=this.revision,t.sendOperation(t.revision,this.buffer),new o(this.buffer)},s.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},s.prototype.transformSelection=function(t){return t},s.prototype.getOperations=function(){return this.client.getOperations(this.client.revision,this.revision-1),this},e}(this),"object"==typeof module&&(module.exports=ot.Client),ot.CodeMirrorAdapter=function(t){"use strict";function e(t){this.cm=t,this.ignoreNextChange=!1,this.changeInProgress=!1,this.selectionChanged=!1,a(this,"onChanges"),a(this,"onChange"),a(this,"onCursorActivity"),a(this,"onFocus"),a(this,"onBlur"),t.on("changes",this.onChanges),t.on("change",this.onChange),t.on("cursorActivity",this.onCursorActivity),t.on("focus",this.onFocus),t.on("blur",this.onBlur)}function n(t,e){return t.line<e.line?-1:t.line>e.line?1:t.ch<e.ch?-1:t.ch>e.ch?1:0}function o(t,e){return n(t,e)<=0}function r(t,e){return o(t,e)?t:e}function i(t,e){return o(t,e)?e:t}function s(t){return t.indexFromPos({line:t.lastLine(),ch:0})+t.getLine(t.lastLine()).length}function a(t,e){var n=t[e];t[e]=function(){n.apply(t,arguments)}}var h=ot.TextOperation,p=ot.Selection;e.prototype.detach=function(){this.cm.off("changes",this.onChanges),this.cm.off("change",this.onChange),this.cm.off("cursorActivity",this.onCursorActivity),this.cm.off("focus",this.onFocus),this.cm.off("blur",this.onBlur)},e.operationFromCodeMirrorChanges=function(t,e){function n(t){return t[t.length-1]}function r(t){if(0===t.length)return 0;for(var e=0,n=0;n<t.length;n++)e+=t[n].length;return e+t.length-1}function i(t,e){return function(i){return o(i,e.from)?t(i):o(e.to,i)?t({line:i.line+e.text.length-1-(e.to.line-e.from.line),ch:e.to.line<i.line?i.ch:e.text.length<=1?i.ch-(e.to.ch-e.from.ch)+r(e.text):i.ch-e.to.ch+n(e.text).length})+r(e.removed)-r(e.text):e.from.line===i.line?t(e.from)+i.ch-e.from.ch:t(e.from)+r(e.removed.slice(0,i.line-e.from.line))+1+i.ch}}for(var a=s(e),p=(new h).retain(a),c=(new h).retain(a),l=function(t){return e.indexFromPos(t)},u=t.length-1;u>=0;u--){var f=t[u];l=i(l,f);var d=l(f.from),g=a-d-r(f.text);p=(new h).retain(d)["delete"](r(f.removed)).insert(f.text.join("\n")).retain(g).compose(p),c=c.compose((new h).retain(d)["delete"](r(f.text)).insert(f.removed.join("\n")).retain(g)),a+=r(f.removed)-r(f.text)}return[p,c]},e.operationFromCodeMirrorChange=e.operationFromCodeMirrorChanges,e.applyOperationToCodeMirror=function(t,e){e.operation(function(){for(var n=t.ops,o=0,r=0,i=n.length;i>r;r++){var s=n[r];if(h.isRetain(s))o+=s;else if(h.isInsert(s))e.replaceRange(s,e.posFromIndex(o),null,"ignoreHistory"),o+=s.length;else if(h.isDelete(s)){var a=e.posFromIndex(o),p=e.posFromIndex(o-s);e.replaceRange("",a,p,"ignoreHistory")}}})},e.prototype.registerCallbacks=function(t){this.callbacks=t},e.prototype.onChange=function(){this.changeInProgress=!0},e.prototype.onChanges=function(t,n){if(!this.ignoreNextChange){var o=e.operationFromCodeMirrorChanges(n,this.cm);this.trigger("change",o[0],o[1])}this.selectionChanged&&this.trigger("selectionChange"),this.changeInProgress=!1,this.ignoreNextChange=!1},e.prototype.onCursorActivity=e.prototype.onFocus=function(){this.changeInProgress?this.selectionChanged=!0:this.trigger("selectionChange")},e.prototype.onBlur=function(){this.cm.somethingSelected()||this.trigger("blur")},e.prototype.getValue=function(){return this.cm.getValue()},e.prototype.getSelection=function(){for(var t=this.cm,e=t.listSelections(),n=[],o=0;o<e.length;o++)n[o]=new p.Range(t.indexFromPos(e[o].anchor),t.indexFromPos(e[o].head));return new p(n)},e.prototype.setSelection=function(t){for(var e=[],n=0;n<t.ranges.length;n++){var o=t.ranges[n];e[n]={anchor:this.cm.posFromIndex(o.anchor),head:this.cm.posFromIndex(o.head)}}this.cm.setSelections(e)};var c=function(){var t={},e=document.createElement("style");document.documentElement.getElementsByTagName("head")[0].appendChild(e);var n=e.sheet;return function(e){t[e]||(t[e]=!0,n.insertRule(e,(n.cssRules||n.rules).length))}}();return e.prototype.setOtherCursor=function(t,e,n){var o=this.cm.posFromIndex(t),r=(this.cm.cursorCoords(o),document.createElement("span"));return r.className="other-client",r.style.display="none",r.setAttribute("data-clientid",n),this.cm.setBookmark(o,{widget:r,insertLeft:!0})},e.prototype.setOtherSelectionRange=function(t,e,n){var o=/^#([0-9a-fA-F]{6})$/.exec(e);if(!o)throw new Error("only six-digit hex colors are allowed.");var s="selection-"+o[1],a=hex2rgb(e),h="."+s+" { background: rgba("+a.red+","+a.green+","+a.blue+",0.2); }";c(h);var p=this.cm.posFromIndex(t.anchor),l=this.cm.posFromIndex(t.head);return this.cm.markText(r(p,l),i(p,l),{className:s})},e.prototype.setOtherSelection=function(t,e,n){for(var o=[],r=0;r<t.ranges.length;r++){var i=t.ranges[r];i.isEmpty()?o[r]=this.setOtherCursor(i.head,e,n):o[r]=this.setOtherSelectionRange(i,e,n)}return{clear:function(){for(var t=0;t<o.length;t++)o[t].clear()}}},e.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},e.prototype.applyOperation=function(t){this.ignoreNextChange=!0,e.applyOperationToCodeMirror(t,this.cm)},e.prototype.registerUndo=function(t){this.cm.undo=t},e.prototype.registerRedo=function(t){this.cm.redo=t},e}(this),ot.SocketIOAdapter=function(){"use strict";function t(t){this.socket=t;var e=this;t.on("client_left",function(t){e.trigger("client_left",t)}),t.on("set_name",function(t,n){e.trigger("set_name",t,n)}),t.on("set_color",function(t,n){e.trigger("set_color",t,n)}),t.on("ack",function(t){e.trigger("ack",t)}),t.on("operation",function(t,n,o,r){e.trigger("operation",n,o),e.trigger("selection",t,r)}),t.on("operations",function(t,n){n=LZString.decompressFromUTF16(n),n=JSON.parse(n),e.trigger("operations",t,n)}),t.on("selection",function(t,n){e.trigger("selection",t,n)}),t.on("reconnect",function(){e.trigger("reconnect")})}return t.prototype.sendOperation=function(t,e,n){e=LZString.compressToUTF16(JSON.stringify(e)),this.socket.emit("operation",t,e,n)},t.prototype.sendSelection=function(t){this.socket.emit("selection",t)},t.prototype.getOperations=function(t,e){this.socket.emit("get_operations",t,e)},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},t}(),ot.AjaxAdapter=function(){"use strict";function t(t,e,n){"/"!==t[t.length-1]&&(t+="/"),this.path=t,this.ownUserName=e,this.majorRevision=n.major||0,this.minorRevision=n.minor||0,this.poll()}return t.prototype.renderRevisionPath=function(){return"revision/"+this.majorRevision+"-"+this.minorRevision},t.prototype.handleResponse=function(t){var e,n=t.operations;for(e=0;e<n.length;e++)n[e].user===this.ownUserName?this.trigger("ack"):this.trigger("operation",n[e].operation);n.length>0&&(this.majorRevision+=n.length,this.minorRevision=0);var o=t.events;if(o){for(e=0;e<o.length;e++){var r=o[e].user;if(r!==this.ownUserName)switch(o[e].event){case"joined":this.trigger("set_name",r,r);break;case"left":this.trigger("client_left",r);break;case"selection":this.trigger("selection",r,o[e].selection)}}this.minorRevision+=o.length}var i=t.users;i&&(delete i[this.ownUserName],this.trigger("clients",i)),t.revision&&(this.majorRevision=t.revision.major,this.minorRevision=t.revision.minor)},t.prototype.poll=function(){var t=this;$.ajax({url:this.path+this.renderRevisionPath(),type:"GET",dataType:"json",timeout:5e3,success:function(e){t.handleResponse(e),t.poll()},error:function(){setTimeout(function(){t.poll()},500)}})},t.prototype.sendOperation=function(t,e,n){if(t!==this.majorRevision)throw new Error("Revision numbers out of sync");var o=this;$.ajax({url:this.path+this.renderRevisionPath(),type:"POST",data:JSON.stringify({operation:e,selection:n}),contentType:"application/json",processData:!1,success:function(t){},error:function(){setTimeout(function(){o.sendOperation(t,e,n)},500)}})},t.prototype.sendSelection=function(t){$.ajax({url:this.path+this.renderRevisionPath()+"/selection",type:"POST",data:JSON.stringify(t),contentType:"application/json",processData:!1,timeout:1e3})},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},t}(),ot.EditorClient=function(){"use strict";function t(t,e){this.selectionBefore=t,this.selectionAfter=e}function e(t,e){this.clientId=t,this.selection=e}function n(t,e,n,o,r,i){this.id=t,this.listEl=e,this.editorAdapter=n,this.name=o,this.color=r,this.li=document.createElement("li"),o&&(this.li.textContent=o,this.listEl.appendChild(this.li)),r?this.setForceColor(r):this.setColor(o?s(o):Math.random()),i&&this.updateSelection(i)}function o(t,e,n,o){c.call(this,t),this.serverAdapter=n,this.editorAdapter=o,this.undoManager=new u,this.initializeClientList(),this.initializeClients(e);var r=this;this.editorAdapter.registerCallbacks({change:function(t,e){r.onChange(t,e)},selectionChange:function(){r.onSelectionChange()},blur:function(){r.onBlur()}}),this.editorAdapter.registerUndo(function(){r.undo()}),this.editorAdapter.registerRedo(function(){r.redo()}),this.serverAdapter.registerCallbacks({client_left:function(t){r.onClientLeft(t)},set_name:function(t,e){r.getClientObject(t).setName(e)},set_color:function(t,e){r.getClientObject(t).setForceColor(e)},ack:function(t){r.serverAck(t)},operation:function(t,e){r.applyServer(t,f.fromJSON(e))},operations:function(t,e){r.applyOperations(t,e)},selection:function(t,e){e?r.getClientObject(t).updateSelection(r.transformSelection(l.fromJSON(e))):r.getClientObject(t).removeSelection()},clients:function(t){var e;for(e in r.clients)r.clients.hasOwnProperty(e)&&!t.hasOwnProperty(e)&&r.onClientLeft(e);for(e in t)if(t.hasOwnProperty(e)){var n=r.getClientObject(e);t[e].name&&n.setName(t[e].name);var o=t[e].selection;o?r.clients[e].updateSelection(r.transformSelection(l.fromJSON(o))):r.clients[e].removeSelection()}},reconnect:function(){r.serverReconnect()}})}function r(t,e,n){function o(t){var e=Math.round(255*t).toString(16);return 1===e.length?"0"+e:e}return"#"+o(t)+o(e)+o(n)}function i(t,e,n){if(0===e)return r(n,n,n);var o=.5>n?n*(1+e):n+e-e*n,i=2*n-o,s=function(t){return 0>t&&(t+=1),t>1&&(t-=1),1>6*t?i+6*(o-i)*t:1>2*t?o:2>3*t?i+6*(o-i)*(2/3-t):i};return r(s(t+1/3),s(t),s(t-1/3))}function s(t){for(var e=1,n=0;n<t.length;n++)e=17*(e+t.charCodeAt(n))%360;return e/360}function a(t,e){function n(){}n.prototype=e.prototype,t.prototype=new n,t.prototype.constructor=t}function h(t){return t[t.length-1]}function p(t){t.parentNode&&t.parentNode.removeChild(t)}var c=ot.Client,l=ot.Selection,u=ot.UndoManager,f=ot.TextOperation,d=ot.WrappedOperation;return t.prototype.invert=function(){return new t(this.selectionAfter,this.selectionBefore)},t.prototype.compose=function(e){return new t(this.selectionBefore,e.selectionAfter)},t.prototype.transform=function(e){return new t(this.selectionBefore.transform(e),this.selectionAfter.transform(e))},e.fromJSON=function(t){return new e(t.clientId,t.selection&&l.fromJSON(t.selection))},e.prototype.transform=function(t){return new e(this.clientId,this.selection&&this.selection.transform(t))},n.prototype.setColor=function(t){this.hue=t,this.color=i(t,.75,.5),this.lightColor=i(t,.5,.9),this.li&&(this.li.style.color=this.color)},n.prototype.setForceColor=function(t){this.hue=null,this.color=t,this.lightColor=t,this.li&&(this.li.style.color=this.color)},n.prototype.setName=function(t){this.name!==t&&(this.name=t,this.li.textContent=t,this.li.parentNode||this.listEl.appendChild(this.li),this.setColor(s(t)))},n.prototype.updateSelection=function(t){this.removeSelection(),this.selection=t,this.mark=this.editorAdapter.setOtherSelection(t,t.position===t.selectionEnd?this.color:this.lightColor,this.id)},n.prototype.remove=function(){this.li&&p(this.li),this.removeSelection()},n.prototype.removeSelection=function(){this.mark&&(this.mark.clear(),this.mark=null)},a(o,c),o.prototype.addClient=function(t,e){this.clients[t]=new n(t,this.clientListEl,this.editorAdapter,e.name||t,e.color||null,e.selection?l.fromJSON(e.selection):null)},o.prototype.initializeClients=function(t){this.clients={};for(var e in t)t.hasOwnProperty(e)&&this.addClient(e,t[e])},o.prototype.getClientObject=function(t){var e=this.clients[t];return e?e:this.clients[t]=new n(t,this.clientListEl,this.editorAdapter)},o.prototype.onClientLeft=function(t){var e=this.clients[t];e&&(e.remove(),delete this.clients[t])},o.prototype.initializeClientList=function(){this.clientListEl=document.createElement("ul")},o.prototype.applyUnredo=function(t){this.undoManager.add(t.invert(this.editorAdapter.getValue())),this.editorAdapter.applyOperation(t.wrapped),this.selection=t.meta.selectionAfter,this.editorAdapter.setSelection(this.selection),this.applyClient(t.wrapped)},o.prototype.undo=function(){var t=this;this.undoManager.canUndo()&&this.undoManager.performUndo(function(e){t.applyUnredo(e)})},o.prototype.redo=function(){var t=this;this.undoManager.canRedo()&&this.undoManager.performRedo(function(e){t.applyUnredo(e)})},o.prototype.onChange=function(e,n){var o=this.selection;this.updateSelection();var r=new t(o,this.selection),i=(new d(e,r),this.undoManager.undoStack.length>0&&n.shouldBeComposedWithInverted(h(this.undoManager.undoStack).wrapped)),s=new t(this.selection,o);this.undoManager.add(new d(n,s),i),this.applyClient(e)},o.prototype.updateSelection=function(){this.selection=this.editorAdapter.getSelection()},o.prototype.onSelectionChange=function(){var t=this.selection;this.updateSelection(),t&&this.selection.equals(t)||this.sendSelection(this.selection)},o.prototype.onBlur=function(){this.selection=null,this.sendSelection(null)},o.prototype.sendSelection=function(t){this.state instanceof c.AwaitingWithBuffer||this.serverAdapter.sendSelection(t)},o.prototype.sendOperation=function(t,e){this.serverAdapter.sendOperation(t,e.toJSON(),this.selection)},o.prototype.getOperations=function(t,e){this.serverAdapter.getOperations(t,e)},o.prototype.applyOperation=function(t){this.editorAdapter.applyOperation(t),this.updateSelection(),this.undoManager.transform(new d(t,null))},o}(); \ No newline at end of file
diff --git a/public/vendor/ot/selection.js b/public/vendor/ot/selection.js
new file mode 100755
index 00000000..72bf8bd6
--- /dev/null
+++ b/public/vendor/ot/selection.js
@@ -0,0 +1,117 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.Selection = (function (global) {
+ 'use strict';
+
+ var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
+
+ // Range has `anchor` and `head` properties, which are zero-based indices into
+ // the document. The `anchor` is the side of the selection that stays fixed,
+ // `head` is the side of the selection where the cursor is. When both are
+ // equal, the range represents a cursor.
+ function Range (anchor, head) {
+ this.anchor = anchor;
+ this.head = head;
+ }
+
+ Range.fromJSON = function (obj) {
+ return new Range(obj.anchor, obj.head);
+ };
+
+ Range.prototype.equals = function (other) {
+ return this.anchor === other.anchor && this.head === other.head;
+ };
+
+ Range.prototype.isEmpty = function () {
+ return this.anchor === this.head;
+ };
+
+ Range.prototype.transform = function (other) {
+ function transformIndex (index) {
+ var newIndex = index;
+ var ops = other.ops;
+ for (var i = 0, l = other.ops.length; i < l; i++) {
+ if (TextOperation.isRetain(ops[i])) {
+ index -= ops[i];
+ } else if (TextOperation.isInsert(ops[i])) {
+ newIndex += ops[i].length;
+ } else {
+ newIndex -= Math.min(index, -ops[i]);
+ index += ops[i];
+ }
+ if (index < 0) { break; }
+ }
+ return newIndex;
+ }
+
+ var newAnchor = transformIndex(this.anchor);
+ if (this.anchor === this.head) {
+ return new Range(newAnchor, newAnchor);
+ }
+ return new Range(newAnchor, transformIndex(this.head));
+ };
+
+ // A selection is basically an array of ranges. Every range represents a real
+ // selection or a cursor in the document (when the start position equals the
+ // end position of the range). The array must not be empty.
+ function Selection (ranges) {
+ this.ranges = ranges || [];
+ }
+
+ Selection.Range = Range;
+
+ // Convenience method for creating selections only containing a single cursor
+ // and no real selection range.
+ Selection.createCursor = function (position) {
+ return new Selection([new Range(position, position)]);
+ };
+
+ Selection.fromJSON = function (obj) {
+ var objRanges = obj.ranges || obj;
+ for (var i = 0, ranges = []; i < objRanges.length; i++) {
+ ranges[i] = Range.fromJSON(objRanges[i]);
+ }
+ return new Selection(ranges);
+ };
+
+ Selection.prototype.equals = function (other) {
+ if (this.position !== other.position) { return false; }
+ if (this.ranges.length !== other.ranges.length) { return false; }
+ // FIXME: Sort ranges before comparing them?
+ for (var i = 0; i < this.ranges.length; i++) {
+ if (!this.ranges[i].equals(other.ranges[i])) { return false; }
+ }
+ return true;
+ };
+
+ Selection.prototype.somethingSelected = function () {
+ for (var i = 0; i < this.ranges.length; i++) {
+ if (!this.ranges[i].isEmpty()) { return true; }
+ }
+ return false;
+ };
+
+ // Return the more current selection information.
+ Selection.prototype.compose = function (other) {
+ return other;
+ };
+
+ // Update the selection with respect to an operation.
+ Selection.prototype.transform = function (other) {
+ for (var i = 0, newRanges = []; i < this.ranges.length; i++) {
+ newRanges[i] = this.ranges[i].transform(other);
+ }
+ return new Selection(newRanges);
+ };
+
+ return Selection;
+
+}(this));
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.Selection;
+}
diff --git a/public/vendor/ot/socketio-adapter.js b/public/vendor/ot/socketio-adapter.js
new file mode 100755
index 00000000..329d4f3e
--- /dev/null
+++ b/public/vendor/ot/socketio-adapter.js
@@ -0,0 +1,66 @@
+/*global ot */
+
+ot.SocketIOAdapter = (function () {
+ 'use strict';
+
+ function SocketIOAdapter(socket) {
+ this.socket = socket;
+
+ var self = this;
+ socket.on('client_left', function (clientId) {
+ self.trigger('client_left', clientId);
+ });
+ socket.on('set_name', function (clientId, name) {
+ self.trigger('set_name', clientId, name);
+ });
+ socket.on('set_color', function (clientId, color) {
+ self.trigger('set_color', clientId, color);
+ });
+ socket.on('ack', function (revision) {
+ self.trigger('ack', revision);
+ });
+ socket.on('operation', function (clientId, revision, operation, selection) {
+ self.trigger('operation', revision, operation);
+ self.trigger('selection', clientId, selection);
+ });
+ socket.on('operations', function (head, operations) {
+ operations = LZString.decompressFromUTF16(operations);
+ operations = JSON.parse(operations);
+ self.trigger('operations', head, operations);
+ });
+ socket.on('selection', function (clientId, selection) {
+ self.trigger('selection', clientId, selection);
+ });
+ socket.on('reconnect', function () {
+ self.trigger('reconnect');
+ });
+ }
+
+ SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) {
+ operation = LZString.compressToUTF16(JSON.stringify(operation));
+ this.socket.emit('operation', revision, operation, selection);
+ };
+
+ SocketIOAdapter.prototype.sendSelection = function (selection) {
+ this.socket.emit('selection', selection);
+ };
+
+ SocketIOAdapter.prototype.getOperations = function (base, head) {
+ this.socket.emit('get_operations', base, head);
+ };
+
+ SocketIOAdapter.prototype.registerCallbacks = function (cb) {
+ this.callbacks = cb;
+ };
+
+ SocketIOAdapter.prototype.trigger = function (event) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var action = this.callbacks && this.callbacks[event];
+ if (action) {
+ action.apply(this, args);
+ }
+ };
+
+ return SocketIOAdapter;
+
+}()); \ No newline at end of file
diff --git a/public/vendor/ot/text-operation.js b/public/vendor/ot/text-operation.js
new file mode 100755
index 00000000..d5468497
--- /dev/null
+++ b/public/vendor/ot/text-operation.js
@@ -0,0 +1,530 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.TextOperation = (function () {
+ 'use strict';
+
+ // Constructor for new operations.
+ function TextOperation () {
+ if (!this || this.constructor !== TextOperation) {
+ // => function was called without 'new'
+ return new TextOperation();
+ }
+
+ // When an operation is applied to an input string, you can think of this as
+ // if an imaginary cursor runs over the entire string and skips over some
+ // parts, deletes some parts and inserts characters at some positions. These
+ // actions (skip/delete/insert) are stored as an array in the "ops" property.
+ this.ops = [];
+ // An operation's baseLength is the length of every string the operation
+ // can be applied to.
+ this.baseLength = 0;
+ // The targetLength is the length of every string that results from applying
+ // the operation on a valid input string.
+ this.targetLength = 0;
+ }
+
+ TextOperation.prototype.equals = function (other) {
+ if (this.baseLength !== other.baseLength) { return false; }
+ if (this.targetLength !== other.targetLength) { return false; }
+ if (this.ops.length !== other.ops.length) { return false; }
+ for (var i = 0; i < this.ops.length; i++) {
+ if (this.ops[i] !== other.ops[i]) { return false; }
+ }
+ return true;
+ };
+
+ // Operation are essentially lists of ops. There are three types of ops:
+ //
+ // * Retain ops: Advance the cursor position by a given number of characters.
+ // Represented by positive ints.
+ // * Insert ops: Insert a given string at the current cursor position.
+ // Represented by strings.
+ // * Delete ops: Delete the next n characters. Represented by negative ints.
+
+ var isRetain = TextOperation.isRetain = function (op) {
+ return typeof op === 'number' && op > 0;
+ };
+
+ var isInsert = TextOperation.isInsert = function (op) {
+ return typeof op === 'string';
+ };
+
+ var isDelete = TextOperation.isDelete = function (op) {
+ return typeof op === 'number' && op < 0;
+ };
+
+
+ // After an operation is constructed, the user of the library can specify the
+ // actions of an operation (skip/insert/delete) with these three builder
+ // methods. They all return the operation for convenient chaining.
+
+ // Skip over a given number of characters.
+ TextOperation.prototype.retain = function (n) {
+ if (typeof n !== 'number') {
+ throw new Error("retain expects an integer");
+ }
+ if (n === 0) { return this; }
+ this.baseLength += n;
+ this.targetLength += n;
+ if (isRetain(this.ops[this.ops.length-1])) {
+ // The last op is a retain op => we can merge them into one op.
+ this.ops[this.ops.length-1] += n;
+ } else {
+ // Create a new op.
+ this.ops.push(n);
+ }
+ return this;
+ };
+
+ // Insert a string at the current position.
+ TextOperation.prototype.insert = function (str) {
+ if (typeof str !== 'string') {
+ throw new Error("insert expects a string");
+ }
+ if (str === '') { return this; }
+ this.targetLength += str.length;
+ var ops = this.ops;
+ if (isInsert(ops[ops.length-1])) {
+ // Merge insert op.
+ ops[ops.length-1] += str;
+ } else if (isDelete(ops[ops.length-1])) {
+ // It doesn't matter when an operation is applied whether the operation
+ // is delete(3), insert("something") or insert("something"), delete(3).
+ // Here we enforce that in this case, the insert op always comes first.
+ // This makes all operations that have the same effect when applied to
+ // a document of the right length equal in respect to the `equals` method.
+ if (isInsert(ops[ops.length-2])) {
+ ops[ops.length-2] += str;
+ } else {
+ ops[ops.length] = ops[ops.length-1];
+ ops[ops.length-2] = str;
+ }
+ } else {
+ ops.push(str);
+ }
+ return this;
+ };
+
+ // Delete a string at the current position.
+ TextOperation.prototype['delete'] = function (n) {
+ if (typeof n === 'string') { n = n.length; }
+ if (typeof n !== 'number') {
+ throw new Error("delete expects an integer or a string");
+ }
+ if (n === 0) { return this; }
+ if (n > 0) { n = -n; }
+ this.baseLength -= n;
+ if (isDelete(this.ops[this.ops.length-1])) {
+ this.ops[this.ops.length-1] += n;
+ } else {
+ this.ops.push(n);
+ }
+ return this;
+ };
+
+ // Tests whether this operation has no effect.
+ TextOperation.prototype.isNoop = function () {
+ return this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0]));
+ };
+
+ // Pretty printing.
+ TextOperation.prototype.toString = function () {
+ // map: build a new array by applying a function to every element in an old
+ // array.
+ var map = Array.prototype.map || function (fn) {
+ var arr = this;
+ var newArr = [];
+ for (var i = 0, l = arr.length; i < l; i++) {
+ newArr[i] = fn(arr[i]);
+ }
+ return newArr;
+ };
+ return map.call(this.ops, function (op) {
+ if (isRetain(op)) {
+ return "retain " + op;
+ } else if (isInsert(op)) {
+ return "insert '" + op + "'";
+ } else {
+ return "delete " + (-op);
+ }
+ }).join(', ');
+ };
+
+ // Converts operation into a JSON value.
+ TextOperation.prototype.toJSON = function () {
+ return this.ops;
+ };
+
+ // Converts a plain JS object into an operation and validates it.
+ TextOperation.fromJSON = function (ops) {
+ var o = new TextOperation();
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (isRetain(op)) {
+ o.retain(op);
+ } else if (isInsert(op)) {
+ o.insert(op);
+ } else if (isDelete(op)) {
+ o['delete'](op);
+ } else {
+ throw new Error("unknown operation: " + JSON.stringify(op));
+ }
+ }
+ return o;
+ };
+
+ // Apply an operation to a string, returning a new string. Throws an error if
+ // there's a mismatch between the input string and the operation.
+ TextOperation.prototype.apply = function (str) {
+ var operation = this;
+ if (str.length !== operation.baseLength) {
+ throw new Error("The operation's base length must be equal to the string's length.");
+ }
+ var newStr = [], j = 0;
+ var strIndex = 0;
+ var ops = this.ops;
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (isRetain(op)) {
+ if (strIndex + op > str.length) {
+ throw new Error("Operation can't retain more characters than are left in the string.");
+ }
+ // Copy skipped part of the old string.
+ newStr[j++] = str.slice(strIndex, strIndex + op);
+ strIndex += op;
+ } else if (isInsert(op)) {
+ // Insert string.
+ newStr[j++] = op;
+ } else { // delete op
+ strIndex -= op;
+ }
+ }
+ if (strIndex !== str.length) {
+ throw new Error("The operation didn't operate on the whole string.");
+ }
+ return newStr.join('');
+ };
+
+ // Computes the inverse of an operation. The inverse of an operation is the
+ // operation that reverts the effects of the operation, e.g. when you have an
+ // operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello ");
+ // skip(6);'. The inverse should be used for implementing undo.
+ TextOperation.prototype.invert = function (str) {
+ var strIndex = 0;
+ var inverse = new TextOperation();
+ var ops = this.ops;
+ for (var i = 0, l = ops.length; i < l; i++) {
+ var op = ops[i];
+ if (isRetain(op)) {
+ inverse.retain(op);
+ strIndex += op;
+ } else if (isInsert(op)) {
+ inverse['delete'](op.length);
+ } else { // delete op
+ inverse.insert(str.slice(strIndex, strIndex - op));
+ strIndex -= op;
+ }
+ }
+ return inverse;
+ };
+
+ // Compose merges two consecutive operations into one operation, that
+ // preserves the changes of both. Or, in other words, for each input string S
+ // and a pair of consecutive operations A and B,
+ // apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
+ TextOperation.prototype.compose = function (operation2) {
+ var operation1 = this;
+ if (operation1.targetLength !== operation2.baseLength) {
+ throw new Error("The base length of the second operation has to be the target length of the first operation");
+ }
+
+ var operation = new TextOperation(); // the combined operation
+ var ops1 = operation1.ops, ops2 = operation2.ops; // for fast access
+ var i1 = 0, i2 = 0; // current index into ops1 respectively ops2
+ var op1 = ops1[i1++], op2 = ops2[i2++]; // current ops
+ while (true) {
+ // Dispatch on the type of op1 and op2
+ if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
+ // end condition: both ops1 and ops2 have been processed
+ break;
+ }
+
+ if (isDelete(op1)) {
+ operation['delete'](op1);
+ op1 = ops1[i1++];
+ continue;
+ }
+ if (isInsert(op2)) {
+ operation.insert(op2);
+ op2 = ops2[i2++];
+ continue;
+ }
+
+ if (typeof op1 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too short.");
+ }
+ if (typeof op2 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too long.");
+ }
+
+ if (isRetain(op1) && isRetain(op2)) {
+ if (op1 > op2) {
+ operation.retain(op2);
+ op1 = op1 - op2;
+ op2 = ops2[i2++];
+ } else if (op1 === op2) {
+ operation.retain(op1);
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ operation.retain(op1);
+ op2 = op2 - op1;
+ op1 = ops1[i1++];
+ }
+ } else if (isInsert(op1) && isDelete(op2)) {
+ if (op1.length > -op2) {
+ op1 = op1.slice(-op2);
+ op2 = ops2[i2++];
+ } else if (op1.length === -op2) {
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ op2 = op2 + op1.length;
+ op1 = ops1[i1++];
+ }
+ } else if (isInsert(op1) && isRetain(op2)) {
+ if (op1.length > op2) {
+ operation.insert(op1.slice(0, op2));
+ op1 = op1.slice(op2);
+ op2 = ops2[i2++];
+ } else if (op1.length === op2) {
+ operation.insert(op1);
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ operation.insert(op1);
+ op2 = op2 - op1.length;
+ op1 = ops1[i1++];
+ }
+ } else if (isRetain(op1) && isDelete(op2)) {
+ if (op1 > -op2) {
+ operation['delete'](op2);
+ op1 = op1 + op2;
+ op2 = ops2[i2++];
+ } else if (op1 === -op2) {
+ operation['delete'](op2);
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ operation['delete'](op1);
+ op2 = op2 + op1;
+ op1 = ops1[i1++];
+ }
+ } else {
+ throw new Error(
+ "This shouldn't happen: op1: " +
+ JSON.stringify(op1) + ", op2: " +
+ JSON.stringify(op2)
+ );
+ }
+ }
+ return operation;
+ };
+
+ function getSimpleOp (operation, fn) {
+ var ops = operation.ops;
+ var isRetain = TextOperation.isRetain;
+ switch (ops.length) {
+ case 1:
+ return ops[0];
+ case 2:
+ return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null);
+ case 3:
+ if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; }
+ }
+ return null;
+ }
+
+ function getStartIndex (operation) {
+ if (isRetain(operation.ops[0])) { return operation.ops[0]; }
+ return 0;
+ }
+
+ // When you use ctrl-z to undo your latest changes, you expect the program not
+ // to undo every single keystroke but to undo your last sentence you wrote at
+ // a stretch or the deletion you did by holding the backspace key down. This
+ // This can be implemented by composing operations on the undo stack. This
+ // method can help decide whether two operations should be composed. It
+ // returns true if the operations are consecutive insert operations or both
+ // operations delete text at the same position. You may want to include other
+ // factors like the time since the last change in your decision.
+ TextOperation.prototype.shouldBeComposedWith = function (other) {
+ if (this.isNoop() || other.isNoop()) { return true; }
+
+ var startA = getStartIndex(this), startB = getStartIndex(other);
+ var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
+ if (!simpleA || !simpleB) { return false; }
+
+ if (isInsert(simpleA) && isInsert(simpleB)) {
+ return startA + simpleA.length === startB;
+ }
+
+ if (isDelete(simpleA) && isDelete(simpleB)) {
+ // there are two possibilities to delete: with backspace and with the
+ // delete key.
+ return (startB - simpleB === startA) || startA === startB;
+ }
+
+ return false;
+ };
+
+ // Decides whether two operations should be composed with each other
+ // if they were inverted, that is
+ // `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`.
+ TextOperation.prototype.shouldBeComposedWithInverted = function (other) {
+ if (this.isNoop() || other.isNoop()) { return true; }
+
+ var startA = getStartIndex(this), startB = getStartIndex(other);
+ var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
+ if (!simpleA || !simpleB) { return false; }
+
+ if (isInsert(simpleA) && isInsert(simpleB)) {
+ return startA + simpleA.length === startB || startA === startB;
+ }
+
+ if (isDelete(simpleA) && isDelete(simpleB)) {
+ return startB - simpleB === startA;
+ }
+
+ return false;
+ };
+
+ // Transform takes two operations A and B that happened concurrently and
+ // produces two operations A' and B' (in an array) such that
+ // `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
+ // heart of OT.
+ TextOperation.transform = function (operation1, operation2) {
+ if (operation1.baseLength !== operation2.baseLength) {
+ throw new Error("Both operations have to have the same base length");
+ }
+
+ var operation1prime = new TextOperation();
+ var operation2prime = new TextOperation();
+ var ops1 = operation1.ops, ops2 = operation2.ops;
+ var i1 = 0, i2 = 0;
+ var op1 = ops1[i1++], op2 = ops2[i2++];
+ while (true) {
+ // At every iteration of the loop, the imaginary cursor that both
+ // operation1 and operation2 have that operates on the input string must
+ // have the same position in the input string.
+
+ if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
+ // end condition: both ops1 and ops2 have been processed
+ break;
+ }
+
+ // next two cases: one or both ops are insert ops
+ // => insert the string in the corresponding prime operation, skip it in
+ // the other one. If both op1 and op2 are insert ops, prefer op1.
+ if (isInsert(op1)) {
+ operation1prime.insert(op1);
+ operation2prime.retain(op1.length);
+ op1 = ops1[i1++];
+ continue;
+ }
+ if (isInsert(op2)) {
+ operation1prime.retain(op2.length);
+ operation2prime.insert(op2);
+ op2 = ops2[i2++];
+ continue;
+ }
+
+ if (typeof op1 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too short.");
+ }
+ if (typeof op2 === 'undefined') {
+ throw new Error("Cannot compose operations: first operation is too long.");
+ }
+
+ var minl;
+ if (isRetain(op1) && isRetain(op2)) {
+ // Simple case: retain/retain
+ if (op1 > op2) {
+ minl = op2;
+ op1 = op1 - op2;
+ op2 = ops2[i2++];
+ } else if (op1 === op2) {
+ minl = op2;
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ minl = op1;
+ op2 = op2 - op1;
+ op1 = ops1[i1++];
+ }
+ operation1prime.retain(minl);
+ operation2prime.retain(minl);
+ } else if (isDelete(op1) && isDelete(op2)) {
+ // Both operations delete the same string at the same position. We don't
+ // need to produce any operations, we just skip over the delete ops and
+ // handle the case that one operation deletes more than the other.
+ if (-op1 > -op2) {
+ op1 = op1 - op2;
+ op2 = ops2[i2++];
+ } else if (op1 === op2) {
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ op2 = op2 - op1;
+ op1 = ops1[i1++];
+ }
+ // next two cases: delete/retain and retain/delete
+ } else if (isDelete(op1) && isRetain(op2)) {
+ if (-op1 > op2) {
+ minl = op2;
+ op1 = op1 + op2;
+ op2 = ops2[i2++];
+ } else if (-op1 === op2) {
+ minl = op2;
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ minl = -op1;
+ op2 = op2 + op1;
+ op1 = ops1[i1++];
+ }
+ operation1prime['delete'](minl);
+ } else if (isRetain(op1) && isDelete(op2)) {
+ if (op1 > -op2) {
+ minl = -op2;
+ op1 = op1 + op2;
+ op2 = ops2[i2++];
+ } else if (op1 === -op2) {
+ minl = op1;
+ op1 = ops1[i1++];
+ op2 = ops2[i2++];
+ } else {
+ minl = op1;
+ op2 = op2 + op1;
+ op1 = ops1[i1++];
+ }
+ operation2prime['delete'](minl);
+ } else {
+ throw new Error("The two operations aren't compatible");
+ }
+ }
+
+ return [operation1prime, operation2prime];
+ };
+
+ return TextOperation;
+
+}());
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.TextOperation;
+} \ No newline at end of file
diff --git a/public/vendor/ot/undo-manager.js b/public/vendor/ot/undo-manager.js
new file mode 100755
index 00000000..19e89f18
--- /dev/null
+++ b/public/vendor/ot/undo-manager.js
@@ -0,0 +1,111 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.UndoManager = (function () {
+ 'use strict';
+
+ var NORMAL_STATE = 'normal';
+ var UNDOING_STATE = 'undoing';
+ var REDOING_STATE = 'redoing';
+
+ // Create a new UndoManager with an optional maximum history size.
+ function UndoManager (maxItems) {
+ this.maxItems = maxItems || 50;
+ this.state = NORMAL_STATE;
+ this.dontCompose = false;
+ this.undoStack = [];
+ this.redoStack = [];
+ }
+
+ // Add an operation to the undo or redo stack, depending on the current state
+ // of the UndoManager. The operation added must be the inverse of the last
+ // edit. When `compose` is true, compose the operation with the last operation
+ // unless the last operation was alread pushed on the redo stack or was hidden
+ // by a newer operation on the undo stack.
+ UndoManager.prototype.add = function (operation, compose) {
+ if (this.state === UNDOING_STATE) {
+ this.redoStack.push(operation);
+ this.dontCompose = true;
+ } else if (this.state === REDOING_STATE) {
+ this.undoStack.push(operation);
+ this.dontCompose = true;
+ } else {
+ var undoStack = this.undoStack;
+ if (!this.dontCompose && compose && undoStack.length > 0) {
+ undoStack.push(operation.compose(undoStack.pop()));
+ } else {
+ undoStack.push(operation);
+ if (undoStack.length > this.maxItems) { undoStack.shift(); }
+ }
+ this.dontCompose = false;
+ this.redoStack = [];
+ }
+ };
+
+ function transformStack (stack, operation) {
+ var newStack = [];
+ var Operation = operation.constructor;
+ for (var i = stack.length - 1; i >= 0; i--) {
+ var pair = Operation.transform(stack[i], operation);
+ if (typeof pair[0].isNoop !== 'function' || !pair[0].isNoop()) {
+ newStack.push(pair[0]);
+ }
+ operation = pair[1];
+ }
+ return newStack.reverse();
+ }
+
+ // Transform the undo and redo stacks against a operation by another client.
+ UndoManager.prototype.transform = function (operation) {
+ this.undoStack = transformStack(this.undoStack, operation);
+ this.redoStack = transformStack(this.redoStack, operation);
+ };
+
+ // Perform an undo by calling a function with the latest operation on the undo
+ // stack. The function is expected to call the `add` method with the inverse
+ // of the operation, which pushes the inverse on the redo stack.
+ UndoManager.prototype.performUndo = function (fn) {
+ this.state = UNDOING_STATE;
+ if (this.undoStack.length === 0) { throw new Error("undo not possible"); }
+ fn(this.undoStack.pop());
+ this.state = NORMAL_STATE;
+ };
+
+ // The inverse of `performUndo`.
+ UndoManager.prototype.performRedo = function (fn) {
+ this.state = REDOING_STATE;
+ if (this.redoStack.length === 0) { throw new Error("redo not possible"); }
+ fn(this.redoStack.pop());
+ this.state = NORMAL_STATE;
+ };
+
+ // Is the undo stack not empty?
+ UndoManager.prototype.canUndo = function () {
+ return this.undoStack.length !== 0;
+ };
+
+ // Is the redo stack not empty?
+ UndoManager.prototype.canRedo = function () {
+ return this.redoStack.length !== 0;
+ };
+
+ // Whether the UndoManager is currently performing an undo.
+ UndoManager.prototype.isUndoing = function () {
+ return this.state === UNDOING_STATE;
+ };
+
+ // Whether the UndoManager is currently performing a redo.
+ UndoManager.prototype.isRedoing = function () {
+ return this.state === REDOING_STATE;
+ };
+
+ return UndoManager;
+
+}());
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.UndoManager;
+}
diff --git a/public/vendor/ot/wrapped-operation.js b/public/vendor/ot/wrapped-operation.js
new file mode 100755
index 00000000..91050f4e
--- /dev/null
+++ b/public/vendor/ot/wrapped-operation.js
@@ -0,0 +1,80 @@
+if (typeof ot === 'undefined') {
+ // Export for browsers
+ var ot = {};
+}
+
+ot.WrappedOperation = (function (global) {
+ 'use strict';
+
+ // A WrappedOperation contains an operation and corresponing metadata.
+ function WrappedOperation (operation, meta) {
+ this.wrapped = operation;
+ this.meta = meta;
+ }
+
+ WrappedOperation.prototype.apply = function () {
+ return this.wrapped.apply.apply(this.wrapped, arguments);
+ };
+
+ WrappedOperation.prototype.invert = function () {
+ var meta = this.meta;
+ return new WrappedOperation(
+ this.wrapped.invert.apply(this.wrapped, arguments),
+ meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
+ meta.invert.apply(meta, arguments) : meta
+ );
+ };
+
+ // Copy all properties from source to target.
+ function copy (source, target) {
+ for (var key in source) {
+ if (source.hasOwnProperty(key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ function composeMeta (a, b) {
+ if (a && typeof a === 'object') {
+ if (typeof a.compose === 'function') { return a.compose(b); }
+ var meta = {};
+ copy(a, meta);
+ copy(b, meta);
+ return meta;
+ }
+ return b;
+ }
+
+ WrappedOperation.prototype.compose = function (other) {
+ return new WrappedOperation(
+ this.wrapped.compose(other.wrapped),
+ composeMeta(this.meta, other.meta)
+ );
+ };
+
+ function transformMeta (meta, operation) {
+ if (meta && typeof meta === 'object') {
+ if (typeof meta.transform === 'function') {
+ return meta.transform(operation);
+ }
+ }
+ return meta;
+ }
+
+ WrappedOperation.transform = function (a, b) {
+ var transform = a.wrapped.constructor.transform;
+ var pair = transform(a.wrapped, b.wrapped);
+ return [
+ new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
+ new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
+ ];
+ };
+
+ return WrappedOperation;
+
+}(this));
+
+// Export for CommonJS
+if (typeof module === 'object') {
+ module.exports = ot.WrappedOperation;
+} \ No newline at end of file
diff --git a/public/views/body.ejs b/public/views/body.ejs
index bf713cba..1367d9f9 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -58,7 +58,7 @@
<h4 class="modal-title" id="myModalLabel">This page need refresh</h4>
</div>
<div class="modal-body">
- <h5>This page have a mismatch client version or incorrect user state.</h5>
+ <h5>This page have a mismatch client version or incorrect user state or errors.</h5>
<strong>Please refresh this page.</strong>
</div>
<div class="modal-footer">
diff --git a/public/views/foot.ejs b/public/views/foot.ejs
index e950cf51..b545841f 100644
--- a/public/views/foot.ejs
+++ b/public/views/foot.ejs
@@ -8,6 +8,8 @@
<script src="/vendor/codemirror/codemirror.min.js" defer></script>
<script src="/vendor/inlineAttachment/inline-attachment.js" defer></script>
<script src="/vendor/inlineAttachment/codemirror.inline-attachment.js" defer></script>
+<!--ot-->
+<script src="/vendor/ot/ot.min.js" defer></script>
<!--others-->
<script src="/vendor/socket.io-1.3.5.js" defer></script>
<script src="/vendor/remarkable.min.js" defer></script>